mirror of
https://github.com/shouptech/humulus.git
synced 2026-02-03 17:09:44 +00:00
Create a recipe
This commit is contained in:
parent
02b15956d9
commit
5af167e394
8 changed files with 298 additions and 51 deletions
|
|
@ -12,32 +12,4 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
|
||||
from flask import Flask
|
||||
|
||||
|
||||
def create_app(test_config=None):
|
||||
# create and configure the app
|
||||
app = Flask(__name__, instance_relative_config=True)
|
||||
|
||||
if test_config is not None:
|
||||
# Load the test config if provided
|
||||
app.config.from_mapping(test_config)
|
||||
else:
|
||||
# Load config from configuration provided via ENV
|
||||
app.config.from_envvar('HUMULUS_SETTINGS')
|
||||
|
||||
from . import couch
|
||||
couch.init_app(app)
|
||||
|
||||
# Register blueprint for index page
|
||||
from . import home
|
||||
app.register_blueprint(home.bp)
|
||||
app.add_url_rule('/', endpoint='index')
|
||||
|
||||
# Register blueprint for recipes
|
||||
from . import recipes
|
||||
app.register_blueprint(recipes.bp)
|
||||
|
||||
return app
|
||||
from humulus.app import create_app
|
||||
|
|
|
|||
43
src/humulus/app.py
Normal file
43
src/humulus/app.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# 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 os
|
||||
|
||||
from flask import Flask
|
||||
|
||||
|
||||
def create_app(test_config=None):
|
||||
# create and configure the app
|
||||
app = Flask(__name__, instance_relative_config=True)
|
||||
|
||||
if test_config is not None:
|
||||
# Load the test config if provided
|
||||
app.config.from_mapping(test_config)
|
||||
else:
|
||||
# Load config from configuration provided via ENV
|
||||
app.config.from_envvar('HUMULUS_SETTINGS')
|
||||
|
||||
from . import couch
|
||||
couch.init_app(app)
|
||||
|
||||
# Register blueprint for index page
|
||||
from . import home
|
||||
app.register_blueprint(home.bp)
|
||||
app.add_url_rule('/', endpoint='index')
|
||||
|
||||
# Register blueprint for recipes
|
||||
from . import recipes
|
||||
app.register_blueprint(recipes.bp)
|
||||
|
||||
return app
|
||||
|
|
@ -18,19 +18,58 @@ from decimal import Decimal
|
|||
|
||||
from flask import Blueprint, flash, redirect, render_template, url_for
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, DecimalField, TextAreaField
|
||||
from wtforms import (Form, StringField, DecimalField, TextAreaField, FieldList,
|
||||
FormField, SelectField)
|
||||
from wtforms.validators import DataRequired
|
||||
|
||||
from humulus.couch import get_doc_or_404, put_doc
|
||||
|
||||
bp = Blueprint('recipes', __name__, url_prefix='/recipes')
|
||||
|
||||
class RecipeForm(FlaskForm):
|
||||
|
||||
class FermentableForm(Form):
|
||||
"""Form for fermentables.
|
||||
|
||||
CSRF is disabled for this subform (using `Form as parent class) because it
|
||||
is never used by itself.
|
||||
"""
|
||||
|
||||
name = StringField('Name', validators=[DataRequired()])
|
||||
efficiency = DecimalField('Batch Efficiency', validators=[DataRequired()])
|
||||
volume = DecimalField('Batch Volume', validators=[DataRequired()])
|
||||
type = SelectField('Type', validators=[DataRequired()],
|
||||
choices=[(c, c) for c in ['Grain', 'LME', 'DME', 'Sugar',
|
||||
'Non-fermentable', 'Other']])
|
||||
amount = DecimalField('Amount (lb)', validators=[DataRequired()])
|
||||
ppg = DecimalField('PPG', validators=[DataRequired()])
|
||||
color = DecimalField('Color (°L)', validators=[DataRequired()])
|
||||
|
||||
@property
|
||||
def doc(self):
|
||||
"""Returns a dictionary that can be deserialized into JSON.
|
||||
|
||||
Used for putting into CouchDB.
|
||||
"""
|
||||
return {
|
||||
'name': self.name.data,
|
||||
'type': self.type.data,
|
||||
'amount': str(self.amount.data),
|
||||
'ppg': str(self.ppg.data),
|
||||
'color': str(self.color.data)
|
||||
}
|
||||
|
||||
|
||||
class RecipeForm(FlaskForm):
|
||||
"""Form for recipes."""
|
||||
name = StringField('Name', validators=[DataRequired()])
|
||||
efficiency = DecimalField('Batch Efficiency (%)', validators=[DataRequired()])
|
||||
volume = DecimalField('Batch Volume (gal)', validators=[DataRequired()])
|
||||
notes = TextAreaField('Notes')
|
||||
|
||||
fermentables = FieldList(
|
||||
FormField(FermentableForm),
|
||||
min_entries=0,
|
||||
max_entries=20
|
||||
)
|
||||
|
||||
@property
|
||||
def doc(self):
|
||||
"""Returns a dictionary that can be deserialized into JSON.
|
||||
|
|
@ -41,7 +80,8 @@ class RecipeForm(FlaskForm):
|
|||
'name': self.name.data,
|
||||
'efficiency': str(self.efficiency.data),
|
||||
'volume': str(self.volume.data),
|
||||
'notes': self.notes.data
|
||||
'notes': self.notes.data,
|
||||
'fermentables': [f.doc for f in self.fermentables]
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.bd-placeholder-img {
|
||||
.placeholder-img {
|
||||
font-size: 1.125rem;
|
||||
text-anchor: middle;
|
||||
-webkit-user-select: none;
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.bd-placeholder-img-lg {
|
||||
.placeholder-img-lg {
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ limitations under the License.
|
|||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
||||
<main role="main" class="container">
|
||||
{% block alert %}
|
||||
{% for category, message in get_flashed_messages(with_categories=true) %}
|
||||
|
|
@ -54,7 +54,6 @@ limitations under the License.
|
|||
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" 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>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-#}
|
||||
{% macro render_field_with_errors(field) %}
|
||||
{% macro render_field_with_errors(field, size='') %}
|
||||
<div class="form-group">
|
||||
{{ field.label }} {{ field(class_='form-control', **kwargs)|safe }}
|
||||
{{ field.label }} {{ field(class_='form-control ' + size, **kwargs)|safe }}
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}
|
||||
<div class="container">
|
||||
|
|
|
|||
|
|
@ -20,14 +20,165 @@ limitations under the License.
|
|||
|
||||
{% block body %}
|
||||
<div class="row"><h1>Create a new recipe</h1></div>
|
||||
<div class="row">
|
||||
<form method="POST" action="{{ url_for('recipes.create') }}">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ render_field_with_errors(form.name) }}
|
||||
{{ render_field_with_errors(form.efficiency) }}
|
||||
{{ render_field_with_errors(form.volume) }}
|
||||
{{ render_field_with_errors(form.notes) }}
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
<div class="row">
|
||||
<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.volume) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col"><h3>Fermentables</h3></div>
|
||||
</div>
|
||||
<div id="ferms" data-length="{{ form.fermentables|length }}">
|
||||
{% for fermentable in form.fermentables %}
|
||||
<div class="border pl-2 pr-2 pt-1 pb-1 ferm-form" data-index="{{ loop.index0 }}">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{{ render_field_with_errors(fermentable.form.name, 'form-control-sm') }}
|
||||
</div>
|
||||
</div>
|
||||
<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.amount, 'form-control-sm') }}</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.color, 'form-control-sm') }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<button type="button" class="float-right btn btn-sm btn-outline-danger rem-ferm">Remove fermentable</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="row pt-1 pb-1">
|
||||
<div class="col">
|
||||
<button type="button" id="add-ferm" class="btn btn-secondary btn-sm">Add a fermentable</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">{{ render_field_with_errors(form.notes) }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col"><button type="submit" class="btn btn-primary">Create Recipe</button></div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
// Remove a fermentable
|
||||
function removeFerm() {
|
||||
var $removedForm = $(this).closest('.ferm-form');
|
||||
var removedIndex = parseInt($removedForm.data('index'));
|
||||
$removedForm.remove();
|
||||
var $fermsDiv = $('#ferms');
|
||||
$fermsDiv.data('length', $fermsDiv.data('length') - 1);
|
||||
console.log('calling adjust');
|
||||
adjustFermIndices(removedIndex);
|
||||
}
|
||||
|
||||
// Add a fermentable
|
||||
function addFerm() {
|
||||
var $fermsDiv = $('#ferms');
|
||||
var fermsLength = $fermsDiv.data('length');
|
||||
if (fermsLength == 20) {
|
||||
window.alert("Can't have more than 20 fermentables.");
|
||||
return;
|
||||
}
|
||||
var newFerm = `<div class="border pl-2 pr-2 pt-1 pb-1 ferm-form" data-index="${fermsLength}">` +
|
||||
// Name field
|
||||
'<div class="row"><div class="col"><div class="form-group">' +
|
||||
`<label for="fermentables-${fermsLength}-name">Name</label>` +
|
||||
`<input class="form-control form-control-sm" id="fermentables-${fermsLength}-name"` +
|
||||
` name="fermentables-${fermsLength}-name" required type="text" value="">` +
|
||||
'</div></div></div>' + // End name field
|
||||
'<div class="row">' +
|
||||
// Type field
|
||||
'<div class="col-sm"><div class="form-group">' +
|
||||
`<label for="fermentables-${fermsLength}-type">Type</label>` +
|
||||
`<select class="form-control form-control-sm" id="fermentables-${fermsLength}-type"` +
|
||||
` name="fermentables-${fermsLength}-type" required>` +
|
||||
'<option value="Grain">Grain</option>' +
|
||||
'<option value="LME">LME</option>' +
|
||||
'<option value="DME">DME</option>' +
|
||||
'<option value="Sugar">Sugar</option>' +
|
||||
'<option value="Non-fermentable">Non-fermentable</option>' +
|
||||
'<option value="Other">Other</option></select>' +
|
||||
'</div></div>' + // End type field
|
||||
// Amount field
|
||||
'<div class="col-sm"><div class="form-group">' +
|
||||
`<label for="fermentables-${fermsLength}-amount">Amount (lb)</label>` +
|
||||
`<input class="form-control form-control-sm" id="fermentables-${fermsLength}-amount"` +
|
||||
` name="fermentables-${fermsLength}-amount" required type="text" value="">` +
|
||||
'</div></div>' + // End amount field
|
||||
// PPG field
|
||||
'<div class="col-sm"><div class="form-group">' +
|
||||
`<label for="fermentables-${fermsLength}-ppg">PPG</label>` +
|
||||
`<input class="form-control form-control-sm" id="fermentables-${fermsLength}-ppg"` +
|
||||
` name="fermentables-${fermsLength}-ppg" required type="text" value="">` +
|
||||
'</div></div>' + // End PPG field
|
||||
// Color field
|
||||
'<div class="col-sm"><div class="form-group">' +
|
||||
`<label for="fermentables-${fermsLength}-color">Color (°L)</label>` +
|
||||
`<input class="form-control form-control-sm" id="fermentables-${fermsLength}-color"` +
|
||||
` name="fermentables-${fermsLength}-color" required type="text" value="">` +
|
||||
'</div></div>' + // End PPG field
|
||||
'</div>' +
|
||||
// Remove button
|
||||
'<div class="row"><div class="col">' +
|
||||
'<button type="button" class="float-right btn btn-sm btn-outline-danger rem-ferm">' +
|
||||
'Remove fermentable</button>' +
|
||||
'</div></div>' + // End remove button
|
||||
'</div>';
|
||||
$fermsDiv.append(newFerm);
|
||||
$fermsDiv.data('length', fermsLength + 1);
|
||||
$('.rem-ferm').unbind('click');
|
||||
$('.rem-ferm').click(removeFerm);
|
||||
}
|
||||
|
||||
// Correct all the indexes for the removed form
|
||||
function adjustFermIndices(removedIndex) {
|
||||
var $forms = $('.ferm-form')
|
||||
console.log($forms)
|
||||
$forms.each(function(i) {
|
||||
var $form = $(this);
|
||||
var index = parseInt($form.data('index'));
|
||||
var newIndex = index - 1;
|
||||
console.log('index: ' + index)
|
||||
console.log('newIndex:' + newIndex);
|
||||
console.log('removedIndex: ' + removedIndex);
|
||||
if (index <= removedIndex) {
|
||||
// Skip this one
|
||||
console.log('index < removedIndex is true');
|
||||
return true;
|
||||
}
|
||||
console.log("I didn't quit");
|
||||
|
||||
// Change form index
|
||||
$form.data('index', newIndex);
|
||||
|
||||
// Change ID in form input, select, and labels
|
||||
$form.find('input').each(function(j) {
|
||||
var $item = $(this)
|
||||
$item.attr('id', $item.attr('id').replace(index, newIndex));
|
||||
$item.attr('name', $item.attr('name').replace(index, newIndex));
|
||||
});
|
||||
$form.find('select').each(function(j) {
|
||||
var $item = $(this)
|
||||
$item.attr('id', $item.attr('id').replace(index, newIndex));
|
||||
$item.attr('name', $item.attr('name').replace(index, newIndex));
|
||||
});
|
||||
//$form.find('label').each(function(j) {
|
||||
// var $item = $(this)
|
||||
// $item.attr('for', $item.attr('id').replace(index, newIndex));
|
||||
//});
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$('#add-ferm').click(addFerm);
|
||||
$('.rem-ferm').click(removeFerm);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,14 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from humulus.couch import get_db
|
||||
from humulus.recipes import FermentableForm, RecipeForm
|
||||
|
||||
|
||||
def test_create(client, app):
|
||||
"""Test success in creating a recipe document."""
|
||||
# Test GET
|
||||
response = client.get('/recipes/create')
|
||||
assert response.status_code == 200
|
||||
|
|
@ -40,6 +44,7 @@ def test_create(client, app):
|
|||
|
||||
|
||||
def test_info(client):
|
||||
"""Test success in retrieving a recipe document."""
|
||||
# Validate 404
|
||||
response = client.get('/recipes/info/thisdoesnotexist')
|
||||
assert response.status_code == 404
|
||||
|
|
@ -48,3 +53,40 @@ def test_info(client):
|
|||
response = client.get('/recipes/info/awesome-lager')
|
||||
assert response.status_code == 200
|
||||
assert b'Awesome Lager' in response.data
|
||||
|
||||
|
||||
def test_recipe_form_doc(app):
|
||||
"""Test if a recipeform can be turned into a document.
|
||||
|
||||
This test also tests that subforms can be turned into a document. Subforms
|
||||
are not tested individually since they will never be used individually.
|
||||
"""
|
||||
with app.app_context():
|
||||
recipe = RecipeForm()
|
||||
|
||||
ferm = FermentableForm()
|
||||
ferm.name.data = 'Test'
|
||||
ferm.type.data = 'Grain'
|
||||
ferm.amount.data = Decimal('5.5')
|
||||
ferm.ppg.data = Decimal('37')
|
||||
ferm.color.data = Decimal('1.8')
|
||||
|
||||
recipe.name.data = 'Test'
|
||||
recipe.efficiency.data = Decimal('65')
|
||||
recipe.volume.data = Decimal('5.5')
|
||||
recipe.notes.data = 'This is a test'
|
||||
recipe.fermentables = [ferm]
|
||||
|
||||
assert recipe.doc == {
|
||||
'name': 'Test',
|
||||
'efficiency': '65',
|
||||
'volume': '5.5',
|
||||
'notes': 'This is a test',
|
||||
'fermentables': [{
|
||||
'name': 'Test',
|
||||
'type': 'Grain',
|
||||
'amount': '5.5',
|
||||
'ppg': '37',
|
||||
'color': '1.8',
|
||||
}],
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue