mirror of
https://github.com/shouptech/humulus.git
synced 2026-02-03 14:59:43 +00:00
parent
4290121ac9
commit
1057e86177
13 changed files with 67 additions and 53 deletions
|
|
@ -28,6 +28,12 @@ steps:
|
||||||
- pip install codecov
|
- pip install codecov
|
||||||
- codecov
|
- codecov
|
||||||
|
|
||||||
|
- name: linting
|
||||||
|
image: python:3.6
|
||||||
|
commands:
|
||||||
|
- pip install flake8
|
||||||
|
- flake8
|
||||||
|
|
||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
name: publish
|
name: publish
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,5 @@
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from humulus.app import create_app
|
from humulus.app import create_app
|
||||||
|
|
||||||
|
__all__ = ['create_app', ]
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
# 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.
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from flask import Flask, render_template
|
from flask import Flask, render_template
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ from flask import abort, current_app, g
|
||||||
from flask.cli import with_appcontext
|
from flask.cli import with_appcontext
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
|
|
||||||
|
|
||||||
def get_couch():
|
def get_couch():
|
||||||
"""Connect to the configured CouchDB."""
|
"""Connect to the configured CouchDB."""
|
||||||
if 'couch' not in g:
|
if 'couch' not in g:
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ def recipe_fg(recipe):
|
||||||
) / 200
|
) / 200
|
||||||
)
|
)
|
||||||
return '{:.3f}'.format(
|
return '{:.3f}'.format(
|
||||||
round(1 + (og - 1 - og_delta)*(1 - attenuation) + og_delta, 3)
|
round(1 + (og - 1 - og_delta) * (1 - attenuation) + og_delta, 3)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -123,9 +123,11 @@ def sort_hops(hops, form=False):
|
||||||
by_use[hop['use']].append(hop)
|
by_use[hop['use']].append(hop)
|
||||||
|
|
||||||
if form:
|
if form:
|
||||||
key = lambda hop: float(hop.duration.data)
|
def key(hop):
|
||||||
|
return float(hop.duration.data)
|
||||||
else:
|
else:
|
||||||
key = lambda hop: float(hop['duration'])
|
def key(hop):
|
||||||
|
return float(hop['duration'])
|
||||||
|
|
||||||
hops_sorted = sorted(by_use['FWH'], key=key, reverse=True)
|
hops_sorted = sorted(by_use['FWH'], key=key, reverse=True)
|
||||||
hops_sorted.extend(sorted(by_use['Boil'], key=key, reverse=True))
|
hops_sorted.extend(sorted(by_use['Boil'], key=key, reverse=True))
|
||||||
|
|
@ -145,7 +147,7 @@ def ferm_pct(fermentables):
|
||||||
total += float(ferm['amount'])
|
total += float(ferm['amount'])
|
||||||
# Add a pct to each ferm
|
# Add a pct to each ferm
|
||||||
for ferm in fermentables:
|
for ferm in fermentables:
|
||||||
ferm['pct'] = 100*float(ferm['amount'])/total
|
ferm['pct'] = 100 * float(ferm['amount']) / total
|
||||||
return fermentables
|
return fermentables
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,8 @@ bp = Blueprint('recipes', __name__, url_prefix='/recipes')
|
||||||
class FermentableForm(Form):
|
class FermentableForm(Form):
|
||||||
"""Form for fermentables.
|
"""Form for fermentables.
|
||||||
|
|
||||||
CSRF is disabled for this form.yeast.form (using `Form as parent class) because it
|
CSRF is disabled for this form.yeast.form (using `Form as parent class)
|
||||||
is never used by itself.
|
because it is never used by itself.
|
||||||
"""
|
"""
|
||||||
name = StringField('Name', validators=[DataRequired()])
|
name = StringField('Name', validators=[DataRequired()])
|
||||||
type = SelectField('Type', validators=[DataRequired()],
|
type = SelectField('Type', validators=[DataRequired()],
|
||||||
|
|
@ -67,8 +67,8 @@ class FermentableForm(Form):
|
||||||
class HopForm(Form):
|
class HopForm(Form):
|
||||||
"""Form for hops.
|
"""Form for hops.
|
||||||
|
|
||||||
CSRF is disabled for this form.yeast.form (using `Form as parent class) because it
|
CSRF is disabled for this form.yeast.form (using `Form as parent class)
|
||||||
is never used by itself.
|
because it is never used by itself.
|
||||||
"""
|
"""
|
||||||
name = StringField('Name', validators=[DataRequired()])
|
name = StringField('Name', validators=[DataRequired()])
|
||||||
use = SelectField('Usage', validators=[DataRequired()],
|
use = SelectField('Usage', validators=[DataRequired()],
|
||||||
|
|
@ -96,8 +96,8 @@ class HopForm(Form):
|
||||||
class YeastForm(Form):
|
class YeastForm(Form):
|
||||||
"""Form for yeast.
|
"""Form for yeast.
|
||||||
|
|
||||||
CSRF is disabled for this form.yeast.form (using `Form as parent class) because it
|
CSRF is disabled for this form.yeast.form (using `Form as parent class)
|
||||||
is never used by itself.
|
because it is never used by itself.
|
||||||
"""
|
"""
|
||||||
name = StringField('Name', validators=[Optional()])
|
name = StringField('Name', validators=[Optional()])
|
||||||
type = SelectField('Type', default='',
|
type = SelectField('Type', default='',
|
||||||
|
|
@ -192,10 +192,10 @@ class RecipeForm(FlaskForm):
|
||||||
recipe['fermentables'] = [f.doc for f in self.fermentables]
|
recipe['fermentables'] = [f.doc for f in self.fermentables]
|
||||||
recipe['hops'] = [h.doc for h in self.hops]
|
recipe['hops'] = [h.doc for h in self.hops]
|
||||||
if (
|
if (
|
||||||
self.yeast.doc['name'] and
|
self.yeast.doc['name'] and
|
||||||
self.yeast.doc['low_attenuation'] != "None" and
|
self.yeast.doc['low_attenuation'] != "None" and
|
||||||
self.yeast.doc['high_attenuation'] != "None"
|
self.yeast.doc['high_attenuation'] != "None"
|
||||||
):
|
):
|
||||||
recipe['yeast'] = self.yeast.doc
|
recipe['yeast'] = self.yeast.doc
|
||||||
return recipe
|
return recipe
|
||||||
|
|
||||||
|
|
@ -360,7 +360,7 @@ def update(id):
|
||||||
'Your changes have been lost.'.format(recipe['name'])
|
'Your changes have been lost.'.format(recipe['name'])
|
||||||
),
|
),
|
||||||
'danger'
|
'danger'
|
||||||
)
|
)
|
||||||
return redirect(url_for('recipes.info', id=id))
|
return redirect(url_for('recipes.info', id=id))
|
||||||
# Copy values from submitted form to the existing recipe and save
|
# Copy values from submitted form to the existing recipe and save
|
||||||
for key, value in form.doc.items():
|
for key, value in form.doc.items():
|
||||||
|
|
|
||||||
|
|
@ -65,23 +65,26 @@ def sub_to_doc(sub):
|
||||||
doc['ibu']['low'] = (sub.find('./stats/ibu/low').text
|
doc['ibu']['low'] = (sub.find('./stats/ibu/low').text
|
||||||
if sub.find('./stats/ibu/low') is not None else '0')
|
if sub.find('./stats/ibu/low') is not None else '0')
|
||||||
doc['ibu']['high'] = (sub.find('./stats/ibu/high').text
|
doc['ibu']['high'] = (sub.find('./stats/ibu/high').text
|
||||||
if sub.find('./stats/ibu/high') is not None else '100')
|
if sub.find('./stats/ibu/high') is not None
|
||||||
|
else '100')
|
||||||
doc['og']['low'] = (sub.find('./stats/og/low').text
|
doc['og']['low'] = (sub.find('./stats/og/low').text
|
||||||
if sub.find('./stats/og/low') is not None else '1.0')
|
if sub.find('./stats/og/low') is not None else '1.0')
|
||||||
doc['og']['high'] = (sub.find('./stats/og/high').text
|
doc['og']['high'] = (sub.find('./stats/og/high').text
|
||||||
if sub.find('./stats/og/high') is not None else '1.2')
|
if sub.find('./stats/og/high') is not None else '1.2')
|
||||||
doc['fg']['low'] = (sub.find('./stats/fg/low').text
|
doc['fg']['low'] = (sub.find('./stats/fg/low').text
|
||||||
if sub.find('./stats/fg/low') is not None else '1.0')
|
if sub.find('./stats/fg/low') is not None else '1.0')
|
||||||
doc['fg']['high'] = (sub.find('./stats/fg/high').text
|
doc['fg']['high'] = (sub.find('./stats/fg/high').text
|
||||||
if sub.find('./stats/fg/high') is not None else '1.2')
|
if sub.find('./stats/fg/high') is not None else '1.2')
|
||||||
doc['srm']['low'] = (sub.find('./stats/srm/low').text
|
doc['srm']['low'] = (sub.find('./stats/srm/low').text
|
||||||
if sub.find('./stats/srm/low') is not None else '0')
|
if sub.find('./stats/srm/low') is not None else '0')
|
||||||
doc['srm']['high'] = (sub.find('./stats/srm/high').text
|
doc['srm']['high'] = (sub.find('./stats/srm/high').text
|
||||||
if sub.find('./stats/srm/high') is not None else '100')
|
if sub.find('./stats/srm/high') is not None
|
||||||
|
else '100')
|
||||||
doc['abv']['low'] = (sub.find('./stats/abv/low').text
|
doc['abv']['low'] = (sub.find('./stats/abv/low').text
|
||||||
if sub.find('./stats/abv/low') is not None else '0')
|
if sub.find('./stats/abv/low') is not None else '0')
|
||||||
doc['abv']['high'] = (sub.find('./stats/abv/high').text
|
doc['abv']['high'] = (sub.find('./stats/abv/high').text
|
||||||
if sub.find('./stats/abv/high') is not None else '100')
|
if sub.find('./stats/abv/high') is not None
|
||||||
|
else '100')
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -121,10 +124,10 @@ def get_styles_list():
|
||||||
def import_command():
|
def import_command():
|
||||||
"""CLI command to import BJCP styles."""
|
"""CLI command to import BJCP styles."""
|
||||||
url = current_app.config.get(
|
url = current_app.config.get(
|
||||||
'BJCP_STYLES_URL',
|
'BJCP_STYLES_URL',
|
||||||
('https://raw.githubusercontent.com/meanphil'
|
('https://raw.githubusercontent.com/meanphil'
|
||||||
'/bjcp-guidelines-2015/master/styleguide.xml')
|
'/bjcp-guidelines-2015/master/styleguide.xml')
|
||||||
)
|
)
|
||||||
import_styles(url)
|
import_styles(url)
|
||||||
click.echo("Imported BJCP styles.")
|
click.echo("Imported BJCP styles.")
|
||||||
|
|
||||||
|
|
@ -153,11 +156,11 @@ def index():
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'styles/index.html',
|
'styles/index.html',
|
||||||
rows=rows[(page-1)*limit:page*limit],
|
rows=rows[(page - 1) * limit:page * limit],
|
||||||
descending=descending,
|
descending=descending,
|
||||||
sort_by=sort_by,
|
sort_by=sort_by,
|
||||||
page=page,
|
page=page,
|
||||||
num_pages=math.ceil(len(rows)/limit),
|
num_pages=math.ceil(len(rows) / limit),
|
||||||
limit=limit
|
limit=limit
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import pytest
|
||||||
from humulus import create_app
|
from humulus import create_app
|
||||||
from humulus.couch import build_couch, get_couch, put_doc
|
from humulus.couch import build_couch, get_couch, put_doc
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def app():
|
def app():
|
||||||
dbname = 'test_{}'.format(str(uuid.uuid4()))
|
dbname = 'test_{}'.format(str(uuid.uuid4()))
|
||||||
|
|
@ -126,7 +127,8 @@ def app():
|
||||||
})
|
})
|
||||||
|
|
||||||
# Add a test style
|
# Add a test style
|
||||||
put_doc({'$type': 'style',
|
put_doc({
|
||||||
|
'$type': 'style',
|
||||||
'_id': '1A',
|
'_id': '1A',
|
||||||
'abv': {'high': '100', 'low': '0'},
|
'abv': {'high': '100', 'low': '0'},
|
||||||
'appearance': 'Good looking',
|
'appearance': 'Good looking',
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,6 @@
|
||||||
# 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 flask import session
|
|
||||||
|
|
||||||
|
|
||||||
def test_login(client, auth):
|
def test_login(client, auth):
|
||||||
# Test GET
|
# Test GET
|
||||||
|
|
@ -43,6 +41,7 @@ def test_login(client, auth):
|
||||||
assert session['logged_in']
|
assert session['logged_in']
|
||||||
assert session.permanent
|
assert session.permanent
|
||||||
|
|
||||||
|
|
||||||
def test_logout(client, auth):
|
def test_logout(client, auth):
|
||||||
# Login
|
# Login
|
||||||
auth.login()
|
auth.login()
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,9 @@
|
||||||
# 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.
|
||||||
|
|
||||||
import uuid
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from humulus.couch import *
|
from humulus.couch import put_doc, get_doc, update_doc, put_designs, get_view
|
||||||
|
|
||||||
|
|
||||||
def test_put_doc(app):
|
def test_put_doc(app):
|
||||||
|
|
@ -73,7 +72,7 @@ def test_put_designs(app, monkeypatch):
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
# Test initial load of designs
|
# Test initial load of designs
|
||||||
monkeypatch.setattr(Path, 'parent', testpath/'assets/initial')
|
monkeypatch.setattr(Path, 'parent', testpath / 'assets/initial')
|
||||||
put_designs()
|
put_designs()
|
||||||
|
|
||||||
recipes = get_doc('_design/recipes')
|
recipes = get_doc('_design/recipes')
|
||||||
|
|
@ -86,7 +85,7 @@ def test_put_designs(app, monkeypatch):
|
||||||
assert recipes['_rev'] == rev
|
assert recipes['_rev'] == rev
|
||||||
|
|
||||||
# Test that changes can be loaded
|
# Test that changes can be loaded
|
||||||
monkeypatch.setattr(Path, 'parent', testpath/'assets/changed')
|
monkeypatch.setattr(Path, 'parent', testpath / 'assets/changed')
|
||||||
put_designs()
|
put_designs()
|
||||||
recipes = get_doc('_design/recipes')
|
recipes = get_doc('_design/recipes')
|
||||||
assert 'by-date' in recipes['views']
|
assert 'by-date' in recipes['views']
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from humulus.filters import *
|
from humulus.filters import (recipe_abv, recipe_fg, recipe_ibu, sort_hops,
|
||||||
|
recipe_ibu_ratio, recipe_og, recipe_srm, ferm_pct)
|
||||||
from humulus.recipes import HopForm
|
from humulus.recipes import HopForm
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import json
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from humulus.couch import get_db, get_doc, put_doc
|
from humulus.couch import get_doc
|
||||||
from humulus.recipes import FermentableForm, HopForm, RecipeForm, YeastForm
|
from humulus.recipes import FermentableForm, HopForm, RecipeForm, YeastForm
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -41,7 +41,6 @@ def test_index(client):
|
||||||
response.data
|
response.data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Test sort by name descending
|
# Test sort by name descending
|
||||||
response = client.get('/recipes/?descending=true&sort_by=name')
|
response = client.get('/recipes/?descending=true&sort_by=name')
|
||||||
assert (
|
assert (
|
||||||
|
|
@ -119,6 +118,7 @@ def test_index(client):
|
||||||
response.data
|
response.data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_create(client, app, auth):
|
def test_create(client, app, auth):
|
||||||
"""Test success in creating a recipe document."""
|
"""Test success in creating a recipe document."""
|
||||||
# Test GET without login
|
# Test GET without login
|
||||||
|
|
@ -203,7 +203,7 @@ def test_update(client, app, auth):
|
||||||
query_string={'rev': ''}, data=doc)
|
query_string={'rev': ''}, data=doc)
|
||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
with client.session_transaction() as session:
|
with client.session_transaction() as session:
|
||||||
flash_message = dict(session['_flashes']).pop('danger', None)
|
flash_message = dict(session['_flashes']).pop('danger', None)
|
||||||
assert 'Update conflict' in flash_message
|
assert 'Update conflict' in flash_message
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -145,20 +145,21 @@ def test_import_styles(monkeypatch):
|
||||||
monkeypatch.setattr('humulus.styles.put_doc', fake_put_doc)
|
monkeypatch.setattr('humulus.styles.put_doc', fake_put_doc)
|
||||||
|
|
||||||
import_styles(None)
|
import_styles(None)
|
||||||
assert PutRecorder.doc == {'$type': 'style',
|
assert PutRecorder.doc == {
|
||||||
'_id': '1A',
|
'$type': 'style',
|
||||||
'abv': {'high': '100', 'low': '0'},
|
'_id': '1A',
|
||||||
'appearance': 'Good looking',
|
'abv': {'high': '100', 'low': '0'},
|
||||||
'aroma': 'Smelly',
|
'appearance': 'Good looking',
|
||||||
'fg': {'high': '1.2', 'low': '1.0'},
|
'aroma': 'Smelly',
|
||||||
'flavor': 'Good tasting',
|
'fg': {'high': '1.2', 'low': '1.0'},
|
||||||
'ibu': {'high': '100', 'low': '0'},
|
'flavor': 'Good tasting',
|
||||||
'impression': 'Refreshing',
|
'ibu': {'high': '100', 'low': '0'},
|
||||||
'mouthfeel': 'Good feeling',
|
'impression': 'Refreshing',
|
||||||
'name': 'Test Style',
|
'mouthfeel': 'Good feeling',
|
||||||
'og': {'high': '1.2', 'low': '1.0'},
|
'name': 'Test Style',
|
||||||
'srm': {'high': '100', 'low': '0'}
|
'og': {'high': '1.2', 'low': '1.0'},
|
||||||
}
|
'srm': {'high': '100', 'low': '0'}
|
||||||
|
}
|
||||||
|
|
||||||
MockDB.db = {'1A': ''}
|
MockDB.db = {'1A': ''}
|
||||||
PutRecorder.doc = None
|
PutRecorder.doc = None
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue