mirror of
https://github.com/shouptech/humulus.git
synced 2026-02-03 17:19:42 +00:00
Add page to view recipes matching a style
This commit is contained in:
parent
5968623b76
commit
a647bc8151
10 changed files with 76 additions and 29 deletions
|
|
@ -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": {},
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"language": "javascript",
|
"language": "javascript",
|
||||||
"views": {
|
"views": {
|
||||||
"by-category": {
|
"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}"
|
"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": {
|
"by-name": {
|
||||||
"map": "function (doc) {\n if (doc.$type == \"style\") {\n emit(doc.name, doc.name)\n }\n}"
|
"map": "function (doc) {\n if (doc.$type == \"style\") {\n emit(doc.name, doc.name)\n }\n}"
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,8 @@ def sub_to_doc(sub):
|
||||||
The returned dictionary can be placed right into CouchDB if you want.
|
The returned dictionary can be placed right into CouchDB if you want.
|
||||||
"""
|
"""
|
||||||
doc = {
|
doc = {
|
||||||
'_id': 'style_{}'.format(sub.attrib['id']),
|
'_id': '{}'.format(sub.attrib['id']),
|
||||||
'$type': 'style',
|
'$type': 'style',
|
||||||
'id': sub.attrib['id'],
|
|
||||||
'name': sub.find('name').text,
|
'name': sub.find('name').text,
|
||||||
'aroma': sub.find('aroma').text,
|
'aroma': sub.find('aroma').text,
|
||||||
'appearance': sub.find('appearance').text,
|
'appearance': sub.find('appearance').text,
|
||||||
|
|
@ -91,7 +90,7 @@ def import_styles(url):
|
||||||
`url` defaults to the official BJCP XML styleguide.
|
`url` defaults to the official BJCP XML styleguide.
|
||||||
|
|
||||||
Each subcategory is converted to JSON and then put to a couchdb. The _id of
|
Each subcategory is converted to JSON and then put to a couchdb. The _id of
|
||||||
the subsequent document will be `style_<number><letter>`, i.e., style_1A.
|
the subsequent document will be `<number><letter>`, i.e., 1A.
|
||||||
If the style already exists in the database, it will be skipped.
|
If the style already exists in the database, it will be skipped.
|
||||||
"""
|
"""
|
||||||
db = get_db()
|
db = get_db()
|
||||||
|
|
@ -167,3 +166,15 @@ def index():
|
||||||
@login_required
|
@login_required
|
||||||
def info(id):
|
def info(id):
|
||||||
return render_template('styles/info.html', style=get_doc_or_404(id))
|
return render_template('styles/info.html', style=get_doc_or_404(id))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/info/<id>/recipes')
|
||||||
|
def recipes(id):
|
||||||
|
style = get_doc_or_404(id)
|
||||||
|
view = get_view('_design/recipes', 'by-style')
|
||||||
|
try:
|
||||||
|
rows = view(include_docs=True, descending=True, key=id)['rows']
|
||||||
|
except requests.exceptions.HTTPError:
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
return render_template('styles/recipes.html', style=style, rows=rows)
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,7 @@
|
||||||
<div class="row"><h1>{{ recipe.name }}</h1></div>
|
<div class="row"><h1>{{ recipe.name }}</h1></div>
|
||||||
{% if style %}
|
{% if style %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% if session.logged_in %}
|
<h2><a href="{{ url_for('styles.recipes', id=style._id) }}">{{ style._id }} {{ style.name }}</a></h2>
|
||||||
<h2><a href="{{ url_for('styles.info', id=style._id) }}">{{ style.id }} {{ style.name }}</a></h2>
|
|
||||||
{% else %}
|
|
||||||
<h2>{{ style.id }} {{ style.name }}</h2>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{#-
|
{#-
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
</thead>
|
</thead>
|
||||||
{% for row in rows %}
|
{% for row in rows %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ row.doc.id }}</td>
|
<td>{{ row.doc._id }}</td>
|
||||||
<td><a href="{{ url_for('styles.info', id=row.doc._id) }}">{{ row.doc.name }}</a></td>
|
<td><a href="{{ url_for('styles.info', id=row.doc._id) }}">{{ row.doc.name }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,11 @@
|
||||||
|
|
||||||
|
|
||||||
{% extends '_base.html' %}
|
{% extends '_base.html' %}
|
||||||
{% block title %}{{ style.id }} {{ style.name }}{% endblock %}
|
{% block title %}{{ style._id }} {{ style.name }}{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="row mb-3"><h1>{{ style.id }} {{ style.name }}</h1></div>
|
<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.impression, 'Overall Impression') }}
|
||||||
{{ render_detail(style.appearance, 'Appearance') }}
|
{{ render_detail(style.appearance, 'Appearance') }}
|
||||||
{{ render_detail(style.aroma, 'Aroma') }}
|
{{ render_detail(style.aroma, 'Aroma') }}
|
||||||
|
|
|
||||||
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 %}
|
||||||
|
|
@ -78,7 +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': 'style_1A',
|
'style': '1A',
|
||||||
'fermentables': [
|
'fermentables': [
|
||||||
{
|
{
|
||||||
'name': '2row',
|
'name': '2row',
|
||||||
|
|
@ -127,7 +127,7 @@ def app():
|
||||||
|
|
||||||
# Add a test style
|
# Add a test style
|
||||||
put_doc({'$type': 'style',
|
put_doc({'$type': 'style',
|
||||||
'_id': 'style_1A',
|
'_id': '1A',
|
||||||
'abv': {'high': '100', 'low': '0'},
|
'abv': {'high': '100', 'low': '0'},
|
||||||
'appearance': 'Good looking',
|
'appearance': 'Good looking',
|
||||||
'aroma': 'Smelly',
|
'aroma': 'Smelly',
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@ def test_create(client, app, auth):
|
||||||
'name': 'Test',
|
'name': 'Test',
|
||||||
'notes': 'Test',
|
'notes': 'Test',
|
||||||
'volume': '5.5',
|
'volume': '5.5',
|
||||||
'style': 'style_1A'
|
'style': '1A'
|
||||||
}
|
}
|
||||||
response = client.post('/recipes/create', data=data)
|
response = client.post('/recipes/create', data=data)
|
||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
|
|
@ -148,7 +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'] == 'style_1A'
|
assert doc['style'] == '1A'
|
||||||
|
|
||||||
|
|
||||||
def test_update(client, app, auth):
|
def test_update(client, app, auth):
|
||||||
|
|
@ -296,7 +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 = 'style_1A'
|
recipe.style.data = '1A'
|
||||||
|
|
||||||
assert recipe.doc == {
|
assert recipe.doc == {
|
||||||
'name': 'Test',
|
'name': 'Test',
|
||||||
|
|
@ -307,7 +307,7 @@ def test_recipe_form_doc(app):
|
||||||
'fermentables': [],
|
'fermentables': [],
|
||||||
'hops': [],
|
'hops': [],
|
||||||
'$type': 'recipe',
|
'$type': 'recipe',
|
||||||
'style': 'style_1A'
|
'style': '1A'
|
||||||
}
|
}
|
||||||
|
|
||||||
ferm = FermentableForm()
|
ferm = FermentableForm()
|
||||||
|
|
@ -340,7 +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': 'style_1A',
|
'style': '1A',
|
||||||
'fermentables': [{
|
'fermentables': [{
|
||||||
'name': 'Test',
|
'name': 'Test',
|
||||||
'type': 'Grain',
|
'type': 'Grain',
|
||||||
|
|
|
||||||
|
|
@ -84,9 +84,8 @@ TEST_XML = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
def test_sub_to_doc():
|
def test_sub_to_doc():
|
||||||
assert sub_to_doc(ET.fromstring(COMPLETE_STYLE)) == {
|
assert sub_to_doc(ET.fromstring(COMPLETE_STYLE)) == {
|
||||||
'_id': 'style_1A',
|
'_id': '1A',
|
||||||
'$type': 'style',
|
'$type': 'style',
|
||||||
'id': '1A',
|
|
||||||
'name': 'Test Style',
|
'name': 'Test Style',
|
||||||
'aroma': 'Smelly',
|
'aroma': 'Smelly',
|
||||||
'appearance': 'Good looking',
|
'appearance': 'Good looking',
|
||||||
|
|
@ -107,9 +106,8 @@ def test_sub_to_doc():
|
||||||
}
|
}
|
||||||
|
|
||||||
assert sub_to_doc(ET.fromstring(INCOMPLETE_STYLE)) == {
|
assert sub_to_doc(ET.fromstring(INCOMPLETE_STYLE)) == {
|
||||||
'_id': 'style_2B',
|
'_id': '2B',
|
||||||
'$type': 'style',
|
'$type': 'style',
|
||||||
'id': '2B',
|
|
||||||
'name': 'Test Style',
|
'name': 'Test Style',
|
||||||
'aroma': 'Smelly',
|
'aroma': 'Smelly',
|
||||||
'appearance': 'Good looking',
|
'appearance': 'Good looking',
|
||||||
|
|
@ -148,14 +146,13 @@ def test_import_styles(monkeypatch):
|
||||||
|
|
||||||
import_styles(None)
|
import_styles(None)
|
||||||
assert PutRecorder.doc == {'$type': 'style',
|
assert PutRecorder.doc == {'$type': 'style',
|
||||||
'_id': 'style_1A',
|
'_id': '1A',
|
||||||
'abv': {'high': '100', 'low': '0'},
|
'abv': {'high': '100', 'low': '0'},
|
||||||
'appearance': 'Good looking',
|
'appearance': 'Good looking',
|
||||||
'aroma': 'Smelly',
|
'aroma': 'Smelly',
|
||||||
'fg': {'high': '1.2', 'low': '1.0'},
|
'fg': {'high': '1.2', 'low': '1.0'},
|
||||||
'flavor': 'Good tasting',
|
'flavor': 'Good tasting',
|
||||||
'ibu': {'high': '100', 'low': '0'},
|
'ibu': {'high': '100', 'low': '0'},
|
||||||
'id': '1A',
|
|
||||||
'impression': 'Refreshing',
|
'impression': 'Refreshing',
|
||||||
'mouthfeel': 'Good feeling',
|
'mouthfeel': 'Good feeling',
|
||||||
'name': 'Test Style',
|
'name': 'Test Style',
|
||||||
|
|
@ -163,7 +160,7 @@ def test_import_styles(monkeypatch):
|
||||||
'srm': {'high': '100', 'low': '0'}
|
'srm': {'high': '100', 'low': '0'}
|
||||||
}
|
}
|
||||||
|
|
||||||
MockDB.db = {'style_1A': ''}
|
MockDB.db = {'1A': ''}
|
||||||
PutRecorder.doc = None
|
PutRecorder.doc = None
|
||||||
import_styles(None)
|
import_styles(None)
|
||||||
assert PutRecorder.doc is None
|
assert PutRecorder.doc is None
|
||||||
|
|
@ -188,7 +185,7 @@ def test_get_styles_choices(app):
|
||||||
styles = get_styles_list()
|
styles = get_styles_list()
|
||||||
assert styles == [
|
assert styles == [
|
||||||
['', ''],
|
['', ''],
|
||||||
['style_1A', '1A Test Style']
|
['1A', '1A Test Style']
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -213,12 +210,12 @@ def test_index(auth, client):
|
||||||
def test_info(auth, client):
|
def test_info(auth, client):
|
||||||
"""Test success in retrieving a style's info page"""
|
"""Test success in retrieving a style's info page"""
|
||||||
# Test not logged in
|
# Test not logged in
|
||||||
response = client.get('/styles/info/style_1A')
|
response = client.get('/styles/info/1A')
|
||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
|
|
||||||
# Login and test
|
# Login and test
|
||||||
auth.login()
|
auth.login()
|
||||||
response = client.get('/styles/info/style_1A')
|
response = client.get('/styles/info/1A')
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert b'1A' in response.data
|
assert b'1A' in response.data
|
||||||
assert b'Test Style' in response.data
|
assert b'Test Style' in response.data
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue