diff --git a/src/humulus/recipes.py b/src/humulus/recipes.py
index 525f16c..e854b14 100644
--- a/src/humulus/recipes.py
+++ b/src/humulus/recipes.py
@@ -37,7 +37,7 @@ bp = Blueprint('recipes', __name__, url_prefix='/recipes')
class FermentableForm(Form):
"""Form for fermentables.
- CSRF is disabled for this form.yeast.form (using `Form as parent class)
+ CSRF is disabled for this form (using `Form as parent class)
because it is never used by itself.
"""
name = StringField('Name', validators=[DataRequired()])
@@ -67,7 +67,7 @@ class FermentableForm(Form):
class HopForm(Form):
"""Form for hops.
- CSRF is disabled for this form.yeast.form (using `Form as parent class)
+ CSRF is disabled for this form (using `Form as parent class)
because it is never used by itself.
"""
name = StringField('Name', validators=[DataRequired()])
@@ -96,7 +96,7 @@ class HopForm(Form):
class YeastForm(Form):
"""Form for yeast.
- CSRF is disabled for this form.yeast.form (using `Form as parent class)
+ CSRF is disabled for this form (using `Form as parent class)
because it is never used by itself.
"""
name = StringField('Name', validators=[Optional()])
@@ -148,6 +148,63 @@ class YeastForm(Form):
return yeast
+class MashStepForm(Form):
+ """Form for mash steps.
+
+ CSRF is disabled for this form (using `Form as parent class)
+ because it is never used by itself.
+ """
+ name = StringField('Step Name', validators=[DataRequired()])
+ type = SelectField('Type',
+ choices=[(c, c) for c in ['Infusion',
+ 'Temperature',
+ 'Decoction']])
+ temp = DecimalField('Temperature (°F)', validators=[DataRequired()])
+ time = DecimalField('Time (min)', validators=[DataRequired()])
+ amount = DecimalField('Water Amount (gal)')
+
+ @property
+ def doc(self):
+ """Returns a dictionary that can be deserialized into JSON.
+
+ Used for putting into CouchDB.
+ """
+ step = {
+ 'name': self.name.data,
+ 'type': self.type.data,
+ 'temp': str(self.temp.data),
+ 'time': str(self.time.data),
+ }
+ if self.amount.data:
+ step['amount'] = str(self.amount.data)
+ return step
+
+
+class MashForm(Form):
+ """Form for mash.
+
+ CSRF is disabled for this form (using `Form as parent class)
+ because it is never used by itself.
+ """
+ name = StringField('Mash Name', validators=[Optional()])
+ steps = FieldList(
+ FormField(MashStepForm),
+ min_entries=0,
+ max_entries=20
+ )
+
+ @property
+ def doc(self):
+ """Returns a dictionary that can be deserialized into JSON.
+
+ Used for putting into CouchDB.
+ """
+ return {
+ 'name': self.name.data,
+ 'steps': [s.doc for s in self.steps]
+ }
+
+
class RecipeForm(FlaskForm):
"""Form for recipes."""
name = StringField('Name', validators=[DataRequired()])
@@ -171,6 +228,7 @@ class RecipeForm(FlaskForm):
max_entries=20
)
yeast = FormField(YeastForm)
+ mash = FormField(MashForm)
style = SelectField('Style', choices=[], validators=[Optional()])
@property
@@ -197,6 +255,8 @@ class RecipeForm(FlaskForm):
self.yeast.doc['high_attenuation'] != "None"
):
recipe['yeast'] = self.yeast.doc
+ if self.mash.doc['name']:
+ recipe['mash'] = self.mash.doc
return recipe
def copyfrom(self, data):
@@ -227,35 +287,52 @@ class RecipeForm(FlaskForm):
})
if 'yeast' in data:
- yeast = data['yeast']
- self.yeast.form.name.data = yeast['name']
+ self.yeast.form.name.data = data['yeast']['name']
self.yeast.form.low_attenuation.data = (
- Decimal(yeast['low_attenuation'])
+ Decimal(data['yeast']['low_attenuation'])
)
self.yeast.form.high_attenuation.data = (
- Decimal(yeast['high_attenuation'])
+ Decimal(data['yeast']['high_attenuation'])
)
- if 'type' in yeast:
- self.yeast.form.type.data = yeast['type']
- if 'lab' in yeast:
- self.yeast.form.lab.data = yeast['lab']
- if 'code' in yeast:
- self.yeast.form.code.data = yeast['code']
- if 'flocculation' in yeast:
- self.yeast.form.flocculation.data = yeast['flocculation']
- if 'min_temperature' in yeast:
+ if 'type' in data['yeast']:
+ self.yeast.form.type.data = data['yeast']['type']
+ if 'lab' in data['yeast']:
+ self.yeast.form.lab.data = data['yeast']['lab']
+ if 'code' in data['yeast']:
+ self.yeast.form.code.data = data['yeast']['code']
+ if 'flocculation' in data['yeast']:
+ self.yeast.form.flocculation.data = (
+ data['yeast']['flocculation']
+ )
+ if 'min_temperature' in data['yeast']:
self.yeast.form.min_temperature.data = (
- Decimal(yeast['min_temperature'])
+ Decimal(data['yeast']['min_temperature'])
)
- if 'max_temperature' in yeast:
+ if 'max_temperature' in data['yeast']:
self.yeast.form.max_temperature.data = (
- Decimal(yeast['max_temperature'])
+ Decimal(data['yeast']['max_temperature'])
)
- if 'abv_tolerance' in yeast:
+ if 'abv_tolerance' in data['yeast']:
self.yeast.form.abv_tolerance.data = (
- Decimal(yeast['abv_tolerance'])
+ Decimal(data['yeast']['abv_tolerance'])
)
+ if 'mash' in data:
+ if 'name' in data['mash']:
+ self.mash.form.name.data = data['mash']['name']
+ if 'steps' in data['mash']:
+ for step in data['mash']['steps']:
+ new_step = {
+ 'name': step['name'],
+ 'type': step['type'],
+ 'temp': Decimal(step['temp']),
+ 'time': Decimal(step['time'])
+ }
+ if 'amount' in step:
+ new_step['amount'] = Decimal(step['amount'])
+ print(new_step)
+ self.mash.steps.append_entry(new_step)
+
class ImportForm(FlaskForm):
upload = FileField(validators=[FileRequired()])
diff --git a/src/humulus/static/recipes.js b/src/humulus/static/recipes.js
index c105df7..1053f65 100644
--- a/src/humulus/static/recipes.js
+++ b/src/humulus/static/recipes.js
@@ -90,6 +90,11 @@ function removeHop() {
removeForm($(this), '.hop-form', '#hops');
}
+// Remove a mash step
+function removeMashStep() {
+ removeForm($(this), '.mash-form', '#mash');
+}
+
// Add a fermentable
function addFerm() {
var $fermsDiv = $('#ferms');
@@ -212,6 +217,58 @@ function addHop() {
rebindChangeEvents();
}
+// Add a new mash step
+function addMashStep() {
+ var $mashDiv = $('#mash');
+ var stepsLength = $mashDiv.data('length');
+ if (stepsLength == 20) {
+ window.alert("Can't have more than 20 mash steps.");
+ return;
+ }
+
+ var newStep = `
';
+
+ $mashDiv.append(newStep);
+ $('.rem-step').unbind('click');
+ $('.rem-step').click(removeMashStep);
+}
+
// Calculate recipe's original gravity
function calculateOG() {
var fermsLength = $('#ferms').data('length');
@@ -414,6 +471,8 @@ $(document).ready(function() {
$('.rem-ferm').click(removeFerm);
$('#add-hop').click(addHop);
$('.rem-hop').click(removeHop);
+ $('#add-step').click(addMashStep);
+ $('.rem-step').click(removeMashStep);
// Register change events
rebindChangeEvents();
diff --git a/src/humulus/templates/recipes/_macros.html b/src/humulus/templates/recipes/_macros.html
index a7f99a6..59d88ef 100644
--- a/src/humulus/templates/recipes/_macros.html
+++ b/src/humulus/templates/recipes/_macros.html
@@ -130,6 +130,36 @@ function getSpecsURL() {
+ {#-
+ Mash & Steps
+ -#}
+
+
+ {{ render_field_with_errors(form.mash.form.name, 'form-control-sm') }}
+ {% for step in form.mash.form.steps %}
+
+ {% endfor %}
+
+
{#-
Recipe Notes
-#}
diff --git a/src/humulus/templates/recipes/info.html b/src/humulus/templates/recipes/info.html
index 6264a85..ee58f2e 100644
--- a/src/humulus/templates/recipes/info.html
+++ b/src/humulus/templates/recipes/info.html
@@ -200,11 +200,41 @@
{% endif %}
+{% if recipe.mash and recipe.mash.name %}
+Mash
+{{ recipe.mash.name }}
+
+
+
+
+ | # |
+ Step Name |
+ Type |
+ Temp |
+ Time |
+ Amount |
+
+
+ {% for step in recipe.mash.steps %}
+
+ | {{ loop.index }} |
+ {{ step.name }} |
+ {{ step.type }} |
+ {% if step.temp %}{{ step.temp }} °F{% endif %} |
+ {% if step.time %}{{ step.time }} min.{% endif %} |
+ {% if step.amount %}{{ step.amount }} gal.{% endif %} |
+
+ {% endfor %}
+
+
+{% endif %}
{#-
Recipe Notes
-#}
+{% if recipe.notes %}
Recipe Notes
+{% endif %}
{#-
Buttons to do things
-#}
diff --git a/tests/conftest.py b/tests/conftest.py
index 21043e4..743111f 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -123,6 +123,16 @@ def app():
'min_temperature': '60',
'max_temperature': '72',
'abv_tolerance': '10'
+ },
+ 'mash': {
+ 'name': 'Single Infusion',
+ 'steps': [{
+ 'name': 'Infusion',
+ 'type': 'Infusion',
+ 'temp': '152',
+ 'time': '60',
+ 'amount': '3.5'
+ }]
}
})
diff --git a/tests/test_recipes.py b/tests/test_recipes.py
index b1c69f1..7f3f740 100644
--- a/tests/test_recipes.py
+++ b/tests/test_recipes.py
@@ -17,7 +17,8 @@ from decimal import Decimal
from io import BytesIO
from humulus.couch import get_doc
-from humulus.recipes import FermentableForm, HopForm, RecipeForm, YeastForm
+from humulus.recipes import (FermentableForm, HopForm, RecipeForm, YeastForm,
+ MashForm, MashStepForm)
def test_index(client):
@@ -247,6 +248,30 @@ def test_info_json(client):
assert response.get_json()['name'] == 'Awesome Lager'
+def test_step_form_doc(app):
+ """Evaluates conditionals in generation of doc from a step form."""
+ step = MashStepForm()
+ step.name.data = 'Test Mash Step'
+ step.type.data = 'Infusion'
+ step.temp.data = Decimal('152')
+ step.time.data = Decimal('60')
+ assert step.doc == {
+ 'name': 'Test Mash Step',
+ 'type': 'Infusion',
+ 'temp': '152',
+ 'time': '60'
+ }
+
+ step.amount.data = Decimal('3.5')
+ assert step.doc == {
+ 'name': 'Test Mash Step',
+ 'type': 'Infusion',
+ 'temp': '152',
+ 'time': '60',
+ 'amount': '3.5'
+ }
+
+
def test_yeast_form_doc(app):
"""Evaluates conditionals in generation of doc from a yeast form."""
yeast = YeastForm()
@@ -329,8 +354,19 @@ def test_recipe_form_doc(app):
yeast.low_attenuation.data = '70'
yeast.high_attenuation.data = '75'
+ step = MashStepForm()
+ step.name.data = 'Test Mash Step'
+ step.type.data = 'Infusion'
+ step.temp.data = Decimal('152')
+ step.time.data = Decimal('60')
+ step.amount.data = Decimal('3.5')
+ mash = MashForm()
+ mash.name.data = 'Single Infusion'
+ mash.steps = [step]
+
recipe.fermentables = [ferm]
recipe.hops = [hop]
+ recipe.mash = mash
recipe.yeast = yeast
assert recipe.doc == {
@@ -359,6 +395,16 @@ def test_recipe_form_doc(app):
'name': 'Test',
'low_attenuation': '70',
'high_attenuation': '75'
+ },
+ 'mash': {
+ 'name': 'Single Infusion',
+ 'steps': [{
+ 'name': 'Test Mash Step',
+ 'type': 'Infusion',
+ 'temp': '152',
+ 'time': '60',
+ 'amount': '3.5'
+ }]
}
}
@@ -404,3 +450,122 @@ def test_recipe_create_json(client, sample_recipes, auth):
response = client.post('/recipes/create/json', buffered=True,
content_type='multipart/form-data', data=data)
assert response.status_code == 200
+
+
+def test_copyfrom(app, sample_recipes):
+ recipe = {
+ 'name': 'Test',
+ 'type': 'All-Grain',
+ 'efficiency': '65',
+ 'volume': '5.5',
+ 'notes': 'Notes',
+ 'style': '18A',
+ 'fermentables': [{
+ 'name': 'Test',
+ 'type': 'Grain',
+ 'amount': '1',
+ 'ppg': '36',
+ 'color': '4'
+ }],
+ 'hops': [{
+ 'name': 'Test',
+ 'use': 'Boil',
+ 'alpha': '5.5',
+ 'duration': '30',
+ 'amount': '1'
+ }]
+ }
+
+ with app.app_context():
+ form = RecipeForm()
+ form.copyfrom(recipe)
+ assert form.name.data == recipe['name']
+ assert form.type.data == recipe['type']
+ assert form.efficiency.data == Decimal(recipe['efficiency'])
+ assert form.volume.data == Decimal(recipe['volume'])
+ assert form.notes.data == recipe['notes']
+ assert len(form.fermentables) == len(recipe['fermentables'])
+ assert form.fermentables[0].form.name.data == \
+ recipe['fermentables'][0]['name']
+ assert form.fermentables[0].form.type.data == \
+ recipe['fermentables'][0]['type']
+ assert form.fermentables[0].form.amount.data == \
+ Decimal(recipe['fermentables'][0]['amount'])
+ assert form.fermentables[0].form.ppg.data == \
+ Decimal(recipe['fermentables'][0]['ppg'])
+ assert form.fermentables[0].form.color.data == \
+ Decimal(recipe['fermentables'][0]['color'])
+ assert len(form.hops) == len(recipe['hops'])
+ assert form.hops[0].form.name.data == recipe['hops'][0]['name']
+ assert form.hops[0].form.use.data == recipe['hops'][0]['use']
+ assert form.hops[0].form.alpha.data == Decimal(recipe['hops'][0]['alpha'])
+ assert form.hops[0].form.duration.data == \
+ Decimal(recipe['hops'][0]['duration'])
+ assert form.hops[0].form.amount.data == \
+ Decimal(recipe['hops'][0]['amount'])
+
+ recipe['yeast'] = {
+ 'name': 'Test', 'low_attenuation': '65', 'high_attenuation': '68'
+ }
+ recipe['mash'] = {}
+ with app.app_context():
+ form = RecipeForm()
+ form.copyfrom(recipe)
+ assert form.yeast.form.name.data == recipe['yeast']['name']
+ assert form.yeast.form.low_attenuation.data == \
+ Decimal(recipe['yeast']['low_attenuation'])
+ assert form.yeast.form.high_attenuation.data == \
+ Decimal(recipe['yeast']['high_attenuation'])
+
+ recipe['yeast'].update({
+ 'type': 'Liquid',
+ 'lab': 'Test',
+ 'code': 'Test',
+ 'flocculation': 'Low',
+ 'min_temperature': '65',
+ 'max_temperature': '68',
+ 'abv_tolerance': '15'
+ })
+ with app.app_context():
+ form = RecipeForm()
+ form.copyfrom(recipe)
+ assert form.yeast.form.type.data == recipe['yeast']['type']
+ assert form.yeast.form.lab.data == recipe['yeast']['lab']
+ assert form.yeast.form.code.data == recipe['yeast']['code']
+ assert form.yeast.form.flocculation.data == recipe['yeast']['flocculation']
+ assert form.yeast.form.min_temperature.data == \
+ Decimal(recipe['yeast']['min_temperature'])
+ assert form.yeast.form.max_temperature.data == \
+ Decimal(recipe['yeast']['max_temperature'])
+ assert form.yeast.form.abv_tolerance.data == \
+ Decimal(recipe['yeast']['abv_tolerance'])
+
+ recipe['mash'] = {
+ 'name': 'Test',
+ 'steps': [{
+ 'name': 'Infusion',
+ 'type': 'Infusion',
+ 'temp': '152',
+ 'time': '60'
+ }]
+ }
+ with app.app_context():
+ form = RecipeForm()
+ form.copyfrom(recipe)
+ assert form.mash.form.name.data == recipe['mash']['name']
+ assert len(form.mash.form.steps) == len(recipe['mash']['steps'])
+ assert form.mash.form.steps[0].form.name.data == \
+ recipe['mash']['steps'][0]['name']
+ assert form.mash.form.steps[0].form.type.data == \
+ recipe['mash']['steps'][0]['type']
+ assert form.mash.form.steps[0].form.temp.data == \
+ Decimal(recipe['mash']['steps'][0]['temp'])
+ assert form.mash.form.steps[0].form.time.data == \
+ Decimal(recipe['mash']['steps'][0]['time'])
+
+ recipe['mash']['steps'][0]['amount'] = '3.5'
+ with app.app_context():
+ form = RecipeForm()
+ form.copyfrom(recipe)
+ assert form.mash.form.steps[0].form.amount.data == \
+ Decimal(recipe['mash']['steps'][0]['amount'])