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

Compare commits

...

3 commits

Author SHA1 Message Date
868d12fced Add info endpoint 2019-07-08 12:40:01 -06:00
dd25448ed9 Add styles to navbar 2019-07-08 12:13:02 -06:00
7be88cd934 Add style list view 2019-07-08 12:06:40 -06:00
7 changed files with 268 additions and 5 deletions

View file

@ -33,9 +33,6 @@ def create_app(test_config=None):
from . import couch
couch.init_app(app)
from . import styles
styles.init_app(app)
# Register blueprint for index page
from . import home
app.register_blueprint(home.bp)
@ -49,6 +46,11 @@ def create_app(test_config=None):
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)

View file

@ -14,14 +14,19 @@
# 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 current_app
from flask import Blueprint, abort, current_app, render_template, request
from flask.cli import with_appcontext
from humulus.couch import get_db, put_doc
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.
@ -114,3 +119,37 @@ def import_command():
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))

View file

@ -40,6 +40,11 @@
<li class="nav-item {% if request.url_rule.endpoint == 'recipes.index' %}active{% endif %}">
<a class="nav-link" href="{{ url_for('recipes.index') }}">Recipes</a>
</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 class="navbar-nav ml-auto">
<li class="nav-item active">

View 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 &darr;</a>
{% elif sort_by == 'category' %}
<a class="text-dark" href="{{ url_for('styles.index', descending='true', sort_by='category', limit=limit) }}">Category &uarr;</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 &darr;</a>
{% elif sort_by == 'name' %}
<a class="text-dark" href="{{ url_for('styles.index', descending='true', sort_by='name', limit=limit) }}">Name &uarr;</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 %}

View 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.
-#}
{% 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>
{{ 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 %}

View file

@ -122,6 +122,23 @@ def app():
}
})
# Add a test style
put_doc({'$type': 'style',
'_id': 'style_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
with app.app_context():

View file

@ -180,3 +180,35 @@ def test_import_command(runner, monkeypatch):
result = runner.invoke(args=['import-styles'])
assert Recorder.called
assert 'Imported BJCP styles.' in result.output
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/style_1A')
assert response.status_code == 302
# Login and test
auth.login()
response = client.get('/styles/info/style_1A')
assert response.status_code == 200
assert b'1A' in response.data
assert b'Test Style' in response.data