Allow to edit a recipe
This commit is contained in:
parent
da844e0a08
commit
d30e5f9053
29
cuizin/db.py
29
cuizin/db.py
@ -40,7 +40,7 @@ class Recipe(Model):
|
||||
# Author
|
||||
author = CharField(null=True)
|
||||
# Picture as a binary blob
|
||||
picture = BlobField(null=True)
|
||||
picture = TextField(null=True)
|
||||
# Short description
|
||||
short_description = TextField(null=True)
|
||||
# Number of persons as text, as it can be either "N persons" or "N parts"
|
||||
@ -69,7 +69,18 @@ class Recipe(Model):
|
||||
setattr(self, field, value)
|
||||
# Download picture and save it as a blob
|
||||
if d.get('picture_url', None):
|
||||
self.picture = requests.get(d['picture_url']).content
|
||||
try:
|
||||
picture = requests.get(d['picture_url']).content
|
||||
picture_mime = (
|
||||
'data:%s;base64' % magic.from_buffer(picture,
|
||||
mime=True)
|
||||
)
|
||||
self.picture = '%s,%s' % (
|
||||
picture_mime,
|
||||
base64.b64encode(picture).decode('utf-8')
|
||||
)
|
||||
except requests.exceptions.InvalidSchema:
|
||||
self.picture = d['picture_url']
|
||||
|
||||
def update_from_weboob(self, weboob_obj):
|
||||
"""
|
||||
@ -86,16 +97,4 @@ class Recipe(Model):
|
||||
"""
|
||||
Dict conversion function, for serialization in the API.
|
||||
"""
|
||||
serialized = model_to_dict(self)
|
||||
# Dump picture as a base64 string, compatible with HTML `src` attribute
|
||||
# for images.
|
||||
if serialized['picture']:
|
||||
picture_mime = (
|
||||
'data:%s;base64' % magic.from_buffer(serialized['picture'],
|
||||
mime=True)
|
||||
)
|
||||
serialized['picture'] = '%s,%s' % (
|
||||
picture_mime,
|
||||
base64.b64encode(serialized['picture']).decode('utf-8')
|
||||
)
|
||||
return serialized
|
||||
return model_to_dict(self)
|
||||
|
@ -66,6 +66,15 @@ export function postRecipeManually(recipe) {
|
||||
}
|
||||
|
||||
|
||||
export function editRecipe(id, recipe) {
|
||||
return fetch(`${constants.API_URL}api/v1/recipe/${id}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(recipe),
|
||||
})
|
||||
.then(_postProcessRecipes);
|
||||
}
|
||||
|
||||
|
||||
export function deleteRecipe(id) {
|
||||
return fetch(`${constants.API_URL}api/v1/recipe/${id}`, {
|
||||
method: 'DELETE',
|
||||
|
@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<v-flex xs12>
|
||||
<h2>{{ $t('new.add_manually') }}</h2>
|
||||
<v-form v-model="isValidAdd">
|
||||
<ErrorDialog :v-model="error" :description="$t('error.title')" />
|
||||
|
||||
<h2 v-if="recipe">{{ $t('new.edit_recipe') }}</h2>
|
||||
<h2 v-else>{{ $t('new.add_manually') }}</h2>
|
||||
|
||||
<v-form v-model="isValidForm">
|
||||
<v-text-field
|
||||
:label="$t('new.title')"
|
||||
v-model="title"
|
||||
@ -13,6 +17,9 @@
|
||||
v-model="picture_url"
|
||||
:rules="urlRules"
|
||||
></v-text-field>
|
||||
<p>
|
||||
<img :src="picture_url" />
|
||||
</p>
|
||||
<v-text-field
|
||||
:label="$t('new.short_description')"
|
||||
v-model="short_description"
|
||||
@ -71,9 +78,17 @@
|
||||
required
|
||||
></v-text-field>
|
||||
|
||||
<v-btn
|
||||
@click="submitEdit"
|
||||
:disabled="!isValidForm || isImporting"
|
||||
v-if="recipe"
|
||||
>
|
||||
{{ $t('new.edit') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@click="submitAdd"
|
||||
:disabled="!isValidAdd || isImporting"
|
||||
:disabled="!isValidForm || isImporting"
|
||||
v-else
|
||||
>
|
||||
{{ $t('new.add') }}
|
||||
</v-btn>
|
||||
@ -85,8 +100,17 @@
|
||||
import * as api from '@/api';
|
||||
import * as rules from '@/rules';
|
||||
|
||||
import ErrorDialog from '@/components/ErrorDialog';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ErrorDialog,
|
||||
},
|
||||
props: {
|
||||
recipe: {
|
||||
default: null,
|
||||
type: Object,
|
||||
},
|
||||
value: Boolean,
|
||||
},
|
||||
computed: {
|
||||
@ -100,18 +124,31 @@ export default {
|
||||
},
|
||||
},
|
||||
data() {
|
||||
let defaultPreparationTime = null;
|
||||
if (this.recipe &&
|
||||
this.recipe.preparation_time !== null && this.recipe.preparation_time !== undefined
|
||||
) {
|
||||
defaultPreparationTime = this.recipe.preparation_time;
|
||||
}
|
||||
let defaultCookingTime = null;
|
||||
if (this.recipe &&
|
||||
this.recipe.cooking_time !== null && this.recipe.cooking_time !== undefined
|
||||
) {
|
||||
defaultCookingTime = this.recipe.cooking_time;
|
||||
}
|
||||
return {
|
||||
error: null,
|
||||
url: null,
|
||||
isValidAdd: false,
|
||||
title: null,
|
||||
picture_url: null,
|
||||
short_description: null,
|
||||
nb_person: null,
|
||||
preparation_time: null,
|
||||
cooking_time: null,
|
||||
isValidForm: false,
|
||||
title: (this.recipe && this.recipe.title) || null,
|
||||
picture_url: (this.recipe && this.recipe.picture) || null,
|
||||
short_description: (this.recipe && this.recipe.short_description) || null,
|
||||
nb_person: (this.recipe && this.recipe.nb_person) || null,
|
||||
preparation_time: defaultPreparationTime,
|
||||
cooking_time: defaultCookingTime,
|
||||
new_ingredient: null,
|
||||
ingredients: [],
|
||||
instructions: null,
|
||||
ingredients: (this.recipe && this.recipe.ingredients) || [],
|
||||
instructions: (this.recipe && this.recipe.instructions.join('\n\n').replace(/\n{2,}/, '\n\n')) || null,
|
||||
urlRules: rules.url,
|
||||
};
|
||||
},
|
||||
@ -149,6 +186,29 @@ export default {
|
||||
this.error = error;
|
||||
});
|
||||
},
|
||||
submitEdit() {
|
||||
this.isImporting = true;
|
||||
api.editRecipe(this.recipe.id, {
|
||||
title: this.title,
|
||||
picture_url: this.picture_url,
|
||||
short_description: this.short_description,
|
||||
preparation_time: this.preparation_time,
|
||||
cooking_time: this.cooking_time,
|
||||
nb_person: this.nb_person,
|
||||
ingredients: this.ingredients,
|
||||
instructions: this.instructions,
|
||||
})
|
||||
.then(response => this.$router.push({
|
||||
name: 'Recipe',
|
||||
params: {
|
||||
recipeId: response.recipes[0].id,
|
||||
},
|
||||
}))
|
||||
.catch((error) => {
|
||||
this.isImporting = false;
|
||||
this.error = error;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -157,4 +217,8 @@ export default {
|
||||
.transparent {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: 150px;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,8 +1,11 @@
|
||||
export default {
|
||||
error: {
|
||||
title: 'Error',
|
||||
unable_delete_recipe: 'Unable to delete recipe: ',
|
||||
unable_fetch_recipe: 'Unable to fetch recipe: ',
|
||||
unable_import_recipe: 'Unable to import recipe: ',
|
||||
unable_load_recipes: 'Unable to load recipes: ',
|
||||
unable_refetch_recipe: 'Unable to refetch recipe: ',
|
||||
},
|
||||
home: {
|
||||
onboarding: 'Start by adding a recipe with the "+" button on the top right corner!',
|
||||
@ -16,6 +19,8 @@ export default {
|
||||
add_ingredient: 'Add ingredient',
|
||||
add_manually: 'Add manually',
|
||||
cooking_time: 'Cooking time',
|
||||
edit: 'Edit',
|
||||
edit_recipe: 'Edit recipe',
|
||||
import: 'Import',
|
||||
import_from_url: 'Import from URL',
|
||||
importing: 'Importing…',
|
||||
@ -30,6 +35,7 @@ export default {
|
||||
short_description: 'Short description',
|
||||
title: 'Title',
|
||||
title_is_required: 'Title is required',
|
||||
updating: 'Updating…',
|
||||
url_is_required: 'URL is required',
|
||||
url_must_be_valid: 'URL must be a valid one',
|
||||
},
|
||||
@ -38,6 +44,7 @@ export default {
|
||||
delete: 'Delete',
|
||||
delete_recipe: 'Delete recipe',
|
||||
delete_recipe_description: 'This will delete this recipe. Are you sure?',
|
||||
edit: 'Edit',
|
||||
ingredients: 'Ingredients',
|
||||
instructions: 'Instructions',
|
||||
none: 'Aucun',
|
||||
|
@ -1,8 +1,11 @@
|
||||
export default {
|
||||
error: {
|
||||
title: 'Erreur',
|
||||
unable_delete_recipe: 'Impossible de supprimer la recette : ',
|
||||
unable_fetch_recipe: 'Impossible de récupérer la recette : ',
|
||||
unable_import_recipe: 'Impossible d\'importer la recette : ',
|
||||
unable_load_recipes: 'Impossible de charger les recettes : ',
|
||||
unable_refetch_recipe: 'Impossible de réactualiser la recette : ',
|
||||
},
|
||||
home: {
|
||||
onboarding: 'Commencez par ajouter une recette avec le bouton "+" dans le coin supérieur droit !',
|
||||
@ -16,6 +19,8 @@ export default {
|
||||
add_ingredient: 'Ajouter un ingrédient',
|
||||
add_manually: 'Ajouter manuellement',
|
||||
cooking_time: 'Temps de cuisson',
|
||||
edit: 'Modifier',
|
||||
edit_recipe: 'Modifier une recette',
|
||||
import: 'Importer',
|
||||
import_from_url: 'Importer depuis l\'URL',
|
||||
importing: 'En cours d\'import…',
|
||||
@ -30,6 +35,7 @@ export default {
|
||||
short_description: 'Description brève',
|
||||
title: 'Titre',
|
||||
title_is_required: 'Un titre est requis',
|
||||
updating: 'En cours de mise à jour…',
|
||||
url_is_required: 'Une URL est requise',
|
||||
url_must_be_valid: 'L\'URL est invalide',
|
||||
},
|
||||
@ -38,6 +44,7 @@ export default {
|
||||
delete: 'Supprimer',
|
||||
delete_recipe: 'Suppression d\'une recette',
|
||||
delete_recipe_description: 'Vous allez supprimer cette recette. En êtes-vous sûr ?',
|
||||
edit: 'Modifier',
|
||||
ingredients: 'Ingrédients',
|
||||
instructions: 'Instructions',
|
||||
none: 'Aucun',
|
||||
|
@ -1,6 +1,8 @@
|
||||
import Vue from 'vue';
|
||||
import Router from 'vue-router';
|
||||
|
||||
import Home from '@/views/Home';
|
||||
import Edit from '@/views/Edit';
|
||||
import New from '@/views/New';
|
||||
import Recipe from '@/views/Recipe';
|
||||
|
||||
@ -23,5 +25,10 @@ export default new Router({
|
||||
name: 'Recipe',
|
||||
component: Recipe,
|
||||
},
|
||||
{
|
||||
path: '/edit/:recipeId',
|
||||
name: 'Edit',
|
||||
component: Edit,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
72
cuizin/js_src/views/Edit.vue
Normal file
72
cuizin/js_src/views/Edit.vue
Normal file
@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<v-container text-xs-center v-if="isLoading">
|
||||
<v-layout row>
|
||||
<Loader></Loader>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
<v-container text-xs-center v-else-if="isImporting">
|
||||
<v-layout row wrap>
|
||||
<Loader></Loader>
|
||||
|
||||
<v-flex xs12>
|
||||
<p>{{ $t('new.updating') }}</p>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
<v-container text-xs-center v-else class="panel">
|
||||
<v-layout row wrap>
|
||||
<EditForm v-model="isImporting" :recipe="recipe"></EditForm>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from '@/api';
|
||||
|
||||
import EditForm from '@/components/EditForm';
|
||||
import Loader from '@/components/Loader';
|
||||
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditForm,
|
||||
Loader,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isImporting: false,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
recipe: null,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.loadRecipe();
|
||||
},
|
||||
watch: {
|
||||
// call again the method if the route changes
|
||||
$route: 'loadRecipe',
|
||||
},
|
||||
methods: {
|
||||
handleRecipesResponse(response) {
|
||||
if (response.recipes.length < 1) {
|
||||
this.$router.replace({
|
||||
name: 'Home',
|
||||
});
|
||||
}
|
||||
this.recipe = response.recipes[0];
|
||||
this.isLoading = false;
|
||||
},
|
||||
loadRecipe() {
|
||||
this.isLoading = true;
|
||||
|
||||
api.loadRecipe(this.$route.params.recipeId)
|
||||
.then(this.handleRecipesResponse)
|
||||
.catch((error) => {
|
||||
this.isLoading = false;
|
||||
this.error = error;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
@ -18,13 +18,15 @@
|
||||
<h3 class="headline mb-0">{{ recipe.title }}</h3>
|
||||
</div>
|
||||
</v-card-title>
|
||||
<p>{{ recipe.short_description }}</p>
|
||||
<v-layout row text-xs-center>
|
||||
<v-flex xs6>
|
||||
<p><v-icon>timelapse</v-icon> {{ $tc('misc.Nmins', recipe.preparation_time, { count: recipe.preparation_time ? recipe.preparation_time : '?' }) }}</p>
|
||||
<v-layout row text-xs-center wrap>
|
||||
<v-flex xs12 v-if="recipe.short_description">
|
||||
<p>{{ recipe.short_description }}</p>
|
||||
</v-flex>
|
||||
<v-flex xs6>
|
||||
<p><v-icon>whatshot</v-icon> {{ $tc('misc.Nmins', recipe.cooking_time, { count: recipe.cooking_time ? recipe.cooking_time : '?' }) }}</p>
|
||||
<p><v-icon>timelapse</v-icon> {{ $tc('misc.Nmins', recipe.preparation_time, { count: timeOrUnknown(recipe.preparation_time) }) }}</p>
|
||||
</v-flex>
|
||||
<v-flex xs6>
|
||||
<p><v-icon>whatshot</v-icon> {{ $tc('misc.Nmins', recipe.cooking_time, { count: timeOrUnknown(recipe.cooking_time) }) }}</p>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-card>
|
||||
@ -72,6 +74,12 @@ export default {
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
timeOrUnknown(time) {
|
||||
if (time !== null && time !== undefined) {
|
||||
return time;
|
||||
}
|
||||
return '?';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -2,9 +2,9 @@
|
||||
<v-container grid-list-md class="panel">
|
||||
<Loader v-if="isLoading"></Loader>
|
||||
<v-layout row v-else>
|
||||
<ErrorDialog :v-model="errorDelete" description="Unable to delete recipe: " />
|
||||
<ErrorDialog :v-model="errorFetch" description="Unable to fetch recipe: " />
|
||||
<ErrorDialog :v-model="errorRefetch" description="Unable to refetch recipe: " />
|
||||
<ErrorDialog :v-model="errorDelete" :description="$t('error.unable_delete_recipe')" />
|
||||
<ErrorDialog :v-model="errorFetch" :description="$t('error.unable_fetch_recipe')" />
|
||||
<ErrorDialog :v-model="errorRefetch" :description="$t('error.unable_refetch_recipe')" />
|
||||
|
||||
<v-dialog v-model="refetchConfirm" max-width="500px">
|
||||
<v-card>
|
||||
@ -46,8 +46,8 @@
|
||||
<p>{{ recipe.nb_person }}</p>
|
||||
</v-flex>
|
||||
<v-flex xs6>
|
||||
<p><v-icon>timelapse</v-icon> {{ $t('recipe.preparation') }} {{ $tc('misc.Nmins', recipe.preparation_time, { count: recipe.preparation_time ? recipe.preparation_time : '?' }) }}</p>
|
||||
<p><v-icon>whatshot</v-icon> {{ $t('recipe.cooking') }} {{ $tc('misc.Nmins', recipe.cooking_time, { count: recipe.cooking_time ? recipe.cooking_time : '?' }) }}</p>
|
||||
<p><v-icon>timelapse</v-icon> {{ $t('recipe.preparation') }} {{ $tc('misc.Nmins', recipe.preparation_time, { count: timeOrUnknown(recipe.preparation_time) }) }}</p>
|
||||
<p><v-icon>whatshot</v-icon> {{ $t('recipe.cooking') }} {{ $tc('misc.Nmins', recipe.cooking_time, { count: timeOrUnknown(recipe.cooking_time) }) }}</p>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
<p>{{ recipe.short_description }}</p>
|
||||
@ -68,6 +68,9 @@
|
||||
<v-btn :href="recipe.url" :title="$t('recipe.website')" v-if="recipe.url">
|
||||
<v-icon class="fa-icon">fa-external-link</v-icon>
|
||||
</v-btn>
|
||||
<v-btn :to="{name: 'Edit', params: { recipeId: recipe.id }}" :title="$t('recipe.edit')">
|
||||
<v-icon>edit</v-icon>
|
||||
</v-btn>
|
||||
<v-btn @click.stop="deleteConfirm = true" :title="$t('recipe.delete')">
|
||||
<v-icon>delete</v-icon>
|
||||
</v-btn>
|
||||
@ -110,6 +113,12 @@ export default {
|
||||
$route: 'loadRecipe',
|
||||
},
|
||||
methods: {
|
||||
timeOrUnknown(time) {
|
||||
if (time !== null && time !== undefined) {
|
||||
return time;
|
||||
}
|
||||
return '?';
|
||||
},
|
||||
handleRecipesResponse(response) {
|
||||
if (response.recipes.length < 1) {
|
||||
this.$router.replace({
|
||||
|
@ -119,6 +119,32 @@ def api_v1_recipe(id):
|
||||
}
|
||||
|
||||
|
||||
@app.route('/api/v1/recipe/:id', ['POST', 'OPTIONS'])
|
||||
def api_v1_recipe_edit(id):
|
||||
"""
|
||||
Edit a given recipe from db
|
||||
"""
|
||||
# CORS
|
||||
if bottle.request.method == 'OPTIONS':
|
||||
return ''
|
||||
|
||||
data = json.loads(bottle.request.body.read().decode('utf-8'))
|
||||
|
||||
recipe = db.Recipe.select().where(
|
||||
db.Recipe.id == id
|
||||
).first()
|
||||
if not recipe:
|
||||
return bottle.abort(400, 'No recipe with id %s.' % id)
|
||||
recipe.update_from_dict(data)
|
||||
recipe.save()
|
||||
|
||||
return {
|
||||
'recipes': [
|
||||
recipe.to_dict()
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@app.route('/api/v1/recipe/:id/refetch', ['GET', 'OPTIONS'])
|
||||
def api_v1_recipe_refetch(id):
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user