Add a form to manually add a recipe
This commit is contained in:
parent
e128171ddc
commit
f60b59d789
24
cuizin/db.py
24
cuizin/db.py
@ -56,21 +56,31 @@ class Recipe(Model):
|
||||
class Meta:
|
||||
database = database
|
||||
|
||||
def update_from_weboob(self, weboob_obj):
|
||||
def update_from_dict(self, d):
|
||||
"""
|
||||
Update fields taking values from the Weboob object.
|
||||
Update field taking values from a dict of values.
|
||||
"""
|
||||
# Set fields
|
||||
for field in ['title', 'url', 'author', 'picture_url',
|
||||
'short_description', 'preparation_time', 'cooking_time',
|
||||
'ingredients', 'instructions']:
|
||||
value = getattr(weboob_obj, field)
|
||||
'ingredients', 'instructions', 'nb_person']:
|
||||
value = d.get(field, None)
|
||||
if value:
|
||||
setattr(self, field, value)
|
||||
# Serialize number of person
|
||||
self.nb_person = '-'.join(str(num) for num in weboob_obj.nb_person)
|
||||
# Download picture and save it as a blob
|
||||
self.picture = requests.get(weboob_obj.picture_url).content
|
||||
if d.get('picture_url', None):
|
||||
self.picture = requests.get(d['picture_url']).content
|
||||
|
||||
def update_from_weboob(self, weboob_obj):
|
||||
"""
|
||||
Update fields taking values from the Weboob object.
|
||||
"""
|
||||
weboob_dict = dict(weboob_obj.iter_fields())
|
||||
if weboob_dict.get('nb_person', None):
|
||||
weboob_dict['nb_person'] = '-'.join(
|
||||
str(num) for num in weboob_dict['nb_person']
|
||||
)
|
||||
self.update_from_dict(weboob_dict)
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
|
@ -28,4 +28,9 @@ export default {
|
||||
color: rgba(0,0,0,.87);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.panel {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@ -16,7 +16,7 @@ function _postProcessRecipes(response) {
|
||||
parsed.recipes = parsed.recipes.map(item => Object.assign(
|
||||
item,
|
||||
{
|
||||
instructions: item.instructions.split(/\r\n/).map(
|
||||
instructions: item.instructions.split(/[\r\n]\n/).map(
|
||||
line => line.trim(),
|
||||
),
|
||||
},
|
||||
@ -48,8 +48,17 @@ export function refetchRecipe(id) {
|
||||
}
|
||||
|
||||
|
||||
export function postRecipe(recipe) {
|
||||
return fetch(`${constants.API_URL}api/v1/recipes`, {
|
||||
export function postRecipeByUrl(recipe) {
|
||||
return fetch(`${constants.API_URL}api/v1/recipes/by_url`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(recipe),
|
||||
})
|
||||
.then(_postProcessRecipes);
|
||||
}
|
||||
|
||||
|
||||
export function postRecipeManually(recipe) {
|
||||
return fetch(`${constants.API_URL}api/v1/recipes/manually`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(recipe),
|
||||
})
|
||||
|
@ -12,7 +12,7 @@
|
||||
:key="recipe.title"
|
||||
xs12 sm6 md3 lg2>
|
||||
<v-card :to="{name: 'Recipe', params: { recipeId: recipe.id }}">
|
||||
<v-card-media :src="recipe.picture" height="200px"></v-card-media>
|
||||
<v-card-media :src="recipe.picture" height="200px" class="grey"></v-card-media>
|
||||
<v-card-title primary-title>
|
||||
<div>
|
||||
<h3 class="headline mb-0">{{ recipe.title }}</h3>
|
||||
@ -21,10 +21,10 @@
|
||||
<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 }) }}</p>
|
||||
<p><v-icon>timelapse</v-icon> {{ $tc('misc.Nmins', recipe.preparation_time, { count: recipe.preparation_time ? recipe.preparation_time : '?' }) }}</p>
|
||||
</v-flex>
|
||||
<v-flex xs6>
|
||||
<p><v-icon>whatshot</v-icon> {{ $tc('misc.Nmins', recipe.cooking_time, { count: recipe.cooking_time }) }}</p>
|
||||
<p><v-icon>whatshot</v-icon> {{ $tc('misc.Nmins', recipe.cooking_time, { count: recipe.cooking_time ? recipe.cooking_time : '?' }) }}</p>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-card>
|
||||
|
@ -8,55 +8,50 @@
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
<v-container text-xs-center v-else>
|
||||
<v-container text-xs-center v-else class="panel">
|
||||
<v-layout row wrap>
|
||||
<ErrorDialog v-model="error" :description="$t('error.unable_import_recipe')" />
|
||||
|
||||
<v-flex xs12>
|
||||
<h2>{{ $t('new.import_from_url') }}</h2>
|
||||
<v-form v-model="validImport">
|
||||
<v-form v-model="isValidImport">
|
||||
<v-text-field
|
||||
label="URL"
|
||||
v-model="url"
|
||||
required
|
||||
:rules="urlRules"
|
||||
:rules="requiredUrlRules"
|
||||
></v-text-field>
|
||||
<v-btn
|
||||
@click="submitImport"
|
||||
:disabled="!validImport || isImporting"
|
||||
:disabled="!isValidImport || isImporting"
|
||||
>
|
||||
{{ $t('new.import') }}
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
<v-layout row wrap mt-5 v-if="featureAddManually">
|
||||
<v-layout row wrap mt-5>
|
||||
<v-flex xs12>
|
||||
<h2>{{ $t('new.add_manually') }}</h2>
|
||||
<v-form v-model="validAdd">
|
||||
<v-form v-model="isValidAdd">
|
||||
<v-text-field
|
||||
:label="$t('new.title')"
|
||||
v-model="title"
|
||||
required
|
||||
:rules="[v => !!v || $t('new.title_is_required')]"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
:label="$t('new.picture')"
|
||||
v-model="picture"
|
||||
:label="$t('new.picture_url')"
|
||||
v-model="picture_url"
|
||||
:rules="urlRules"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
:label="$t('new.short_description')"
|
||||
v-model="short_description"
|
||||
textarea
|
||||
></v-text-field>
|
||||
<v-layout row>
|
||||
<v-flex xs4 mr-3>
|
||||
<v-text-field
|
||||
:label="$t('new.nb_persons')"
|
||||
v-model="nb_person"
|
||||
type="number"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
<v-flex xs4 mx-3>
|
||||
<v-layout row wrap>
|
||||
<v-flex xs12 md5>
|
||||
<v-text-field
|
||||
:label="$t('new.preparation_time')"
|
||||
v-model="preparation_time"
|
||||
@ -64,7 +59,7 @@
|
||||
:suffix="$t('new.mins')"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
<v-flex xs4 ml-3>
|
||||
<v-flex xs12 md5 offset-md2>
|
||||
<v-text-field
|
||||
:label="$t('new.cooking_time')"
|
||||
v-model="cooking_time"
|
||||
@ -74,20 +69,43 @@
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
<v-text-field
|
||||
:label="$t('new.ingredients')"
|
||||
v-model="ingredients"
|
||||
textarea
|
||||
:label="$t('new.nb_persons')"
|
||||
v-model="nb_person"
|
||||
></v-text-field>
|
||||
<v-layout row>
|
||||
<v-flex xs12 class="text-xs-left">
|
||||
<h3>{{ $t('new.ingredients') }}</h3>
|
||||
<v-list v-if="ingredients.length" class="transparent">
|
||||
<v-list-tile v-for="ingredient in ingredients" :key="ingredient">
|
||||
<v-list-tile-action>
|
||||
<v-btn flat icon color="red" v-on:click="() => removeIngredient(ingredient)">
|
||||
<v-icon>delete</v-icon>
|
||||
</v-btn>
|
||||
</v-list-tile-action>
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title>{{ ingredient }}</v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
</v-list>
|
||||
<p class="ml-5 my-3" v-else>{{ $t('new.none') }}</p>
|
||||
<v-text-field
|
||||
:label="$t('new.add_ingredient')"
|
||||
v-model="new_ingredient"
|
||||
@keyup.enter.native="addIngredient"
|
||||
></v-text-field>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
<v-text-field
|
||||
:label="$t('new.instructions')"
|
||||
v-model="instructions"
|
||||
:rules="[v => !!v || $t('new.instructions_are_required')]"
|
||||
textarea
|
||||
required
|
||||
></v-text-field>
|
||||
|
||||
<v-btn
|
||||
@click="submitAdd"
|
||||
:disabled="!validAdd"
|
||||
:disabled="!isValidAdd || isImporting"
|
||||
>
|
||||
{{ $t('new.add') }}
|
||||
</v-btn>
|
||||
@ -110,38 +128,56 @@ export default {
|
||||
Loader,
|
||||
},
|
||||
data() {
|
||||
const urlRules = [
|
||||
(v) => {
|
||||
if (!v) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
new URL(v); // eslint-disable-line no-new
|
||||
return true;
|
||||
} catch (e) {
|
||||
return $t('new.url_must_be_valid');
|
||||
}
|
||||
},
|
||||
];
|
||||
return {
|
||||
error: null,
|
||||
url: null,
|
||||
validImport: false,
|
||||
isValidImport: false,
|
||||
isImporting: false,
|
||||
validAdd: false,
|
||||
isValidAdd: false,
|
||||
title: null,
|
||||
picture: null,
|
||||
picture_url: null,
|
||||
short_description: null,
|
||||
nb_person: null,
|
||||
preparation_time: null,
|
||||
cooking_time: null,
|
||||
ingredients: null,
|
||||
new_ingredient: null,
|
||||
ingredients: [],
|
||||
instructions: null,
|
||||
urlRules: [
|
||||
v => !!v || 'URL is required',
|
||||
(v) => {
|
||||
try {
|
||||
new URL(v); // eslint-disable-line no-new
|
||||
return true;
|
||||
} catch (e) {
|
||||
return 'URL must be valid';
|
||||
}
|
||||
},
|
||||
],
|
||||
featureAddManually: false,
|
||||
requiredUrlRules: Array.concat(
|
||||
[],
|
||||
[v => !!v || $t('new.url_is_required')],
|
||||
urlRules,
|
||||
),
|
||||
urlRules,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
addIngredient() {
|
||||
this.ingredients.push(this.new_ingredient);
|
||||
this.new_ingredient = null;
|
||||
},
|
||||
removeIngredient(ingredient) {
|
||||
const index = this.ingredients.indexOf(ingredient);
|
||||
if (index !== -1) {
|
||||
this.ingredients.splice(index, 1);
|
||||
}
|
||||
},
|
||||
submitImport() {
|
||||
this.isImporting = true;
|
||||
api.postRecipe({ url: this.url })
|
||||
api.postRecipeByUrl({ url: this.url })
|
||||
.then(response => this.$router.push({
|
||||
name: 'Recipe',
|
||||
params: {
|
||||
@ -154,8 +190,34 @@ export default {
|
||||
});
|
||||
},
|
||||
submitAdd() {
|
||||
// TODO
|
||||
this.isImporting = true;
|
||||
api.postRecipeManually({
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
.transparent {
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
|
@ -46,23 +46,26 @@
|
||||
<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 }) }}</p>
|
||||
<p><v-icon>whatshot</v-icon> {{ $t('recipe.cooking') }} {{ $tc('misc.Nmins', recipe.cooking_time, { count: recipe.cooking_time }) }}</p>
|
||||
<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>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
<p>{{ recipe.short_description }}</p>
|
||||
|
||||
<h2>{{ $t('recipe.ingredients') }}</h2>
|
||||
<ul class="ml-5">
|
||||
<ul class="ml-5" v-if="recipe.ingredients && recipe.ingredients.length">
|
||||
<li v-for="ingredient in recipe.ingredients">
|
||||
{{ ingredient }}
|
||||
</li>
|
||||
</ul>
|
||||
<p class="ml-5 my-3" v-else>{{ $t('new.none') }}</p>
|
||||
|
||||
<h2 class="mt-3">{{ $t('recipe.instructions') }}</h2>
|
||||
<p v-for="item in recipe.instructions">
|
||||
{{ item }}
|
||||
</p>
|
||||
<p v-if="recipe.url" class="text-xs-center">
|
||||
<v-btn :href="recipe.url" :title="$t('recipe.website')">
|
||||
<p class="text-xs-center">
|
||||
<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 @click.stop="deleteConfirm = true" :title="$t('recipe.delete')">
|
||||
@ -156,10 +159,6 @@ img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.panel {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.fa-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
@ -13,19 +13,25 @@ export default {
|
||||
},
|
||||
new: {
|
||||
add: 'Add',
|
||||
add_ingredient: 'Add ingredient',
|
||||
add_manually: 'Add manually',
|
||||
cooking_time: 'Cooking time',
|
||||
import: 'Import',
|
||||
import_from_url: 'Import from URL',
|
||||
importing: 'Importing…',
|
||||
ingredients: 'Ingredients',
|
||||
ingredients: 'Ingredients:',
|
||||
instructions: 'Instructions',
|
||||
instructions_are_required: 'Instructions are required',
|
||||
mins: 'mins',
|
||||
nb_opersons: 'Number of persons',
|
||||
picture: 'Picture',
|
||||
nb_persons: 'Serves',
|
||||
none: 'None',
|
||||
picture_url: 'Picture URL',
|
||||
preparation_time: 'Preparation time',
|
||||
short_description: 'Short description',
|
||||
title: 'Title',
|
||||
title_is_required: 'Title is required',
|
||||
url_is_required: 'URL is required',
|
||||
url_must_be_valid: 'URL must be a valid one',
|
||||
},
|
||||
recipe: {
|
||||
cooking: 'Cooking:',
|
||||
@ -34,6 +40,7 @@ export default {
|
||||
delete_recipe_description: 'This will delete this recipe. Are you sure?',
|
||||
ingredients: 'Ingredients',
|
||||
instructions: 'Instructions',
|
||||
none: 'Aucun',
|
||||
preparation: 'Preparation:',
|
||||
refetch: 'Refetch',
|
||||
refetch_recipe: 'Refetch recipe',
|
||||
|
@ -13,19 +13,25 @@ export default {
|
||||
},
|
||||
new: {
|
||||
add: 'Ajouter',
|
||||
add_ingredient: 'Ajouter un ingrédient',
|
||||
add_manually: 'Ajouter manuellement',
|
||||
cooking_time: 'Temps de cuisson',
|
||||
import: 'Importer',
|
||||
import_from_url: 'Importer depuis l\'URL',
|
||||
importing: 'En cours d\'import…',
|
||||
ingredients: 'Ingrédients',
|
||||
ingredients: 'Ingrédients :',
|
||||
instructions: 'Instructions',
|
||||
instructions_are_required: 'Des instructions sont requises',
|
||||
mins: 'mins',
|
||||
nb_opersons: 'Nombre de personnes',
|
||||
picture: 'Photo',
|
||||
nb_persons: 'Nombre de personnes / parts',
|
||||
none: 'Aucun',
|
||||
picture_url: 'URL d\'une photo',
|
||||
preparation_time: 'Temps de préparation',
|
||||
short_description: 'Description brève',
|
||||
title: 'Titre',
|
||||
title_is_required: 'Un titre est requis',
|
||||
url_is_required: 'Une URL est requise',
|
||||
url_must_be_valid: 'L\'URL est invalide',
|
||||
},
|
||||
recipe: {
|
||||
cooking: 'Cuisson :',
|
||||
@ -34,6 +40,7 @@ export default {
|
||||
delete_recipe_description: 'Vous allez supprimer cette recette. En êtes-vous sûr ?',
|
||||
ingredients: 'Ingrédients',
|
||||
instructions: 'Instructions',
|
||||
none: 'Aucun',
|
||||
preparation: 'Préparation :',
|
||||
refetch: 'Réactualiser',
|
||||
refetch_recipe: 'Réactualiser la recette',
|
||||
|
@ -56,8 +56,8 @@ def api_v1_recipes():
|
||||
}
|
||||
|
||||
|
||||
@app.post('/api/v1/recipes')
|
||||
def api_v1_recipes_post():
|
||||
@app.post('/api/v1/recipes/by_url')
|
||||
def api_v1_recipes_post_by_url():
|
||||
"""
|
||||
Create a new recipe from URL
|
||||
"""
|
||||
@ -80,6 +80,27 @@ def api_v1_recipes_post():
|
||||
bottle.redirect('/api/v1/recipe/%s' % recipe.id, 301)
|
||||
|
||||
|
||||
@app.post('/api/v1/recipes/manually')
|
||||
def api_v1_recipes_post_manual():
|
||||
"""
|
||||
Create a new recipe manually
|
||||
"""
|
||||
data = json.loads(bottle.request.body.read().decode('utf-8'))
|
||||
|
||||
try:
|
||||
# Try to add
|
||||
recipe = db.Recipe()
|
||||
recipe.update_from_dict(data)
|
||||
recipe.save()
|
||||
return {
|
||||
'recipes': [recipe.to_dict()]
|
||||
}
|
||||
except peewee.IntegrityError:
|
||||
return {
|
||||
'error': 'Duplicate recipe.'
|
||||
}
|
||||
|
||||
|
||||
@app.route('/api/v1/recipe/:id', ['GET', 'OPTIONS'])
|
||||
def api_v1_recipe(id):
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user