From 9b1a739347765643b491ecf066acc9835c6dd376 Mon Sep 17 00:00:00 2001 From: Mike Shoup Date: Mon, 22 Jul 2019 13:27:16 -0600 Subject: [PATCH] Add pre-commit hooks, and add black to linting (#40) Closes #39 --- .drone.yml | 10 +- .pre-commit-config.yaml | 10 + pyproject.toml | 3 + requirements-dev.txt | 6 + setup.cfg | 5 +- src/humulus/__init__.py | 2 +- src/humulus/app.py | 18 +- src/humulus/auth.py | 39 ++- src/humulus/couch.py | 52 ++-- src/humulus/filters.py | 116 +++---- src/humulus/home.py | 16 +- src/humulus/recipes.py | 476 +++++++++++++++-------------- src/humulus/styles.py | 227 ++++++++------ tests/conftest.py | 457 +++++++++++++-------------- tests/test_auth.py | 26 +- tests/test_couch.py | 64 ++-- tests/test_filters.py | 106 ++++--- tests/test_home.py | 18 +- tests/test_recipes.py | 661 +++++++++++++++++++++------------------- tests/test_styles.py | 154 +++++----- 20 files changed, 1317 insertions(+), 1149 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 pyproject.toml create mode 100644 requirements-dev.txt diff --git a/.drone.yml b/.drone.yml index 27243e2..47b2ccc 100644 --- a/.drone.yml +++ b/.drone.yml @@ -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 @@ -27,11 +26,8 @@ steps: # Upload coverage report - pip install codecov - codecov - -- name: linting - image: python:3.6 - commands: - - pip install flake8 + # Perform linting checks + - black --check src tests - flake8 --- diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..78cdc68 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +repos: +- repo: https://github.com/ambv/black + rev: stable + hooks: + - id: black + language_version: python3.6 +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v1.2.3 + hooks: + - id: flake8 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c539595 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.black] +line-length = 79 +target_version = ['py35','py36','py37'] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..3cad500 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,6 @@ +pytest +coverage +pre-commit +black +flake8 +-e . diff --git a/setup.cfg b/setup.cfg index a9f4bf6..6727527 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/src/humulus/__init__.py b/src/humulus/__init__.py index 5af77a7..1f7fb50 100644 --- a/src/humulus/__init__.py +++ b/src/humulus/__init__.py @@ -14,4 +14,4 @@ from humulus.app import create_app -__all__ = ['create_app', ] +__all__ = ["create_app"] diff --git a/src/humulus/app.py b/src/humulus/app.py index 19a8319..2e8aaac 100644 --- a/src/humulus/app.py +++ b/src/humulus/app.py @@ -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, ) diff --git a/src/humulus/auth.py b/src/humulus/auth.py index f8182ac..d8666fa 100644 --- a/src/humulus/auth.py +++ b/src/humulus/auth.py @@ -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")) diff --git a/src/humulus/couch.py b/src/humulus/couch.py index 114bafb..49a269e 100644 --- a/src/humulus/couch.py +++ b/src/humulus/couch.py @@ -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) diff --git a/src/humulus/filters.py b/src/humulus/filters.py index e0b4b79..1d58a12 100644 --- a/src/humulus/filters.py +++ b/src/humulus/filters.py @@ -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 diff --git a/src/humulus/home.py b/src/humulus/home.py index 7d3a167..de07c28 100644 --- a/src/humulus/home.py +++ b/src/humulus/home.py @@ -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 diff --git a/src/humulus/recipes.py b/src/humulus/recipes.py index e854b14..963c763 100644 --- a/src/humulus/recipes.py +++ b/src/humulus/recipes.py @@ -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/') +@bp.route("/info/") 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//json') +@bp.route("/info//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/', methods=('POST',)) +@bp.route("/delete/", 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/', methods=('GET', 'POST')) +@bp.route("/update/", 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"] + ) diff --git a/src/humulus/styles.py b/src/humulus/styles.py index 388fbb3..76bf266 100644 --- a/src/humulus/styles.py +++ b/src/humulus/styles.py @@ -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/') +@bp.route("/info/") @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//json') +@bp.route("/info//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//recipes') +@bp.route("/info//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) diff --git a/tests/conftest.py b/tests/conftest.py index 743111f..fae8e60 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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", + }, + }, } diff --git a/tests/test_auth.py b/tests/test_auth.py index ed30b2c..4ffd85b 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -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) diff --git a/tests/test_couch.py b/tests/test_couch.py index 33c8805..2c47d97 100644 --- a/tests/test_couch.py +++ b/tests/test_couch.py @@ -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 diff --git a/tests/test_filters.py b/tests/test_filters.py index de6bbf7..6199d1a 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -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}, ] diff --git a/tests/test_home.py b/tests/test_home.py index 50e3240..0486396 100644 --- a/tests/test_home.py +++ b/tests/test_home.py @@ -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"} diff --git a/tests/test_recipes.py b/tests/test_recipes.py index 7f3f740..bbb2e11 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -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&sort_by=name">Name ↑' in - response.data + b'"/recipes/?descending=true&sort_by=name">Name ↑' + in response.data ) assert ( - b'"/recipes/?descending=false&sort_by=date">Created On' in - response.data + b'"/recipes/?descending=false&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&sort_by=name">Name ↓' in - response.data + b'"/recipes/?descending=false&sort_by=name">Name ↓' + in response.data ) assert ( - b'"/recipes/?descending=false&sort_by=date">Created On' in - response.data + b'"/recipes/?descending=false&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&sort_by=name">Name' in - response.data + b'"/recipes/?descending=false&sort_by=name">Name' in response.data ) assert ( - b'"/recipes/?descending=true&sort_by=date">Created On ↑' in - response.data + b'"/recipes/?descending=true&sort_by=date">Created On ↑' + 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&sort_by=name">Name' in - response.data + b'"/recipes/?descending=false&sort_by=name">Name' in response.data ) assert ( - b'"/recipes/?descending=false&sort_by=date">Created On ↓' in - response.data + b'"/recipes/?descending=false&sort_by=date">Created On ↓' + 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&sort_by=name">Name' in - response.data + b'"/recipes/?descending=false&sort_by=name">Name' in response.data ) assert ( - b'"/recipes/?descending=true&sort_by=volume">Batch Size ↑' in - response.data + b'"/recipes/?descending=true&sort_by=volume">Batch Size ↑' + 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&sort_by=name">Name' in - response.data + b'"/recipes/?descending=false&sort_by=name">Name' in response.data ) assert ( - b'"/recipes/?descending=false&sort_by=volume">Batch Size ↓' in - response.data + b'"/recipes/?descending=false&sort_by=volume">Batch Size ↓' + 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&sort_by=name">Name' in - response.data + b'"/recipes/?descending=false&sort_by=name">Name' in response.data ) assert ( - b'"/recipes/?descending=true&sort_by=type">Type ↑' in - response.data + b'"/recipes/?descending=true&sort_by=type">Type ↑' + 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&sort_by=name">Name' in - response.data + b'"/recipes/?descending=false&sort_by=name">Name' in response.data ) assert ( - b'"/recipes/?descending=false&sort_by=type">Type ↓' in - response.data + b'"/recipes/?descending=false&sort_by=type">Type ↓' + 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"] + ) diff --git a/tests/test_styles.py b/tests/test_styles.py index ca8d5ae..62ff021 100644 --- a/tests/test_styles.py +++ b/tests/test_styles.py @@ -16,7 +16,7 @@ import xml.etree.ElementTree as ET from humulus.styles import import_styles, sub_to_doc, get_styles_list -COMPLETE_STYLE = ''' +COMPLETE_STYLE = """ Test Style Smelly Good looking @@ -52,9 +52,9 @@ COMPLETE_STYLE = ''' -''' +""" -INCOMPLETE_STYLE = ''' +INCOMPLETE_STYLE = """ Test Style Smelly Good looking @@ -62,9 +62,9 @@ INCOMPLETE_STYLE = ''' Good feeling Refreshing -''' +""" -TEST_XML = ''' +TEST_XML = """ @@ -79,46 +79,46 @@ TEST_XML = ''' -''' +""" 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()