mirror of
https://github.com/shouptech/humulus.git
synced 2026-02-03 16:09:44 +00:00
parent
e11fd79deb
commit
8e949d18e4
14 changed files with 716 additions and 10 deletions
|
|
@ -46,6 +46,11 @@ def create_app(test_config=None):
|
||||||
from . import auth
|
from . import auth
|
||||||
app.register_blueprint(auth.bp)
|
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
|
# Register custom filters
|
||||||
from . import filters
|
from . import filters
|
||||||
filters.create_filters(app)
|
filters.create_filters(app)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@
|
||||||
},
|
},
|
||||||
"by-type": {
|
"by-type": {
|
||||||
"map": "function (doc) {\n if (doc.$type == \"recipe\" && doc.type && doc.name) {\n emit(doc.type, doc.name)\n }\n}"
|
"map": "function (doc) {\n if (doc.$type == \"recipe\" && doc.type && doc.name) {\n emit(doc.type, doc.name)\n }\n}"
|
||||||
|
},
|
||||||
|
"by-style": {
|
||||||
|
"map": "function (doc) {\n if (doc.$type == \"recipe\" && doc.style && doc.name) {\n emit(doc.style, doc.name)\n }\n}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lists": {},
|
"lists": {},
|
||||||
|
|
|
||||||
15
src/humulus/designs/styles.json
Normal file
15
src/humulus/designs/styles.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"_id": "_design/styles",
|
||||||
|
"language": "javascript",
|
||||||
|
"views": {
|
||||||
|
"by-category": {
|
||||||
|
"map": "function (doc) {\n if (doc.$type == \"style\") {\n category = doc._id.match(/[0-9]+|[a-zA-Z]+/g)\n category[0] = parseInt(category[0])\n emit(category, doc.name)\n }\n}"
|
||||||
|
},
|
||||||
|
"by-name": {
|
||||||
|
"map": "function (doc) {\n if (doc.$type == \"style\") {\n emit(doc.name, doc.name)\n }\n}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lists": {},
|
||||||
|
"indexes": {},
|
||||||
|
"shows": {}
|
||||||
|
}
|
||||||
|
|
@ -27,7 +27,9 @@ from wtforms import (Form, StringField, DecimalField, TextAreaField, FieldList,
|
||||||
from wtforms.validators import DataRequired, Optional
|
from wtforms.validators import DataRequired, Optional
|
||||||
|
|
||||||
from humulus.auth import login_required
|
from humulus.auth import login_required
|
||||||
from humulus.couch import get_doc_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')
|
||||||
|
|
||||||
|
|
@ -169,6 +171,7 @@ class RecipeForm(FlaskForm):
|
||||||
max_entries=20
|
max_entries=20
|
||||||
)
|
)
|
||||||
yeast = FormField(YeastForm)
|
yeast = FormField(YeastForm)
|
||||||
|
style = SelectField('Style', choices=[], validators=[Optional()])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def doc(self):
|
def doc(self):
|
||||||
|
|
@ -182,7 +185,8 @@ class RecipeForm(FlaskForm):
|
||||||
'volume': str(self.volume.data),
|
'volume': str(self.volume.data),
|
||||||
'notes': self.notes.data,
|
'notes': self.notes.data,
|
||||||
'$type': 'recipe',
|
'$type': 'recipe',
|
||||||
'type': self.type.data
|
'type': self.type.data,
|
||||||
|
'style': self.style.data
|
||||||
}
|
}
|
||||||
|
|
||||||
recipe['fermentables'] = [f.doc for f in self.fermentables]
|
recipe['fermentables'] = [f.doc for f in self.fermentables]
|
||||||
|
|
@ -202,6 +206,7 @@ class RecipeForm(FlaskForm):
|
||||||
self.efficiency.data = Decimal(data['efficiency'])
|
self.efficiency.data = Decimal(data['efficiency'])
|
||||||
self.volume.data = Decimal(data['volume'])
|
self.volume.data = Decimal(data['volume'])
|
||||||
self.notes.data = data['notes']
|
self.notes.data = data['notes']
|
||||||
|
self.style.data = data['style']
|
||||||
|
|
||||||
for fermentable in data['fermentables']:
|
for fermentable in data['fermentables']:
|
||||||
self.fermentables.append_entry({
|
self.fermentables.append_entry({
|
||||||
|
|
@ -282,6 +287,7 @@ def index():
|
||||||
@login_required
|
@login_required
|
||||||
def create():
|
def create():
|
||||||
form = RecipeForm()
|
form = RecipeForm()
|
||||||
|
form.style.choices = get_styles_list()
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
response = put_doc(form.doc)
|
response = put_doc(form.doc)
|
||||||
|
|
@ -308,7 +314,17 @@ def create_json():
|
||||||
|
|
||||||
@bp.route('/info/<id>')
|
@bp.route('/info/<id>')
|
||||||
def info(id):
|
def info(id):
|
||||||
return render_template('recipes/info.html', recipe=get_doc_or_404(id))
|
recipe = get_doc_or_404(id)
|
||||||
|
|
||||||
|
style = None
|
||||||
|
if recipe['style'] != '':
|
||||||
|
try:
|
||||||
|
style = get_doc(recipe['style'])
|
||||||
|
except KeyError:
|
||||||
|
flash('Could not find style `{}`.'.format(recipe['style']),
|
||||||
|
'warning')
|
||||||
|
|
||||||
|
return render_template('recipes/info.html', recipe=recipe, style=style)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/info/<id>/json')
|
@bp.route('/info/<id>/json')
|
||||||
|
|
@ -334,6 +350,7 @@ def delete(id):
|
||||||
def update(id):
|
def update(id):
|
||||||
# Get the recipe from the database and validate it is the same revision
|
# Get the recipe from the database and validate it is the same revision
|
||||||
form = RecipeForm()
|
form = RecipeForm()
|
||||||
|
form.style.choices = get_styles_list()
|
||||||
recipe = get_doc_or_404(id)
|
recipe = get_doc_or_404(id)
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
if recipe['_rev'] != request.args.get('rev', None):
|
if recipe['_rev'] != request.args.get('rev', None):
|
||||||
|
|
|
||||||
176
src/humulus/styles.py
Normal file
176
src/humulus/styles.py
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
"""This module has functions for working with BJCP styles."""
|
||||||
|
|
||||||
|
# Copyright 2019 Mike Shoup
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import math
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
import click
|
||||||
|
import requests
|
||||||
|
from flask import Blueprint, abort, current_app, render_template, request
|
||||||
|
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')
|
||||||
|
|
||||||
|
|
||||||
|
def sub_to_doc(sub):
|
||||||
|
"""Coverts sub (XML) to a dictionary document.
|
||||||
|
|
||||||
|
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': {}
|
||||||
|
}
|
||||||
|
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')
|
||||||
|
return doc
|
||||||
|
|
||||||
|
|
||||||
|
def import_styles(url):
|
||||||
|
"""Parses BJCP styles in XML format from `url`.
|
||||||
|
|
||||||
|
`url` defaults to the official BJCP XML styleguide.
|
||||||
|
|
||||||
|
Each subcategory is converted to JSON and then put to a couchdb. The _id of
|
||||||
|
the subsequent document will be `<number><letter>`, i.e., 1A.
|
||||||
|
If the style already exists in the database, it will be skipped.
|
||||||
|
"""
|
||||||
|
db = get_db()
|
||||||
|
root = ET.fromstring(requests.get(url).text)
|
||||||
|
subs = root.findall('./class[@type="beer"]/category/subcategory')
|
||||||
|
for sub in subs:
|
||||||
|
doc = sub_to_doc(sub)
|
||||||
|
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']:
|
||||||
|
print(row)
|
||||||
|
styles.append([row['id'], '{}{} {}'.format(
|
||||||
|
row['key'][0],
|
||||||
|
row['key'][1],
|
||||||
|
row['value']
|
||||||
|
)])
|
||||||
|
return 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')
|
||||||
|
)
|
||||||
|
import_styles(url)
|
||||||
|
click.echo("Imported BJCP styles.")
|
||||||
|
|
||||||
|
|
||||||
|
def init_app(app):
|
||||||
|
"""Register the CLI command with the app."""
|
||||||
|
app.cli.add_command(import_command)
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
view = get_view('_design/styles', 'by-{}'.format(sort_by))
|
||||||
|
try:
|
||||||
|
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],
|
||||||
|
descending=descending,
|
||||||
|
sort_by=sort_by,
|
||||||
|
page=page,
|
||||||
|
num_pages=math.ceil(len(rows)/limit),
|
||||||
|
limit=limit
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/info/<id>')
|
||||||
|
@login_required
|
||||||
|
def info(id):
|
||||||
|
return render_template('styles/info.html', style=get_doc_or_404(id))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/info/<id>/recipes')
|
||||||
|
def recipes(id):
|
||||||
|
style = get_doc_or_404(id)
|
||||||
|
view = get_view('_design/recipes', 'by-style')
|
||||||
|
rows = view(include_docs=True, descending=True, key=id)['rows']
|
||||||
|
return render_template('styles/recipes.html', style=style, rows=rows)
|
||||||
|
|
@ -40,6 +40,11 @@
|
||||||
<li class="nav-item {% if request.url_rule.endpoint == 'recipes.index' %}active{% endif %}">
|
<li class="nav-item {% if request.url_rule.endpoint == 'recipes.index' %}active{% endif %}">
|
||||||
<a class="nav-link" href="{{ url_for('recipes.index') }}">Recipes</a>
|
<a class="nav-link" href="{{ url_for('recipes.index') }}">Recipes</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% if session.logged_in %}
|
||||||
|
<li class="nav-item {% if request.url_rule.endpoint == 'styles.index' %}active{% endif %}">
|
||||||
|
<a class="nav-link" href="{{ url_for('styles.index') }}">Styles</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="navbar-nav ml-auto">
|
<ul class="navbar-nav ml-auto">
|
||||||
<li class="nav-item active">
|
<li class="nav-item active">
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,12 @@
|
||||||
-#}
|
-#}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-6">{{ render_field_with_errors(form.name) }}</div>
|
<div class="col-sm-6">{{ render_field_with_errors(form.name) }}</div>
|
||||||
<div class="col-sm-2">{{ render_field_with_errors(form.type) }}</div>
|
<div class="col-sm-3">{{ render_field_with_errors(form.efficiency, 'ingredient-field') }}</div>
|
||||||
<div class="col-sm-2">{{ render_field_with_errors(form.efficiency, 'ingredient-field') }}</div>
|
<div class="col-sm-3">{{ render_field_with_errors(form.volume, 'ingredient-field') }}</div>
|
||||||
<div class="col-sm-2">{{ render_field_with_errors(form.volume, 'ingredient-field') }}</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm">{{ render_field_with_errors(form.style) }}</div>
|
||||||
|
<div class="col-sm">{{ render_field_with_errors(form.type) }}</div>
|
||||||
</div>
|
</div>
|
||||||
{#-
|
{#-
|
||||||
Fermentable Ingredients
|
Fermentable Ingredients
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,11 @@
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="row"><h1>{{ recipe.name }}</h1></div>
|
<div class="row"><h1>{{ recipe.name }}</h1></div>
|
||||||
|
{% if style %}
|
||||||
|
<div class="row">
|
||||||
|
<h2><a href="{{ url_for('styles.recipes', id=style._id) }}">{{ style._id }} {{ style.name }}</a></h2>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{#-
|
{#-
|
||||||
Recipe Details
|
Recipe Details
|
||||||
-#}
|
-#}
|
||||||
|
|
|
||||||
84
src/humulus/templates/styles/index.html
Normal file
84
src/humulus/templates/styles/index.html
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
{#-
|
||||||
|
Copyright 2019 Mike Shoup
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-#}
|
||||||
|
|
||||||
|
{% extends '_base.html' %}
|
||||||
|
{% block title %}Styles{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="row"><h1>Styles</h1></div>
|
||||||
|
<div class="row">
|
||||||
|
<table class="table table-hover table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
{% if sort_by == 'category' and descending %}
|
||||||
|
<a class="text-dark" href="{{ url_for('styles.index', descending='false', sort_by='category', limit=limit) }}">Category ↓</a>
|
||||||
|
{% elif sort_by == 'category' %}
|
||||||
|
<a class="text-dark" href="{{ url_for('styles.index', descending='true', sort_by='category', limit=limit) }}">Category ↑</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="text-dark" href="{{ url_for('styles.index', descending='false', sort_by='category', limit=limit) }}">Category</a>
|
||||||
|
{% endif %}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{% if sort_by == 'name' and descending %}
|
||||||
|
<a class="text-dark" href="{{ url_for('styles.index', descending='false', sort_by='name', limit=limit) }}">Name ↓</a>
|
||||||
|
{% elif sort_by == 'name' %}
|
||||||
|
<a class="text-dark" href="{{ url_for('styles.index', descending='true', sort_by='name', limit=limit) }}">Name ↑</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="text-dark" href="{{ url_for('styles.index', descending='false', sort_by='name', limit=limit) }}">Name</a>
|
||||||
|
{% endif %}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{% for row in rows %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ row.doc._id }}</td>
|
||||||
|
<td><a href="{{ url_for('styles.info', id=row.doc._id) }}">{{ row.doc.name }}</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<nav>
|
||||||
|
<ul class="pagination flex-wrap">
|
||||||
|
<li class="page-item{% if page == 1 %} disabled {% endif %}">
|
||||||
|
<a class="page-link" href="{{ url_for('styles.index', descending=descending, sort_by=sort_by, page=page-1, limit=limit) }}">Previous</a>
|
||||||
|
</li>
|
||||||
|
{% for num in range(1, num_pages+1) %}
|
||||||
|
<li class="page-item{% if num == page %} active{% endif %}">
|
||||||
|
<a class="page-link" href="{{ url_for('styles.index', descending=descending, sort_by=sort_by, page=num, limit=limit) }}">{{ num }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
<li class="page-item{% if page == num_pages %} disabled {% endif %}">
|
||||||
|
<a class="page-link" href="{{ url_for('styles.index', descending=descending, sort_by=sort_by, page=page+1, limit=limit) }}">Next</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-secondary dropdown-toggle ml-2" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
|
Limit
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
|
||||||
|
<a class="dropdown-item{% if limit == 5 %} active {% endif %}" href="{{ url_for('styles.index', descending=descending, sort_by=sort_by, page=page, limit=5) }}">5</a>
|
||||||
|
<a class="dropdown-item{% if limit == 10 %} active {% endif %}" href="{{ url_for('styles.index', descending=descending, sort_by=sort_by, page=page, limit=10) }}">10</a>
|
||||||
|
<a class="dropdown-item{% if limit == 20 %} active {% endif %}" href="{{ url_for('styles.index', descending=descending, sort_by=sort_by, page=page, limit=20) }}">20</a>
|
||||||
|
<a class="dropdown-item{% if limit == 50 %} active {% endif %}" href="{{ url_for('styles.index', descending=descending, sort_by=sort_by, page=page, limit=50) }}">50</a>
|
||||||
|
<a class="dropdown-item{% if limit == 100 %} active {% endif %}" href="{{ url_for('styles.index', descending=descending, sort_by=sort_by, page=page, limit=100) }}">100</a>
|
||||||
|
<a class="dropdown-item{% if limit == 200 %} active {% endif %}" href="{{ url_for('styles.index', descending=descending, sort_by=sort_by, page=page, limit=200) }}">200</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
85
src/humulus/templates/styles/info.html
Normal file
85
src/humulus/templates/styles/info.html
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
{#-
|
||||||
|
Copyright 2019 Mike Shoup
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-#}
|
||||||
|
{% from "_macros.html" import moment %}
|
||||||
|
|
||||||
|
{% macro render_detail(field, header) %}
|
||||||
|
<div class="row">
|
||||||
|
<h2>{{ header }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<p>{{ field }}</p>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
{% extends '_base.html' %}
|
||||||
|
{% block title %}{{ style._id }} {{ style.name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="row mb-3"><h1>{{ style._id }} {{ style.name }}</h1></div>
|
||||||
|
<p><a href="{{ url_for('styles.recipes', id=style._id) }}">View recipes using this style</a></p>
|
||||||
|
{{ render_detail(style.impression, 'Overall Impression') }}
|
||||||
|
{{ render_detail(style.appearance, 'Appearance') }}
|
||||||
|
{{ render_detail(style.aroma, 'Aroma') }}
|
||||||
|
{{ render_detail(style.flavor, 'Flavor') }}
|
||||||
|
{{ render_detail(style.mouthfeel, 'Mouthfeel') }}
|
||||||
|
|
||||||
|
{% if style.comments %}
|
||||||
|
{{ render_detail(style.comments, 'Comments') }}
|
||||||
|
{% endif %}
|
||||||
|
{% if style.history %}
|
||||||
|
{{ render_detail(style.history, 'History') }}
|
||||||
|
{% endif %}
|
||||||
|
{% if style.ingredients %}
|
||||||
|
{{ render_detail(style.ingredients, 'Characteristic Ingredients') }}
|
||||||
|
{% endif %}
|
||||||
|
{% if style.comparison %}
|
||||||
|
{{ render_detail(style.comparison, 'Style Comparisons') }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<h2>Vital Statistics</h2>
|
||||||
|
<table class="table">
|
||||||
|
<tr>
|
||||||
|
<th>IBU</th>
|
||||||
|
<td>{{ style.ibu.low }} - {{ style.ibu.high }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>OG</th>
|
||||||
|
<td>{{ '%.3f' | format(style.og.low|float) }} - {{ '%.3f' | format(style.og.high|float) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>FG</th>
|
||||||
|
<td>{{ '%.3f' | format(style.fg.low|float) }} - {{ '%.3f' | format(style.fg.high|float) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>SRM</th>
|
||||||
|
<td>{{ style.srm.low }} - {{ style.srm.high }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>ABV</th>
|
||||||
|
<td>{{ style.abv.low }} - {{ style.abv.high }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if style.examples %}
|
||||||
|
{{ render_detail(style.examples, 'Commercial Examples') }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row"><a href="{{ url_for('styles.index') }}">Back to styles list</a></div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
39
src/humulus/templates/styles/recipes.html
Normal file
39
src/humulus/templates/styles/recipes.html
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
{#-
|
||||||
|
Copyright 2019 Mike Shoup
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-#}
|
||||||
|
|
||||||
|
{% extends '_base.html' %}
|
||||||
|
{% block title %}Recipes for {{ style._id }} {{ style.name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{% if session.logged_in %}
|
||||||
|
<div class="row mb-3"><h1>Recipes for <a href="{{ url_for('styles.info', id=style._id) }}">{{ style._id }} {{ style.name }}</a></h1></div>
|
||||||
|
{% else %}
|
||||||
|
<div class="row mb-3"><h1>Recipes for {{ style._id }} {{ style.name }}</h1></div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<th>Name</th>
|
||||||
|
</thead>
|
||||||
|
{% for row in rows %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{{ url_for('recipes.info', id=row.doc._id) }}">{{ row.doc.name }}</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -50,7 +50,8 @@ def app():
|
||||||
'notes': 'Test',
|
'notes': 'Test',
|
||||||
'volume': '5.5',
|
'volume': '5.5',
|
||||||
'fermentables': [],
|
'fermentables': [],
|
||||||
'hops': []
|
'hops': [],
|
||||||
|
'style': ''
|
||||||
})
|
})
|
||||||
put_doc({
|
put_doc({
|
||||||
'_id': 'partial-yeast-recipe',
|
'_id': 'partial-yeast-recipe',
|
||||||
|
|
@ -66,7 +67,8 @@ def app():
|
||||||
'name': 'US-05',
|
'name': 'US-05',
|
||||||
'low_attenuation': '60',
|
'low_attenuation': '60',
|
||||||
'high_attenuation': '72',
|
'high_attenuation': '72',
|
||||||
}
|
},
|
||||||
|
'style': ''
|
||||||
})
|
})
|
||||||
put_doc({
|
put_doc({
|
||||||
'_id': 'full-recipe',
|
'_id': 'full-recipe',
|
||||||
|
|
@ -76,6 +78,7 @@ def app():
|
||||||
'name': 'Awesome Beer',
|
'name': 'Awesome Beer',
|
||||||
'notes': 'This is a test beer that contains most possible fields.',
|
'notes': 'This is a test beer that contains most possible fields.',
|
||||||
'volume': '2.5',
|
'volume': '2.5',
|
||||||
|
'style': '1A',
|
||||||
'fermentables': [
|
'fermentables': [
|
||||||
{
|
{
|
||||||
'name': '2row',
|
'name': '2row',
|
||||||
|
|
@ -122,6 +125,23 @@ def app():
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# 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'}
|
||||||
|
})
|
||||||
|
|
||||||
yield app
|
yield app
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
|
|
@ -164,6 +184,7 @@ def sample_recipes():
|
||||||
'lager': {
|
'lager': {
|
||||||
'efficiency': '72',
|
'efficiency': '72',
|
||||||
'type': 'All-Grain',
|
'type': 'All-Grain',
|
||||||
|
'style': '',
|
||||||
'fermentables': [
|
'fermentables': [
|
||||||
{
|
{
|
||||||
'amount': '9.5',
|
'amount': '9.5',
|
||||||
|
|
@ -222,6 +243,7 @@ def sample_recipes():
|
||||||
'sweetstout': {
|
'sweetstout': {
|
||||||
'efficiency': '72',
|
'efficiency': '72',
|
||||||
'type': 'All-Grain',
|
'type': 'All-Grain',
|
||||||
|
'style': '',
|
||||||
'fermentables': [
|
'fermentables': [
|
||||||
{
|
{
|
||||||
'amount': '2.75',
|
'amount': '2.75',
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,7 @@ def test_create(client, app, auth):
|
||||||
'name': 'Test',
|
'name': 'Test',
|
||||||
'notes': 'Test',
|
'notes': 'Test',
|
||||||
'volume': '5.5',
|
'volume': '5.5',
|
||||||
|
'style': '1A'
|
||||||
}
|
}
|
||||||
response = client.post('/recipes/create', data=data)
|
response = client.post('/recipes/create', data=data)
|
||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
|
|
@ -147,6 +148,7 @@ def test_create(client, app, auth):
|
||||||
assert doc['notes'] == 'Test'
|
assert doc['notes'] == 'Test'
|
||||||
assert doc['volume'] == '5.5'
|
assert doc['volume'] == '5.5'
|
||||||
assert doc['efficiency'] == '65'
|
assert doc['efficiency'] == '65'
|
||||||
|
assert doc['style'] == '1A'
|
||||||
|
|
||||||
|
|
||||||
def test_update(client, app, auth):
|
def test_update(client, app, auth):
|
||||||
|
|
@ -205,8 +207,12 @@ def test_update(client, app, auth):
|
||||||
assert 'Update conflict' in flash_message
|
assert 'Update conflict' in flash_message
|
||||||
|
|
||||||
|
|
||||||
def test_info(client):
|
def test_info(client, monkeypatch):
|
||||||
"""Test success in retrieving a recipe document."""
|
"""Test success in retrieving a recipe document."""
|
||||||
|
def mock_get_doc(id):
|
||||||
|
# This function always raises KeyError
|
||||||
|
raise KeyError(id)
|
||||||
|
|
||||||
# Validate 404
|
# Validate 404
|
||||||
response = client.get('/recipes/info/thisdoesnotexist')
|
response = client.get('/recipes/info/thisdoesnotexist')
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
@ -216,6 +222,17 @@ def test_info(client):
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert b'Awesome Lager' in response.data
|
assert b'Awesome Lager' in response.data
|
||||||
|
|
||||||
|
# Validate response for recipe with style
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
def test_info_json(client):
|
def test_info_json(client):
|
||||||
"""Test success in retrieving a JSON recipe."""
|
"""Test success in retrieving a JSON recipe."""
|
||||||
|
|
@ -279,6 +296,7 @@ def test_recipe_form_doc(app):
|
||||||
recipe.volume.data = Decimal('5.5')
|
recipe.volume.data = Decimal('5.5')
|
||||||
recipe.notes.data = 'This is a test'
|
recipe.notes.data = 'This is a test'
|
||||||
recipe.type.data = 'All-Grain'
|
recipe.type.data = 'All-Grain'
|
||||||
|
recipe.style.data = '1A'
|
||||||
|
|
||||||
assert recipe.doc == {
|
assert recipe.doc == {
|
||||||
'name': 'Test',
|
'name': 'Test',
|
||||||
|
|
@ -289,6 +307,7 @@ def test_recipe_form_doc(app):
|
||||||
'fermentables': [],
|
'fermentables': [],
|
||||||
'hops': [],
|
'hops': [],
|
||||||
'$type': 'recipe',
|
'$type': 'recipe',
|
||||||
|
'style': '1A'
|
||||||
}
|
}
|
||||||
|
|
||||||
ferm = FermentableForm()
|
ferm = FermentableForm()
|
||||||
|
|
@ -321,6 +340,7 @@ def test_recipe_form_doc(app):
|
||||||
'volume': '5.5',
|
'volume': '5.5',
|
||||||
'notes': 'This is a test',
|
'notes': 'This is a test',
|
||||||
'$type': 'recipe',
|
'$type': 'recipe',
|
||||||
|
'style': '1A',
|
||||||
'fermentables': [{
|
'fermentables': [{
|
||||||
'name': 'Test',
|
'name': 'Test',
|
||||||
'type': 'Grain',
|
'type': 'Grain',
|
||||||
|
|
|
||||||
228
tests/test_styles.py
Normal file
228
tests/test_styles.py
Normal file
|
|
@ -0,0 +1,228 @@
|
||||||
|
# Copyright 2019 Mike Shoup
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
from humulus.styles import import_styles, sub_to_doc, get_styles_list
|
||||||
|
|
||||||
|
COMPLETE_STYLE = '''<subcategory id="1A">
|
||||||
|
<name>Test Style</name>
|
||||||
|
<aroma>Smelly</aroma>
|
||||||
|
<appearance>Good looking</appearance>
|
||||||
|
<flavor>Good tasting</flavor>
|
||||||
|
<mouthfeel>Good feeling</mouthfeel>
|
||||||
|
<impression>Refreshing</impression>
|
||||||
|
<comments>Comments</comments>
|
||||||
|
<history>Old</history>
|
||||||
|
<ingredients>Grains, Hops, and Water</ingredients>
|
||||||
|
<comparison>Comparison</comparison>
|
||||||
|
<examples>Examples</examples>
|
||||||
|
<tags>one, two</tags>
|
||||||
|
<stats>
|
||||||
|
<ibu flexible="false">
|
||||||
|
<low>1</low>
|
||||||
|
<high>2</high>
|
||||||
|
</ibu>
|
||||||
|
<og flexible="false">
|
||||||
|
<low>1.010</low>
|
||||||
|
<high>1.020</high>
|
||||||
|
</og>
|
||||||
|
<fg flexible="false">
|
||||||
|
<low>1.000</low>
|
||||||
|
<high>1.010</high>
|
||||||
|
</fg>
|
||||||
|
<srm flexible="false">
|
||||||
|
<low>1</low>
|
||||||
|
<high>2</high>
|
||||||
|
</srm>
|
||||||
|
<abv flexible="false">
|
||||||
|
<low>1</low>
|
||||||
|
<high>2</high>
|
||||||
|
</abv>
|
||||||
|
</stats>
|
||||||
|
</subcategory>
|
||||||
|
'''
|
||||||
|
|
||||||
|
INCOMPLETE_STYLE = '''<subcategory id="2B">
|
||||||
|
<name>Test Style</name>
|
||||||
|
<aroma>Smelly</aroma>
|
||||||
|
<appearance>Good looking</appearance>
|
||||||
|
<flavor>Good tasting</flavor>
|
||||||
|
<mouthfeel>Good feeling</mouthfeel>
|
||||||
|
<impression>Refreshing</impression>
|
||||||
|
</subcategory>
|
||||||
|
'''
|
||||||
|
|
||||||
|
TEST_XML = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<styleguide>
|
||||||
|
<class type="beer">
|
||||||
|
<category id="1">
|
||||||
|
<subcategory id="1A">
|
||||||
|
<name>Test Style</name>
|
||||||
|
<aroma>Smelly</aroma>
|
||||||
|
<appearance>Good looking</appearance>
|
||||||
|
<flavor>Good tasting</flavor>
|
||||||
|
<mouthfeel>Good feeling</mouthfeel>
|
||||||
|
<impression>Refreshing</impression>
|
||||||
|
</subcategory>
|
||||||
|
</category>
|
||||||
|
</class>
|
||||||
|
</styleguide>
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def test_sub_to_doc():
|
||||||
|
assert sub_to_doc(ET.fromstring(COMPLETE_STYLE)) == {
|
||||||
|
'_id': '1A',
|
||||||
|
'$type': 'style',
|
||||||
|
'name': 'Test Style',
|
||||||
|
'aroma': 'Smelly',
|
||||||
|
'appearance': 'Good looking',
|
||||||
|
'flavor': 'Good tasting',
|
||||||
|
'mouthfeel': 'Good feeling',
|
||||||
|
'impression': 'Refreshing',
|
||||||
|
'comments': 'Comments',
|
||||||
|
'history': 'Old',
|
||||||
|
'ingredients': 'Grains, Hops, and Water',
|
||||||
|
'comparison': 'Comparison',
|
||||||
|
'examples': 'Examples',
|
||||||
|
'tags': ['one', 'two'],
|
||||||
|
'ibu': {'low': '1', 'high': '2'},
|
||||||
|
'og': {'low': '1.010', 'high': '1.020'},
|
||||||
|
'fg': {'low': '1.000', 'high': '1.010'},
|
||||||
|
'srm': {'low': '1', 'high': '2'},
|
||||||
|
'abv': {'low': '1', 'high': '2'}
|
||||||
|
}
|
||||||
|
|
||||||
|
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'}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_styles(monkeypatch):
|
||||||
|
class PutRecorder:
|
||||||
|
doc = None
|
||||||
|
|
||||||
|
class MockDB:
|
||||||
|
db = {}
|
||||||
|
|
||||||
|
def fake_get_db():
|
||||||
|
return MockDB.db
|
||||||
|
|
||||||
|
def fake_put_doc(doc):
|
||||||
|
PutRecorder.doc = doc
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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'}
|
||||||
|
}
|
||||||
|
|
||||||
|
MockDB.db = {'1A': ''}
|
||||||
|
PutRecorder.doc = None
|
||||||
|
import_styles(None)
|
||||||
|
assert PutRecorder.doc is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_command(runner, monkeypatch):
|
||||||
|
class Recorder:
|
||||||
|
called = False
|
||||||
|
|
||||||
|
def fake_import_styles(url):
|
||||||
|
Recorder.called = True
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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']
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_index(auth, client):
|
||||||
|
"""Test success in retrieving index."""
|
||||||
|
# Test not logged in
|
||||||
|
response = client.get('/styles/')
|
||||||
|
assert response.status_code == 302
|
||||||
|
|
||||||
|
# Login and test get
|
||||||
|
auth.login()
|
||||||
|
response = client.get('/styles/')
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b'1A' in response.data
|
||||||
|
assert b'Test Style' in response.data
|
||||||
|
|
||||||
|
# Test for bad request
|
||||||
|
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')
|
||||||
|
assert response.status_code == 302
|
||||||
|
|
||||||
|
# Login and test
|
||||||
|
auth.login()
|
||||||
|
response = client.get('/styles/info/1A')
|
||||||
|
assert response.status_code == 200
|
||||||
|
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')
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b'Awesome Beer' in response.data
|
||||||
Loading…
Add table
Reference in a new issue