mirror of
https://github.com/shouptech/humulus.git
synced 2026-02-03 19:39:43 +00:00
Add basic stuff with creating recipes.
This commit is contained in:
parent
e4dc793e9a
commit
02b15956d9
12 changed files with 463 additions and 5 deletions
10
setup.py
10
setup.py
|
|
@ -1,11 +1,11 @@
|
|||
# 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.
|
||||
|
|
@ -22,6 +22,10 @@ with open(path.join(this_directory, 'README.rst'), encoding='utf-8') as f:
|
|||
|
||||
install_requires = [
|
||||
'Flask==1.0.3',
|
||||
'Flask-WTF==0.14.2',
|
||||
'simplejson==3.16.0',
|
||||
'python-slugify==3.0.2',
|
||||
'cloudant==2.12.0',
|
||||
]
|
||||
|
||||
setup(
|
||||
|
|
|
|||
|
|
@ -20,11 +20,24 @@ from flask import Flask
|
|||
def create_app(test_config=None):
|
||||
# create and configure the app
|
||||
app = Flask(__name__, instance_relative_config=True)
|
||||
app.config.from_envvar('HUMULUS_SETTINGS')
|
||||
|
||||
if test_config is not None:
|
||||
# Load the test config if provided
|
||||
app.config.from_mapping(test_config)
|
||||
else:
|
||||
# Load config from configuration provided via ENV
|
||||
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')
|
||||
|
||||
# Register blueprint for recipes
|
||||
from . import recipes
|
||||
app.register_blueprint(recipes.bp)
|
||||
|
||||
return app
|
||||
|
|
|
|||
108
src/humulus/couch.py
Normal file
108
src/humulus/couch.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
"""This module has functions for interacting with CouchDB"""
|
||||
|
||||
# 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 time
|
||||
import uuid
|
||||
|
||||
import click
|
||||
from cloudant import CouchDB
|
||||
from flask import abort, current_app, g
|
||||
from flask.cli import with_appcontext
|
||||
from slugify import slugify
|
||||
|
||||
def get_couch():
|
||||
"""Connect to the configured CouchDB."""
|
||||
if 'couch' not in g:
|
||||
g.couch = CouchDB(
|
||||
current_app.config['COUCH_USERNAME'],
|
||||
current_app.config['COUCH_PASSWORD'],
|
||||
url=current_app.config['COUCH_URL'],
|
||||
connect=True,
|
||||
auto_renew=True
|
||||
)
|
||||
return g.couch
|
||||
|
||||
|
||||
def get_db():
|
||||
"""Returns a database to interact with."""
|
||||
return get_couch()[current_app.config['COUCH_DATABASE']]
|
||||
|
||||
|
||||
def close_couch(e=None):
|
||||
"""Disconnect from CouchDB."""
|
||||
couch = g.pop('couch', None)
|
||||
if couch is not None:
|
||||
couch.disconnect()
|
||||
|
||||
|
||||
def build_couch():
|
||||
"""Create any necessary databases and design documents."""
|
||||
couch = get_couch()
|
||||
dbname = current_app.config['COUCH_DATABASE']
|
||||
couch.create_database(dbname, throw_on_exists=False)
|
||||
|
||||
|
||||
@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.')
|
||||
|
||||
|
||||
def init_app(app):
|
||||
"""Register the teardown and CLI command with the app."""
|
||||
app.teardown_appcontext(close_couch)
|
||||
app.cli.add_command(build_couch_command)
|
||||
|
||||
|
||||
def put_doc(doc):
|
||||
"""Put a doc on the couch.
|
||||
|
||||
If doc has a name field, the name will be slufigied and used as an id.
|
||||
Otherwise, the id will be a random UUID.
|
||||
"""
|
||||
db = get_db()
|
||||
|
||||
if 'name' in doc and '_id' not in doc:
|
||||
# Slugify the name, use that for id
|
||||
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)
|
||||
i += 1
|
||||
elif '_id' not in doc:
|
||||
# Use a UUID for name
|
||||
doc['_id'] = str(uuid.uuid4())
|
||||
|
||||
return db.create_document(doc)
|
||||
|
||||
|
||||
def get_doc(id):
|
||||
"""Gets a doc from CouchDB and returns it."""
|
||||
db = get_db()
|
||||
return db[id]
|
||||
|
||||
|
||||
def get_doc_or_404(id):
|
||||
"""Tries to get a doc, otherwise abort with 404."""
|
||||
try:
|
||||
doc = get_doc(id)
|
||||
except KeyError:
|
||||
abort(404)
|
||||
return doc
|
||||
62
src/humulus/recipes.py
Normal file
62
src/humulus/recipes.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
"""This module handles routes for the recipes"""
|
||||
|
||||
# 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 decimal import Decimal
|
||||
|
||||
from flask import Blueprint, flash, redirect, render_template, url_for
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, DecimalField, TextAreaField
|
||||
from wtforms.validators import DataRequired
|
||||
|
||||
from humulus.couch import get_doc_or_404, put_doc
|
||||
|
||||
bp = Blueprint('recipes', __name__, url_prefix='/recipes')
|
||||
|
||||
class RecipeForm(FlaskForm):
|
||||
name = StringField('Name', validators=[DataRequired()])
|
||||
efficiency = DecimalField('Batch Efficiency', validators=[DataRequired()])
|
||||
volume = DecimalField('Batch Volume', validators=[DataRequired()])
|
||||
notes = TextAreaField('Notes')
|
||||
|
||||
@property
|
||||
def doc(self):
|
||||
"""Returns a dictionary that can be deserialized into JSON.
|
||||
|
||||
Used for putting into CouchDB.
|
||||
"""
|
||||
return {
|
||||
'name': self.name.data,
|
||||
'efficiency': str(self.efficiency.data),
|
||||
'volume': str(self.volume.data),
|
||||
'notes': self.notes.data
|
||||
}
|
||||
|
||||
|
||||
@bp.route('/create', methods=('GET', 'POST'))
|
||||
def create():
|
||||
form = RecipeForm()
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@bp.route('/info/<id>')
|
||||
def info(id):
|
||||
return render_template('recipes/info.html', recipe=get_doc_or_404(id))
|
||||
|
|
@ -42,7 +42,14 @@ limitations under the License.
|
|||
</nav>
|
||||
|
||||
<main role="main" class="container">
|
||||
{% block body %}{% endblock %}
|
||||
{% block alert %}
|
||||
{% for category, message in get_flashed_messages(with_categories=true) %}
|
||||
<div class="container">
|
||||
<div class="alert alert-{{ category }}" role="alert">{{ message }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
{% block body %}{% endblock %}
|
||||
</main><!-- /.container -->
|
||||
|
||||
|
||||
|
|
|
|||
27
src/humulus/templates/_macros.html
Normal file
27
src/humulus/templates/_macros.html
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{#-
|
||||
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.
|
||||
-#}
|
||||
{% macro render_field_with_errors(field) %}
|
||||
<div class="form-group">
|
||||
{{ field.label }} {{ field(class_='form-control', **kwargs)|safe }}
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}
|
||||
<div class="container">
|
||||
<div class="alert alert-warning" role="alert">{{ error }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
33
src/humulus/templates/recipes/create.html
Normal file
33
src/humulus/templates/recipes/create.html
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{#-
|
||||
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 render_field_with_errors %}
|
||||
|
||||
{% extends '_base.html' %}
|
||||
{% block title %}Create Recipe{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="row"><h1>Create a new recipe</h1></div>
|
||||
<div class="row">
|
||||
<form method="POST" action="{{ url_for('recipes.create') }}">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ render_field_with_errors(form.name) }}
|
||||
{{ render_field_with_errors(form.efficiency) }}
|
||||
{{ render_field_with_errors(form.volume) }}
|
||||
{{ render_field_with_errors(form.notes) }}
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
23
src/humulus/templates/recipes/info.html
Normal file
23
src/humulus/templates/recipes/info.html
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{#-
|
||||
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 render_field_with_errors %}
|
||||
|
||||
{% extends '_base.html' %}
|
||||
{% block title %}{{ recipe.name }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="row"><h1>{{ recipe.name }}</h1></div>
|
||||
{% endblock %}
|
||||
62
tests/conftest.py
Normal file
62
tests/conftest.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# 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 uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from humulus import create_app
|
||||
from humulus.couch import build_couch, get_couch, put_doc
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
dbname = 'test_{}'.format(str(uuid.uuid4()))
|
||||
app = create_app({
|
||||
'COUCH_URL': 'http://127.0.0.1:5984',
|
||||
'COUCH_USERNAME': 'admin',
|
||||
'COUCH_PASSWORD': 'password',
|
||||
'COUCH_DATABASE': dbname,
|
||||
'WTF_CSRF_ENABLED': False,
|
||||
'SECRET_KEY': 'testing'
|
||||
})
|
||||
|
||||
with app.app_context():
|
||||
# Create the database
|
||||
build_couch()
|
||||
# Add a test doc
|
||||
put_doc({'data': 'test', '_id': 'foobar'})
|
||||
|
||||
# Add a test recipe
|
||||
put_doc({
|
||||
'_id': 'awesome-lager',
|
||||
'efficiency': '65',
|
||||
'name': 'Awesome Lager',
|
||||
'notes': 'Test',
|
||||
'volume': '5.5'
|
||||
})
|
||||
|
||||
yield app
|
||||
|
||||
with app.app_context():
|
||||
get_couch().delete_database(dbname)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner(app):
|
||||
return app.test_cli_runner()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(app):
|
||||
return app.test_client()
|
||||
51
tests/test_couch.py
Normal file
51
tests/test_couch.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# 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 uuid
|
||||
|
||||
from humulus.couch import get_doc, put_doc
|
||||
|
||||
|
||||
def test_put_doc(app):
|
||||
with app.app_context():
|
||||
data = {'foo': 'bar'}
|
||||
response = put_doc(data)
|
||||
assert '_id' in response
|
||||
|
||||
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-2'
|
||||
|
||||
|
||||
def test_build_couch_command(runner, monkeypatch):
|
||||
class Recorder(object):
|
||||
called = False
|
||||
|
||||
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
|
||||
assert Recorder.called
|
||||
|
||||
|
||||
def test_get_doc(app):
|
||||
with app.app_context():
|
||||
assert get_doc('foobar')['data'] == 'test'
|
||||
18
tests/test_home.py
Normal file
18
tests/test_home.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# 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.
|
||||
|
||||
|
||||
def test_home(client):
|
||||
response = client.get('/')
|
||||
assert b'Home' in response.data
|
||||
50
tests/test_recipes.py
Normal file
50
tests/test_recipes.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# 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 humulus.couch import get_db
|
||||
|
||||
|
||||
def test_create(client, app):
|
||||
# Test GET
|
||||
response = client.get('/recipes/create')
|
||||
assert response.status_code == 200
|
||||
|
||||
# Test POST
|
||||
data = {
|
||||
'efficiency': '65',
|
||||
'name': 'Test',
|
||||
'notes': 'Test',
|
||||
'volume': '5.5'
|
||||
}
|
||||
response = client.post('/recipes/create', data=data)
|
||||
assert response.status_code == 302
|
||||
|
||||
with app.app_context():
|
||||
doc = get_db()['test']
|
||||
|
||||
assert doc['name'] == 'Test'
|
||||
assert doc['notes'] == 'Test'
|
||||
assert doc['volume'] == '5.5'
|
||||
assert doc['efficiency'] == '65'
|
||||
|
||||
|
||||
def test_info(client):
|
||||
# Validate 404
|
||||
response = client.get('/recipes/info/thisdoesnotexist')
|
||||
assert response.status_code == 404
|
||||
|
||||
# Validate response for existing doc
|
||||
response = client.get('/recipes/info/awesome-lager')
|
||||
assert response.status_code == 200
|
||||
assert b'Awesome Lager' in response.data
|
||||
Loading…
Add table
Reference in a new issue