diff --git a/src/humulus/couch.py b/src/humulus/couch.py index c97939c..e5212e0 100644 --- a/src/humulus/couch.py +++ b/src/humulus/couch.py @@ -90,13 +90,12 @@ def put_doc(doc): # Use a UUID for name doc['_id'] = str(uuid.uuid4()) - return db.create_document(doc) + return db.create_document(doc, throw_on_exists=True) def get_doc(id): """Gets a doc from CouchDB and returns it.""" - db = get_db() - return db[id] + return get_db()[id] def get_doc_or_404(id): diff --git a/src/humulus/recipes.py b/src/humulus/recipes.py index 9632c43..ca08c1c 100644 --- a/src/humulus/recipes.py +++ b/src/humulus/recipes.py @@ -16,7 +16,7 @@ from decimal import Decimal -from flask import Blueprint, flash, redirect, render_template, url_for +from flask import Blueprint, flash, redirect, render_template, request, url_for from flask_wtf import FlaskForm from wtforms import (Form, StringField, DecimalField, TextAreaField, FieldList, FormField, SelectField) @@ -92,7 +92,7 @@ class YeastForm(Form): CSRF is disabled for this subform (using `Form as parent class) because it is never used by itself. """ - name = StringField('Name', validators=[DataRequired()]) + name = StringField('Name', validators=[Optional()]) type = SelectField('Type', default='', choices=[(c, c) for c in ['', 'Liquid', 'Dry']], validators=[Optional()]) @@ -103,9 +103,9 @@ class YeastForm(Form): 'Medium', 'High']], validators=[Optional()]) low_attenuation = DecimalField('Low Attenuation', - validators=[DataRequired()]) + validators=[Optional()]) high_attenuation = DecimalField('High Attenuation', - validators=[DataRequired()]) + validators=[Optional()]) min_temperature = DecimalField('Min Temp (°F)', validators=[Optional()]) max_temperature = DecimalField('Max Temp (°F)', @@ -203,3 +203,30 @@ def delete(id): recipe = get_doc_or_404(id) recipe.delete() return redirect(url_for('home.index')) + + +@bp.route('/update/', methods=('GET', 'POST')) +def update(id): + # Get the recipe from the database and validate it is the same revision + recipe = get_doc_or_404(id) + + form = RecipeForm() + if form.validate_on_submit(): + if recipe['_rev'] != request.args.get('rev', None): + flash( + ( + 'Update conflict for recipe: {}. ' + 'Your changes have been lost.'.format(recipe['name']) + ), + 'error' + ) + 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 + recipe.save() + + flash('Updated recipe: {}'.format(form.name.data), 'success') + return redirect(url_for('recipes.info', id=id)) + + return render_template('recipes/update.html', form=form) diff --git a/src/humulus/templates/recipes/update.html b/src/humulus/templates/recipes/update.html new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_recipes.py b/tests/test_recipes.py index 79b35e1..120a686 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -14,7 +14,7 @@ from decimal import Decimal -from humulus.couch import get_db +from humulus.couch import get_db, get_doc from humulus.recipes import FermentableForm, HopForm, RecipeForm, YeastForm @@ -30,15 +30,12 @@ def test_create(client, app): 'name': 'Test', 'notes': 'Test', 'volume': '5.5', - 'yeast-name': 'Test', - 'yeast-low_attenuation': '60', - 'yeast-high_attenuation': '75', } response = client.post('/recipes/create', data=data) assert response.status_code == 302 with app.app_context(): - doc = get_db()['test'] + doc = get_doc('test') assert doc['name'] == 'Test' assert doc['notes'] == 'Test' @@ -46,6 +43,42 @@ def test_create(client, app): assert doc['efficiency'] == '65' +def test_update(client, app): + """Test success in updating a recipe document.""" + # Test GET + id = 'awesome-lager' + response = client.get('/recipes/update/{}'.format(id)) + + # Get the doc, test a change, then post the update + with app.app_context(): + data = get_doc(id) + # Remove values that should not be posted with the data + rev = data.pop('_rev') # Save the revision, will be used later + data.pop('_id') + # Make a change to the data + data['name'] = '{} TEST'.format(data['name']) + + # Test valid response + response = client.post('/recipes/update/{}'.format(id), + query_string={'rev': rev}, data=data) + assert response.status_code == 302 + with client.session_transaction() as session: + flash_message = dict(session['_flashes']).get('error') + assert flash_message is None + + # Test response without valid/conflicted rev + response = client.post('/recipes/update/{}'.format(id), + query_string={'rev': ''}, data=data) + assert response.status_code == 302 + with client.session_transaction() as session: + flash_message = dict(session['_flashes']).get('error') + assert 'Update conflict' in flash_message + + with app.app_context(): + updated = get_doc(id) + assert updated['name'] == data['name'] + + def test_info(client): """Test success in retrieving a recipe document.""" # Validate 404