1
0
Fork 0
mirror of https://github.com/shouptech/humulus.git synced 2026-02-03 21:09:41 +00:00

Compare commits

..

No commits in common. "c128d78f5f550e5d8cd642ec36dc1375a0033148" and "95e5a352951e95e7b18731d99736917d765ca776" have entirely different histories.

10 changed files with 60 additions and 258 deletions

View file

@ -70,8 +70,6 @@ def recipe_ibu(recipe):
bigness = 1.65 * 0.000125**(float(recipe_og(recipe)) - 1)
ibu = 0.0
for h in recipe['hops']:
if h['use'] != 'Boil' and h['use'] != 'FWH':
continue
mgl = (
float(h['alpha']) * float(h['amount']) * 7490.0 /
(float(recipe['volume']) * 100.0)

View file

@ -14,25 +14,6 @@
limitations under the License.
*/
// Calculate percentage and background color for display functions
function calculatePct(value, low, high) {
var pct = Math.round(100 * (value-low )/ (high - low));
var color = 'bg-success';
if (pct > 85) {
color = 'bg-danger';
if (pct > 100) {
pct = 100;
}
} else if (pct <=15 ) {
color = 'bg-warning';
if (pct <= 0) {
pct = 5; // We want a little bit of bar showing
}
}
return {pct: pct, color: color};
}
// unbinds and re-binds the change event
function rebindChangeEvents() {
$('.ingredient-field').unbind('change');
@ -109,7 +90,7 @@ function addFerm() {
// Type field
'<div class="col-sm"><div class="form-group">' +
`<label for="fermentables-${fermsLength}-type">Type</label>` +
`<select class="custom-select custom-select-sm ingredient-field" id="fermentables-${fermsLength}-type"` +
`<select class="form-control form-control-sm ingredient-field" id="fermentables-${fermsLength}-type"` +
` name="fermentables-${fermsLength}-type" required>` +
'<option value="Grain">Grain</option>' +
'<option value="LME">LME</option>' +
@ -171,7 +152,7 @@ function addHop() {
// Usage field
'<div class="col-sm"><div class="form-group">' +
`<label for="hops-${hopsLength}-use">Usage</label>` +
`<select class="custom-select custom-select-sm ingredient-field" id="hops-${hopsLength}-use"` +
`<select class="form-control form-control-sm ingredient-field" id="hops-${hopsLength}-use"` +
` name="hops-${hopsLength}-use" required>` +
'<option value="Boil">Boil</option>' +
'<option value="FWH">FWH</option>' +
@ -239,16 +220,8 @@ function calculateOG() {
}
// display OG
function displayOG(data) {
var og = calculateOG().toFixed(3);
if (data !== null) {
var result = calculatePct(og, data.og.low, data.og.high)
$('#estimated-og').html(`<div class="progress spec-progress"><div class="progress-bar ${result.color}" ` +
`role="progressbar" style="width: ${result.pct}%;" aria-valuenow="${og}" aria-valuemin="${data.og.low}" ` +
`aria-valuemax="${data.og.high}">${og}</div></div>`);
} else {
$('#estimated-og').text(og);
}
function displayOG() {
$('#estimated-og').text(calculateOG().toFixed(3));
}
// Calculate final gravity
@ -276,16 +249,8 @@ function calculateFG() {
}
// Display FG
function displayFG(data) {
var fg = calculateFG().toFixed(3);
if (data !== null) {
var result = calculatePct(fg, data.fg.low, data.fg.high)
$('#estimated-fg').html(`<div class="progress spec-progress"><div class="progress-bar ${result.color}" ` +
`role="progressbar" style="width: ${result.pct}%;" aria-valuenow="${fg}" aria-valuemin="${data.fg.low}" ` +
`aria-valuemax="${data.fg.high}">${fg}</div></div>`);
} else {
$('#estimated-fg').text(fg);
}
function displayFG() {
$('#estimated-fg').text(calculateFG().toFixed(3));
}
// Calculate ABV
@ -294,16 +259,9 @@ function calculate_abv() {
}
// Display ABV
function displayABV(data) {
function displayABV() {
var abv = calculate_abv().toFixed(1);
if (data !== null) {
var result = calculatePct(abv, data.abv.low, data.abv.high)
$('#estimated-abv').html(`<div class="progress spec-progress"><div class="progress-bar ${result.color}" ` +
`role="progressbar" style="width: ${result.pct}%;" aria-valuenow="${abv}" aria-valuemin="${data.abv.low}" ` +
`aria-valuemax="${data.abv.high}">${abv} %/vol.</div></div>`);
} else {
$('#estimated-abv').text(`${abv} %/vol.`);
}
}
// Calculate IBU
@ -332,17 +290,9 @@ function calculateIBU() {
}
// Display IBU
function displayIBU(data) {
function displayIBU() {
var ibu = calculateIBU().toFixed();
if (data !== null) {
var result = calculatePct(ibu, data.ibu.low, data.ibu.high)
$('#estimated-ibu').html(`<div class="progress spec-progress"><div class="progress-bar ${result.color}" ` +
`role="progressbar" style="width: ${result.pct}%;" aria-valuenow="${ibu}" aria-valuemin="${data.ibu.low}" ` +
`aria-valuemax="${data.ibu.high}">${ibu} IBU</div></div>`);
} else {
$('#estimated-ibu').text(`${ibu} IBU`);
}
}
// Calculate IBU Ratio
@ -373,39 +323,19 @@ function calculateSRM() {
}
// Display SRM
function displaySRM(data) {
function displaySRM() {
var srm = calculateSRM().toFixed();
if (data !== null) {
var result = calculatePct(srm, data.srm.low, data.srm.high)
$('#estimated-srm').html(`<div class="progress spec-progress"><div class="progress-bar ${result.color}" ` +
`role="progressbar" style="width: ${result.pct}%;" aria-valuenow="${srm}" aria-valuemin="${data.srm.low}" ` +
`aria-valuemax="${data.srm.high}">${srm} SRM</div></div>`);
} else {
$('#estimated-srm').text(`${srm} SRM`);
}
}
// Display all specifications
function displayAll() {
$.getJSON(getSpecsURL()).done(
function (json) {
displayOG(json);
displayFG(json);
displayABV(json);
displayIBU(json);
displayOG();
displayFG();
displayABV();
displayIBU();
displayIBURatio();
displaySRM(json);
}
).fail(
function () {
displayOG(null);
displayFG(null);
displayABV(null);
displayIBU(null);
displayIBURatio();
displaySRM(null);
}
);
displaySRM();
}
$(document).ready(function() {

View file

@ -35,8 +35,3 @@ body {
.above-footer {
padding-bottom: 14rem;
}
.spec-progress {
height: 1.5rem;
font-size: 1.125rem;
}

View file

@ -19,8 +19,7 @@ import xml.etree.ElementTree as ET
import click
import requests
from flask import (Blueprint, abort, current_app, render_template, request,
jsonify)
from flask import Blueprint, abort, current_app, render_template, request
from flask.cli import with_appcontext
from humulus.auth import login_required
@ -169,31 +168,6 @@ def info(id):
return render_template('styles/info.html', style=get_doc_or_404(id))
@bp.route('/info/<id>/json')
@login_required
def info_json(id):
"""Returns JSON for the style.
If the 'specs' argument is present (regardless of value), only return
specs.
"""
style = get_doc_or_404(id)
# Remove fields not needed for specs
if request.args.get('specs', None) is not None:
return jsonify({
'ibu': style['ibu'],
'og': style['og'],
'fg': style['fg'],
'abv': style['abv'],
'srm': style['srm']
})
# Remove fields not needed for export
style.pop('_id')
style.pop('_rev')
style.pop('$type')
return jsonify(style)
@bp.route('/info/<id>/recipes')
def recipes(id):
style = get_doc_or_404(id)

View file

@ -57,7 +57,7 @@
</ul>
</div>
</nav>
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha384-tsQFqpEReu7ZLhBV2VZlAu7zcOV+rXbYlF2cqB8txI/8aZajjp4Bqd+V6D5IgvKT" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="{{ url_for('static', filename='moment.min.js')}}"></script>
<main role="main" class="container">
{% block alert %}

View file

@ -68,7 +68,7 @@
</div>
</div>
</div>
</div>
</div>
{% endmacro %}
{#
@ -77,30 +77,3 @@
{% macro moment(timestamp, format="llll") %}
<script>document.write(moment.utc("{{ timestamp }}").local().format("llll"));</script>
{% endmacro %}
{#
Render a bootstrap progressbar
#}
{% macro render_progressbar(value, min, max, unit='', class='') %}
{#
Determine percentage. Coerce to between 0 and 100.
Set an appropriate background.
#}
{% set pct = (100*(value-min))/(max-min) %}
{% if pct > 85 %}
{% set color = 'bg-danger' %}
{% if pct > 100 %}
{% set pct = 100 %}
{% endif %}
{% elif pct > 15 %}
{% set color = 'bg-success' %}
{% else %}
{% set color = 'bg-warning' %}
{% if pct <= 0 %}
{% set pct = 5 %} {# we want a little bit of the bar showing. #}
{% endif %}
{% endif %}
<div class="progress {{ class }}">
<div class="progress-bar {{ color }}" role="progressbar" style="width: {{ pct|int }}%;" aria-valuenow="{{ value }}" aria-valuemin="{{ min }}" aria-valuemax="{{ max }}">{{ value }}{{ unit }}</div>
</div>
{% endmacro %}

View file

@ -15,19 +15,6 @@
-#}
{% from "_macros.html" import render_field_with_errors %}
{#
Renders a javascript function to return a style's URL.
#}
{% macro get_specs_url() %}
{% set id = 'ID' %}
{% set specs = 'y' %}
<script type="text/javascript">
function getSpecsURL() {
return '{{ url_for("styles.info_json", id=id, specs=specs) }}'.replace('ID', $('#style').val());
}
</script>
{% endmacro %}
{#
Used to render a form for creating/updating a recipe
#}
@ -43,8 +30,8 @@ function getSpecsURL() {
<div class="col-sm-3">{{ render_field_with_errors(form.volume, 'ingredient-field') }}</div>
</div>
<div class="row">
<div class="col-sm">{{ render_field_with_errors(form.style, 'ingredient-field', base_class='custom-select') }}</div>
<div class="col-sm">{{ render_field_with_errors(form.type, base_class='custom-select') }}</div>
<div class="col-sm">{{ render_field_with_errors(form.style) }}</div>
<div class="col-sm">{{ render_field_with_errors(form.type) }}</div>
</div>
{#-
Fermentable Ingredients
@ -59,7 +46,7 @@ function getSpecsURL() {
</div>
</div>
<div class="row">
<div class="col-sm">{{ render_field_with_errors(fermentable.form.type, 'ingredient-field custom-select-sm', base_class='custom-select') }}</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 ingredient-field') }}</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 ingredient-field') }}</div>
@ -90,7 +77,7 @@ function getSpecsURL() {
</div>
</div>
<div class="row">
<div class="col-sm">{{ render_field_with_errors(hop.form.use, 'ingredient-field custom-select-sm', base_class='custom-select') }}</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 ingredient-field') }}</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 ingredient-field') }}</div>
@ -116,14 +103,14 @@ function getSpecsURL() {
<div class="border pl-2 pr-2 pt-1 pb-1 yeast-form">
<div class="row">
<div class="col-sm-6">{{ render_field_with_errors(form.yeast.form.name, 'form-control-sm') }}</div>
<div class="col-sm-2">{{ render_field_with_errors(form.yeast.form.type, 'custom-select-sm', base_class='custom-select') }}</div>
<div class="col-sm-2">{{ render_field_with_errors(form.yeast.form.type, 'form-control-sm') }}</div>
<div class="col-sm-2">{{ render_field_with_errors(form.yeast.form.lab, 'form-control-sm') }}</div>
<div class="col-sm-2">{{ render_field_with_errors(form.yeast.form.code, 'form-control-sm') }}</div>
</div>
<div class="row">
<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 ingredient-field') }}</div>
<div class="col-sm">{{ render_field_with_errors(form.yeast.form.flocculation, 'custom-select-sm', base_class='custom-select') }}</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.max_temperature, 'form-control-sm') }}</div>
<div class="col-sm">{{ render_field_with_errors(form.yeast.form.abv_tolerance, 'form-control-sm') }}</div>
@ -143,7 +130,6 @@ function getSpecsURL() {
<div class="col"><button type="submit" class="btn btn-primary">{{ button_content }}</button></div>
</div>
</form>
{{ get_specs_url() }}
<script src="{{ url_for('static', filename='recipes.js') }}"></script>
{% endmacro %}

View file

@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-#}
{% import '_macros.html' as macros %}
{% from "_macros.html" import render_field_with_errors, render_delete_button, render_delete_modal, moment %}
{% extends '_base.html' %}
{% block title %}{{ recipe.name }}{% endblock %}
@ -30,7 +30,7 @@
-#}
<div class="row border-top"><h2>Details</h2></div>
<div class="row">
<div class="col-sm">
<div class="col">
<dl>
<dt>Recipe Type</dt>
<dd>{{ recipe.type }}</dd>
@ -40,73 +40,39 @@
<dd>{{ recipe.volume|float|round(1) }} gal.</dd>
</dl>
</div>
<div class="col-sm">
<div class="col">
<dl>
{% if 'created' in recipe %}
<dt>Created</dt>
<dd>{{ macros.moment(recipe.created) }}</dd>
<dd>{{ moment(recipe.created) }}</dd>
{% endif %}
{% if 'updated' in recipe %}
<dt>Updated</dt>
<dd>{{ macros.moment(recipe.updated) }}</dd>
<dd>{{ moment(recipe.updated) }}</dd>
{% endif %}
</dl>
</div>
</div>
<div class="row"><h3>Estimated Specifications</h3></div>
<div class="row">
<div class="col-sm">
<div class="col">
<dl>
<dt>Original Gravity</dt>
<dd>
{% if style %}
{{ macros.render_progressbar(recipe|recipe_og|float, style.og.low|float, style.og.high|float, class='spec-progress') }}
{% else %}
{{ recipe|recipe_og }}
{% endif %}
</dd>
<dd>{{ recipe|recipe_og }}</dd>
<dt>Final Gravity</dt>
<dd>
{% if style %}
{{ macros.render_progressbar(recipe|recipe_fg|float, style.fg.low|float, style.fg.high|float, class='spec-progress') }}
{% else %}
{{ recipe|recipe_fg }}
{% endif %}
</dd>
</dl>
</div>
<div class="col-sm">
<dl>
<dd>{{ recipe|recipe_fg }}</dd>
<dt>Alcohol</dt>
<dd>
{% if style %}
{{ macros.render_progressbar(recipe|recipe_abv|float, style.abv.low|float, style.abv.high|float, unit=' %/vol', class='spec-progress') }}
{% else %}
{{ recipe|recipe_abv }} %/vol.
{% endif %}
</dd>
<dt>Color</dt>
<dd>
{% if style %}
{{ macros.render_progressbar(recipe|recipe_srm|float, style.srm.low|float, style.srm.high|float, unit=' SRM', class='spec-progress') }}
{% else %}
{{ recipe|recipe_srm }} SRM
{% endif %}
</dd>
<dd>{{ recipe|recipe_abv }} %/vol.</dd>
</dl>
</div>
<div class="col-sm">
<div class="col">
<dl>
<dt>Bitterness</dt>
<dd>
{% if style %}
{{ macros.render_progressbar(recipe|recipe_ibu|float, style.ibu.low|float, style.ibu.high|float, unit=' IBU', class='spec-progress') }}
{% else %}
{{ recipe|recipe_ibu }} IBU
{% endif %}
</dd>
<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>
@ -209,9 +175,9 @@
{% if session.logged_in %}
<div class="row mt-4 pt-1 border-top">
<a class="btn btn-secondary mr-1" href="{{ url_for('recipes.update', id=recipe._id) }}">Update Recipe</a>
{{ macros.render_delete_button('Delete Recipe', 'deleteRecipe', 'btn-danger') }}
{{ render_delete_button('Delete Recipe', 'deleteRecipe', 'btn-danger') }}
</div>
{{ macros.render_delete_modal(url_for('recipes.delete', id=recipe._id), 'deleteRecipe', recipe.name) }}
{{ render_delete_modal(url_for('recipes.delete', id=recipe._id), 'deleteRecipe', recipe.name) }}
{% endif %}
<div class="row mt-4"><a href="{{ url_for('recipes.info_json', id=recipe._id) }}">Export JSON</a></div>
<div class="row">Recipe revision: {{ recipe._rev }}</div>

View file

@ -36,7 +36,7 @@ def test_recipe_fg(sample_recipes):
def test_recipe_ibu(sample_recipes):
assert recipe_ibu(sample_recipes['lager']) == '24'
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')
@ -44,7 +44,7 @@ def test_recipe_ibu(sample_recipes):
def test_recipe_ibu_ratio(sample_recipes):
assert recipe_ibu_ratio(sample_recipes['lager']) == '0.44'
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')

View file

@ -226,23 +226,3 @@ def test_recipes(client):
response = client.get('/styles/info/1A/recipes')
assert response.status_code == 200
assert b'Awesome Beer' in response.data
def test_info_json(auth, client):
"""Test success in retrieving a style's json document."""
# Test not logged in
response = client.get('/styles/info/1A/json')
assert response.status_code == 302
# Login and test
auth.login()
response = client.get('/styles/info/1A/json')
assert response.status_code == 200
assert response.is_json
assert response.get_json()['name'] == 'Test Style'
# Test for specs only
response = client.get('/styles/info/1A/json?specs=y')
assert response.status_code == 200
assert response.is_json
assert 'name' not in response.get_json()