1
0
Fork 0
mirror of https://github.com/shouptech/humulus.git synced 2026-02-03 14:59:43 +00:00

Add pre-commit hooks, and add black to linting (#40)

Closes #39
This commit is contained in:
Emma 2019-07-22 13:27:16 -06:00 committed by GitHub
parent 7cccb8fe84
commit 9b1a739347
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1317 additions and 1149 deletions

View file

@ -17,8 +17,7 @@ steps:
from_secret: CODECOV_TOKEN from_secret: CODECOV_TOKEN
commands: commands:
# Install pre-requisites # Install pre-requisites
- pip install coverage pytest - pip install -r requirements-dev.txt
- pip install -e .
# Wait for couch # Wait for couch
- until curl "$COUCH_URL" ; do sleep 1 ; done - until curl "$COUCH_URL" ; do sleep 1 ; done
# Run tests # Run tests
@ -27,11 +26,8 @@ steps:
# Upload coverage report # Upload coverage report
- pip install codecov - pip install codecov
- codecov - codecov
# Perform linting checks
- name: linting - black --check src tests
image: python:3.6
commands:
- pip install flake8
- flake8 - flake8
--- ---

10
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,10 @@
repos:
- repo: https://github.com/ambv/black
rev: stable
hooks:
- id: black
language_version: python3.6
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v1.2.3
hooks:
- id: flake8

3
pyproject.toml Normal file
View file

@ -0,0 +1,3 @@
[tool.black]
line-length = 79
target_version = ['py35','py36','py37']

6
requirements-dev.txt Normal file
View file

@ -0,0 +1,6 @@
pytest
coverage
pre-commit
black
flake8
-e .

View file

@ -11,5 +11,8 @@ source = humulus
[flake8] [flake8]
exclude = .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.eggs,*.egg,instance exclude = .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.eggs,*.egg,instance
show-source = True show-source = True
ignore = W504
count = True count = True
max-line-length = 79
max-complexity = 18
ignore = W503
select = B,C,E,F,W,T4,B9

View file

@ -14,4 +14,4 @@
from humulus.app import create_app from humulus.app import create_app
__all__ = ['create_app', ] __all__ = ["create_app"]

View file

@ -26,31 +26,37 @@ def create_app(test_config=None):
app.config.from_mapping(test_config) app.config.from_mapping(test_config)
else: else:
# Load config from configuration provided via ENV # Load config from configuration provided via ENV
app.config.from_envvar('HUMULUS_SETTINGS') app.config.from_envvar("HUMULUS_SETTINGS")
from . import couch from . import couch
couch.init_app(app) couch.init_app(app)
# Register blueprint for index page # Register blueprint for index page
from . import home from . import home
app.register_blueprint(home.bp) app.register_blueprint(home.bp)
app.add_url_rule('/', endpoint='index') app.add_url_rule("/", endpoint="index")
# Register blueprint for recipes # Register blueprint for recipes
from . import recipes from . import recipes
app.register_blueprint(recipes.bp) app.register_blueprint(recipes.bp)
# Register auth blueprint # Register auth blueprint
from . import auth from . import auth
app.register_blueprint(auth.bp) app.register_blueprint(auth.bp)
# Register styles blueprint and cli commands # Register styles blueprint and cli commands
from . import styles from . import styles
styles.init_app(app) styles.init_app(app)
app.register_blueprint(styles.bp) app.register_blueprint(styles.bp)
# Register custom filters # Register custom filters
from . import filters from . import filters
filters.create_filters(app) filters.create_filters(app)
# Register custom error handlers # Register custom error handlers
@ -62,13 +68,13 @@ def create_app(test_config=None):
def bad_request(e): def bad_request(e):
return ( return (
render_template('_error.html', code=400, message='400 Bad Request'), render_template("_error.html", code=400, message="400 Bad Request"),
400 400,
) )
def page_not_found(e): def page_not_found(e):
return ( return (
render_template('_error.html', code=404, message='404 Not Found'), render_template("_error.html", code=404, message="404 Not Found"),
404 404,
) )

View file

@ -16,50 +16,59 @@
import functools import functools
from flask import (Blueprint, current_app, flash, redirect, render_template, from flask import (
session, url_for) Blueprint,
current_app,
flash,
redirect,
render_template,
session,
url_for,
)
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import PasswordField, BooleanField from wtforms import PasswordField, BooleanField
from wtforms.validators import DataRequired from wtforms.validators import DataRequired
bp = Blueprint('auth', __name__) bp = Blueprint("auth", __name__)
class LoginForm(FlaskForm): class LoginForm(FlaskForm):
"""Form for login.""" """Form for login."""
password = PasswordField('Password', validators=[DataRequired()])
permanent = BooleanField('Stay logged in') password = PasswordField("Password", validators=[DataRequired()])
permanent = BooleanField("Stay logged in")
def login_required(view): def login_required(view):
"""View decorator that redirects anonymous users to the login page.""" """View decorator that redirects anonymous users to the login page."""
@functools.wraps(view) @functools.wraps(view)
def wrapped_view(**kwargs): def wrapped_view(**kwargs):
logged_in = session.get('logged_in', False) logged_in = session.get("logged_in", False)
if not logged_in: if not logged_in:
return redirect(url_for('auth.login')) return redirect(url_for("auth.login"))
return view(**kwargs) return view(**kwargs)
return wrapped_view return wrapped_view
@bp.route('/login', methods=('GET', 'POST')) @bp.route("/login", methods=("GET", "POST"))
def login(): def login():
form = LoginForm() form = LoginForm()
if form.validate_on_submit(): if form.validate_on_submit():
if form.password.data == current_app.config['HUMULUS_PASSWORD']: if form.password.data == current_app.config["HUMULUS_PASSWORD"]:
session.clear() session.clear()
session.permanent = form.permanent.data session.permanent = form.permanent.data
session['logged_in'] = True session["logged_in"] = True
return redirect(url_for('index')) return redirect(url_for("index"))
flash('Password is invalid.', category='warning') flash("Password is invalid.", category="warning")
return render_template('auth/login.html', form=form) return render_template("auth/login.html", form=form)
@bp.route('/logout') @bp.route("/logout")
def logout(): def logout():
session.clear() session.clear()
return redirect(url_for('index')) return redirect(url_for("index"))

View file

@ -29,25 +29,25 @@ 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:
g.couch = CouchDB( g.couch = CouchDB(
current_app.config['COUCH_USERNAME'], current_app.config["COUCH_USERNAME"],
current_app.config['COUCH_PASSWORD'], current_app.config["COUCH_PASSWORD"],
url=current_app.config['COUCH_URL'], url=current_app.config["COUCH_URL"],
connect=True, connect=True,
auto_renew=True auto_renew=True,
) )
return g.couch return g.couch
def get_db(): def get_db():
"""Returns a database to interact with.""" """Returns a database to interact with."""
return get_couch()[current_app.config['COUCH_DATABASE']] return get_couch()[current_app.config["COUCH_DATABASE"]]
def close_couch(e=None): def close_couch(e=None):
"""Disconnect from CouchDB.""" """Disconnect from CouchDB."""
couch = g.pop('couch', None) couch = g.pop("couch", None)
if couch is not None: if couch is not None:
couch.disconnect() couch.disconnect()
@ -55,17 +55,17 @@ def close_couch(e=None):
def build_couch(): def build_couch():
"""Create any necessary databases and design documents.""" """Create any necessary databases and design documents."""
couch = get_couch() couch = get_couch()
dbname = current_app.config['COUCH_DATABASE'] dbname = current_app.config["COUCH_DATABASE"]
couch.create_database(dbname, throw_on_exists=False) couch.create_database(dbname, throw_on_exists=False)
put_designs() put_designs()
@click.command('build-couch') @click.command("build-couch")
@with_appcontext @with_appcontext
def build_couch_command(): def build_couch_command():
"""Builds the couch for easy relaxing.""" """Builds the couch for easy relaxing."""
build_couch() build_couch()
click.echo('Built a couch. Please have a seat.') click.echo("Built a couch. Please have a seat.")
def init_app(app): def init_app(app):
@ -82,22 +82,22 @@ def put_doc(doc):
""" """
db = get_db() db = get_db()
if 'name' in doc and '_id' not in doc: if "name" in doc and "_id" not in doc:
# Slugify the name, use that for id # Slugify the name, use that for id
slug = slugify(doc['name']) slug = slugify(doc["name"])
doc['_id'] = slug doc["_id"] = slug
i = 1 i = 1
# Check if id exists and append/increment a number until it doesn't. # Check if id exists and append/increment a number until it doesn't.
while doc['_id'] in db: while doc["_id"] in db:
doc['_id'] = slug + '-{}'.format(i) doc["_id"] = slug + "-{}".format(i)
i += 1 i += 1
elif '_id' not in doc: elif "_id" not in doc:
# Use a UUID for name # Use a UUID for name
doc['_id'] = str(uuid.uuid4()) doc["_id"] = str(uuid.uuid4())
# Add a created timestamp # Add a created timestamp
# Timestamps are written to couchdb in ISO-8601 format # Timestamps are written to couchdb in ISO-8601 format
doc['created'] = datetime.utcnow().isoformat(timespec='seconds') doc["created"] = datetime.utcnow().isoformat(timespec="seconds")
return db.create_document(doc, throw_on_exists=True) return db.create_document(doc, throw_on_exists=True)
@ -107,7 +107,7 @@ def update_doc(doc):
Adds an 'updated' field representing the current time the doc was updated. Adds an 'updated' field representing the current time the doc was updated.
""" """
doc['updated'] = datetime.utcnow().isoformat(timespec='seconds') doc["updated"] = datetime.utcnow().isoformat(timespec="seconds")
doc.save() doc.save()
@ -133,23 +133,23 @@ def put_designs():
""" """
here = Path(__file__).parent here = Path(__file__).parent
for filename in here.glob('designs/*.json'): for filename in here.glob("designs/*.json"):
with open(filename, 'r') as fp: with open(filename, "r") as fp:
data = json.load(fp) data = json.load(fp)
# See if document already exists # See if document already exists
if data['_id'] in get_db(): if data["_id"] in get_db():
doc = get_doc(data['_id']) doc = get_doc(data["_id"])
# Popping off the revision and storing it. Then compare # Popping off the revision and storing it. Then compare
rev = doc.pop('_rev') rev = doc.pop("_rev")
doc.pop('created', None) doc.pop("created", None)
if data == doc: if data == doc:
get_db().clear() get_db().clear()
return return
# Copy the values of data to doc. # Copy the values of data to doc.
for k in data: for k in data:
doc[k] = data[k] doc[k] = data[k]
doc['_rev'] = rev # Add the revision back doc["_rev"] = rev # Add the revision back
doc.save() doc.save()
else: else:
put_doc(data) put_doc(data)

View file

@ -19,120 +19,122 @@ import math
def recipe_og(recipe): def recipe_og(recipe):
"""Returns a recipe's Original Gravity""" """Returns a recipe's Original Gravity"""
if 'fermentables' not in recipe: if "fermentables" not in recipe:
return '0.000' return "0.000"
points = 0 points = 0
grain_points = 0 grain_points = 0
# Loop through fermentables, adding up points # Loop through fermentables, adding up points
for fermentable in recipe['fermentables']: for fermentable in recipe["fermentables"]:
if fermentable['type'] == 'Grain': if fermentable["type"] == "Grain":
grain_points += ( grain_points += float(fermentable["amount"]) * float(
float(fermentable['amount']) * float(fermentable['ppg']) fermentable["ppg"]
) )
else: else:
points += ( points += float(fermentable["amount"]) * float(fermentable["ppg"])
float(fermentable['amount']) * float(fermentable['ppg']) points += grain_points * float(recipe["efficiency"]) / 100
) return "{:.3f}".format(
points += grain_points * float(recipe['efficiency']) / 100 round(1 + points / (1000 * float(recipe["volume"])), 3)
return '{:.3f}'.format(
round(1 + points / (1000 * float(recipe['volume'])), 3)
) )
def recipe_fg(recipe): def recipe_fg(recipe):
"""Returns a recipe's final gravity""" """Returns a recipe's final gravity"""
if 'yeast' not in recipe or 'fermentables' not in recipe: if "yeast" not in recipe or "fermentables" not in recipe:
return '0.000' return "0.000"
og = float(recipe_og(recipe)) og = float(recipe_og(recipe))
og_delta = 0.0 og_delta = 0.0
# Adjust original gravity by removing nonfermentables (i.e., Lactose) # Adjust original gravity by removing nonfermentables (i.e., Lactose)
for fermentable in recipe['fermentables']: for fermentable in recipe["fermentables"]:
if fermentable['type'] == 'Non-fermentable': if fermentable["type"] == "Non-fermentable":
og_delta += ( og_delta += (
float(fermentable['amount']) * float(fermentable['ppg']) / float(fermentable["amount"])
(1000 * float(recipe['volume'])) * float(fermentable["ppg"])
/ (1000 * float(recipe["volume"]))
) )
attenuation = ( attenuation = (
( float(recipe["yeast"]["low_attenuation"])
float(recipe['yeast']['low_attenuation']) + + float(recipe["yeast"]["high_attenuation"])
float(recipe['yeast']['high_attenuation'])
) / 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)
) )
def recipe_ibu(recipe): def recipe_ibu(recipe):
"""Return a recipe's IBU""" """Return a recipe's IBU"""
if 'hops' not in recipe: if "hops" not in recipe:
return '0' return "0"
bigness = 1.65 * 0.000125 ** (float(recipe_og(recipe)) - 1) bigness = 1.65 * 0.000125 ** (float(recipe_og(recipe)) - 1)
ibu = 0.0 ibu = 0.0
for h in recipe['hops']: for h in recipe["hops"]:
if h['use'] != 'Boil' and h['use'] != 'FWH': if h["use"] != "Boil" and h["use"] != "FWH":
continue continue
mgl = ( mgl = (
float(h['alpha']) * float(h['amount']) * 7490.0 / float(h["alpha"])
(float(recipe['volume']) * 100.0) * float(h["amount"])
* 7490.0
/ (float(recipe["volume"]) * 100.0)
) )
btf = (1 - math.exp(-0.04 * float(h['duration']))) / 4.15 btf = (1 - math.exp(-0.04 * float(h["duration"]))) / 4.15
ibu += bigness * btf * mgl ibu += bigness * btf * mgl
return '{:.0f}'.format(ibu) return "{:.0f}".format(ibu)
def recipe_ibu_ratio(recipe): def recipe_ibu_ratio(recipe):
"""Return a recipe's IBU ratio""" """Return a recipe's IBU ratio"""
if 'fermentables' not in recipe or 'hops' not in recipe: if "fermentables" not in recipe or "hops" not in recipe:
return '0' return "0"
if len(recipe['fermentables']) == 0: if len(recipe["fermentables"]) == 0:
return '0' # Otherwise a divide by zero error will occur return "0" # Otherwise a divide by zero error will occur
og = float(recipe_og(recipe)) og = float(recipe_og(recipe))
ibu = float(recipe_ibu(recipe)) ibu = float(recipe_ibu(recipe))
return '{:.2f}'.format(round(0.001 * ibu / (og - 1), 2)) return "{:.2f}".format(round(0.001 * ibu / (og - 1), 2))
def recipe_abv(recipe): def recipe_abv(recipe):
"""Return a recipe's finished ABV""" """Return a recipe's finished ABV"""
if 'fermentables' not in recipe or 'yeast' not in recipe: if "fermentables" not in recipe or "yeast" not in recipe:
return '0' return "0"
og = float(recipe_og(recipe)) og = float(recipe_og(recipe))
fg = float(recipe_fg(recipe)) fg = float(recipe_fg(recipe))
return '{:.1f}'.format(round((og - fg) * 131.25, 1)) return "{:.1f}".format(round((og - fg) * 131.25, 1))
def recipe_srm(recipe): def recipe_srm(recipe):
"""Return a recipe's SRM""" """Return a recipe's SRM"""
if 'fermentables' not in recipe: if "fermentables" not in recipe:
return '0' return "0"
mcu = 0 mcu = 0
for f in recipe['fermentables']: for f in recipe["fermentables"]:
mcu += float(f['amount']) * float(f['color']) / float(recipe['volume']) mcu += float(f["amount"]) * float(f["color"]) / float(recipe["volume"])
return '{:.0f}'.format(1.4922 * (mcu**0.6859)) return "{:.0f}".format(1.4922 * (mcu ** 0.6859))
def sort_hops(hops, form=False): def sort_hops(hops, form=False):
"""Sorts a list of hops by use in recipe.""" """Sorts a list of hops by use in recipe."""
by_use = {'FWH': [], 'Boil': [], 'Whirlpool': [], 'Dry-Hop': []} by_use = {"FWH": [], "Boil": [], "Whirlpool": [], "Dry-Hop": []}
# Split hops into each use type. # Split hops into each use type.
for hop in hops: for hop in hops:
if form: if form:
by_use[hop.use.data].append(hop) by_use[hop.use.data].append(hop)
else: else:
by_use[hop['use']].append(hop) by_use[hop["use"]].append(hop)
if form: if form:
def key(hop): def key(hop):
return float(hop.duration.data) return float(hop.duration.data)
else:
def key(hop):
return float(hop['duration'])
hops_sorted = sorted(by_use['FWH'], key=key, reverse=True) else:
hops_sorted.extend(sorted(by_use['Boil'], key=key, reverse=True))
hops_sorted.extend(sorted(by_use['Whirlpool'], key=key, reverse=True)) def key(hop):
hops_sorted.extend(sorted(by_use['Dry-Hop'], key=key, reverse=True)) return float(hop["duration"])
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["Whirlpool"], key=key, reverse=True))
hops_sorted.extend(sorted(by_use["Dry-Hop"], key=key, reverse=True))
return hops_sorted return hops_sorted
@ -144,10 +146,10 @@ def ferm_pct(fermentables):
total = 0 total = 0
# Calculate total # Calculate total
for ferm in fermentables: for ferm in 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

