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

Formatting fixes for black

This commit is contained in:
Emma 2019-07-22 13:19:02 -06:00
parent 9cc60bd8f2
commit 346b3a5ea8
18 changed files with 1298 additions and 1146 deletions

View file

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

View file

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

View file

@ -11,5 +11,8 @@ source = humulus
[flake8]
exclude = .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.eggs,*.egg,instance
show-source = True
ignore = W504
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
__all__ = ['create_app', ]
__all__ = ["create_app"]

View file

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

View file

@ -16,50 +16,59 @@
import functools
from flask import (Blueprint, current_app, flash, redirect, render_template,
session, url_for)
from flask import (
Blueprint,
current_app,
flash,
redirect,
render_template,
session,
url_for,
)
from flask_wtf import FlaskForm
from wtforms import PasswordField, BooleanField
from wtforms.validators import DataRequired
bp = Blueprint('auth', __name__)
bp = Blueprint("auth", __name__)
class LoginForm(FlaskForm):
"""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):
"""View decorator that redirects anonymous users to the login page."""
@functools.wraps(view)
def wrapped_view(**kwargs):
logged_in = session.get('logged_in', False)
logged_in = session.get("logged_in", False)
if not logged_in:
return redirect(url_for('auth.login'))
return redirect(url_for("auth.login"))
return view(**kwargs)
return wrapped_view
@bp.route('/login', methods=('GET', 'POST'))
@bp.route("/login", methods=("GET", "POST"))
def login():
form = LoginForm()
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.permanent = form.permanent.data
session['logged_in'] = True
return redirect(url_for('index'))
flash('Password is invalid.', category='warning')
return render_template('auth/login.html', form=form)
session["logged_in"] = True
return redirect(url_for("index"))
flash("Password is invalid.", category="warning")
return render_template("auth/login.html", form=form)
@bp.route('/logout')
@bp.route("/logout")
def logout():
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():
"""Connect to the configured CouchDB."""
if 'couch' not in g:
if "couch" not in g:
g.couch = CouchDB(
current_app.config['COUCH_USERNAME'],
current_app.config['COUCH_PASSWORD'],
url=current_app.config['COUCH_URL'],
current_app.config["COUCH_USERNAME"],
current_app.config["COUCH_PASSWORD"],
url=current_app.config["COUCH_URL"],
connect=True,
auto_renew=True
auto_renew=True,
)
return g.couch
def get_db():
"""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):
"""Disconnect from CouchDB."""
couch = g.pop('couch', None)
couch = g.pop("couch", None)
if couch is not None:
couch.disconnect()
@ -55,17 +55,17 @@ def close_couch(e=None):
def build_couch():
"""Create any necessary databases and design documents."""
couch = get_couch()
dbname = current_app.config['COUCH_DATABASE']
dbname = current_app.config["COUCH_DATABASE"]
couch.create_database(dbname, throw_on_exists=False)
put_designs()
@click.command('build-couch')
@click.command("build-couch")
@with_appcontext
def build_couch_command():
"""Builds the couch for easy relaxing."""
build_couch()
click.echo('Built a couch. Please have a seat.')
click.echo("Built a couch. Please have a seat.")
def init_app(app):
@ -82,22 +82,22 @@ def put_doc(doc):
"""
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
slug = slugify(doc['name'])
doc['_id'] = slug
slug = slugify(doc["name"])
doc["_id"] = slug
i = 1
# Check if id exists and append/increment a number until it doesn't.
while doc['_id'] in db:
doc['_id'] = slug + '-{}'.format(i)
while doc["_id"] in db:
doc["_id"] = slug + "-{}".format(i)
i += 1
elif '_id' not in doc:
elif "_id" not in doc:
# Use a UUID for name
doc['_id'] = str(uuid.uuid4())
doc["_id"] = str(uuid.uuid4())
# Add a created timestamp
# 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)
@ -107,7 +107,7 @@ def update_doc(doc):
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()
@ -133,23 +133,23 @@ def put_designs():
"""
here = Path(__file__).parent
for filename in here.glob('designs/*.json'):
with open(filename, 'r') as fp:
for filename in here.glob("designs/*.json"):
with open(filename, "r") as fp:
data = json.load(fp)
# See if document already exists
if data['_id'] in get_db():
doc = get_doc(data['_id'])
if data["_id"] in get_db():
doc = get_doc(data["_id"])
# Popping off the revision and storing it. Then compare
rev = doc.pop('_rev')
doc.pop('created', None)
rev = doc.pop("_rev")
doc.pop("created", None)
if data == doc:
get_db().clear()
return
# Copy the values of data to doc.
for k in data:
doc[k] = data[k]
doc['_rev'] = rev # Add the revision back
doc["_rev"] = rev # Add the revision back
doc.save()
else:
put_doc(data)

View file

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

View file

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

View file