View file

@ -18,20 +18,20 @@ from flask import Blueprint, redirect, url_for, request, jsonify
from humulus.couch import get_db from humulus.couch import get_db
bp = Blueprint('home', __name__) bp = Blueprint("home", __name__)
@bp.route('/') @bp.route("/")
def index(): def index():
"""Renders the homepage template""" """Renders the homepage template"""
return redirect(url_for('recipes.index')) return redirect(url_for("recipes.index"))
@bp.route('/status') @bp.route("/status")
def status(): def status():
if request.args.get('couch', default=False): if request.args.get("couch", default=False):
if get_db().exists(): if get_db().exists():
return jsonify({'ping': 'ok', 'couch': 'ok'}), 200 return jsonify({"ping": "ok", "couch": "ok"}), 200
else: else:
return jsonify({'ping': 'ok', 'couch': 'not_exist'}), 500 return jsonify({"ping": "ok", "couch": "not_exist"}), 500
return jsonify({'ping': 'ok'}), 200 return jsonify({"ping": "ok"}), 200

View file

@ -18,20 +18,40 @@ import json
from decimal import Decimal from decimal import Decimal
import requests import requests
from flask import (abort, Blueprint, flash, redirect, render_template, jsonify, from flask import (
request, url_for) abort,
Blueprint,
flash,
redirect,
render_template,
jsonify,
request,
url_for,
)
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired from flask_wtf.file import FileField, FileRequired
from wtforms import (Form, StringField, DecimalField, TextAreaField, FieldList, from wtforms import (
FormField, SelectField) Form,
StringField,
DecimalField,
TextAreaField,
FieldList,
FormField,
SelectField,
)
from wtforms.validators import DataRequired, Optional from wtforms.validators import DataRequired, Optional
from humulus.auth import login_required from humulus.auth import login_required
from humulus.couch import (get_doc, get_doc_or_404, put_doc, update_doc, from humulus.couch import (
get_view) get_doc,
get_doc_or_404,
put_doc,
update_doc,
get_view,
)
from humulus.styles import get_styles_list from humulus.styles import get_styles_list
bp = Blueprint('recipes', __name__, url_prefix='/recipes') bp = Blueprint("recipes", __name__, url_prefix="/recipes")
class FermentableForm(Form): class FermentableForm(Form):
@ -40,14 +60,26 @@ class FermentableForm(Form):
CSRF is disabled for this form (using `Form as parent class) CSRF is disabled for this form (using `Form as parent class)
because it is never used by itself. because it is never used by itself.
""" """
name = StringField('Name', validators=[DataRequired()])
type = SelectField('Type', validators=[DataRequired()], name = StringField("Name", validators=[DataRequired()])
choices=[(c, c) for c in ['Grain', 'LME', 'DME', type = SelectField(
'Sugar', 'Non-fermentable', "Type",
'Other']]) validators=[DataRequired()],
amount = DecimalField('Amount (lb)', validators=[DataRequired()]) choices=[
ppg = DecimalField('PPG', validators=[DataRequired()]) (c, c)
color = DecimalField('Color (°L)', validators=[DataRequired()]) 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 @property
def doc(self): def doc(self):
@ -56,11 +88,11 @@ class FermentableForm(Form):
Used for putting into CouchDB. Used for putting into CouchDB.
""" """
return { return {
'name': self.name.data, "name": self.name.data,
'type': self.type.data, "type": self.type.data,
'amount': str(self.amount.data), "amount": str(self.amount.data),
'ppg': str(self.ppg.data), "ppg": str(self.ppg.data),
'color': str(self.color.data) "color": str(self.color.data),
} }
@ -70,13 +102,16 @@ class HopForm(Form):
CSRF is disabled for this form (using `Form as parent class) CSRF is disabled for this form (using `Form as parent class)
because it is never used by itself. because it is never used by itself.
""" """
name = StringField('Name', validators=[DataRequired()])
use = SelectField('Usage', validators=[DataRequired()], name = StringField("Name", validators=[DataRequired()])
choices=[(c, c) for c in ['Boil', 'FWH', 'Whirlpool', use = SelectField(
'Dry-Hop']]) "Usage",
alpha = DecimalField('Alpha Acid %', validators=[DataRequired()]) validators=[DataRequired()],
duration = DecimalField('Duration (min/day)', validators=[DataRequired()]) choices=[(c, c) for c in ["Boil", "FWH", "Whirlpool", "Dry-Hop"]],
amount = DecimalField('Amount (oz)', validators=[DataRequired()]) )
alpha = DecimalField("Alpha Acid %", validators=[DataRequired()])
duration = DecimalField("Duration (min/day)", validators=[DataRequired()])
amount = DecimalField("Amount (oz)", validators=[DataRequired()])
@property @property
def doc(self): def doc(self):
@ -85,11 +120,11 @@ class HopForm(Form):
Used for putting into CouchDB. Used for putting into CouchDB.
""" """
return { return {
'name': self.name.data, "name": self.name.data,
'use': self.use.data, "use": self.use.data,
'alpha': str(self.alpha.data), "alpha": str(self.alpha.data),
'duration': str(self.duration.data), "duration": str(self.duration.data),
'amount': str(self.amount.data) "amount": str(self.amount.data),
} }
@ -99,26 +134,29 @@ class YeastForm(Form):
CSRF is disabled for this form (using `Form as parent class) CSRF is disabled for this form (using `Form as parent class)
because it is never used by itself. because it is never used by itself.
""" """
name = StringField('Name', validators=[Optional()])
type = SelectField('Type', default='', name = StringField("Name", validators=[Optional()])
choices=[(c, c) for c in ['', 'Liquid', 'Dry']], type = SelectField(
validators=[Optional()]) "Type",
lab = StringField('Lab') default="",
code = StringField('Lab Code') choices=[(c, c) for c in ["", "Liquid", "Dry"]],
flocculation = SelectField('Flocculation', default='', validators=[Optional()],
choices=[(c, c) for c in ['', 'Low', )
'Medium', 'High']], lab = StringField("Lab")
validators=[Optional()]) code = StringField("Lab Code")
low_attenuation = DecimalField('Low Attenuation', flocculation = SelectField(
validators=[Optional()]) "Flocculation",
high_attenuation = DecimalField('High Attenuation', default="",
validators=[Optional()]) choices=[(c, c) for c in ["", "Low", "Medium", "High"]],
min_temperature = DecimalField('Min Temp (°F)', validators=[Optional()],
validators=[Optional()]) )
max_temperature = DecimalField('Max Temp (°F)', low_attenuation = DecimalField("Low Attenuation", validators=[Optional()])
validators=[Optional()]) high_attenuation = DecimalField(
abv_tolerance = DecimalField('ABV % tolerance', "High Attenuation", validators=[Optional()]
validators=[Optional()]) )
min_temperature = DecimalField("Min Temp (°F)", validators=[Optional()])
max_temperature = DecimalField("Max Temp (°F)", validators=[Optional()])
abv_tolerance = DecimalField("ABV % tolerance", validators=[Optional()])
@property @property
def doc(self): def doc(self):
@ -127,24 +165,24 @@ class YeastForm(Form):
Used for putting into CouchDB. Used for putting into CouchDB.
""" """
yeast = { yeast = {
'name': self.name.data, "name": self.name.data,
'low_attenuation': str(self.low_attenuation.data), "low_attenuation": str(self.low_attenuation.data),
'high_attenuation': str(self.high_attenuation.data) "high_attenuation": str(self.high_attenuation.data),
} }
if self.type.data: if self.type.data:
yeast['type'] = self.type.data yeast["type"] = self.type.data
if self.lab.data: if self.lab.data:
yeast['lab'] = self.lab.data yeast["lab"] = self.lab.data
if self.code.data: if self.code.data:
yeast['code'] = self.code.data yeast["code"] = self.code.data
if self.flocculation.data: if self.flocculation.data:
yeast['flocculation'] = self.flocculation.data yeast["flocculation"] = self.flocculation.data
if self.min_temperature.data: if self.min_temperature.data:
yeast['min_temperature'] = str(self.min_temperature.data) yeast["min_temperature"] = str(self.min_temperature.data)
if self.max_temperature.data: if self.max_temperature.data:
yeast['max_temperature'] = str(self.max_temperature.data) yeast["max_temperature"] = str(self.max_temperature.data)
if self.abv_tolerance.data: if self.abv_tolerance.data:
yeast['abv_tolerance'] = str(self.abv_tolerance.data) yeast["abv_tolerance"] = str(self.abv_tolerance.data)
return yeast return yeast
@ -154,14 +192,15 @@ class MashStepForm(Form):
CSRF is disabled for this form (using `Form as parent class) CSRF is disabled for this form (using `Form as parent class)
because it is never used by itself. because it is never used by itself.
""" """
name = StringField('Step Name', validators=[DataRequired()])
type = SelectField('Type', name = StringField("Step Name", validators=[DataRequired()])
choices=[(c, c) for c in ['Infusion', type = SelectField(
'Temperature', "Type",
'Decoction']]) choices=[(c, c) for c in ["Infusion", "Temperature", "Decoction"]],
temp = DecimalField('Temperature (°F)', validators=[DataRequired()]) )
time = DecimalField('Time (min)', validators=[DataRequired()]) temp = DecimalField("Temperature (°F)", validators=[DataRequired()])
amount = DecimalField('Water Amount (gal)') time = DecimalField("Time (min)", validators=[DataRequired()])
amount = DecimalField("Water Amount (gal)")
@property @property
def doc(self): def doc(self):
@ -170,13 +209,13 @@ class MashStepForm(Form):
Used for putting into CouchDB. Used for putting into CouchDB.
""" """
step = { step = {
'name': self.name.data, "name": self.name.data,
'type': self.type.data, "type": self.type.data,
'temp': str(self.temp.data), "temp": str(self.temp.data),
'time': str(self.time.data), "time": str(self.time.data),
} }
if self.amount.data: if self.amount.data:
step['amount'] = str(self.amount.data) step["amount"] = str(self.amount.data)
return step return step
@ -186,12 +225,9 @@ class MashForm(Form):
CSRF is disabled for this form (using `Form as parent class) CSRF is disabled for this form (using `Form as parent class)
because it is never used by itself. because it is never used by itself.
""" """
name = StringField('Mash Name', validators=[Optional()])
steps = FieldList( name = StringField("Mash Name", validators=[Optional()])
FormField(MashStepForm), steps = FieldList(FormField(MashStepForm), min_entries=0, max_entries=20)
min_entries=0,
max_entries=20
)
@property @property
def doc(self): def doc(self):
@ -199,37 +235,31 @@ class MashForm(Form):
Used for putting into CouchDB. Used for putting into CouchDB.
""" """
return { return {"name": self.name.data, "steps": [s.doc for s in self.steps]}
'name': self.name.data,
'steps': [s.doc for s in self.steps]
}
class RecipeForm(FlaskForm): class RecipeForm(FlaskForm):
"""Form for recipes.""" """Form for recipes."""
name = StringField('Name', validators=[DataRequired()])
type = SelectField('Type', default='', name = StringField("Name", validators=[DataRequired()])
choices=[(c, c) for c in ['All-Grain', type = SelectField(
'Partial Extract', "Type",
'Extract']], default="",
validators=[Optional()]) choices=[(c, c) for c in ["All-Grain", "Partial Extract", "Extract"]],
efficiency = DecimalField('Batch Efficiency (%)', validators=[Optional()],
validators=[DataRequired()]) )
volume = DecimalField('Batch Volume (gal)', validators=[DataRequired()]) efficiency = DecimalField(
notes = TextAreaField('Notes') "Batch Efficiency (%)", validators=[DataRequired()]
)
volume = DecimalField("Batch Volume (gal)", validators=[DataRequired()])
notes = TextAreaField("Notes")
fermentables = FieldList( fermentables = FieldList(
FormField(FermentableForm), FormField(FermentableForm), min_entries=0, max_entries=20
min_entries=0,
max_entries=20
)
hops = FieldList(
FormField(HopForm),
min_entries=0,
max_entries=20
) )
hops = FieldList(FormField(HopForm), min_entries=0, max_entries=20)
yeast = FormField(YeastForm) yeast = FormField(YeastForm)
mash = FormField(MashForm) mash = FormField(MashForm)
style = SelectField('Style', choices=[], validators=[Optional()]) style = SelectField("Style", choices=[], validators=[Optional()])
@property @property
def doc(self): def doc(self):
@ -238,98 +268,102 @@ class RecipeForm(FlaskForm):
Used for putting into CouchDB. Used for putting into CouchDB.
""" """
recipe = { recipe = {
'name': self.name.data, "name": self.name.data,
'efficiency': str(self.efficiency.data), "efficiency": str(self.efficiency.data),
'volume': str(self.volume.data), "volume": str(self.volume.data),
'notes': self.notes.data, "notes": self.notes.data,
'$type': 'recipe', "$type": "recipe",
'type': self.type.data, "type": self.type.data,
'style': self.style.data "style": self.style.data,
} }
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"]
self.yeast.doc['low_attenuation'] != "None" and and self.yeast.doc["low_attenuation"] != "None"
self.yeast.doc['high_attenuation'] != "None" and self.yeast.doc["high_attenuation"] != "None"
): ):
recipe['yeast'] = self.yeast.doc recipe["yeast"] = self.yeast.doc
if self.mash.doc['name']: if self.mash.doc["name"]:
recipe['mash'] = self.mash.doc recipe["mash"] = self.mash.doc
return recipe return recipe
def copyfrom(self, data): def copyfrom(self, data):
"""Copies from a dictionary (data) into the current object""" """Copies from a dictionary (data) into the current object"""
self.name.data = data['name'] self.name.data = data["name"]
self.type.data = data['type'] self.type.data = data["type"]
self.efficiency.data = Decimal(data['efficiency']) self.efficiency.data = Decimal(data["efficiency"])
self.volume.data = Decimal(data['volume']) self.volume.data = Decimal(data["volume"])
self.notes.data = data['notes'] self.notes.data = data["notes"]
self.style.data = data['style'] self.style.data = data["style"]
for fermentable in data['fermentables']: for fermentable in data["fermentables"]:
self.fermentables.append_entry({ self.fermentables.append_entry(
'name': fermentable['name'], {
'type': fermentable['type'], "name": fermentable["name"],
'amount': Decimal(fermentable['amount']), "type": fermentable["type"],
'ppg': Decimal(fermentable['ppg']), "amount": Decimal(fermentable["amount"]),
'color': Decimal(fermentable['color']) "ppg": Decimal(fermentable["ppg"]),
}) "color": Decimal(fermentable["color"]),
for hop in data['hops']:
self.hops.append_entry({
'name': hop['name'],
'use': hop['use'],
'alpha': Decimal(hop['alpha']),
'duration': Decimal(hop['duration']),
'amount': Decimal(hop['amount']),
})
if 'yeast' in data:
self.yeast.form.name.data = data['yeast']['name']
self.yeast.form.low_attenuation.data = (
Decimal(data['yeast']['low_attenuation'])
)
self.yeast.form.high_attenuation.data = (
Decimal(data['yeast']['high_attenuation'])
)
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(data['yeast']['min_temperature'])
)
if 'max_temperature' in data['yeast']:
self.yeast.form.max_temperature.data = (
Decimal(data['yeast']['max_temperature'])
)
if 'abv_tolerance' in data['yeast']:
self.yeast.form.abv_tolerance.data = (
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'])
for hop in data["hops"]:
self.hops.append_entry(
{
"name": hop["name"],
"use": hop["use"],
"alpha": Decimal(hop["alpha"]),
"duration": Decimal(hop["duration"]),
"amount": Decimal(hop["amount"]),
}
)
if "yeast" in data:
self.yeast.form.name.data = data["yeast"]["name"]
self.yeast.form.low_attenuation.data = Decimal(
data["yeast"]["low_attenuation"]
)
self.yeast.form.high_attenuation.data = Decimal(
data["yeast"]["high_attenuation"]
)
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(
data["yeast"]["min_temperature"]
)
if "max_temperature" in data["yeast"]:
self.yeast.form.max_temperature.data = Decimal(
data["yeast"]["max_temperature"]
)
if "abv_tolerance" in data["yeast"]:
self.yeast.form.abv_tolerance.data = 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) print(new_step)
self.mash.steps.append_entry(new_step) self.mash.steps.append_entry(new_step)
@ -338,29 +372,25 @@ class ImportForm(FlaskForm):
upload = FileField(validators=[FileRequired()]) upload = FileField(validators=[FileRequired()])
@bp.route('/') @bp.route("/")
def index(): def index():
descending = ( descending = request.args.get(
request.args.get('descending', default='false', type=str).lower() in "descending", default="false", type=str
['true', 'yes'] ).lower() in ["true", "yes"]
) sort_by = request.args.get("sort_by", default="name", type=str)
sort_by = request.args.get('sort_by', default='name', type=str)
view = get_view('_design/recipes', 'by-{}'.format(sort_by)) view = get_view("_design/recipes", "by-{}".format(sort_by))
try: try:
rows = view(include_docs=True, descending=descending)['rows'] rows = view(include_docs=True, descending=descending)["rows"]
except requests.exceptions.HTTPError: except requests.exceptions.HTTPError:
abort(400) abort(400)
return render_template( return render_template(
'recipes/index.html', "recipes/index.html", rows=rows, descending=descending, sort_by=sort_by
rows=rows,
descending=descending,
sort_by=sort_by
) )
@bp.route('/create', methods=('GET', 'POST')) @bp.route("/create", methods=("GET", "POST"))
@login_required @login_required
def create(): def create():
form = RecipeForm() form = RecipeForm()
@ -368,12 +398,12 @@ def create():
if form.validate_on_submit(): if form.validate_on_submit():
response = put_doc(form.doc) response = put_doc(form.doc)
flash('Created recipe: {}'.format(form.name.data), 'success') flash("Created recipe: {}".format(form.name.data), "success")
return redirect(url_for('recipes.info', id=response['_id'])) return redirect(url_for("recipes.info", id=response["_id"]))
return render_template('recipes/create.html', form=form) return render_template("recipes/create.html", form=form)
@bp.route('/create/json', methods=('GET', 'POST')) @bp.route("/create/json", methods=("GET", "POST"))
@login_required @login_required
def create_json(): def create_json():
form = ImportForm() form = ImportForm()
@ -382,47 +412,48 @@ def create_json():
try: try:
recipe.copyfrom(json.load(form.upload.data)) recipe.copyfrom(json.load(form.upload.data))
except Exception as e: except Exception as e:
flash('Error converting data from JSON: {}'.format(e), 'warning') flash("Error converting data from JSON: {}".format(e), "warning")
return render_template('recipes/create_json.html', form=form) return render_template("recipes/create_json.html", form=form)
response = put_doc(recipe.doc) response = put_doc(recipe.doc)
return redirect(url_for('recipes.info', id=response['_id'])) return redirect(url_for("recipes.info", id=response["_id"]))
return render_template('recipes/create_json.html', form=form) return render_template("recipes/create_json.html", form=form)
@bp.route('/info/<id>') @bp.route("/info/<id>")
def info(id): def info(id):
recipe = get_doc_or_404(id) recipe = get_doc_or_404(id)
style = None style = None
if recipe['style'] != '': if recipe["style"] != "":
try: try:
style = get_doc(recipe['style']) style = get_doc(recipe["style"])
except KeyError: except KeyError:
flash('Could not find style `{}`.'.format(recipe['style']), flash(
'warning') "Could not find style `{}`.".format(recipe["style"]), "warning"
)
return render_template('recipes/info.html', recipe=recipe, style=style) return render_template("recipes/info.html", recipe=recipe, style=style)
@bp.route('/info/<id>/json') @bp.route("/info/<id>/json")
def info_json(id): def info_json(id):
recipe = get_doc_or_404(id) recipe = get_doc_or_404(id)
# Remove fields specific not intended for export # Remove fields specific not intended for export
recipe.pop('_id') recipe.pop("_id")
recipe.pop('_rev') recipe.pop("_rev")
recipe.pop('$type') recipe.pop("$type")
return jsonify(recipe) return jsonify(recipe)
@bp.route('/delete/<id>', methods=('POST',)) @bp.route("/delete/<id>", methods=("POST",))
@login_required @login_required
def delete(id): def delete(id):
recipe = get_doc_or_404(id) recipe = get_doc_or_404(id)
recipe.delete() recipe.delete()
return redirect(url_for('home.index')) return redirect(url_for("home.index"))
@bp.route('/update/<id>', methods=('GET', 'POST')) @bp.route("/update/<id>", methods=("GET", "POST"))
@login_required @login_required
def update(id): def update(id):
# Get the recipe from the database and validate it is the same revision # Get the recipe from the database and validate it is the same revision
@ -430,24 +461,25 @@ def update(id):
form.style.choices = get_styles_list() form.style.choices = get_styles_list()
recipe = get_doc_or_404(id) recipe = get_doc_or_404(id)
if form.validate_on_submit(): if form.validate_on_submit():
if recipe['_rev'] != request.args.get('rev', None): if recipe["_rev"] != request.args.get("rev", None):
flash( flash(
( (
'Update conflict for recipe: {}. ' "Update conflict for recipe: {}. "
'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():
recipe[key] = value recipe[key] = value
update_doc(recipe) update_doc(recipe)
flash('Updated recipe: {}'.format(form.name.data), 'success') flash("Updated recipe: {}".format(form.name.data), "success")
return redirect(url_for('recipes.info', id=id)) return redirect(url_for("recipes.info", id=id))
else: else:
form.copyfrom(recipe) form.copyfrom(recipe)
return render_template('recipes/update.html', form=form, return render_template(
id=id, rev=recipe['_rev']) "recipes/update.html", form=form, id=id, rev=recipe["_rev"]
)

View file

@ -19,14 +19,20 @@ import xml.etree.ElementTree as ET
import click import click
import requests import requests
from flask import (Blueprint, abort, current_app, render_template, request, from flask import (
jsonify) Blueprint,
abort,
current_app,
render_template,
request,
jsonify,
)
from flask.cli import with_appcontext from flask.cli import with_appcontext
from humulus.auth import login_required from humulus.auth import login_required
from humulus.couch import get_db, put_doc, get_view, get_doc_or_404 from humulus.couch import get_db, put_doc, get_view, get_doc_or_404
bp = Blueprint('styles', __name__, url_prefix='/styles') bp = Blueprint("styles", __name__, url_prefix="/styles")
def sub_to_doc(sub): def sub_to_doc(sub):
@ -35,56 +41,83 @@ def sub_to_doc(sub):
The returned dictionary can be placed right into CouchDB if you want. The returned dictionary can be placed right into CouchDB if you want.
""" """
doc = { doc = {
'_id': '{}'.format(sub.attrib['id']), "_id": "{}".format(sub.attrib["id"]),
'$type': 'style', "$type": "style",
'name': sub.find('name').text, "name": sub.find("name").text,
'aroma': sub.find('aroma').text, "aroma": sub.find("aroma").text,
'appearance': sub.find('appearance').text, "appearance": sub.find("appearance").text,
'flavor': sub.find('flavor').text, "flavor": sub.find("flavor").text,
'mouthfeel': sub.find('mouthfeel').text, "mouthfeel": sub.find("mouthfeel").text,
'impression': sub.find('impression').text, "impression": sub.find("impression").text,
'ibu': {}, "ibu": {},
'og': {}, "og": {},
'fg': {}, "fg": {},
'srm': {}, "srm": {},
'abv': {} "abv": {},
} }
if sub.find('comments') is not None: if sub.find("comments") is not None:
doc['comments'] = sub.find('comments').text doc["comments"] = sub.find("comments").text
if sub.find('history') is not None: if sub.find("history") is not None:
doc['history'] = sub.find('history').text doc["history"] = sub.find("history").text
if sub.find('ingredients') is not None: if sub.find("ingredients") is not None:
doc['ingredients'] = sub.find('ingredients').text doc["ingredients"] = sub.find("ingredients").text
if sub.find('comparison') is not None: if sub.find("comparison") is not None:
doc['comparison'] = sub.find('comparison').text doc["comparison"] = sub.find("comparison").text
if sub.find('examples') is not None: if sub.find("examples") is not None:
doc['examples'] = sub.find('examples').text doc["examples"] = sub.find("examples").text
if sub.find('tags') is not None: if sub.find("tags") is not None:
doc['tags'] = sub.find('tags').text.split(', ') doc["tags"] = sub.find("tags").text.split(", ")
doc['ibu']['low'] = (sub.find('./stats/ibu/low').text doc["ibu"]["low"] = (
if sub.find('./stats/ibu/low') is not None else '0') sub.find("./stats/ibu/low").text
doc['ibu']['high'] = (sub.find('./stats/ibu/high').text if sub.find("./stats/ibu/low") is not None
if sub.find('./stats/ibu/high') is not None else "0"
else '100') )
doc['og']['low'] = (sub.find('./stats/og/low').text doc["ibu"]["high"] = (
if sub.find('./stats/og/low') is not None else '1.0') sub.find("./stats/ibu/high").text
doc['og']['high'] = (sub.find('./stats/og/high').text if sub.find("./stats/ibu/high") is not None
if sub.find('./stats/og/high') is not None else '1.2') else "100"
doc['fg']['low'] = (sub.find('./stats/fg/low').text )
if sub.find('./stats/fg/low') is not None else '1.0') doc["og"]["low"] = (
doc['fg']['high'] = (sub.find('./stats/fg/high').text sub.find("./stats/og/low").text
if sub.find('./stats/fg/high') is not None else '1.2') if sub.find("./stats/og/low") is not None
doc['srm']['low'] = (sub.find('./stats/srm/low').text else "1.0"
if sub.find('./stats/srm/low') is not None else '0') )
doc['srm']['high'] = (sub.find('./stats/srm/high').text doc["og"]["high"] = (
if sub.find('./stats/srm/high') is not None sub.find("./stats/og/high").text
else '100') if sub.find("./stats/og/high") is not None
doc['abv']['low'] = (sub.find('./stats/abv/low').text else "1.2"
if sub.find('./stats/abv/low') is not None else '0') )
doc['abv']['high'] = (sub.find('./stats/abv/high').text doc["fg"]["low"] = (
if sub.find('./stats/abv/high') is not None sub.find("./stats/fg/low").text
else '100') if sub.find("./stats/fg/low") is not None
else "1.0"
)
doc["fg"]["high"] = (
sub.find("./stats/fg/high").text
if sub.find("./stats/fg/high") is not None
else "1.2"
)
doc["srm"]["low"] = (
sub.find("./stats/srm/low").text
if sub.find("./stats/srm/low") is not None
else "0"
)
doc["srm"]["high"] = (
sub.find("./stats/srm/high").text
if sub.find("./stats/srm/high") is not None
else "100"
)
doc["abv"]["low"] = (
sub.find("./stats/abv/low").text
if sub.find("./stats/abv/low") is not None
else "0"
)
doc["abv"]["high"] = (
sub.find("./stats/abv/high").text
if sub.find("./stats/abv/high") is not None
else "100"
)
return doc return doc
@ -102,31 +135,34 @@ def import_styles(url):
subs = root.findall('./class[@type="beer"]/category/subcategory') subs = root.findall('./class[@type="beer"]/category/subcategory')
for sub in subs: for sub in subs:
doc = sub_to_doc(sub) doc = sub_to_doc(sub)
if doc['_id'] not in db: if doc["_id"] not in db:
put_doc(doc) put_doc(doc)
def get_styles_list(): def get_styles_list():
"""Returns a list containing id and names of all styles.""" """Returns a list containing id and names of all styles."""
view = get_view('_design/styles', 'by-category') view = get_view("_design/styles", "by-category")
styles = [['', '']] styles = [["", ""]]
for row in view(include_docs=False)['rows']: for row in view(include_docs=False)["rows"]:
styles.append([row['id'], '{}{} {}'.format( styles.append(
row['key'][0], [
row['key'][1], row["id"],
row['value'] "{}{} {}".format(row["key"][0], row["key"][1], row["value"]),
)]) ]
)
return styles return styles
@click.command('import-styles') @click.command("import-styles")
@with_appcontext @with_appcontext
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' (
'/bjcp-guidelines-2015/master/styleguide.xml') "https://raw.githubusercontent.com/meanphil"
"/bjcp-guidelines-2015/master/styleguide.xml"
),
) )
import_styles(url) import_styles(url)
click.echo("Imported BJCP styles.") click.echo("Imported BJCP styles.")
@ -137,41 +173,40 @@ def init_app(app):
app.cli.add_command(import_command) app.cli.add_command(import_command)
@bp.route('/') @bp.route("/")
@login_required @login_required
def index(): def index():
descending = ( descending = request.args.get(
request.args.get('descending', default='false', type=str).lower() in "descending", default="false", type=str
['true', 'yes'] ).lower() in ["true", "yes"]
) sort_by = request.args.get("sort_by", default="category", type=str)
sort_by = request.args.get('sort_by', default='category', type=str) page = request.args.get("page", default=1, type=int)
page = request.args.get('page', default=1, type=int) limit = request.args.get("limit", default=20, type=int)
limit = request.args.get('limit', default=20, type=int)
view = get_view('_design/styles', 'by-{}'.format(sort_by)) view = get_view("_design/styles", "by-{}".format(sort_by))
try: try:
rows = view(include_docs=True, descending=descending)['rows'] rows = view(include_docs=True, descending=descending)["rows"]
except requests.exceptions.HTTPError: except requests.exceptions.HTTPError:
abort(400) abort(400)
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], # noqa
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,
) )
@bp.route('/info/<id>') @bp.route("/info/<id>")
@login_required @login_required
def info(id): def info(id):
return render_template('styles/info.html', style=get_doc_or_404(id)) return render_template("styles/info.html", style=get_doc_or_404(id))
@bp.route('/info/<id>/json') @bp.route("/info/<id>/json")
@login_required @login_required
def info_json(id): def info_json(id):
"""Returns JSON for the style. """Returns JSON for the style.
@ -181,24 +216,26 @@ def info_json(id):
""" """
style = get_doc_or_404(id) style = get_doc_or_404(id)
# Remove fields not needed for specs # Remove fields not needed for specs
if request.args.get('specs', None) is not None: if request.args.get("specs", None) is not None:
return jsonify({ return jsonify(
'ibu': style['ibu'], {
'og': style['og'], "ibu": style["ibu"],
'fg': style['fg'], "og": style["og"],
'abv': style['abv'], "fg": style["fg"],
'srm': style['srm'] "abv": style["abv"],
}) "srm": style["srm"],
}
)
# Remove fields not needed for export # Remove fields not needed for export
style.pop('_id') style.pop("_id")
style.pop('_rev') style.pop("_rev")
style.pop('$type') style.pop("$type")
return jsonify(style) return jsonify(style)
@bp.route('/info/<id>/recipes') @bp.route("/info/<id>/recipes")
def recipes(id): def recipes(id):
style = get_doc_or_404(id) style = get_doc_or_404(id)
view = get_view('_design/recipes', 'by-style') view = get_view("_design/recipes", "by-style")
rows = view(include_docs=True, descending=True, key=id)['rows'] rows = view(include_docs=True, descending=True, key=id)["rows"]
return render_template('styles/recipes.html', style=style, rows=rows) return render_template("styles/recipes.html", style=style, rows=rows)