@ -18,20 +18,40 @@ import json
from decimal import Decimal
import requests
from flask import (abort, Blueprint, flash, redirect, render_template, jsonify,
request, url_for)
from flask import (
abort,
Blueprint,
flash,
redirect,
render_template,
jsonify,
request,
url_for,
)
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired
from wtforms import (Form, StringField, DecimalField, TextAreaField, FieldList,
FormField, SelectField)
from wtforms import (
Form,
StringField,
DecimalField,
TextAreaField,
FieldList,
FormField,
SelectField,
)
from wtforms.validators import DataRequired, Optional
from humulus.auth import login_required
from humulus.couch import (get_doc, get_doc_or_404, put_doc, update_doc,
get_view)
from humulus.couch import (
get_doc,
get_doc_or_404,
put_doc,
update_doc,
get_view,
)
from humulus.styles import get_styles_list
bp = Blueprint('recipes', __name__, url_prefix='/recipes')
bp = Blueprint("recipes", __name__, url_prefix="/recipes")
class FermentableForm(Form):
@ -40,14 +60,26 @@ class FermentableForm(Form):
CSRF is disabled for this form (using `Form as parent class)
because it is never used by itself.
"""
name = StringField('Name', validators=[DataRequired()])
type = SelectField('Type', validators=[DataRequired()],
choices=[(c, c) for c in ['Grain', 'LME', 'DME',
'Sugar', 'Non-fermentable',
'Other']])
amount = DecimalField('Amount (lb)', validators=[DataRequired()])
ppg = DecimalField('PPG', validators=[DataRequired()])
color = DecimalField('Color (°L)', validators=[DataRequired()])
name = StringField("Name", validators=[DataRequired()])
type = SelectField(
"Type",
validators=[DataRequired()],
choices=[
(c, c)
for c in [
"Grain",
"LME",
"DME",
"Sugar",
"Non-fermentable",
"Other",
]
],
)
amount = DecimalField("Amount (lb)", validators=[DataRequired()])
ppg = DecimalField("PPG", validators=[DataRequired()])
color = DecimalField("Color (°L)", validators=[DataRequired()])
@property
def doc(self):
@ -56,11 +88,11 @@ class FermentableForm(Form):
Used for putting into CouchDB.
"""
return {
'name': self.name.data,
'type': self.type.data,
'amount': str(self.amount.data),
'ppg': str(self.ppg.data),
'color': str(self.color.data)
"name": self.name.data,
"type": self.type.data,
"amount": str(self.amount.data),
"ppg": str(self.ppg.data),
"color": str(self.color.data),
}
@ -70,13 +102,16 @@ class HopForm(Form):
CSRF is disabled for this form (using `Form as parent class)
because it is never used by itself.
"""
name = StringField('Name', validators=[DataRequired()])
use = SelectField('Usage', validators=[DataRequired()],
choices=[(c, c) for c in ['Boil', 'FWH', 'Whirlpool',
'Dry-Hop']])
alpha = DecimalField('Alpha Acid %', validators=[DataRequired()])
duration = DecimalField('Duration (min/day)', validators=[DataRequired()])
amount = DecimalField('Amount (oz)', validators=[DataRequired()])
name = StringField("Name", validators=[DataRequired()])
use = SelectField(
"Usage",
validators=[DataRequired()],
choices=[(c, c) for c in ["Boil", "FWH", "Whirlpool", "Dry-Hop"]],
)
alpha = DecimalField("Alpha Acid %", validators=[DataRequired()])
duration = DecimalField("Duration (min/day)", validators=[DataRequired()])
amount = DecimalField("Amount (oz)", validators=[DataRequired()])
@property
def doc(self):
@ -85,11 +120,11 @@ class HopForm(Form):
Used for putting into CouchDB.
"""
return {
'name': self.name.data,
'use': self.use.data,
'alpha': str(self.alpha.data),
'duration': str(self.duration.data),
'amount': str(self.amount.data)
"name": self.name.data,
"use": self.use.data,
"alpha": str(self.alpha.data),
"duration": str(self.duration.data),
"amount": str(self.amount.data),
}
@ -99,26 +134,29 @@ class YeastForm(Form):
CSRF is disabled for this form (using `Form as parent class)
because it is never used by itself.
"""
name = StringField('Name', validators=[Optional()])
type = SelectField('Type', default='',
choices=[(c, c) for c in ['', 'Liquid', 'Dry']],
validators=[Optional()])
lab = StringField('Lab')
code = StringField('Lab Code')
flocculation = SelectField('Flocculation', default='',
choices=[(c, c) for c in ['', 'Low',
'Medium', 'High']],
validators=[Optional()])
low_attenuation = DecimalField('Low Attenuation',
validators=[Optional()])
high_attenuation = DecimalField('High Attenuation',
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()])
name = StringField("Name", validators=[Optional()])
type = SelectField(
"Type",
default="",
choices=[(c, c) for c in ["", "Liquid", "Dry"]],
validators=[Optional()],
)
lab = StringField("Lab")
code = StringField("Lab Code")
flocculation = SelectField(
"Flocculation",
default="",
choices=[(c, c) for c in ["", "Low", "Medium", "High"]],
validators=[Optional()],
)
low_attenuation = DecimalField("Low Attenuation", validators=[Optional()])
high_attenuation = DecimalField(
"High Attenuation", 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
def doc(self):
@ -127,24 +165,24 @@ class YeastForm(Form):
Used for putting into CouchDB.
"""
yeast = {
'name': self.name.data,
'low_attenuation': str(self.low_attenuation.data),
'high_attenuation': str(self.high_attenuation.data)
"name": self.name.data,
"low_attenuation": str(self.low_attenuation.data),
"high_attenuation": str(self.high_attenuation.data),
}
if self.type.data:
yeast['type'] = self.type.data
yeast["type"] = self.type.data
if self.lab.data:
yeast['lab'] = self.lab.data
yeast["lab"] = self.lab.data
if self.code.data:
yeast['code'] = self.code.data
yeast["code"] = self.code.data
if self.flocculation.data:
yeast['flocculation'] = self.flocculation.data
yeast["flocculation"] = self.flocculation.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:
yeast['max_temperature'] = str(self.max_temperature.data)
yeast["max_temperature"] = str(self.max_temperature.data)
if self.abv_tolerance.data:
yeast['abv_tolerance'] = str(self.abv_tolerance.data)
yeast["abv_tolerance"] = str(self.abv_tolerance.data)
return yeast
@ -154,14 +192,15 @@ class MashStepForm(Form):
CSRF is disabled for this form (using `Form as parent class)
because it is never used by itself.
"""
name = StringField('Step Name', validators=[DataRequired()])
type = SelectField('Type',
choices=[(c, c) for c in ['Infusion',
'Temperature',
'Decoction']])
temp = DecimalField('Temperature (°F)', validators=[DataRequired()])
time = DecimalField('Time (min)', validators=[DataRequired()])
amount = DecimalField('Water Amount (gal)')
name = StringField("Step Name", validators=[DataRequired()])
type = SelectField(
"Type",
choices=[(c, c) for c in ["Infusion", "Temperature", "Decoction"]],
)
temp = DecimalField("Temperature (°F)", validators=[DataRequired()])
time = DecimalField("Time (min)", validators=[DataRequired()])
amount = DecimalField("Water Amount (gal)")
@property
def doc(self):
@ -170,13 +209,13 @@ class MashStepForm(Form):
Used for putting into CouchDB.
"""
step = {
'name': self.name.data,
'type': self.type.data,
'temp': str(self.temp.data),
'time': str(self.time.data),
"name": self.name.data,
"type": self.type.data,
"temp": str(self.temp.data),
"time": str(self.time.data),
}
if self.amount.data:
step['amount'] = str(self.amount.data)
step["amount"] = str(self.amount.data)
return step
@ -186,12 +225,9 @@ class MashForm(Form):
CSRF is disabled for this form (using `Form as parent class)
because it is never used by itself.
"""
name = StringField('Mash Name', validators=[Optional()])
steps = FieldList(
FormField(MashStepForm),
min_entries=0,
max_entries=20
)
name = StringField("Mash Name", validators=[Optional()])
steps = FieldList(FormField(MashStepForm), min_entries=0, max_entries=20)
@property
def doc(self):
@ -199,37 +235,31 @@ class MashForm(Form):
Used for putting into CouchDB.
"""
return {
'name': self.name.data,
'steps': [s.doc for s in self.steps]
}
return {"name": self.name.data, "steps": [s.doc for s in self.steps]}
class RecipeForm(FlaskForm):
"""Form for recipes."""
name = StringField('Name', validators=[DataRequired()])
type = SelectField('Type', default='',
choices=[(c, c) for c in ['All-Grain',
'Partial Extract',
'Extract']],
validators=[Optional()])
efficiency = DecimalField('Batch Efficiency (%)',
validators=[DataRequired()])
volume = DecimalField('Batch Volume (gal)', validators=[DataRequired()])
notes = TextAreaField('Notes')
name = StringField("Name", validators=[DataRequired()])
type = SelectField(
"Type",
default="",
choices=[(c, c) for c in ["All-Grain", "Partial Extract", "Extract"]],
validators=[Optional()],
)
efficiency = DecimalField(
"Batch Efficiency (%)", validators=[DataRequired()]
)
volume = DecimalField("Batch Volume (gal)", validators=[DataRequired()])
notes = TextAreaField("Notes")
fermentables = FieldList(
FormField(FermentableForm),
min_entries=0,
max_entries=20
)
hops = FieldList(
FormField(HopForm),
min_entries=0,
max_entries=20
FormField(FermentableForm), min_entries=0, max_entries=20
)
hops = FieldList(FormField(HopForm), min_entries=0, max_entries=20)
yeast = FormField(YeastForm)
mash = FormField(MashForm)
style = SelectField('Style', choices=[], validators=[Optional()])
style = SelectField("Style", choices=[], validators=[Optional()])
@property
def doc(self):
@ -238,98 +268,102 @@ class RecipeForm(FlaskForm):
Used for putting into CouchDB.
"""
recipe = {
'name': self.name.data,
'efficiency': str(self.efficiency.data),
'volume': str(self.volume.data),
'notes': self.notes.data,
'$type': 'recipe',
'type': self.type.data,
'style': self.style.data
"name": self.name.data,
"efficiency": str(self.efficiency.data),
"volume": str(self.volume.data),
"notes": self.notes.data,
"$type": "recipe",
"type": self.type.data,
"style": self.style.data,
}
recipe['fermentables'] = [f.doc for f in self.fermentables]
recipe['hops'] = [h.doc for h in self.hops]
recipe["fermentables"] = [f.doc for f in self.fermentables]
recipe["hops"] = [h.doc for h in self.hops]
if (
self.yeast.doc['name'] and
self.yeast.doc['low_attenuation'] != "None" and
self.yeast.doc['high_attenuation'] != "None"
self.yeast.doc["name"]
and self.yeast.doc["low_attenuation"] != "None"
and self.yeast.doc["high_attenuation"] != "None"
):
recipe['yeast'] = self.yeast.doc
if self.mash.doc['name']:
recipe['mash'] = self.mash.doc
recipe["yeast"] = self.yeast.doc
if self.mash.doc["name"]:
recipe["mash"] = self.mash.doc
return recipe
def copyfrom(self, data):
"""Copies from a dictionary (data) into the current object"""
self.name.data = data['name']
self.type.data = data['type']
self.efficiency.data = Decimal(data['efficiency'])
self.volume.data = Decimal(data['volume'])
self.notes.data = data['notes']
self.style.data = data['style']
self.name.data = data["name"]
self.type.data = data["type"]
self.efficiency.data = Decimal(data["efficiency"])
self.volume.data = Decimal(data["volume"])
self.notes.data = data["notes"]
self.style.data = data["style"]
for fermentable in data['fermentables']:
self.fermentables.append_entry({
'name': fermentable['name'],
'type': fermentable['type'],
'amount': Decimal(fermentable['amount']),
'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'])
for fermentable in data["fermentables"]:
self.fermentables.append_entry(
{
"name": fermentable["name"],
"type": fermentable["type"],
"amount": Decimal(fermentable["amount"]),
"ppg": Decimal(fermentable["ppg"]),
"color": Decimal(fermentable["color"]),
}
)
self.yeast.form.high_attenuation.data = (
Decimal(data['yeast']['high_attenuation'])
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 '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 "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 '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 '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 "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']:
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'])
"name": step["name"],
"type": step["type"],
"temp": Decimal(step["temp"]),
"time": Decimal(step["time"]),
}
if 'amount' in step:
new_step['amount'] = Decimal(step['amount'])
if "amount" in step:
new_step["amount"] = Decimal(step["amount"])
print(new_step)
self.mash.steps.append_entry(new_step)
@ -338,29 +372,25 @@ class ImportForm(FlaskForm):
upload = FileField(validators=[FileRequired()])
@bp.route('/')
@bp.route("/")
def index():
descending = (
request.args.get('descending', default='false', type=str).lower() in
['true', 'yes']
)
sort_by = request.args.get('sort_by', default='name', type=str)
descending = request.args.get(
"descending", default="false", type=str
).lower() in ["true", "yes"]
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:
rows = view(include_docs=True, descending=descending)['rows']
rows = view(include_docs=True, descending=descending)["rows"]
except requests.exceptions.HTTPError:
abort(400)
return render_template(
'recipes/index.html',
rows=rows,
descending=descending,
sort_by=sort_by
"recipes/index.html", rows=rows, descending=descending, sort_by=sort_by
)
@bp.route('/create', methods=('GET', 'POST'))
@bp.route("/create", methods=("GET", "POST"))
@login_required
def create():
form = RecipeForm()
@ -368,12 +398,12 @@ def create():
if form.validate_on_submit():
response = put_doc(form.doc)
flash('Created recipe: {}'.format(form.name.data), 'success')
return redirect(url_for('recipes.info', id=response['_id']))
return render_template('recipes/create.html', form=form)
flash("Created recipe: {}".format(form.name.data), "success")
return redirect(url_for("recipes.info", id=response["_id"]))
return render_template("recipes/create.html", form=form)
@bp.route('/create/json', methods=('GET', 'POST'))
@bp.route("/create/json", methods=("GET", "POST"))
@login_required
def create_json():
form = ImportForm()
@ -382,47 +412,48 @@ def create_json():
try:
recipe.copyfrom(json.load(form.upload.data))
except Exception as e:
flash('Error converting data from JSON: {}'.format(e), 'warning')
return render_template('recipes/create_json.html', form=form)
flash("Error converting data from JSON: {}".format(e), "warning")
return render_template("recipes/create_json.html", form=form)
response = put_doc(recipe.doc)
return redirect(url_for('recipes.info', id=response['_id']))
return render_template('recipes/create_json.html', form=form)
return redirect(url_for("recipes.info", id=response["_id"]))
return render_template("recipes/create_json.html", form=form)
@bp.route('/info/<id>')
@bp.route("/info/<id>")
def info(id):
recipe = get_doc_or_404(id)
style = None
if recipe['style'] != '':
if recipe["style"] != "":
try:
style = get_doc(recipe['style'])
style = get_doc(recipe["style"])
except KeyError:
flash('Could not find style `{}`.'.format(recipe['style']),
'warning')
flash(
"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):
recipe = get_doc_or_404(id)
# Remove fields specific not intended for export
recipe.pop('_id')
recipe.pop('_rev')
recipe.pop('$type')
recipe.pop("_id")
recipe.pop("_rev")
recipe.pop("$type")
return jsonify(recipe)
@bp.route('/delete/<id>', methods=('POST',))
@bp.route("/delete/<id>", methods=("POST",))
@login_required
def delete(id):
recipe = get_doc_or_404(id)
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
def update(id):
# 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()
recipe = get_doc_or_404(id)
if form.validate_on_submit():
if recipe['_rev'] != request.args.get('rev', None):
if recipe["_rev"] != request.args.get("rev", None):
flash(
(
'Update conflict for recipe: {}. '
'Your changes have been lost.'.format(recipe['name'])
"Update conflict for recipe: {}. "
"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
for key, value in form.doc.items():
recipe[key] = value
update_doc(recipe)
flash('Updated recipe: {}'.format(form.name.data), 'success')
return redirect(url_for('recipes.info', id=id))
flash("Updated recipe: {}".format(form.name.data), "success")
return redirect(url_for("recipes.info", id=id))
else:
form.copyfrom(recipe)
return render_template('recipes/update.html', form=form,
id=id, rev=recipe['_rev'])
return render_template(
"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 requests
from flask import (Blueprint, abort, current_app, render_template, request,
jsonify)
from flask import (
Blueprint,
abort,
current_app,
render_template,
request,
jsonify,
)
from flask.cli import with_appcontext
from humulus.auth import login_required
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):
@ -35,56 +41,83 @@ def sub_to_doc(sub):
The returned dictionary can be placed right into CouchDB if you want.
"""
doc = {
'_id': '{}'.format(sub.attrib['id']),
'$type': 'style',
'name': sub.find('name').text,
'aroma': sub.find('aroma').text,
'appearance': sub.find('appearance').text,
'flavor': sub.find('flavor').text,
'mouthfeel': sub.find('mouthfeel').text,
'impression': sub.find('impression').text,
'ibu': {},
'og': {},
'fg': {},
'srm': {},
'abv': {}
"_id": "{}".format(sub.attrib["id"]),
"$type": "style",
"name": sub.find("name").text,
"aroma": sub.find("aroma").text,
"appearance": sub.find("appearance").text,
"flavor": sub.find("flavor").text,
"mouthfeel": sub.find("mouthfeel").text,
"impression": sub.find("impression").text,
"ibu": {},
"og": {},
"fg": {},
"srm": {},
"abv": {},
}
if sub.find('comments') is not None:
doc['comments'] = sub.find('comments').text
if sub.find('history') is not None:
doc['history'] = sub.find('history').text
if sub.find('ingredients') is not None:
doc['ingredients'] = sub.find('ingredients').text
if sub.find('comparison') is not None:
doc['comparison'] = sub.find('comparison').text
if sub.find('examples') is not None:
doc['examples'] = sub.find('examples').text
if sub.find('tags') is not None:
doc['tags'] = sub.find('tags').text.split(', ')
if sub.find("comments") is not None:
doc["comments"] = sub.find("comments").text
if sub.find("history") is not None:
doc["history"] = sub.find("history").text
if sub.find("ingredients") is not None:
doc["ingredients"] = sub.find("ingredients").text
if sub.find("comparison") is not None:
doc["comparison"] = sub.find("comparison").text
if sub.find("examples") is not None:
doc["examples"] = sub.find("examples").text
if sub.find("tags") is not None:
doc["tags"] = sub.find("tags").text.split(", ")
doc['ibu']['low'] = (sub.find('./stats/ibu/low').text
if sub.find('./stats/ibu/low') is not None else '0')
doc['ibu']['high'] = (sub.find('./stats/ibu/high').text
if sub.find('./stats/ibu/high') is not None
else '100')
doc['og']['low'] = (sub.find('./stats/og/low').text
if sub.find('./stats/og/low') is not None else '1.0')
doc['og']['high'] = (sub.find('./stats/og/high').text
if sub.find('./stats/og/high') is not None else '1.2')
doc['fg']['low'] = (sub.find('./stats/fg/low').text
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')
doc["ibu"]["low"] = (
sub.find("./stats/ibu/low").text
if sub.find("./stats/ibu/low") is not None
else "0"
)
doc["ibu"]["high"] = (
sub.find("./stats/ibu/high").text
if sub.find("./stats/ibu/high") is not None
else "100"
)
doc["og"]["low"] = (
sub.find("./stats/og/low").text
if sub.find("./stats/og/low") is not None
else "1.0"
)
doc["og"]["high"] = (
sub.find("./stats/og/high").text
if sub.find("./stats/og/high") is not None
else "1.2"
)
doc["fg"]["low"] = (
sub.find("./stats/fg/low").text
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
@ -102,31 +135,34 @@ def import_styles(url):
subs = root.findall('./class[@type="beer"]/category/subcategory')
for sub in subs:
doc = sub_to_doc(sub)
if doc['_id'] not in db:
if doc["_id"] not in db:
put_doc(doc)
def get_styles_list():
"""Returns a list containing id and names of all styles."""
view = get_view('_design/styles', 'by-category')
styles = [['', '']]
for row in view(include_docs=False)['rows']:
styles.append([row['id'], '{}{} {}'.format(
row['key'][0],
row['key'][1],
row['value']
)])
view = get_view("_design/styles", "by-category")
styles = [["", ""]]
for row in view(include_docs=False)["rows"]:
styles.append(
[
row["id"],
"{}{} {}".format(row["key"][0], row["key"][1], row["value"]),
]
)
return styles
@click.command('import-styles')
@click.command("import-styles")
@with_appcontext
def import_command():
"""CLI command to import BJCP styles."""
url = current_app.config.get(
'BJCP_STYLES_URL',
('https://raw.githubusercontent.com/meanphil'
'/bjcp-guidelines-2015/master/styleguide.xml')
"BJCP_STYLES_URL",
(
"https://raw.githubusercontent.com/meanphil"
"/bjcp-guidelines-2015/master/styleguide.xml"
),
)
import_styles(url)
click.echo("Imported BJCP styles.")
@ -137,41 +173,40 @@ def init_app(app):
app.cli.add_command(import_command)
@bp.route('/')
@bp.route("/")
@login_required
def index():
descending = (
request.args.get('descending', default='false', type=str).lower() in
['true', 'yes']
)
sort_by = request.args.get('sort_by', default='category', type=str)
page = request.args.get('page', default=1, type=int)
limit = request.args.get('limit', default=20, type=int)
descending = request.args.get(
"descending", default="false", type=str
).lower() in ["true", "yes"]
sort_by = request.args.get("sort_by", default="category", type=str)
page = request.args.get("page", default=1, 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:
rows = view(include_docs=True, descending=descending)['rows']
rows = view(include_docs=True, descending=descending)["rows"]
except requests.exceptions.HTTPError:
abort(400)
return render_template(
'styles/index.html',
rows=rows[(page - 1) * limit:page * limit],
"styles/index.html",
rows=rows[(page - 1) * limit : page * limit], # noqa
descending=descending,
sort_by=sort_by,
page=page,
num_pages=math.ceil(len(rows) / limit),
limit=limit
limit=limit,
)
@bp.route('/info/<id>')
@bp.route("/info/<id>")
@login_required
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
def info_json(id):
"""Returns JSON for the style.
@ -181,24 +216,26 @@ def info_json(id):
"""
style = get_doc_or_404(id)
# Remove fields not needed for specs
if request.args.get('specs', None) is not None:
return jsonify({
'ibu': style['ibu'],
'og': style['og'],
'fg': style['fg'],
'abv': style['abv'],
'srm': style['srm']
})
if request.args.get("specs", None) is not None:
return jsonify(
{
"ibu": style["ibu"],
"og": style["og"],
"fg": style["fg"],
"abv": style["abv"],
"srm": style["srm"],
}
)
# Remove fields not needed for export
style.pop('_id')
style.pop('_rev')
style.pop('$type')
style.pop("_id")
style.pop("_rev")
style.pop("$type")
return jsonify(style)
@bp.route('/info/<id>/recipes')
@bp.route("/info/<id>/recipes")
def recipes(id):
style = get_doc_or_404(id)
view = get_view('_design/recipes', 'by-style')
rows = view(include_docs=True, descending=True, key=id)['rows']
return render_template('styles/recipes.html', style=style, rows=rows)
view = get_view("_design/recipes", "by-style")
rows = view(include_docs=True, descending=True, key=id)["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
def app():
dbname = 'test_{}'.format(str(uuid.uuid4()))
couchurl = os.environ.get('COUCH_URL', 'http://127.0.0.1:5984')
app = create_app({
'COUCH_URL': couchurl,
'COUCH_USERNAME': 'admin',
'COUCH_PASSWORD': 'password',
'COUCH_DATABASE': dbname,
'WTF_CSRF_ENABLED': False,
'SECRET_KEY': 'testing',
'HUMULUS_PASSWORD': 'password'
})
dbname = "test_{}".format(str(uuid.uuid4()))
couchurl = os.environ.get("COUCH_URL", "http://127.0.0.1:5984")
app = create_app(
{
"COUCH_URL": couchurl,
"COUCH_USERNAME": "admin",
"COUCH_PASSWORD": "password",
"COUCH_DATABASE": dbname,
"WTF_CSRF_ENABLED": False,
"SECRET_KEY": "testing",
"HUMULUS_PASSWORD": "password",
}
)
with app.app_context():
# Create the database
build_couch()
# Add a test doc
put_doc({'data': 'test', '_id': 'foobar'})
put_doc({"data": "test", "_id": "foobar"})
# Add a couple test recipe
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',
'type': 'Grain',
'amount': '5',
'ppg': '37',
'color': '2'
},
{
'name': 'Dextrose',
'type': 'Sugar',
'amount': '1',
'ppg': '46',
'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'
}]
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",
"type": "Grain",
"amount": "5",
"ppg": "37",
"color": "2",
},
{
"name": "Dextrose",
"type": "Sugar",
"amount": "1",
"ppg": "46",
"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",
}
],
},
}
)
# Add a test style
put_doc({
'$type': 'style',
'_id': '1A',
'abv': {'high': '100', 'low': '0'},
'appearance': 'Good looking',
'aroma': 'Smelly',
'fg': {'high': '1.2', 'low': '1.0'},
'flavor': 'Good tasting',
'ibu': {'high': '100', 'low': '0'},
'id': '1A',
'impression': 'Refreshing',
'mouthfeel': 'Good feeling',
'name': 'Test Style',
'og': {'high': '1.2', 'low': '1.0'},
'srm': {'high': '100', 'low': '0'}
})
put_doc(
{
"$type": "style",
"_id": "1A",
"abv": {"high": "100", "low": "0"},
"appearance": "Good looking",
"aroma": "Smelly",
"fg": {"high": "1.2", "low": "1.0"},
"flavor": "Good tasting",
"ibu": {"high": "100", "low": "0"},
"id": "1A",
"impression": "Refreshing",
"mouthfeel": "Good feeling",
"name": "Test Style",
"og": {"high": "1.2", "low": "1.0"},
"srm": {"high": "100", "low": "0"},
}
)
yield app
@ -174,14 +188,11 @@ class AuthActions(object):
def __init__(self, client):
self._client = client
def login(self, password='password'):
return self._client.post(
'/login',
data={'password': password}
)
def login(self, password="password"):
return self._client.post("/login", data={"password": password})
def logout(self):
return self._client.get('/logout')
return self._client.get("/logout")
@pytest.fixture
@ -193,122 +204,122 @@ def auth(client):
def sample_recipes():
"""These sample recipes are useful for testing filters."""
return {
'lager': {
'efficiency': '72',
'type': 'All-Grain',
'style': '',
'fermentables': [
"lager": {
"efficiency": "72",
"type": "All-Grain",
"style": "",
"fermentables": [
{
'amount': '9.5',
'color': '1.80',
'name': 'Pale Malt, 2-row (Rahr) (US)',
'ppg': '37.00',
'type': 'Grain'
"amount": "9.5",
"color": "1.80",
"name": "Pale Malt, 2-row (Rahr) (US)",
"ppg": "37.00",
"type": "Grain",
},
{
'amount': '1',
'color': '0',
'name': 'Corn Sugar (Dextrose)',
'ppg': '46.00',
'type': 'Sugar'
}
"amount": "1",
"color": "0",
"name": "Corn Sugar (Dextrose)",
"ppg": "46.00",
"type": "Sugar",
},
],
'hops': [
"hops": [
{
'alpha': '7.0',
'amount': '1',
'duration': '60',
'name': 'Cluster (US)',
'use': 'Boil'
"alpha": "7.0",
"amount": "1",
"duration": "60",
"name": "Cluster (US)",
"use": "Boil",
},
{
'alpha': '2.8',
'amount': '1',
'duration': '10.00',
'name': 'Saaz (CZ)',
'use': 'Boil'
"alpha": "2.8",
"amount": "1",
"duration": "10.00",
"name": "Saaz (CZ)",
"use": "Boil",
},
{
'alpha': '2.8',
'amount': '1.0',
'duration': '5',
'name': 'Saaz (CZ)',
'use': 'Dry-Hop'
}
"alpha": "2.8",
"amount": "1.0",
"duration": "5",
"name": "Saaz (CZ)",
"use": "Dry-Hop",
},
],
'name': 'Lager',
'notes': 'Test simple dry-hopped lager w/ sugar',
'volume': '5.50',
'yeast': {
'abv_tolerance': '15.00',
'code': 'WLP940',
'flocculation': 'Medium',
'high_attenuation': '78.00',
'lab': 'White Labs',
'low_attenuation': '70.00',
'max_temperature': '55.00',
'min_temperature': '50.00',
'name': 'Mexican Lager',
'type': 'Liquid'
}
"name": "Lager",
"notes": "Test simple dry-hopped lager w/ sugar",
"volume": "5.50",
"yeast": {
"abv_tolerance": "15.00",
"code": "WLP940",
"flocculation": "Medium",
"high_attenuation": "78.00",
"lab": "White Labs",
"low_attenuation": "70.00",
"max_temperature": "55.00",
"min_temperature": "50.00",
"name": "Mexican Lager",
"type": "Liquid",
},
},
'sweetstout': {
'efficiency': '72',
'type': 'All-Grain',
'style': '',
'fermentables': [
"sweetstout": {
"efficiency": "72",
"type": "All-Grain",
"style": "",
"fermentables": [
{
'amount': '2.75',
'color': '3',
'name': 'Pale Malt, 2-row (UK)',
'ppg': '36.00',
'type': 'Grain'
"amount": "2.75",
"color": "3",
"name": "Pale Malt, 2-row (UK)",
"ppg": "36.00",
"type": "Grain",
},
{
'amount': '0.25',
'color': '450',
'name': 'Chocolate Malt (UK)',
'ppg': '34.00',
'type': 'Grain'
"amount": "0.25",
"color": "450",
"name": "Chocolate Malt (UK)",
"ppg": "34.00",
"type": "Grain",
},
{
'amount': '0.5',
'color': '0',
'name': 'Lactose',
'ppg': '35.00',
'type': 'Non-fermentable'
}
"amount": "0.5",
"color": "0",
"name": "Lactose",
"ppg": "35.00",
"type": "Non-fermentable",
},
],
'hops': [
"hops": [
{
'alpha': '5.0',
'amount': '0.5',
'duration': '60',
'name': 'East Kent Goldings (UK)',
'use': 'Boil'
"alpha": "5.0",
"amount": "0.5",
"duration": "60",
"name": "East Kent Goldings (UK)",
"use": "Boil",
},
{
'alpha': '5.0',
'amount': '0.5',
'duration': '30',
'name': 'East Kent Goldings (UK)',
'use': 'Boil'
}
"alpha": "5.0",
"amount": "0.5",
"duration": "30",
"name": "East Kent Goldings (UK)",
"use": "Boil",
},
],
'name': 'Sweet Stout',
'notes': 'Test stout w/ Lactose',
'volume': '2.5',
'yeast': {
'abv_tolerance': '12.00',
'code': '',
'flocculation': 'High',
'high_attenuation': '77.00',
'lab': 'Danstar',
'low_attenuation': '73.00',
'max_temperature': '70.00',
'min_temperature': '57.00',
'name': 'Nottingham',
'type': 'Dry'
}
}
"name": "Sweet Stout",
"notes": "Test stout w/ Lactose",
"volume": "2.5",
"yeast": {
"abv_tolerance": "12.00",
"code": "",
"flocculation": "High",
"high_attenuation": "77.00",
"lab": "Danstar",
"low_attenuation": "73.00",
"max_temperature": "70.00",
"min_temperature": "57.00",
"name": "Nottingham",
"type": "Dry",
},
},
}

View file

@ -15,30 +15,30 @@
def test_login(client, auth):
# Test GET
response = client.get('/login')
response = client.get("/login")
assert response.status_code == 200
# Test failed login
data = {'password': 'invalid'}
response = client.post('/login', data=data)
data = {"password": "invalid"}
response = client.post("/login", data=data)
assert response.status_code == 200
assert b'Password is invalid' in response.data
assert b"Password is invalid" in response.data
# Test successful login
data = {'password': 'password'}
response = client.post('/login', data=data)
data = {"password": "password"}
response = client.post("/login", data=data)
assert response.status_code == 302
with client.session_transaction() as session:
assert session['logged_in']
assert session["logged_in"]
assert not session.permanent
session.clear()
# Test permanent login
data = {'password': 'password', 'permanent': 'y'}
response = client.post('/login', data=data)
data = {"password": "password", "permanent": "y"}
response = client.post("/login", data=data)
assert response.status_code == 302
with client.session_transaction() as session:
assert session['logged_in']
assert session["logged_in"]
assert session.permanent
@ -46,9 +46,9 @@ def test_logout(client, auth):
# Login
auth.login()
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
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):
with app.app_context():
data = {'foo': 'bar'}
data = {"foo": "bar"}
response = put_doc(data)
assert '_id' in response
assert 'created' in response
assert "_id" in response
assert "created" in response
response = put_doc({'name': 'test'})
assert response['_id'] == 'test'
response = put_doc({"name": "test"})
assert response["_id"] == "test"
response = put_doc({'name': 'test'})
assert response['_id'] == 'test-1'
response = put_doc({"name": "test"})
assert response["_id"] == "test-1"
response = put_doc({'name': 'test'})
assert response['_id'] == 'test-2'
response = put_doc({"name": "test"})
assert response["_id"] == "test-2"
def test_update_doc(app):
with app.app_context():
doc = get_doc('awesome-lager')
rev = doc['_rev']
doc['test'] = 'update'
doc = get_doc("awesome-lager")
rev = doc["_rev"]
doc["test"] = "update"
update_doc(doc)
updated_doc = get_doc('awesome-lager')
assert doc['_id'] == updated_doc['_id']
assert rev < updated_doc['_rev']
assert updated_doc['test'] == 'update'
assert 'updated' in updated_doc
updated_doc = get_doc("awesome-lager")
assert doc["_id"] == updated_doc["_id"]
assert rev < updated_doc["_rev"]
assert updated_doc["test"] == "update"
assert "updated" in updated_doc
def test_build_couch_command(runner, monkeypatch):
@ -55,15 +55,15 @@ def test_build_couch_command(runner, monkeypatch):
def fake_build_couch():
Recorder.called = True
monkeypatch.setattr('humulus.couch.build_couch', fake_build_couch)
result = runner.invoke(args=['build-couch'])
assert 'Built a couch. Please have a seat.' in result.output
monkeypatch.setattr("humulus.couch.build_couch", fake_build_couch)
result = runner.invoke(args=["build-couch"])
assert "Built a couch. Please have a seat." in result.output
assert Recorder.called
def test_get_doc(app):
with app.app_context():
assert get_doc('foobar')['data'] == 'test'
assert get_doc("foobar")["data"] == "test"
def test_put_designs(app, monkeypatch):
@ -72,26 +72,26 @@ def test_put_designs(app, monkeypatch):
with app.app_context():
# Test initial load of designs
monkeypatch.setattr(Path, 'parent', testpath / 'assets/initial')
monkeypatch.setattr(Path, "parent", testpath / "assets/initial")
put_designs()
recipes = get_doc('_design/recipes')
assert 'language' in recipes
rev = recipes['_rev']
recipes = get_doc("_design/recipes")
assert "language" in recipes
rev = recipes["_rev"]
# Try again, make sure nothing changed.
put_designs()
recipes = get_doc('_design/recipes')
assert recipes['_rev'] == rev
recipes = get_doc("_design/recipes")
assert recipes["_rev"] == rev
# Test that changes can be loaded
monkeypatch.setattr(Path, 'parent', testpath / 'assets/changed')
monkeypatch.setattr(Path, "parent", testpath / "assets/changed")
put_designs()
recipes = get_doc('_design/recipes')
assert 'by-date' in recipes['views']
recipes = get_doc("_design/recipes")
assert "by-date" in recipes["views"]
def test_get_view(app):
with app.app_context():
view = get_view('_design/recipes', 'by-date')
assert view()['total_rows'] > 0
view = get_view("_design/recipes", "by-date")
assert view()["total_rows"] > 0

View file

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

View file

@ -14,7 +14,7 @@
def test_home(client):
response = client.get('/')
response = client.get("/")
assert response.status_code == 302
@ -27,16 +27,16 @@ def test_status(client, monkeypatch):
def exists(self):
return False
response = client.get('/status')
response = client.get("/status")
assert response.status_code == 200
assert response.get_json() == {'ping': 'ok'}
assert response.get_json() == {"ping": "ok"}
monkeypatch.setattr('humulus.home.get_db', MockDBTrue)
response = client.get('/status?couch=y')
monkeypatch.setattr("humulus.home.get_db", MockDBTrue)
response = client.get("/status?couch=y")
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)
response = client.get('/status?couch=y')
monkeypatch.setattr("humulus.home.get_db", MockDBFalse)
response = client.get("/status?couch=y")
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 humulus.couch import get_doc
from humulus.recipes import (FermentableForm, HopForm, RecipeForm, YeastForm,
MashForm, MashStepForm)
from humulus.recipes import (
FermentableForm,
HopForm,
RecipeForm,
YeastForm,
MashForm,
MashStepForm,
)
def test_index(client):
"""Test success in retrieving index."""
# Test for bad request
response = client.get('/recipes/?sort_by=foobar')
response = client.get("/recipes/?sort_by=foobar")
assert response.status_code == 400
# Verify defaults
response = client.get('/recipes/')
response = client.get("/recipes/")
assert response.status_code == 200
# Assert recipes are returned
assert b'Awesome Lager' in response.data
assert b'Awesome Beer' in response.data
assert b"Awesome Lager" in response.data
assert b"Awesome Beer" in response.data
assert (
b'"/recipes/?descending=true&amp;sort_by=name">Name &uarr;' in
response.data
b'"/recipes/?descending=true&amp;sort_by=name">Name &uarr;'
in response.data
)
assert (
b'"/recipes/?descending=false&amp;sort_by=date">Created On' in
response.data
b'"/recipes/?descending=false&amp;sort_by=date">Created On'
in response.data
)
# Test sort by name descending
response = client.get('/recipes/?descending=true&sort_by=name')
response = client.get("/recipes/?descending=true&sort_by=name")
assert (
b'"/recipes/?descending=false&amp;sort_by=name">Name &darr;' in
response.data
b'"/recipes/?descending=false&amp;sort_by=name">Name &darr;'
in response.data
)
assert (
b'"/recipes/?descending=false&amp;sort_by=date">Created On' in
response.data
b'"/recipes/?descending=false&amp;sort_by=date">Created On'
in response.data
)
# Test sort by date ascending
response = client.get('/recipes/?descending=false&sort_by=date')
response = client.get("/recipes/?descending=false&sort_by=date")
assert (
b'"/recipes/?descending=false&amp;sort_by=name">Name' in
response.data
b'"/recipes/?descending=false&amp;sort_by=name">Name' in response.data
)
assert (
b'"/recipes/?descending=true&amp;sort_by=date">Created On &uarr;' in
response.data
b'"/recipes/?descending=true&amp;sort_by=date">Created On &uarr;'
in response.data
)
# Test sort by date descending
response = client.get('/recipes/?descending=true&sort_by=date')
response = client.get("/recipes/?descending=true&sort_by=date")
assert (
b'"/recipes/?descending=false&amp;sort_by=name">Name' in
response.data
b'"/recipes/?descending=false&amp;sort_by=name">Name' in response.data
)
assert (
b'"/recipes/?descending=false&amp;sort_by=date">Created On &darr;' in
response.data
b'"/recipes/?descending=false&amp;sort_by=date">Created On &darr;'
in response.data
)
# Test sort by volume ascending
response = client.get('/recipes/?descending=false&sort_by=volume')
response = client.get("/recipes/?descending=false&sort_by=volume")
assert (
b'"/recipes/?descending=false&amp;sort_by=name">Name' in
response.data
b'"/recipes/?descending=false&amp;sort_by=name">Name' in response.data
)
assert (
b'"/recipes/?descending=true&amp;sort_by=volume">Batch Size &uarr;' in
response.data
b'"/recipes/?descending=true&amp;sort_by=volume">Batch Size &uarr;'
in response.data
)
# Test sort by volume descending
response = client.get('/recipes/?descending=true&sort_by=volume')
response = client.get("/recipes/?descending=true&sort_by=volume")
assert (
b'"/recipes/?descending=false&amp;sort_by=name">Name' in
response.data
b'"/recipes/?descending=false&amp;sort_by=name">Name' in response.data
)
assert (
b'"/recipes/?descending=false&amp;sort_by=volume">Batch Size &darr;' in
response.data
b'"/recipes/?descending=false&amp;sort_by=volume">Batch Size &darr;'
in response.data
)
# Test sort by type ascending
response = client.get('/recipes/?descending=false&sort_by=type')
response = client.get("/recipes/?descending=false&sort_by=type")
assert (
b'"/recipes/?descending=false&amp;sort_by=name">Name' in
response.data
b'"/recipes/?descending=false&amp;sort_by=name">Name' in response.data
)
assert (
b'"/recipes/?descending=true&amp;sort_by=type">Type &uarr;' in
response.data
b'"/recipes/?descending=true&amp;sort_by=type">Type &uarr;'
in response.data
)
# Test sort by type descending
response = client.get('/recipes/?descending=true&sort_by=type')
response = client.get("/recipes/?descending=true&sort_by=type")
assert (
b'"/recipes/?descending=false&amp;sort_by=name">Name' in
response.data
b'"/recipes/?descending=false&amp;sort_by=name">Name' in response.data
)
assert (
b'"/recipes/?descending=false&amp;sort_by=type">Type &darr;' in
response.data
b'"/recipes/?descending=false&amp;sort_by=type">Type &darr;'
in response.data
)
def test_create(client, app, auth):
"""Test success in creating a recipe document."""
# Test GET without login
response = client.get('/recipes/create')
response = client.get("/recipes/create")
assert response.status_code == 302
# Test GET with login
auth.login()
response = client.get('/recipes/create')
response = client.get("/recipes/create")
assert response.status_code == 200
# Test POST
data = {
'efficiency': '65',
'name': 'Test',
'notes': 'Test',
'volume': '5.5',
'style': '1A'
"efficiency": "65",
"name": "Test",
"notes": "Test",
"volume": "5.5",
"style": "1A",
}
response = client.post('/recipes/create', data=data)
response = client.post("/recipes/create", data=data)
assert response.status_code == 302
with app.app_context():
doc = get_doc('test')
doc = get_doc("test")
assert doc['name'] == 'Test'
assert doc['notes'] == 'Test'
assert doc['volume'] == '5.5'
assert doc['efficiency'] == '65'
assert doc['style'] == '1A'
assert doc["name"] == "Test"
assert doc["notes"] == "Test"
assert doc["volume"] == "5.5"
assert doc["efficiency"] == "65"
assert doc["style"] == "1A"
def test_update(client, app, auth):
"""Test success in updating a recipe document."""
# Test GET without login
response = client.get('/recipes/update/awesome-lager')
response = client.get("/recipes/update/awesome-lager")
assert response.status_code == 302
auth.login()
# 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 b'Awesome Lager' in response.data
assert b"Awesome Lager" in response.data
# 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
test_items = [
b'Awesome Beer',
b'2row',
b'Dextrose',
b'Nugget (US)',
b'CTZ (US)',
b'Northern California Ale'
b"Awesome Beer",
b"2row",
b"Dextrose",
b"Nugget (US)",
b"CTZ (US)",
b"Northern California Ale",
]
for item in test_items:
assert item in response.data
# 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
test_items = [
b'Partial Beer',
b'US-05'
]
test_items = [b"Partial Beer", b"US-05"]
for item in test_items:
assert item in response.data
# Get a doc, make an update, and test a POST
id = 'awesome-lager'
id = "awesome-lager"
with app.app_context():
doc = get_doc(id)
# Remove unneeded fields
doc.pop('_id')
rev = doc.pop('_rev')
response = client.post('/recipes/update/awesome-lager',
query_string={'rev': rev}, data=doc)
doc.pop("_id")
rev = doc.pop("_rev")
response = client.post(
"/recipes/update/awesome-lager", query_string={"rev": rev}, data=doc
)
assert response.status_code == 302
# Test response without valid/conflicted rev
response = client.post('/recipes/update/awesome-lager',
query_string={'rev': ''}, data=doc)
response = client.post(
"/recipes/update/awesome-lager", query_string={"rev": ""}, data=doc
)
assert response.status_code == 302
with client.session_transaction() as session:
flash_message = dict(session['_flashes']).pop('danger', None)
assert 'Update conflict' in flash_message
flash_message = dict(session["_flashes"]).pop("danger", None)
assert "Update conflict" in flash_message
def test_info(client, monkeypatch):
"""Test success in retrieving a recipe document."""
def mock_get_doc(id):
# This function always raises KeyError
raise KeyError(id)
# Validate 404
response = client.get('/recipes/info/thisdoesnotexist')
response = client.get("/recipes/info/thisdoesnotexist")
assert response.status_code == 404
# 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 b'Awesome Lager' in response.data
assert b"Awesome Lager" in response.data
# 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 b'Awesome Beer' in response.data
assert b'Test Style' in response.data
assert b"Awesome Beer" in response.data
assert b"Test Style" in response.data
# Validate warning is flashed when style cannot be found
monkeypatch.setattr('humulus.recipes.get_doc', mock_get_doc)
response = client.get('/recipes/info/full-recipe')
assert b'Could not find style' in response.data
monkeypatch.setattr("humulus.recipes.get_doc", mock_get_doc)
response = client.get("/recipes/info/full-recipe")
assert b"Could not find style" in response.data
def test_info_json(client):
"""Test success in retrieving a JSON recipe."""
# Validate 404
response = client.get('/recipes/info/thisdoesnotexist/json')
response = client.get("/recipes/info/thisdoesnotexist/json")
assert response.status_code == 404
# 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.is_json
assert response.get_json()['name'] == 'Awesome Lager'
assert response.get_json()["name"] == "Awesome Lager"
def test_step_form_doc(app):
"""Evaluates conditionals in generation of doc from a step form."""
step = MashStepForm()
step.name.data = 'Test Mash Step'
step.type.data = 'Infusion'
step.temp.data = Decimal('152')
step.time.data = Decimal('60')
step.name.data = "Test Mash Step"
step.type.data = "Infusion"
step.temp.data = Decimal("152")
step.time.data = Decimal("60")
assert step.doc == {
'name': 'Test Mash Step',
'type': 'Infusion',
'temp': '152',
'time': '60'
"name": "Test Mash Step",
"type": "Infusion",
"temp": "152",
"time": "60",
}
step.amount.data = Decimal('3.5')
step.amount.data = Decimal("3.5")
assert step.doc == {
'name': 'Test Mash Step',
'type': 'Infusion',
'temp': '152',
'time': '60',
'amount': '3.5'
"name": "Test Mash Step",
"type": "Infusion",
"temp": "152",
"time": "60",
"amount": "3.5",
}
def test_yeast_form_doc(app):
"""Evaluates conditionals in generation of doc from a yeast form."""
yeast = YeastForm()
yeast.name.data = 'Test'
yeast.low_attenuation.data = Decimal('60')
yeast.high_attenuation.data = Decimal('75')
yeast.name.data = "Test"
yeast.low_attenuation.data = Decimal("60")
yeast.high_attenuation.data = Decimal("75")
assert yeast.doc == {
'name': 'Test',
'low_attenuation': '60',
'high_attenuation': '75'
"name": "Test",
"low_attenuation": "60",
"high_attenuation": "75",
}
yeast.type.data = 'Dry'
yeast.code.data = 'INIS-001'
yeast.lab.data = 'Inland Island'
yeast.flocculation.data = 'Low'
yeast.min_temperature.data = Decimal('40')
yeast.max_temperature.data = Decimal('50')
yeast.abv_tolerance.data = Decimal('15')
yeast.type.data = "Dry"
yeast.code.data = "INIS-001"
yeast.lab.data = "Inland Island"
yeast.flocculation.data = "Low"
yeast.min_temperature.data = Decimal("40")
yeast.max_temperature.data = Decimal("50")
yeast.abv_tolerance.data = Decimal("15")
assert yeast.doc == {
'name': 'Test',
'low_attenuation': '60',
'high_attenuation': '75',
'flocculation': 'Low',
'type': 'Dry',
'code': 'INIS-001',
'lab': 'Inland Island',
'min_temperature': '40',
'max_temperature': '50',
'abv_tolerance': '15'
"name": "Test",
"low_attenuation": "60",
"high_attenuation": "75",
"flocculation": "Low",
"type": "Dry",
"code": "INIS-001",
"lab": "Inland Island",
"min_temperature": "40",
"max_temperature": "50",
"abv_tolerance": "15",
}
@ -316,52 +316,52 @@ def test_recipe_form_doc(app):
with app.app_context():
recipe = RecipeForm()
recipe.name.data = 'Test'
recipe.efficiency.data = Decimal('65')
recipe.volume.data = Decimal('5.5')
recipe.notes.data = 'This is a test'
recipe.type.data = 'All-Grain'
recipe.style.data = '1A'
recipe.name.data = "Test"
recipe.efficiency.data = Decimal("65")
recipe.volume.data = Decimal("5.5")
recipe.notes.data = "This is a test"
recipe.type.data = "All-Grain"
recipe.style.data = "1A"
assert recipe.doc == {
'name': 'Test',
'efficiency': '65',
'type': 'All-Grain',
'volume': '5.5',
'notes': 'This is a test',
'fermentables': [],
'hops': [],
'$type': 'recipe',
'style': '1A'
"name": "Test",
"efficiency": "65",
"type": "All-Grain",
"volume": "5.5",
"notes": "This is a test",
"fermentables": [],
"hops": [],
"$type": "recipe",
"style": "1A",
}
ferm = FermentableForm()
ferm.name.data = 'Test'
ferm.type.data = 'Grain'
ferm.amount.data = Decimal('5.5')
ferm.ppg.data = Decimal('37')
ferm.color.data = Decimal('1.8')
ferm.name.data = "Test"
ferm.type.data = "Grain"
ferm.amount.data = Decimal("5.5")
ferm.ppg.data = Decimal("37")
ferm.color.data = Decimal("1.8")
hop = HopForm()
hop.name.data = 'Test'
hop.use.data = 'Boil'
hop.alpha.data = Decimal('12.5')
hop.duration.data = Decimal('60')
hop.amount.data = Decimal('0.5')
hop.name.data = "Test"
hop.use.data = "Boil"
hop.alpha.data = Decimal("12.5")
hop.duration.data = Decimal("60")
hop.amount.data = Decimal("0.5")
yeast = YeastForm()
yeast.name.data = 'Test'
yeast.low_attenuation.data = '70'
yeast.high_attenuation.data = '75'
yeast.name.data = "Test"
yeast.low_attenuation.data = "70"
yeast.high_attenuation.data = "75"
step = MashStepForm()
step.name.data = 'Test Mash Step'
step.type.data = 'Infusion'
step.temp.data = Decimal('152')
step.time.data = Decimal('60')
step.amount.data = Decimal('3.5')
step.name.data = "Test Mash Step"
step.type.data = "Infusion"
step.temp.data = Decimal("152")
step.time.data = Decimal("60")
step.amount.data = Decimal("3.5")
mash = MashForm()
mash.name.data = 'Single Infusion'
mash.name.data = "Single Infusion"
mash.steps = [step]
recipe.fermentables = [ferm]
@ -370,202 +370,249 @@ def test_recipe_form_doc(app):
recipe.yeast = yeast
assert recipe.doc == {
'name': 'Test',
'efficiency': '65',
'type': 'All-Grain',
'volume': '5.5',
'notes': 'This is a test',
'$type': 'recipe',
'style': '1A',
'fermentables': [{
'name': 'Test',
'type': 'Grain',
'amount': '5.5',
'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'
"name": "Test",
"efficiency": "65",
"type": "All-Grain",
"volume": "5.5",
"notes": "This is a test",
"$type": "recipe",
"style": "1A",
"fermentables": [
{
"name": "Test",
"type": "Grain",
"amount": "5.5",
"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",
}
],
},
'mash': {
'name': 'Single Infusion',
'steps': [{
'name': 'Test Mash Step',
'type': 'Infusion',
'temp': '152',
'time': '60',
'amount': '3.5'
}]
}
}
def test_recipe_delete(client, auth):
"""Test success in deleting a document."""
# Try to delete a document without logging in
response = client.post('/recipes/delete/awesome-lager')
response = client.get('/recipes/info/awesome-lager')
response = client.post("/recipes/delete/awesome-lager")
response = client.get("/recipes/info/awesome-lager")
assert response.status_code == 200
# Delete document after login
auth.login()
# Try to delete a document without logging in
response = client.post('/recipes/delete/awesome-lager')
response = client.get('/recipes/info/awesome-lager')
response = client.post("/recipes/delete/awesome-lager")
response = client.get("/recipes/info/awesome-lager")
assert response.status_code == 404
def test_recipe_create_json(client, sample_recipes, auth):
"""Test uploading JSON recipe."""
# Test GET without logging in
response = client.get('/recipes/create/json')
response = client.get("/recipes/create/json")
assert response.status_code == 302
# Test GET after logging in
auth.login()
response = client.get('/recipes/create/json')
response = client.get("/recipes/create/json")
assert response.status_code == 200
# Test upload some good data
data = {
'upload': (BytesIO(json.dumps(sample_recipes['sweetstout']).encode()),
'sweetstout.json')
"upload": (
BytesIO(json.dumps(sample_recipes["sweetstout"]).encode()),
"sweetstout.json",
)
}
response = client.post('/recipes/create/json', buffered=True,
content_type='multipart/form-data', data=data)
response = client.post(
"/recipes/create/json",
buffered=True,
content_type="multipart/form-data",
data=data,
)
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
data = {'upload': (BytesIO(b'NOT JSON'), 'file')}
response = client.post('/recipes/create/json', buffered=True,
content_type='multipart/form-data', data=data)
data = {"upload": (BytesIO(b"NOT JSON"), "file")}
response = client.post(
"/recipes/create/json",
buffered=True,
content_type="multipart/form-data",
data=data,
)
assert response.status_code == 200
def test_copyfrom(app, sample_recipes):
recipe = {
'name': 'Test',
'type': 'All-Grain',
'efficiency': '65',
'volume': '5.5',
'notes': 'Notes',
'style': '18A',
'fermentables': [{
'name': 'Test',
'type': 'Grain',
'amount': '1',
'ppg': '36',
'color': '4'
}],
'hops': [{
'name': 'Test',
'use': 'Boil',
'alpha': '5.5',
'duration': '30',
'amount': '1'
}]
"name": "Test",
"type": "All-Grain",
"efficiency": "65",
"volume": "5.5",
"notes": "Notes",
"style": "18A",
"fermentables": [
{
"name": "Test",
"type": "Grain",
"amount": "1",
"ppg": "36",
"color": "4",
}
],
"hops": [
{
"name": "Test",
"use": "Boil",
"alpha": "5.5",
"duration": "30",
"amount": "1",
}
],
}
with app.app_context():
form = RecipeForm()
form.copyfrom(recipe)
assert form.name.data == recipe['name']
assert form.type.data == recipe['type']
assert form.efficiency.data == Decimal(recipe['efficiency'])
assert form.volume.data == Decimal(recipe['volume'])
assert form.notes.data == recipe['notes']
assert len(form.fermentables) == len(recipe['fermentables'])
assert form.fermentables[0].form.name.data == \
recipe['fermentables'][0]['name']
assert form.fermentables[0].form.type.data == \
recipe['fermentables'][0]['type']
assert form.fermentables[0].form.amount.data == \
Decimal(recipe['fermentables'][0]['amount'])
assert form.fermentables[0].form.ppg.data == \
Decimal(recipe['fermentables'][0]['ppg'])
assert form.fermentables[0].form.color.data == \
Decimal(recipe['fermentables'][0]['color'])
assert len(form.hops) == len(recipe['hops'])
assert form.hops[0].form.name.data == recipe['hops'][0]['name']
assert form.hops[0].form.use.data == recipe['hops'][0]['use']
assert form.hops[0].form.alpha.data == Decimal(recipe['hops'][0]['alpha'])
assert form.hops[0].form.duration.data == \
Decimal(recipe['hops'][0]['duration'])
assert form.hops[0].form.amount.data == \
Decimal(recipe['hops'][0]['amount'])
assert form.name.data == recipe["name"]
assert form.type.data == recipe["type"]
assert form.efficiency.data == Decimal(recipe["efficiency"])
assert form.volume.data == Decimal(recipe["volume"])
assert form.notes.data == recipe["notes"]
assert len(form.fermentables) == len(recipe["fermentables"])
assert (
form.fermentables[0].form.name.data
== recipe["fermentables"][0]["name"]
)
assert (
form.fermentables[0].form.type.data
== recipe["fermentables"][0]["type"]
)
assert form.fermentables[0].form.amount.data == Decimal(
recipe["fermentables"][0]["amount"]
)
assert form.fermentables[0].form.ppg.data == Decimal(
recipe["fermentables"][0]["ppg"]
)
assert form.fermentables[0].form.color.data == Decimal(
recipe["fermentables"][0]["color"]
)
assert len(form.hops) == len(recipe["hops"])
assert form.hops[0].form.name.data == recipe["hops"][0]["name"]
assert form.hops[0].form.use.data == recipe["hops"][0]["use"]
assert form.hops[0].form.alpha.data == Decimal(recipe["hops"][0]["alpha"])
assert form.hops[0].form.duration.data == Decimal(
recipe["hops"][0]["duration"]
)
assert form.hops[0].form.amount.data == Decimal(
recipe["hops"][0]["amount"]
)
recipe['yeast'] = {
'name': 'Test', 'low_attenuation': '65', 'high_attenuation': '68'
recipe["yeast"] = {
"name": "Test",
"low_attenuation": "65",
"high_attenuation": "68",
}
recipe['mash'] = {}
recipe["mash"] = {}
with app.app_context():
form = RecipeForm()
form.copyfrom(recipe)
assert form.yeast.form.name.data == recipe['yeast']['name']
assert form.yeast.form.low_attenuation.data == \
Decimal(recipe['yeast']['low_attenuation'])
assert form.yeast.form.high_attenuation.data == \
Decimal(recipe['yeast']['high_attenuation'])
assert form.yeast.form.name.data == recipe["yeast"]["name"]
assert form.yeast.form.low_attenuation.data == Decimal(
recipe["yeast"]["low_attenuation"]
)
assert form.yeast.form.high_attenuation.data == Decimal(
recipe["yeast"]["high_attenuation"]
)
recipe['yeast'].update({
'type': 'Liquid',
'lab': 'Test',
'code': 'Test',
'flocculation': 'Low',
'min_temperature': '65',
'max_temperature': '68',
'abv_tolerance': '15'
})
recipe["yeast"].update(
{
"type": "Liquid",
"lab": "Test",
"code": "Test",
"flocculation": "Low",
"min_temperature": "65",
"max_temperature": "68",
"abv_tolerance": "15",
}
)
with app.app_context():
form = RecipeForm()
form.copyfrom(recipe)
assert form.yeast.form.type.data == recipe['yeast']['type']
assert form.yeast.form.lab.data == recipe['yeast']['lab']
assert form.yeast.form.code.data == recipe['yeast']['code']
assert form.yeast.form.flocculation.data == recipe['yeast']['flocculation']
assert form.yeast.form.min_temperature.data == \
Decimal(recipe['yeast']['min_temperature'])
assert form.yeast.form.max_temperature.data == \
Decimal(recipe['yeast']['max_temperature'])
assert form.yeast.form.abv_tolerance.data == \
Decimal(recipe['yeast']['abv_tolerance'])
assert form.yeast.form.type.data == recipe["yeast"]["type"]
assert form.yeast.form.lab.data == recipe["yeast"]["lab"]
assert form.yeast.form.code.data == recipe["yeast"]["code"]
assert form.yeast.form.flocculation.data == recipe["yeast"]["flocculation"]
assert form.yeast.form.min_temperature.data == Decimal(
recipe["yeast"]["min_temperature"]
)
assert form.yeast.form.max_temperature.data == Decimal(
recipe["yeast"]["max_temperature"]
)
assert form.yeast.form.abv_tolerance.data == Decimal(
recipe["yeast"]["abv_tolerance"]
)
recipe['mash'] = {
'name': 'Test',
'steps': [{
'name': 'Infusion',
'type': 'Infusion',
'temp': '152',
'time': '60'
}]
recipe["mash"] = {
"name": "Test",
"steps": [
{
"name": "Infusion",
"type": "Infusion",
"temp": "152",
"time": "60",
}
],
}
with app.app_context():
form = RecipeForm()
form.copyfrom(recipe)
assert form.mash.form.name.data == recipe['mash']['name']
assert len(form.mash.form.steps) == len(recipe['mash']['steps'])
assert form.mash.form.steps[0].form.name.data == \
recipe['mash']['steps'][0]['name']
assert form.mash.form.steps[0].form.type.data == \
recipe['mash']['steps'][0]['type']
assert form.mash.form.steps[0].form.temp.data == \
Decimal(recipe['mash']['steps'][0]['temp'])
assert form.mash.form.steps[0].form.time.data == \
Decimal(recipe['mash']['steps'][0]['time'])
assert form.mash.form.name.data == recipe["mash"]["name"]
assert len(form.mash.form.steps) == len(recipe["mash"]["steps"])
assert (
form.mash.form.steps[0].form.name.data
== recipe["mash"]["steps"][0]["name"]
)
assert (
form.mash.form.steps[0].form.type.data
== recipe["mash"]["steps"][0]["type"]
)
assert form.mash.form.steps[0].form.temp.data == Decimal(
recipe["mash"]["steps"][0]["temp"]
)
assert form.mash.form.steps[0].form.time.data == Decimal(
recipe["mash"]["steps"][0]["time"]
)
recipe['mash']['steps'][0]['amount'] = '3.5'
recipe["mash"]["steps"][0]["amount"] = "3.5"
with app.app_context():
form = RecipeForm()
form.copyfrom(recipe)
assert form.mash.form.steps[0].form.amount.data == \
Decimal(recipe['mash']['steps'][0]['amount'])
assert form.mash.form.steps[0].form.amount.data == Decimal(
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
COMPLETE_STYLE = '''<subcategory id="1A">
COMPLETE_STYLE = """<subcategory id="1A">
<name>Test Style</name>
<aroma>Smelly</aroma>
<appearance>Good looking</appearance>
@ -52,9 +52,9 @@ COMPLETE_STYLE = '''<subcategory id="1A">
</abv>
</stats>
</subcategory>
'''
"""
INCOMPLETE_STYLE = '''<subcategory id="2B">
INCOMPLETE_STYLE = """<subcategory id="2B">
<name>Test Style</name>
<aroma>Smelly</aroma>
<appearance>Good looking</appearance>
@ -62,9 +62,9 @@ INCOMPLETE_STYLE = '''<subcategory id="2B">
<mouthfeel>Good feeling</mouthfeel>
<impression>Refreshing</impression>
</subcategory>
'''
"""
TEST_XML = '''<?xml version="1.0" encoding="UTF-8"?>
TEST_XML = """<?xml version="1.0" encoding="UTF-8"?>
<styleguide>
<class type="beer">
<category id="1">
@ -79,46 +79,46 @@ TEST_XML = '''<?xml version="1.0" encoding="UTF-8"?>
</category>
</class>
</styleguide>
'''
"""
def test_sub_to_doc():
assert sub_to_doc(ET.fromstring(COMPLETE_STYLE)) == {
'_id': '1A',
'$type': 'style',
'name': 'Test Style',
'aroma': 'Smelly',
'appearance': 'Good looking',
'flavor': 'Good tasting',
'mouthfeel': 'Good feeling',
'impression': 'Refreshing',
'comments': 'Comments',
'history': 'Old',
'ingredients': 'Grains, Hops, and Water',
'comparison': 'Comparison',
'examples': 'Examples',
'tags': ['one', 'two'],
'ibu': {'low': '1', 'high': '2'},
'og': {'low': '1.010', 'high': '1.020'},
'fg': {'low': '1.000', 'high': '1.010'},
'srm': {'low': '1', 'high': '2'},
'abv': {'low': '1', 'high': '2'}
"_id": "1A",
"$type": "style",
"name": "Test Style",
"aroma": "Smelly",
"appearance": "Good looking",
"flavor": "Good tasting",
"mouthfeel": "Good feeling",
"impression": "Refreshing",
"comments": "Comments",
"history": "Old",
"ingredients": "Grains, Hops, and Water",
"comparison": "Comparison",
"examples": "Examples",
"tags": ["one", "two"],
"ibu": {"low": "1", "high": "2"},
"og": {"low": "1.010", "high": "1.020"},
"fg": {"low": "1.000", "high": "1.010"},
"srm": {"low": "1", "high": "2"},
"abv": {"low": "1", "high": "2"},
}
assert sub_to_doc(ET.fromstring(INCOMPLETE_STYLE)) == {
'_id': '2B',
'$type': 'style',
'name': 'Test Style',
'aroma': 'Smelly',
'appearance': 'Good looking',
'flavor': 'Good tasting',
'mouthfeel': 'Good feeling',
'impression': 'Refreshing',
'ibu': {'low': '0', 'high': '100'},
'og': {'low': '1.0', 'high': '1.2'},
'fg': {'low': '1.0', 'high': '1.2'},
'srm': {'low': '0', 'high': '100'},
'abv': {'low': '0', 'high': '100'}
"_id": "2B",
"$type": "style",
"name": "Test Style",
"aroma": "Smelly",
"appearance": "Good looking",
"flavor": "Good tasting",
"mouthfeel": "Good feeling",
"impression": "Refreshing",
"ibu": {"low": "0", "high": "100"},
"og": {"low": "1.0", "high": "1.2"},
"fg": {"low": "1.0", "high": "1.2"},
"srm": {"low": "0", "high": "100"},
"abv": {"low": "0", "high": "100"},
}
@ -138,30 +138,31 @@ def test_import_styles(monkeypatch):
def fake_requests_get(url):
class TestXML:
text = TEST_XML
return TestXML
monkeypatch.setattr('requests.get', fake_requests_get)
monkeypatch.setattr('humulus.styles.get_db', fake_get_db)
monkeypatch.setattr('humulus.styles.put_doc', fake_put_doc)
monkeypatch.setattr("requests.get", fake_requests_get)
monkeypatch.setattr("humulus.styles.get_db", fake_get_db)
monkeypatch.setattr("humulus.styles.put_doc", fake_put_doc)
import_styles(None)
assert PutRecorder.doc == {
'$type': 'style',
'_id': '1A',
'abv': {'high': '100', 'low': '0'},
'appearance': 'Good looking',
'aroma': 'Smelly',
'fg': {'high': '1.2', 'low': '1.0'},
'flavor': 'Good tasting',
'ibu': {'high': '100', 'low': '0'},
'impression': 'Refreshing',
'mouthfeel': 'Good feeling',
'name': 'Test Style',
'og': {'high': '1.2', 'low': '1.0'},
'srm': {'high': '100', 'low': '0'}
"$type": "style",
"_id": "1A",
"abv": {"high": "100", "low": "0"},
"appearance": "Good looking",
"aroma": "Smelly",
"fg": {"high": "1.2", "low": "1.0"},
"flavor": "Good tasting",
"ibu": {"high": "100", "low": "0"},
"impression": "Refreshing",
"mouthfeel": "Good feeling",
"name": "Test Style",
"og": {"high": "1.2", "low": "1.0"},
"srm": {"high": "100", "low": "0"},
}
MockDB.db = {'1A': ''}
MockDB.db = {"1A": ""}
PutRecorder.doc = None
import_styles(None)
assert PutRecorder.doc is None
@ -174,76 +175,73 @@ def test_import_command(runner, monkeypatch):
def fake_import_styles(url):
Recorder.called = True
monkeypatch.setattr('humulus.styles.import_styles', fake_import_styles)
result = runner.invoke(args=['import-styles'])
monkeypatch.setattr("humulus.styles.import_styles", fake_import_styles)
result = runner.invoke(args=["import-styles"])
assert Recorder.called
assert 'Imported BJCP styles.' in result.output
assert "Imported BJCP styles." in result.output
def test_get_styles_choices(app):
"""Test success in getting list of styles."""
with app.app_context():
styles = get_styles_list()
assert styles == [
['', ''],
['1A', '1A Test Style']
]
assert styles == [["", ""], ["1A", "1A Test Style"]]
def test_index(auth, client):
"""Test success in retrieving index."""
# Test not logged in
response = client.get('/styles/')
response = client.get("/styles/")
assert response.status_code == 302
# Login and test get
auth.login()
response = client.get('/styles/')
response = client.get("/styles/")
assert response.status_code == 200
assert b'1A' in response.data
assert b'Test Style' in response.data
assert b"1A" in response.data
assert b"Test Style" in response.data
# Test for bad request
response = client.get('/styles/?sort_by=foobar')
response = client.get("/styles/?sort_by=foobar")
assert response.status_code == 400
def test_info(auth, client):
"""Test success in retrieving a style's info page"""
# Test not logged in
response = client.get('/styles/info/1A')
response = client.get("/styles/info/1A")
assert response.status_code == 302
# Login and test
auth.login()
response = client.get('/styles/info/1A')
response = client.get("/styles/info/1A")
assert response.status_code == 200
assert b'1A' in response.data
assert b'Test Style' in response.data
assert b"1A" in response.data
assert b"Test Style" in response.data
def test_recipes(client):
"""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 b'Awesome Beer' in response.data
assert b"Awesome Beer" in response.data
def test_info_json(auth, client):
"""Test success in retrieving a style's json document."""
# Test not logged in
response = client.get('/styles/info/1A/json')
response = client.get("/styles/info/1A/json")
assert response.status_code == 302
# Login and test
auth.login()
response = client.get('/styles/info/1A/json')
response = client.get("/styles/info/1A/json")
assert response.status_code == 200
assert response.is_json
assert response.get_json()['name'] == 'Test Style'
assert response.get_json()["name"] == "Test Style"
# 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.is_json
assert 'name' not in response.get_json()
assert "name" not in response.get_json()