View file

@ -23,136 +23,150 @@ 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()))
couchurl = os.environ.get('COUCH_URL', 'http://127.0.0.1:5984') couchurl = os.environ.get("COUCH_URL", "http://127.0.0.1:5984")
app = create_app({ app = create_app(
'COUCH_URL': couchurl, {
'COUCH_USERNAME': 'admin', "COUCH_URL": couchurl,
'COUCH_PASSWORD': 'password', "COUCH_USERNAME": "admin",
'COUCH_DATABASE': dbname, "COUCH_PASSWORD": "password",
'WTF_CSRF_ENABLED': False, "COUCH_DATABASE": dbname,
'SECRET_KEY': 'testing', "WTF_CSRF_ENABLED": False,
'HUMULUS_PASSWORD': 'password' "SECRET_KEY": "testing",
}) "HUMULUS_PASSWORD": "password",
}
)
with app.app_context(): with app.app_context():
# Create the database # Create the database
build_couch() build_couch()
# Add a test doc # Add a test doc
put_doc({'data': 'test', '_id': 'foobar'}) put_doc({"data": "test", "_id": "foobar"})
# Add a couple test recipe # Add a couple test recipe
put_doc({ put_doc(
'_id': 'awesome-lager',
'$type': 'recipe',
'type': 'All-Grain',
'efficiency': '65',
'name': 'Awesome Lager',
'notes': 'Test',
'volume': '5.5',
'fermentables': [],
'hops': [],
'style': ''
})
put_doc({
'_id': 'partial-yeast-recipe',
'$type': 'recipe',
'efficiency': '75',
'name': 'Partial Beer',
'type': 'Extract',
'notes': 'Contains only required fields for yeast.',
'volume': '3.5',
'fermentables': [],
'hops': [],
'yeast': {
'name': 'US-05',
'low_attenuation': '60',
'high_attenuation': '72',
},
'style': ''
})
put_doc({
'_id': 'full-recipe',
'$type': 'recipe',
'efficiency': '78',
'type': 'All-Grain',
'name': 'Awesome Beer',
'notes': 'This is a test beer that contains most possible fields.',
'volume': '2.5',
'style': '1A',
'fermentables': [
{ {
'name': '2row', "_id": "awesome-lager",
'type': 'Grain', "$type": "recipe",
'amount': '5', "type": "All-Grain",
'ppg': '37', "efficiency": "65",
'color': '2' "name": "Awesome Lager",
"notes": "Test",
"volume": "5.5",
"fermentables": [],
"hops": [],
"style": "",
}
)
put_doc(
{
"_id": "partial-yeast-recipe",
"$type": "recipe",
"efficiency": "75",
"name": "Partial Beer",
"type": "Extract",
"notes": "Contains only required fields for yeast.",
"volume": "3.5",
"fermentables": [],
"hops": [],
"yeast": {
"name": "US-05",
"low_attenuation": "60",
"high_attenuation": "72",
},
"style": "",
}
)
put_doc(
{
"_id": "full-recipe",
"$type": "recipe",
"efficiency": "78",
"type": "All-Grain",
"name": "Awesome Beer",
"notes": (
"This is a test beer that contains most possible fields."
),
"volume": "2.5",
"style": "1A",
"fermentables": [
{
"name": "2row",
"type": "Grain",
"amount": "5",
"ppg": "37",
"color": "2",
}, },
{ {
'name': 'Dextrose', "name": "Dextrose",
'type': 'Sugar', "type": "Sugar",
'amount': '1', "amount": "1",
'ppg': '46', "ppg": "46",
'color': '1' "color": "1",
},
],
"hops": [
{
"name": "Nugget (US)",
"use": "Boil",
"alpha": "12.5",
"duration": "60",
"amount": "1",
},
{
"name": "CTZ (US)",
"use": "Dry-Hop",
"alpha": "16",
"duration": "5",
"amount": "0.5",
},
],
"yeast": {
"name": "Northern California Ale",
"type": "Liquid",
"lab": "Inland Island",
"code": "INIS-001",
"flocculation": "Medium",
"low_attenuation": "73",
"high_attenuation": "77",
"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",
} }
], ],
'hops': [
{
'name': 'Nugget (US)',
'use': 'Boil',
'alpha': '12.5',
'duration': '60',
'amount': '1'
}, },
{
'name': 'CTZ (US)',
'use': 'Dry-Hop',
'alpha': '16',
'duration': '5',
'amount': '0.5'
} }
], )
'yeast': {
'name': 'Northern California Ale',
'type': 'Liquid',
'lab': 'Inland Island',
'code': 'INIS-001',
'flocculation': 'Medium',
'low_attenuation': '73',
'high_attenuation': '77',
'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'
}]
}
})
# Add a test style # Add a test style
put_doc({ put_doc(
'$type': 'style', {
'_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",
'id': '1A', "ibu": {"high": "100", "low": "0"},
'impression': 'Refreshing', "id": "1A",
'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"},
}
)
yield app yield app
@ -174,14 +188,11 @@ class AuthActions(object):
def __init__(self, client): def __init__(self, client):
self._client = client self._client = client
def login(self, password='password'): def login(self, password="password"):
return self._client.post( return self._client.post("/login", data={"password": password})
'/login',
data={'password': password}
)
def logout(self): def logout(self):
return self._client.get('/logout') return self._client.get("/logout")
@pytest.fixture @pytest.fixture
@ -193,122 +204,122 @@ def auth(client):
def sample_recipes(): def sample_recipes():
"""These sample recipes are useful for testing filters.""" """These sample recipes are useful for testing filters."""
return { return {
'lager': { "lager": {
'efficiency': '72', "efficiency": "72",
'type': 'All-Grain', "type": "All-Grain",
'style': '', "style": "",
'fermentables': [ "fermentables": [
{ {
'amount': '9.5', "amount": "9.5",
'color': '1.80', "color": "1.80",
'name': 'Pale Malt, 2-row (Rahr) (US)', "name": "Pale Malt, 2-row (Rahr) (US)",
'ppg': '37.00', "ppg": "37.00",
'type': 'Grain' "type": "Grain",
}, },
{ {
'amount': '1', "amount": "1",
'color': '0', "color": "0",
'name': 'Corn Sugar (Dextrose)', "name": "Corn Sugar (Dextrose)",
'ppg': '46.00', "ppg": "46.00",
'type': 'Sugar' "type": "Sugar",
} },
], ],
'hops': [ "hops": [
{ {
'alpha': '7.0', "alpha": "7.0",
'amount': '1', "amount": "1",
'duration': '60', "duration": "60",
'name': 'Cluster (US)', "name": "Cluster (US)",
'use': 'Boil' "use": "Boil",
}, },
{ {
'alpha': '2.8', "alpha": "2.8",
'amount': '1', "amount": "1",
'duration': '10.00', "duration": "10.00",
'name': 'Saaz (CZ)', "name": "Saaz (CZ)",
'use': 'Boil' "use": "Boil",
}, },
{ {
'alpha': '2.8', "alpha": "2.8",
'amount': '1.0', "amount": "1.0",
'duration': '5', "duration": "5",
'name': 'Saaz (CZ)', "name": "Saaz (CZ)",
'use': 'Dry-Hop' "use": "Dry-Hop",
} },
], ],
'name': 'Lager', "name": "Lager",
'notes': 'Test simple dry-hopped lager w/ sugar', "notes": "Test simple dry-hopped lager w/ sugar",
'volume': '5.50', "volume": "5.50",
'yeast': { "yeast": {
'abv_tolerance': '15.00', "abv_tolerance": "15.00",
'code': 'WLP940', "code": "WLP940",
'flocculation': 'Medium', "flocculation": "Medium",
'high_attenuation': '78.00', "high_attenuation": "78.00",
'lab': 'White Labs', "lab": "White Labs",
'low_attenuation': '70.00', "low_attenuation": "70.00",
'max_temperature': '55.00', "max_temperature": "55.00",
'min_temperature': '50.00', "min_temperature": "50.00",
'name': 'Mexican Lager', "name": "Mexican Lager",
'type': 'Liquid' "type": "Liquid",
}
}, },
'sweetstout': { },
'efficiency': '72', "sweetstout": {
'type': 'All-Grain', "efficiency": "72",
'style': '', "type": "All-Grain",
'fermentables': [ "style": "",
"fermentables": [
{ {
'amount': '2.75', "amount": "2.75",
'color': '3', "color": "3",
'name': 'Pale Malt, 2-row (UK)', "name": "Pale Malt, 2-row (UK)",
'ppg': '36.00', "ppg": "36.00",
'type': 'Grain' "type": "Grain",
}, },
{ {
'amount': '0.25', "amount": "0.25",
'color': '450', "color": "450",
'name': 'Chocolate Malt (UK)', "name": "Chocolate Malt (UK)",
'ppg': '34.00', "ppg": "34.00",
'type': 'Grain' "type": "Grain",
}, },
{ {
'amount': '0.5', "amount": "0.5",
'color': '0', "color": "0",
'name': 'Lactose', "name": "Lactose",
'ppg': '35.00', "ppg": "35.00",
'type': 'Non-fermentable' "type": "Non-fermentable",
} },
], ],
'hops': [ "hops": [
{ {
'alpha': '5.0', "alpha": "5.0",
'amount': '0.5', "amount": "0.5",
'duration': '60', "duration": "60",
'name': 'East Kent Goldings (UK)', "name": "East Kent Goldings (UK)",
'use': 'Boil' "use": "Boil",
}, },
{ {
'alpha': '5.0', "alpha": "5.0",
'amount': '0.5', "amount": "0.5",
'duration': '30', "duration": "30",
'name': 'East Kent Goldings (UK)', "name": "East Kent Goldings (UK)",
'use': 'Boil' "use": "Boil",
} },
], ],
'name': 'Sweet Stout', "name": "Sweet Stout",
'notes': 'Test stout w/ Lactose', "notes": "Test stout w/ Lactose",
'volume': '2.5', "volume": "2.5",
'yeast': { "yeast": {
'abv_tolerance': '12.00', "abv_tolerance": "12.00",
'code': '', "code": "",
'flocculation': 'High', "flocculation": "High",
'high_attenuation': '77.00', "high_attenuation": "77.00",
'lab': 'Danstar', "lab": "Danstar",
'low_attenuation': '73.00', "low_attenuation": "73.00",
'max_temperature': '70.00', "max_temperature": "70.00",
'min_temperature': '57.00', "min_temperature": "57.00",
'name': 'Nottingham', "name": "Nottingham",
'type': 'Dry' "type": "Dry",
} },
} },
} }

View file

@ -15,30 +15,30 @@
def test_login(client, auth): def test_login(client, auth):
# Test GET # Test GET
response = client.get('/login') response = client.get("/login")
assert response.status_code == 200 assert response.status_code == 200
# Test failed login # Test failed login
data = {'password': 'invalid'} data = {"password": "invalid"}
response = client.post('/login', data=data) response = client.post("/login", data=data)
assert response.status_code == 200 assert response.status_code == 200
assert b'Password is invalid' in response.data assert b"Password is invalid" in response.data
# Test successful login # Test successful login
data = {'password': 'password'} data = {"password": "password"}
response = client.post('/login', data=data) response = client.post("/login", data=data)
assert response.status_code == 302 assert response.status_code == 302
with client.session_transaction() as session: with client.session_transaction() as session:
assert session['logged_in'] assert session["logged_in"]
assert not session.permanent assert not session.permanent
session.clear() session.clear()
# Test permanent login # Test permanent login
data = {'password': 'password', 'permanent': 'y'} data = {"password": "password", "permanent": "y"}
response = client.post('/login', data=data) response = client.post("/login", data=data)
assert response.status_code == 302 assert response.status_code == 302
with client.session_transaction() as session: with client.session_transaction() as session:
assert session['logged_in'] assert session["logged_in"]
assert session.permanent assert session.permanent
@ -46,9 +46,9 @@ def test_logout(client, auth):
# Login # Login
auth.login() auth.login()
with client.session_transaction() as session: with client.session_transaction() as session:
assert session['logged_in'] assert session["logged_in"]
response = client.get('/logout') response = client.get("/logout")
assert response.status_code == 302 assert response.status_code == 302
with client.session_transaction() as session: with client.session_transaction() as session:
assert not session.get('logged_in', False) assert not session.get("logged_in", False)

View file

@ -19,33 +19,33 @@ from humulus.couch import put_doc, get_doc, update_doc, put_designs, get_view
def test_put_doc(app): def test_put_doc(app):
with app.app_context(): with app.app_context():
data = {'foo': 'bar'} data = {"foo": "bar"}
response = put_doc(data) response = put_doc(data)
assert '_id' in response assert "_id" in response
assert 'created' in response assert "created" in response
response = put_doc({'name': 'test'}) response = put_doc({"name": "test"})
assert response['_id'] == 'test' assert response["_id"] == "test"
response = put_doc({'name': 'test'}) response = put_doc({"name": "test"})
assert response['_id'] == 'test-1' assert response["_id"] == "test-1"
response = put_doc({'name': 'test'}) response = put_doc({"name": "test"})
assert response['_id'] == 'test-2' assert response["_id"] == "test-2"
def test_update_doc(app): def test_update_doc(app):
with app.app_context(): with app.app_context():
doc = get_doc('awesome-lager') doc = get_doc("awesome-lager")
rev = doc['_rev'] rev = doc["_rev"]
doc['test'] = 'update' doc["test"] = "update"
update_doc(doc) update_doc(doc)
updated_doc = get_doc('awesome-lager') updated_doc = get_doc("awesome-lager")
assert doc['_id'] == updated_doc['_id'] assert doc["_id"] == updated_doc["_id"]
assert rev < updated_doc['_rev'] assert rev < updated_doc["_rev"]
assert updated_doc['test'] == 'update' assert updated_doc["test"] == "update"
assert 'updated' in updated_doc assert "updated" in updated_doc
def test_build_couch_command(runner, monkeypatch): def test_build_couch_command(runner, monkeypatch):
@ -55,15 +55,15 @@ def test_build_couch_command(runner, monkeypatch):
def fake_build_couch(): def fake_build_couch():
Recorder.called = True Recorder.called = True
monkeypatch.setattr('humulus.couch.build_couch', fake_build_couch) monkeypatch.setattr("humulus.couch.build_couch", fake_build_couch)
result = runner.invoke(args=['build-couch']) result = runner.invoke(args=["build-couch"])
assert 'Built a couch. Please have a seat.' in result.output assert "Built a couch. Please have a seat." in result.output
assert Recorder.called assert Recorder.called
def test_get_doc(app): def test_get_doc(app):
with app.app_context(): with app.app_context():
assert get_doc('foobar')['data'] == 'test' assert get_doc("foobar")["data"] == "test"
def test_put_designs(app, monkeypatch): def test_put_designs(app, monkeypatch):
@ -72,26 +72,26 @@ 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")
assert 'language' in recipes assert "language" in recipes
rev = recipes['_rev'] rev = recipes["_rev"]
# Try again, make sure nothing changed. # Try again, make sure nothing changed.
put_designs() put_designs()
recipes = get_doc('_design/recipes') recipes = get_doc("_design/recipes")
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"]
def test_get_view(app): def test_get_view(app):
with app.app_context(): with app.app_context():
view = get_view('_design/recipes', 'by-date') view = get_view("_design/recipes", "by-date")
assert view()['total_rows'] > 0 assert view()["total_rows"] > 0

View file

@ -14,92 +14,100 @@
from decimal import Decimal from decimal import Decimal
from humulus.filters import (recipe_abv, recipe_fg, recipe_ibu, sort_hops, from humulus.filters import (
recipe_ibu_ratio, recipe_og, recipe_srm, ferm_pct) 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
def test_recipe_og(sample_recipes): def test_recipe_og(sample_recipes):
assert recipe_og(sample_recipes['lager']) == '1.054' assert recipe_og(sample_recipes["lager"]) == "1.054"
assert recipe_og(sample_recipes['sweetstout']) == '1.038' assert recipe_og(sample_recipes["sweetstout"]) == "1.038"
# Remove fermentables, verify 0 is returned # Remove fermentables, verify 0 is returned
sample_recipes['lager'].pop('fermentables') sample_recipes["lager"].pop("fermentables")
assert recipe_og(sample_recipes['lager']) == '0.000' assert recipe_og(sample_recipes["lager"]) == "0.000"
def test_recipe_fg(sample_recipes): def test_recipe_fg(sample_recipes):
assert recipe_fg(sample_recipes['lager']) == '1.014' assert recipe_fg(sample_recipes["lager"]) == "1.014"
assert recipe_fg(sample_recipes['sweetstout']) == '1.015' assert recipe_fg(sample_recipes["sweetstout"]) == "1.015"
# Remove fermentables, verify 0 is returned # Remove fermentables, verify 0 is returned
sample_recipes['lager'].pop('fermentables') sample_recipes["lager"].pop("fermentables")
assert recipe_fg(sample_recipes['lager']) == '0.000' assert recipe_fg(sample_recipes["lager"]) == "0.000"
# Remove yeast, verify 0 is returned # Remove yeast, verify 0 is returned
sample_recipes['sweetstout'].pop('yeast') sample_recipes["sweetstout"].pop("yeast")
assert recipe_fg(sample_recipes['sweetstout']) == '0.000' assert recipe_fg(sample_recipes["sweetstout"]) == "0.000"
def test_recipe_ibu(sample_recipes): def test_recipe_ibu(sample_recipes):
assert recipe_ibu(sample_recipes['lager']) == '24' assert recipe_ibu(sample_recipes["lager"]) == "24"
assert recipe_ibu(sample_recipes['sweetstout']) == '34' assert recipe_ibu(sample_recipes["sweetstout"]) == "34"
# Remove hops, verify 0 is returned # Remove hops, verify 0 is returned
sample_recipes['lager'].pop('hops') sample_recipes["lager"].pop("hops")
assert recipe_ibu(sample_recipes['lager']) == '0' assert recipe_ibu(sample_recipes["lager"]) == "0"
def test_recipe_ibu_ratio(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.44"
assert recipe_ibu_ratio(sample_recipes['sweetstout']) == '0.89' assert recipe_ibu_ratio(sample_recipes["sweetstout"]) == "0.89"
# Remove fermentables, verify 0 is returned # Remove fermentables, verify 0 is returned
sample_recipes['lager'].pop('fermentables') sample_recipes["lager"].pop("fermentables")
assert recipe_ibu_ratio(sample_recipes['lager']) == '0' assert recipe_ibu_ratio(sample_recipes["lager"]) == "0"
# Remove hops, verify 0 is returned # Remove hops, verify 0 is returned
sample_recipes['sweetstout'].pop('hops') sample_recipes["sweetstout"].pop("hops")
assert recipe_ibu_ratio(sample_recipes['sweetstout']) == '0' assert recipe_ibu_ratio(sample_recipes["sweetstout"]) == "0"
def test_recipe_abv(sample_recipes): def test_recipe_abv(sample_recipes):
assert recipe_abv(sample_recipes['lager']) == '5.3' assert recipe_abv(sample_recipes["lager"]) == "5.3"
assert recipe_abv(sample_recipes['sweetstout']) == '3.0' assert recipe_abv(sample_recipes["sweetstout"]) == "3.0"
# Remove fermentables, verify 0 is returned # Remove fermentables, verify 0 is returned
sample_recipes['lager'].pop('fermentables') sample_recipes["lager"].pop("fermentables")
assert recipe_abv(sample_recipes['lager']) == '0' assert recipe_abv(sample_recipes["lager"]) == "0"
# Remove yeast, verify 0 is returned # Remove yeast, verify 0 is returned
sample_recipes['sweetstout'].pop('yeast') sample_recipes["sweetstout"].pop("yeast")
assert recipe_abv(sample_recipes['sweetstout']) == '0' assert recipe_abv(sample_recipes["sweetstout"]) == "0"
def test_recipe_srm(sample_recipes): def test_recipe_srm(sample_recipes):
assert recipe_srm(sample_recipes['lager']) == '3' assert recipe_srm(sample_recipes["lager"]) == "3"
assert recipe_srm(sample_recipes['sweetstout']) == '21' assert recipe_srm(sample_recipes["sweetstout"]) == "21"
# Remove fermentables, verify 0 is returned # Remove fermentables, verify 0 is returned
sample_recipes['lager'].pop('fermentables') sample_recipes["lager"].pop("fermentables")
assert recipe_srm(sample_recipes['lager']) == '0' assert recipe_srm(sample_recipes["lager"]) == "0"
def test_sort_hops(): def test_sort_hops():
# Test with no form # Test with no form
hops = [ hops = [
{'name': '4', 'use': 'Dry-Hop', 'duration': '5'}, {"name": "4", "use": "Dry-Hop", "duration": "5"},
{'name': '3', 'use': 'Whirlpool', 'duration': '10'}, {"name": "3", "use": "Whirlpool", "duration": "10"},
{'name': '2', 'use': 'Boil', 'duration': '5'}, {"name": "2", "use": "Boil", "duration": "5"},
{'name': '1', 'use': 'Boil', 'duration': '15'}, {"name": "1", "use": "Boil", "duration": "15"},
{'name': '0', 'use': 'FWH', 'duration': '60'}, {"name": "0", "use": "FWH", "duration": "60"},
] ]
assert sort_hops(hops) == [ assert sort_hops(hops) == [
{'name': '0', 'use': 'FWH', 'duration': '60'}, {"name": "0", "use": "FWH", "duration": "60"},
{'name': '1', 'use': 'Boil', 'duration': '15'}, {"name": "1", "use": "Boil", "duration": "15"},
{'name': '2', 'use': 'Boil', 'duration': '5'}, {"name": "2", "use": "Boil", "duration": "5"},
{'name': '3', 'use': 'Whirlpool', 'duration': '10'}, {"name": "3", "use": "Whirlpool", "duration": "10"},
{'name': '4', 'use': 'Dry-Hop', 'duration': '5'}, {"name": "4", "use": "Dry-Hop", "duration": "5"},
] ]
# Test with form # Test with form
hop_forms = [] hop_forms = []
for hop in hops: for hop in hops:
form = HopForm() form = HopForm()
form.name.data = hop['name'] form.name.data = hop["name"]
form.use.data = hop['use'] form.use.data = hop["use"]
form.duration.data = Decimal(hop['duration']) form.duration.data = Decimal(hop["duration"])
hop_forms.append(form) hop_forms.append(form)
for num, hop in enumerate(sort_hops(hop_forms, form=True)): for num, hop in enumerate(sort_hops(hop_forms, form=True)):
@ -107,9 +115,9 @@ def test_sort_hops():
def test_ferm_pct(): def test_ferm_pct():
ferms = [{'amount': '4'}, {'amount': '2'}, {'amount': '2'}] ferms = [{"amount": "4"}, {"amount": "2"}, {"amount": "2"}]
assert ferm_pct(ferms) == [ assert ferm_pct(ferms) == [
{'amount': '4', 'pct': 50.0}, {"amount": "4", "pct": 50.0},
{'amount': '2', 'pct': 25.0}, {"amount": "2", "pct": 25.0},
{'amount': '2', 'pct': 25.0} {"amount": "2", "pct": 25.0},
] ]

View file

@ -14,7 +14,7 @@
def test_home(client): def test_home(client):
response = client.get('/') response = client.get("/")
assert response.status_code == 302 assert response.status_code == 302
@ -27,16 +27,16 @@ def test_status(client, monkeypatch):
def exists(self): def exists(self):
return False return False
response = client.get('/status') response = client.get("/status")
assert response.status_code == 200 assert response.status_code == 200
assert response.get_json() == {'ping': 'ok'} assert response.get_json() == {"ping": "ok"}
monkeypatch.setattr('humulus.home.get_db', MockDBTrue) monkeypatch.setattr("humulus.home.get_db", MockDBTrue)
response = client.get('/status?couch=y') response = client.get("/status?couch=y")
assert response.status_code == 200 assert response.status_code == 200
assert response.get_json() == {'ping': 'ok', 'couch': 'ok'} assert response.get_json() == {"ping": "ok", "couch": "ok"}
monkeypatch.setattr('humulus.home.get_db', MockDBFalse) monkeypatch.setattr("humulus.home.get_db", MockDBFalse)
response = client.get('/status?couch=y') response = client.get("/status?couch=y")
assert response.status_code == 500 assert response.status_code == 500
assert response.get_json() == {'ping': 'ok', 'couch': 'not_exist'} assert response.get_json() == {"ping": "ok", "couch": "not_exist"}

View file

@ -17,293 +17,293 @@ from decimal import Decimal
from io import BytesIO from io import BytesIO
from humulus.couch import get_doc from humulus.couch import get_doc
from humulus.recipes import (FermentableForm, HopForm, RecipeForm, YeastForm, from humulus.recipes import (
MashForm, MashStepForm) FermentableForm,
HopForm,
RecipeForm,
YeastForm,
MashForm,
MashStepForm,
)
def test_index(client): def test_index(client):
"""Test success in retrieving index.""" """Test success in retrieving index."""
# Test for bad request # Test for bad request
response = client.get('/recipes/?sort_by=foobar') response = client.get("/recipes/?sort_by=foobar")
assert response.status_code == 400 assert response.status_code == 400
# Verify defaults # Verify defaults
response = client.get('/recipes/') response = client.get("/recipes/")
assert response.status_code == 200 assert response.status_code == 200
# Assert recipes are returned # Assert recipes are returned
assert b'Awesome Lager' in response.data assert b"Awesome Lager" in response.data
assert b'Awesome Beer' in response.data assert b"Awesome Beer" in response.data
assert ( assert (
b'"/recipes/?descending=true&amp;sort_by=name">Name &uarr;' in b'"/recipes/?descending=true&amp;sort_by=name">Name &uarr;'
response.data in response.data
) )
assert ( assert (
b'"/recipes/?descending=false&amp;sort_by=date">Created On' in b'"/recipes/?descending=false&amp;sort_by=date">Created On'
response.data in 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 (
b'"/recipes/?descending=false&amp;sort_by=name">Name &darr;' in b'"/recipes/?descending=false&amp;sort_by=name">Name &darr;'
response.data in response.data
) )
assert ( assert (
b'"/recipes/?descending=false&amp;sort_by=date">Created On' in b'"/recipes/?descending=false&amp;sort_by=date">Created On'
response.data in response.data
) )
# Test sort by date ascending # Test sort by date ascending
response = client.get('/recipes/?descending=false&sort_by=date') response = client.get("/recipes/?descending=false&sort_by=date")
assert ( assert (
b'"/recipes/?descending=false&amp;sort_by=name">Name' in b'"/recipes/?descending=false&amp;sort_by=name">Name' in response.data
response.data
) )
assert ( assert (
b'"/recipes/?descending=true&amp;sort_by=date">Created On &uarr;' in b'"/recipes/?descending=true&amp;sort_by=date">Created On &uarr;'
response.data in response.data
) )
# Test sort by date descending # Test sort by date descending
response = client.get('/recipes/?descending=true&sort_by=date') response = client.get("/recipes/?descending=true&sort_by=date")
assert ( assert (
b'"/recipes/?descending=false&amp;sort_by=name">Name' in b'"/recipes/?descending=false&amp;sort_by=name">Name' in response.data
response.data
) )
assert ( assert (
b'"/recipes/?descending=false&amp;sort_by=date">Created On &darr;' in b'"/recipes/?descending=false&amp;sort_by=date">Created On &darr;'
response.data in response.data
) )
# Test sort by volume ascending # Test sort by volume ascending
response = client.get('/recipes/?descending=false&sort_by=volume') response = client.get("/recipes/?descending=false&sort_by=volume")
assert ( assert (
b'"/recipes/?descending=false&amp;sort_by=name">Name' in b'"/recipes/?descending=false&amp;sort_by=name">Name' in response.data
response.data
) )
assert ( assert (
b'"/recipes/?descending=true&amp;sort_by=volume">Batch Size &uarr;' in b'"/recipes/?descending=true&amp;sort_by=volume">Batch Size &uarr;'
response.data in response.data
) )
# Test sort by volume descending # Test sort by volume descending
response = client.get('/recipes/?descending=true&sort_by=volume') response = client.get("/recipes/?descending=true&sort_by=volume")
assert ( assert (
b'"/recipes/?descending=false&amp;sort_by=name">Name' in b'"/recipes/?descending=false&amp;sort_by=name">Name' in response.data
response.data
) )
assert ( assert (
b'"/recipes/?descending=false&amp;sort_by=volume">Batch Size &darr;' in b'"/recipes/?descending=false&amp;sort_by=volume">Batch Size &darr;'
response.data in response.data
) )
# Test sort by type ascending # Test sort by type ascending
response = client.get('/recipes/?descending=false&sort_by=type') response = client.get("/recipes/?descending=false&sort_by=type")
assert ( assert (
b'"/recipes/?descending=false&amp;sort_by=name">Name' in b'"/recipes/?descending=false&amp;sort_by=name">Name' in response.data
response.data
) )
assert ( assert (
b'"/recipes/?descending=true&amp;sort_by=type">Type &uarr;' in b'"/recipes/?descending=true&amp;sort_by=type">Type &uarr;'
response.data in response.data
) )
# Test sort by type descending # Test sort by type descending
response = client.get('/recipes/?descending=true&sort_by=type') response = client.get("/recipes/?descending=true&sort_by=type")
assert ( assert (
b'"/recipes/?descending=false&amp;sort_by=name">Name' in b'"/recipes/?descending=false&amp;sort_by=name">Name' in response.data
response.data
) )
assert ( assert (
b'"/recipes/?descending=false&amp;sort_by=type">Type &darr;' in b'"/recipes/?descending=false&amp;sort_by=type">Type &darr;'
response.data in 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
response = client.get('/recipes/create') response = client.get("/recipes/create")
assert response.status_code == 302 assert response.status_code == 302
# Test GET with login # Test GET with login
auth.login() auth.login()
response = client.get('/recipes/create') response = client.get("/recipes/create")
assert response.status_code == 200 assert response.status_code == 200
# Test POST # Test POST
data = { data = {
'efficiency': '65', "efficiency": "65",
'name': 'Test', "name": "Test",
'notes': 'Test', "notes": "Test",
'volume': '5.5', "volume": "5.5",
'style': '1A' "style": "1A",
} }
response = client.post('/recipes/create', data=data) response = client.post("/recipes/create", data=data)
assert response.status_code == 302 assert response.status_code == 302
with app.app_context(): with app.app_context():
doc = get_doc('test') doc = get_doc("test")
assert doc['name'] == 'Test' assert doc["name"] == "Test"
assert doc['notes'] == 'Test' assert doc["notes"] == "Test"
assert doc['volume'] == '5.5' assert doc["volume"] == "5.5"
assert doc['efficiency'] == '65' assert doc["efficiency"] == "65"
assert doc['style'] == '1A' assert doc["style"] == "1A"
def test_update(client, app, auth): def test_update(client, app, auth):
"""Test success in updating a recipe document.""" """Test success in updating a recipe document."""
# Test GET without login # Test GET without login
response = client.get('/recipes/update/awesome-lager') response = client.get("/recipes/update/awesome-lager")
assert response.status_code == 302 assert response.status_code == 302
auth.login() auth.login()
# Test GET on a bare minimum recipe # Test GET on a bare minimum recipe
response = client.get('/recipes/update/awesome-lager') response = client.get("/recipes/update/awesome-lager")
assert response.status_code == 200 assert response.status_code == 200
assert b'Awesome Lager' in response.data assert b"Awesome Lager" in response.data
# Test GET on a more complete recipe # Test GET on a more complete recipe
response = client.get('/recipes/update/full-recipe') response = client.get("/recipes/update/full-recipe")
assert response.status_code == 200 assert response.status_code == 200
test_items = [ test_items = [
b'Awesome Beer', b"Awesome Beer",
b'2row', b"2row",
b'Dextrose', b"Dextrose",
b'Nugget (US)', b"Nugget (US)",
b'CTZ (US)', b"CTZ (US)",
b'Northern California Ale' b"Northern California Ale",
] ]
for item in test_items: for item in test_items:
assert item in response.data assert item in response.data
# Test GET on a recipe missing most yeast fields # Test GET on a recipe missing most yeast fields
response = client.get('/recipes/update/partial-yeast-recipe') response = client.get("/recipes/update/partial-yeast-recipe")
assert response.status_code == 200 assert response.status_code == 200
test_items = [ test_items = [b"Partial Beer", b"US-05"]
b'Partial Beer',
b'US-05'
]
for item in test_items: for item in test_items:
assert item in response.data assert item in response.data
# Get a doc, make an update, and test a POST # Get a doc, make an update, and test a POST
id = 'awesome-lager' id = "awesome-lager"
with app.app_context(): with app.app_context():
doc = get_doc(id) doc = get_doc(id)
# Remove unneeded fields # Remove unneeded fields
doc.pop('_id') doc.pop("_id")
rev = doc.pop('_rev') rev = doc.pop("_rev")
response = client.post('/recipes/update/awesome-lager', response = client.post(
query_string={'rev': rev}, data=doc) "/recipes/update/awesome-lager", query_string={"rev": rev}, data=doc
)
assert response.status_code == 302 assert response.status_code == 302
# Test response without valid/conflicted rev # Test response without valid/conflicted rev
response = client.post('/recipes/update/awesome-lager', response = client.post(
query_string={'rev': ''}, data=doc) "/recipes/update/awesome-lager", 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
def test_info(client, monkeypatch): def test_info(client, monkeypatch):
"""Test success in retrieving a recipe document.""" """Test success in retrieving a recipe document."""
def mock_get_doc(id): def mock_get_doc(id):
# This function always raises KeyError # This function always raises KeyError
raise KeyError(id) raise KeyError(id)
# Validate 404 # Validate 404
response = client.get('/recipes/info/thisdoesnotexist') response = client.get("/recipes/info/thisdoesnotexist")
assert response.status_code == 404 assert response.status_code == 404
# Validate response for existing doc # Validate response for existing doc
response = client.get('/recipes/info/awesome-lager') response = client.get("/recipes/info/awesome-lager")
assert response.status_code == 200 assert response.status_code == 200
assert b'Awesome Lager' in response.data assert b"Awesome Lager" in response.data
# Validate response for recipe with style # Validate response for recipe with style
response = client.get('/recipes/info/full-recipe') response = client.get("/recipes/info/full-recipe")
assert response.status_code == 200 assert response.status_code == 200
assert b'Awesome Beer' in response.data assert b"Awesome Beer" in response.data
assert b'Test Style' in response.data assert b"Test Style" in response.data
# Validate warning is flashed when style cannot be found # Validate warning is flashed when style cannot be found
monkeypatch.setattr('humulus.recipes.get_doc', mock_get_doc) monkeypatch.setattr("humulus.recipes.get_doc", mock_get_doc)
response = client.get('/recipes/info/full-recipe') response = client.get("/recipes/info/full-recipe")
assert b'Could not find style' in response.data assert b"Could not find style" in response.data
def test_info_json(client): def test_info_json(client):
"""Test success in retrieving a JSON recipe.""" """Test success in retrieving a JSON recipe."""
# Validate 404 # Validate 404
response = client.get('/recipes/info/thisdoesnotexist/json') response = client.get("/recipes/info/thisdoesnotexist/json")
assert response.status_code == 404 assert response.status_code == 404
# Validate response for existing doc # Validate response for existing doc
response = client.get('/recipes/info/awesome-lager/json') response = client.get("/recipes/info/awesome-lager/json")
assert response.status_code == 200 assert response.status_code == 200
assert response.is_json assert response.is_json
assert response.get_json()['name'] == 'Awesome Lager' assert response.get_json()["name"] == "Awesome Lager"
def test_step_form_doc(app): def test_step_form_doc(app):
"""Evaluates conditionals in generation of doc from a step form.""" """Evaluates conditionals in generation of doc from a step form."""
step = MashStepForm() step = MashStepForm()
step.name.data = 'Test Mash Step' step.name.data = "Test Mash Step"
step.type.data = 'Infusion' step.type.data = "Infusion"
step.temp.data = Decimal('152') step.temp.data = Decimal("152")
step.time.data = Decimal('60') step.time.data = Decimal("60")
assert step.doc == { assert step.doc == {
'name': 'Test Mash Step', "name": "Test Mash Step",
'type': 'Infusion', "type": "Infusion",
'temp': '152', "temp": "152",
'time': '60' "time": "60",
} }
step.amount.data = Decimal('3.5') step.amount.data = Decimal("3.5")
assert step.doc == { assert step.doc == {
'name': 'Test Mash Step', "name": "Test Mash Step",
'type': 'Infusion', "type": "Infusion",
'temp': '152', "temp": "152",
'time': '60', "time": "60",
'amount': '3.5' "amount": "3.5",
} }
def test_yeast_form_doc(app): def test_yeast_form_doc(app):
"""Evaluates conditionals in generation of doc from a yeast form.""" """Evaluates conditionals in generation of doc from a yeast form."""
yeast = YeastForm() yeast = YeastForm()
yeast.name.data = 'Test' yeast.name.data = "Test"
yeast.low_attenuation.data = Decimal('60') yeast.low_attenuation.data = Decimal("60")
yeast.high_attenuation.data = Decimal('75') yeast.high_attenuation.data = Decimal("75")
assert yeast.doc == { assert yeast.doc == {
'name': 'Test', "name": "Test",
'low_attenuation': '60', "low_attenuation": "60",
'high_attenuation': '75' "high_attenuation": "75",
} }
yeast.type.data = 'Dry' yeast.type.data = "Dry"
yeast.code.data = 'INIS-001' yeast.code.data = "INIS-001"
yeast.lab.data = 'Inland Island' yeast.lab.data = "Inland Island"
yeast.flocculation.data = 'Low' yeast.flocculation.data = "Low"
yeast.min_temperature.data = Decimal('40') yeast.min_temperature.data = Decimal("40")
yeast.max_temperature.data = Decimal('50') yeast.max_temperature.data = Decimal("50")
yeast.abv_tolerance.data = Decimal('15') yeast.abv_tolerance.data = Decimal("15")
assert yeast.doc == { assert yeast.doc == {
'name': 'Test', "name": "Test",
'low_attenuation': '60', "low_attenuation": "60",
'high_attenuation': '75', "high_attenuation": "75",
'flocculation': 'Low', "flocculation": "Low",
'type': 'Dry', "type": "Dry",
'code': 'INIS-001', "code": "INIS-001",
'lab': 'Inland Island', "lab": "Inland Island",
'min_temperature': '40', "min_temperature": "40",
'max_temperature': '50', "max_temperature": "50",
'abv_tolerance': '15' "abv_tolerance": "15",
} }
@ -316,52 +316,52 @@ def test_recipe_form_doc(app):
with app.app_context(): with app.app_context():
recipe = RecipeForm() recipe = RecipeForm()
recipe.name.data = 'Test' recipe.name.data = "Test"
recipe.efficiency.data = Decimal('65') recipe.efficiency.data = Decimal("65")
recipe.volume.data = Decimal('5.5') recipe.volume.data = Decimal("5.5")
recipe.notes.data = 'This is a test' recipe.notes.data = "This is a test"
recipe.type.data = 'All-Grain' recipe.type.data = "All-Grain"
recipe.style.data = '1A' recipe.style.data = "1A"
assert recipe.doc == { assert recipe.doc == {
'name': 'Test', "name": "Test",
'efficiency': '65', "efficiency": "65",
'type': 'All-Grain', "type": "All-Grain",
'volume': '5.5', "volume": "5.5",
'notes': 'This is a test', "notes": "This is a test",
'fermentables': [], "fermentables": [],
'hops': [], "hops": [],
'$type': 'recipe', "$type": "recipe",
'style': '1A' "style": "1A",
} }
ferm = FermentableForm() ferm = FermentableForm()
ferm.name.data = 'Test' ferm.name.data = "Test"
ferm.type.data = 'Grain' ferm.type.data = "Grain"
ferm.amount.data = Decimal('5.5') ferm.amount.data = Decimal("5.5")
ferm.ppg.data = Decimal('37') ferm.ppg.data = Decimal("37")
ferm.color.data = Decimal('1.8') ferm.color.data = Decimal("1.8")
hop = HopForm() hop = HopForm()
hop.name.data = 'Test' hop.name.data = "Test"
hop.use.data = 'Boil' hop.use.data = "Boil"
hop.alpha.data = Decimal('12.5') hop.alpha.data = Decimal("12.5")
hop.duration.data = Decimal('60') hop.duration.data = Decimal("60")
hop.amount.data = Decimal('0.5') hop.amount.data = Decimal("0.5")
yeast = YeastForm() yeast = YeastForm()
yeast.name.data = 'Test' yeast.name.data = "Test"
yeast.low_attenuation.data = '70' yeast.low_attenuation.data = "70"
yeast.high_attenuation.data = '75' yeast.high_attenuation.data = "75"
step = MashStepForm() step = MashStepForm()
step.name.data = 'Test Mash Step' step.name.data = "Test Mash Step"
step.type.data = 'Infusion' step.type.data = "Infusion"
step.temp.data = Decimal('152') step.temp.data = Decimal("152")
step.time.data = Decimal('60') step.time.data = Decimal("60")
step.amount.data = Decimal('3.5') step.amount.data = Decimal("3.5")
mash = MashForm() mash = MashForm()
mash.name.data = 'Single Infusion' mash.name.data = "Single Infusion"
mash.steps = [step] mash.steps = [step]
recipe.fermentables = [ferm] recipe.fermentables = [ferm]
@ -370,202 +370,249 @@ def test_recipe_form_doc(app):
recipe.yeast = yeast recipe.yeast = yeast
assert recipe.doc == { assert recipe.doc == {
'name': 'Test', "name": "Test",
'efficiency': '65', "efficiency": "65",
'type': 'All-Grain', "type": "All-Grain",
'volume': '5.5', "volume": "5.5",
'notes': 'This is a test', "notes": "This is a test",
'$type': 'recipe', "$type": "recipe",
'style': '1A', "style": "1A",
'fermentables': [{ "fermentables": [
'name': 'Test', {
'type': 'Grain', "name": "Test",
'amount': '5.5', "type": "Grain",
'ppg': '37', "amount": "5.5",
'color': '1.8', "ppg": "37",
}], "color": "1.8",
'hops': [{
'name': 'Test',
'use': 'Boil',
'alpha': '12.5',
'duration': '60',
'amount': '0.5'
}],
'yeast': {
'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'
}]
} }
],
"hops": [
{
"name": "Test",
"use": "Boil",
"alpha": "12.5",
"duration": "60",
"amount": "0.5",
}
],
"yeast": {
"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",
}
],
},
} }
def test_recipe_delete(client, auth): def test_recipe_delete(client, auth):
"""Test success in deleting a document.""" """Test success in deleting a document."""
# Try to delete a document without logging in # Try to delete a document without logging in
response = client.post('/recipes/delete/awesome-lager') response = client.post("/recipes/delete/awesome-lager")
response = client.get('/recipes/info/awesome-lager') response = client.get("/recipes/info/awesome-lager")
assert response.status_code == 200 assert response.status_code == 200
# Delete document after login # Delete document after login
auth.login() auth.login()
# Try to delete a document without logging in # Try to delete a document without logging in
response = client.post('/recipes/delete/awesome-lager') response = client.post("/recipes/delete/awesome-lager")
response = client.get('/recipes/info/awesome-lager') response = client.get("/recipes/info/awesome-lager")
assert response.status_code == 404 assert response.status_code == 404
def test_recipe_create_json(client, sample_recipes, auth): def test_recipe_create_json(client, sample_recipes, auth):
"""Test uploading JSON recipe.""" """Test uploading JSON recipe."""
# Test GET without logging in # Test GET without logging in
response = client.get('/recipes/create/json') response = client.get("/recipes/create/json")
assert response.status_code == 302 assert response.status_code == 302
# Test GET after logging in # Test GET after logging in
auth.login() auth.login()
response = client.get('/recipes/create/json') response = client.get("/recipes/create/json")
assert response.status_code == 200 assert response.status_code == 200
# Test upload some good data # Test upload some good data
data = { data = {
'upload': (BytesIO(json.dumps(sample_recipes['sweetstout']).encode()), "upload": (
'sweetstout.json') BytesIO(json.dumps(sample_recipes["sweetstout"]).encode()),
"sweetstout.json",
)
} }
response = client.post('/recipes/create/json', buffered=True, response = client.post(
content_type='multipart/form-data', data=data) "/recipes/create/json",
buffered=True,
content_type="multipart/form-data",
data=data,
)
assert response.status_code == 302 assert response.status_code == 302
assert 'recipes/info/sweet-stout' in response.headers['Location'] assert "recipes/info/sweet-stout" in response.headers["Location"]
# Test upload with some bad data # Test upload with some bad data
data = {'upload': (BytesIO(b'NOT JSON'), 'file')} data = {"upload": (BytesIO(b"NOT JSON"), "file")}
response = client.post('/recipes/create/json', buffered=True, response = client.post(
content_type='multipart/form-data', data=data) "/recipes/create/json",
buffered=True,
content_type="multipart/form-data",
data=data,
)
assert response.status_code == 200 assert response.status_code == 200
def test_copyfrom(app, sample_recipes): def test_copyfrom(app, sample_recipes):
recipe = { recipe = {
'name': 'Test', "name": "Test",
'type': 'All-Grain', "type": "All-Grain",
'efficiency': '65', "efficiency": "65",
'volume': '5.5', "volume": "5.5",
'notes': 'Notes', "notes": "Notes",
'style': '18A', "style": "18A",
'fermentables': [{ "fermentables": [
'name': 'Test', {
'type': 'Grain', "name": "Test",
'amount': '1', "type": "Grain",
'ppg': '36', "amount": "1",
'color': '4' "ppg": "36",
}], "color": "4",
'hops': [{ }
'name': 'Test', ],
'use': 'Boil', "hops": [
'alpha': '5.5', {
'duration': '30', "name": "Test",
'amount': '1' "use": "Boil",
}] "alpha": "5.5",
"duration": "30",
"amount": "1",
}
],
} }
with app.app_context(): with app.app_context():
form = RecipeForm() form = RecipeForm()
form.copyfrom(recipe) form.copyfrom(recipe)
assert form.name.data == recipe['name'] assert form.name.data == recipe["name"]
assert form.type.data == recipe['type'] assert form.type.data == recipe["type"]
assert form.efficiency.data == Decimal(recipe['efficiency']) assert form.efficiency.data == Decimal(recipe["efficiency"])
assert form.volume.data == Decimal(recipe['volume']) assert form.volume.data == Decimal(recipe["volume"])
assert form.notes.data == recipe['notes'] assert form.notes.data == recipe["notes"]
assert len(form.fermentables) == len(recipe['fermentables']) assert len(form.fermentables) == len(recipe["fermentables"])
assert form.fermentables[0].form.name.data == \ assert (
recipe['fermentables'][0]['name'] form.fermentables[0].form.name.data
assert form.fermentables[0].form.type.data == \ == recipe["fermentables"][0]["name"]
recipe['fermentables'][0]['type'] )
assert form.fermentables[0].form.amount.data == \ assert (
Decimal(recipe['fermentables'][0]['amount']) form.fermentables[0].form.type.data
assert form.fermentables[0].form.ppg.data == \ == recipe["fermentables"][0]["type"]
Decimal(recipe['fermentables'][0]['ppg']) )
assert form.fermentables[0].form.color.data == \ assert form.fermentables[0].form.amount.data == Decimal(
Decimal(recipe['fermentables'][0]['color']) recipe["fermentables"][0]["amount"]
assert len(form.hops) == len(recipe['hops']) )
assert form.hops[0].form.name.data == recipe['hops'][0]['name'] assert form.fermentables[0].form.ppg.data == Decimal(
assert form.hops[0].form.use.data == recipe['hops'][0]['use'] recipe["fermentables"][0]["ppg"]
assert form.hops[0].form.alpha.data == Decimal(recipe['hops'][0]['alpha']) )
assert form.hops[0].form.duration.data == \ assert form.fermentables[0].form.color.data == Decimal(
Decimal(recipe['hops'][0]['duration']) recipe["fermentables"][0]["color"]
assert form.hops[0].form.amount.data == \ )
Decimal(recipe['hops'][0]['amount']) 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'] = { recipe["yeast"] = {
'name': 'Test', 'low_attenuation': '65', 'high_attenuation': '68' "name": "Test",
"low_attenuation": "65",
"high_attenuation": "68",
} }
recipe['mash'] = {} recipe["mash"] = {}
with app.app_context(): with app.app_context():
form = RecipeForm() form = RecipeForm()
form.copyfrom(recipe) form.copyfrom(recipe)
assert form.yeast.form.name.data == recipe['yeast']['name'] assert form.yeast.form.name.data == recipe["yeast"]["name"]
assert form.yeast.form.low_attenuation.data == \ assert form.yeast.form.low_attenuation.data == Decimal(
Decimal(recipe['yeast']['low_attenuation']) recipe["yeast"]["low_attenuation"]
assert form.yeast.form.high_attenuation.data == \ )
Decimal(recipe['yeast']['high_attenuation']) assert form.yeast.form.high_attenuation.data == Decimal(
recipe["yeast"]["high_attenuation"]
)
recipe['yeast'].update({ recipe["yeast"].update(
'type': 'Liquid', {
'lab': 'Test', "type": "Liquid",
'code': 'Test', "lab": "Test",
'flocculation': 'Low', "code": "Test",
'min_temperature': '65', "flocculation": "Low",
'max_temperature': '68', "min_temperature": "65",
'abv_tolerance': '15' "max_temperature": "68",
}) "abv_tolerance": "15",
}
)
with app.app_context(): with app.app_context():
form = RecipeForm() form = RecipeForm()
form.copyfrom(recipe) form.copyfrom(recipe)
assert form.yeast.form.type.data == recipe['yeast']['type'] assert form.yeast.form.type.data == recipe["yeast"]["type"]
assert form.yeast.form.lab.data == recipe['yeast']['lab'] assert form.yeast.form.lab.data == recipe["yeast"]["lab"]
assert form.yeast.form.code.data == recipe['yeast']['code'] assert form.yeast.form.code.data == recipe["yeast"]["code"]
assert form.yeast.form.flocculation.data == recipe['yeast']['flocculation'] assert form.yeast.form.flocculation.data == recipe["yeast"]["flocculation"]
assert form.yeast.form.min_temperature.data == \ assert form.yeast.form.min_temperature.data == Decimal(
Decimal(recipe['yeast']['min_temperature']) recipe["yeast"]["min_temperature"]
assert form.yeast.form.max_temperature.data == \ )
Decimal(recipe['yeast']['max_temperature']) assert form.yeast.form.max_temperature.data == Decimal(
assert form.yeast.form.abv_tolerance.data == \ recipe["yeast"]["max_temperature"]
Decimal(recipe['yeast']['abv_tolerance']) )
assert form.yeast.form.abv_tolerance.data == Decimal(
recipe["yeast"]["abv_tolerance"]
)
recipe['mash'] = { recipe["mash"] = {
'name': 'Test', "name": "Test",
'steps': [{ "steps": [
'name': 'Infusion', {
'type': 'Infusion', "name": "Infusion",
'temp': '152', "type": "Infusion",
'time': '60' "temp": "152",
}] "time": "60",
}
],
} }
with app.app_context(): with app.app_context():
form = RecipeForm() form = RecipeForm()
form.copyfrom(recipe) form.copyfrom(recipe)
assert form.mash.form.name.data == recipe['mash']['name'] assert form.mash.form.name.data == recipe["mash"]["name"]
assert len(form.mash.form.steps) == len(recipe['mash']['steps']) assert len(form.mash.form.steps) == len(recipe["mash"]["steps"])
assert form.mash.form.steps[0].form.name.data == \ assert (
recipe['mash']['steps'][0]['name'] form.mash.form.steps[0].form.name.data
assert form.mash.form.steps[0].form.type.data == \ == recipe["mash"]["steps"][0]["name"]
recipe['mash']['steps'][0]['type'] )
assert form.mash.form.steps[0].form.temp.data == \ assert (
Decimal(recipe['mash']['steps'][0]['temp']) form.mash.form.steps[0].form.type.data
assert form.mash.form.steps[0].form.time.data == \ == recipe["mash"]["steps"][0]["type"]
Decimal(recipe['mash']['steps'][0]['time']) )
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' recipe["mash"]["steps"][0]["amount"] = "3.5"
with app.app_context(): with app.app_context():
form = RecipeForm() form = RecipeForm()
form.copyfrom(recipe) form.copyfrom(recipe)
assert form.mash.form.steps[0].form.amount.data == \ assert form.mash.form.steps[0].form.amount.data == Decimal(
Decimal(recipe['mash']['steps'][0]['amount']) recipe["mash"]["steps"][0]["amount"]
)

View file

@ -16,7 +16,7 @@ import xml.etree.ElementTree as ET
from humulus.styles import import_styles, sub_to_doc, get_styles_list from humulus.styles import import_styles, sub_to_doc, get_styles_list
COMPLETE_STYLE = '''<subcategory id="1A"> COMPLETE_STYLE = """<subcategory id="1A">
<name>Test Style</name> <name>Test Style</name>
<aroma>Smelly</aroma> <aroma>Smelly</aroma>
<appearance>Good looking</appearance> <appearance>Good looking</appearance>
@ -52,9 +52,9 @@ COMPLETE_STYLE = '''<subcategory id="1A">
</abv> </abv>
</stats> </stats>
</subcategory> </subcategory>
''' """
INCOMPLETE_STYLE = '''<subcategory id="2B"> INCOMPLETE_STYLE = """<subcategory id="2B">
<name>Test Style</name> <name>Test Style</name>
<aroma>Smelly</aroma> <aroma>Smelly</aroma>
<appearance>Good looking</appearance> <appearance>Good looking</appearance>
@ -62,9 +62,9 @@ INCOMPLETE_STYLE = '''<subcategory id="2B">
<mouthfeel>Good feeling</mouthfeel> <mouthfeel>Good feeling</mouthfeel>
<impression>Refreshing</impression> <impression>Refreshing</impression>
</subcategory> </subcategory>
''' """
TEST_XML = '''<?xml version="1.0" encoding="UTF-8"?> TEST_XML = """<?xml version="1.0" encoding="UTF-8"?>
<styleguide> <styleguide>
<class type="beer"> <class type="beer">
<category id="1"> <category id="1">
@ -79,46 +79,46 @@ TEST_XML = '''<?xml version="1.0" encoding="UTF-8"?>
</category> </category>
</class> </class>
</styleguide> </styleguide>
''' """
def test_sub_to_doc(): def test_sub_to_doc():
assert sub_to_doc(ET.fromstring(COMPLETE_STYLE)) == { assert sub_to_doc(ET.fromstring(COMPLETE_STYLE)) == {
'_id': '1A', "_id": "1A",
'$type': 'style', "$type": "style",
'name': 'Test Style', "name": "Test Style",
'aroma': 'Smelly', "aroma": "Smelly",
'appearance': 'Good looking', "appearance": "Good looking",
'flavor': 'Good tasting', "flavor": "Good tasting",
'mouthfeel': 'Good feeling', "mouthfeel": "Good feeling",
'impression': 'Refreshing', "impression": "Refreshing",
'comments': 'Comments', "comments": "Comments",
'history': 'Old', "history": "Old",
'ingredients': 'Grains, Hops, and Water', "ingredients": "Grains, Hops, and Water",
'comparison': 'Comparison', "comparison": "Comparison",
'examples': 'Examples', "examples": "Examples",
'tags': ['one', 'two'], "tags": ["one", "two"],
'ibu': {'low': '1', 'high': '2'}, "ibu": {"low": "1", "high": "2"},
'og': {'low': '1.010', 'high': '1.020'}, "og": {"low": "1.010", "high": "1.020"},
'fg': {'low': '1.000', 'high': '1.010'}, "fg": {"low": "1.000", "high": "1.010"},
'srm': {'low': '1', 'high': '2'}, "srm": {"low": "1", "high": "2"},
'abv': {'low': '1', 'high': '2'} "abv": {"low": "1", "high": "2"},
} }
assert sub_to_doc(ET.fromstring(INCOMPLETE_STYLE)) == { assert sub_to_doc(ET.fromstring(INCOMPLETE_STYLE)) == {
'_id': '2B', "_id": "2B",
'$type': 'style', "$type": "style",
'name': 'Test Style', "name": "Test Style",
'aroma': 'Smelly', "aroma": "Smelly",
'appearance': 'Good looking', "appearance": "Good looking",
'flavor': 'Good tasting', "flavor": "Good tasting",
'mouthfeel': 'Good feeling', "mouthfeel": "Good feeling",
'impression': 'Refreshing', "impression": "Refreshing",
'ibu': {'low': '0', 'high': '100'}, "ibu": {"low": "0", "high": "100"},
'og': {'low': '1.0', 'high': '1.2'}, "og": {"low": "1.0", "high": "1.2"},
'fg': {'low': '1.0', 'high': '1.2'}, "fg": {"low": "1.0", "high": "1.2"},
'srm': {'low': '0', 'high': '100'}, "srm": {"low": "0", "high": "100"},
'abv': {'low': '0', 'high': '100'} "abv": {"low": "0", "high": "100"},
} }
@ -138,30 +138,31 @@ def test_import_styles(monkeypatch):
def fake_requests_get(url): def fake_requests_get(url):
class TestXML: class TestXML:
text = TEST_XML text = TEST_XML
return TestXML return TestXML
monkeypatch.setattr('requests.get', fake_requests_get) monkeypatch.setattr("requests.get", fake_requests_get)
monkeypatch.setattr('humulus.styles.get_db', fake_get_db) monkeypatch.setattr("humulus.styles.get_db", fake_get_db)
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 == { assert PutRecorder.doc == {
'$type': 'style', "$type": "style",
'_id': '1A', "_id": "1A",
'abv': {'high': '100', 'low': '0'}, "abv": {"high": "100", "low": "0"},
'appearance': 'Good looking', "appearance": "Good looking",
'aroma': 'Smelly', "aroma": "Smelly",
'fg': {'high': '1.2', 'low': '1.0'}, "fg": {"high": "1.2", "low": "1.0"},
'flavor': 'Good tasting', "flavor": "Good tasting",
'ibu': {'high': '100', 'low': '0'}, "ibu": {"high": "100", "low": "0"},
'impression': 'Refreshing', "impression": "Refreshing",
'mouthfeel': 'Good feeling', "mouthfeel": "Good feeling",
'name': 'Test Style', "name": "Test Style",
'og': {'high': '1.2', 'low': '1.0'}, "og": {"high": "1.2", "low": "1.0"},
'srm': {'high': '100', 'low': '0'} "srm": {"high": "100", "low": "0"},
} }
MockDB.db = {'1A': ''} MockDB.db = {"1A": ""}
PutRecorder.doc = None PutRecorder.doc = None
import_styles(None) import_styles(None)
assert PutRecorder.doc is None assert PutRecorder.doc is None
@ -174,76 +175,73 @@ def test_import_command(runner, monkeypatch):
def fake_import_styles(url): def fake_import_styles(url):
Recorder.called = True Recorder.called = True
monkeypatch.setattr('humulus.styles.import_styles', fake_import_styles) monkeypatch.setattr("humulus.styles.import_styles", fake_import_styles)
result = runner.invoke(args=['import-styles']) result = runner.invoke(args=["import-styles"])
assert Recorder.called assert Recorder.called
assert 'Imported BJCP styles.' in result.output assert "Imported BJCP styles." in result.output
def test_get_styles_choices(app): def test_get_styles_choices(app):
"""Test success in getting list of styles.""" """Test success in getting list of styles."""
with app.app_context(): with app.app_context():
styles = get_styles_list() styles = get_styles_list()
assert styles == [ assert styles == [["", ""], ["1A", "1A Test Style"]]
['', ''],
['1A', '1A Test Style']
]
def test_index(auth, client): def test_index(auth, client):
"""Test success in retrieving index.""" """Test success in retrieving index."""
# Test not logged in # Test not logged in
response = client.get('/styles/') response = client.get("/styles/")
assert response.status_code == 302 assert response.status_code == 302
# Login and test get # Login and test get
auth.login() auth.login()
response = client.get('/styles/') response = client.get("/styles/")
assert response.status_code == 200 assert response.status_code == 200
assert b'1A' in response.data assert b"1A" in response.data
assert b'Test Style' in response.data assert b"Test Style" in response.data
# Test for bad request # Test for bad request
response = client.get('/styles/?sort_by=foobar') response = client.get("/styles/?sort_by=foobar")
assert response.status_code == 400 assert response.status_code == 400
def test_info(auth, client): def test_info(auth, client):
"""Test success in retrieving a style's info page""" """Test success in retrieving a style's info page"""
# Test not logged in # Test not logged in
response = client.get('/styles/info/1A') response = client.get("/styles/info/1A")
assert response.status_code == 302 assert response.status_code == 302
# Login and test # Login and test
auth.login() auth.login()
response = client.get('/styles/info/1A') response = client.get("/styles/info/1A")
assert response.status_code == 200 assert response.status_code == 200
assert b'1A' in response.data assert b"1A" in response.data
assert b'Test Style' in response.data assert b"Test Style" in response.data
def test_recipes(client): def test_recipes(client):
"""Test success in retrieving list of recipes matching style.""" """Test success in retrieving list of recipes matching style."""
response = client.get('/styles/info/1A/recipes') response = client.get("/styles/info/1A/recipes")
assert response.status_code == 200 assert response.status_code == 200
assert b'Awesome Beer' in response.data assert b"Awesome Beer" in response.data
def test_info_json(auth, client): def test_info_json(auth, client):
"""Test success in retrieving a style's json document.""" """Test success in retrieving a style's json document."""
# Test not logged in # Test not logged in
response = client.get('/styles/info/1A/json') response = client.get("/styles/info/1A/json")
assert response.status_code == 302 assert response.status_code == 302
# Login and test # Login and test
auth.login() auth.login()
response = client.get('/styles/info/1A/json') response = client.get("/styles/info/1A/json")
assert response.status_code == 200 assert response.status_code == 200
assert response.is_json assert response.is_json
assert response.get_json()['name'] == 'Test Style' assert response.get_json()["name"] == "Test Style"
# Test for specs only # Test for specs only
response = client.get('/styles/info/1A/json?specs=y') response = client.get("/styles/info/1A/json?specs=y")
assert response.status_code == 200 assert response.status_code == 200
assert response.is_json assert response.is_json
assert 'name' not in response.get_json() assert "name" not in response.get_json()