diff --git a/cuizin/db.py b/cuizin/db.py index 6f7b71b..4f32cef 100644 --- a/cuizin/db.py +++ b/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): """ diff --git a/cuizin/js_src/App.vue b/cuizin/js_src/App.vue index b4a0488..fb016dc 100644 --- a/cuizin/js_src/App.vue +++ b/cuizin/js_src/App.vue @@ -28,4 +28,9 @@ export default { color: rgba(0,0,0,.87); text-decoration: none; } + +.panel { + max-width: 600px; +} + diff --git a/cuizin/js_src/api.js b/cuizin/js_src/api.js index 8491e47..049b20d 100644 --- a/cuizin/js_src/api.js +++ b/cuizin/js_src/api.js @@ -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), }) diff --git a/cuizin/js_src/components/Home.vue b/cuizin/js_src/components/Home.vue index 17c46ec..d71b497 100644 --- a/cuizin/js_src/components/Home.vue +++ b/cuizin/js_src/components/Home.vue @@ -12,7 +12,7 @@ :key="recipe.title" xs12 sm6 md3 lg2> - +

{{ recipe.title }}

@@ -21,10 +21,10 @@

{{ recipe.short_description }}

-

timelapse {{ $tc('misc.Nmins', recipe.preparation_time, { count: recipe.preparation_time }) }}

+

timelapse {{ $tc('misc.Nmins', recipe.preparation_time, { count: recipe.preparation_time ? recipe.preparation_time : '?' }) }}

-

whatshot {{ $tc('misc.Nmins', recipe.cooking_time, { count: recipe.cooking_time }) }}

+

whatshot {{ $tc('misc.Nmins', recipe.cooking_time, { count: recipe.cooking_time ? recipe.cooking_time : '?' }) }}

diff --git a/cuizin/js_src/components/New.vue b/cuizin/js_src/components/New.vue index 08f989a..9a1d2f3 100644 --- a/cuizin/js_src/components/New.vue +++ b/cuizin/js_src/components/New.vue @@ -8,55 +8,50 @@ - +

{{ $t('new.import_from_url') }}

- + {{ $t('new.import') }}
- +

{{ $t('new.add_manually') }}

- + - - - - - + + - + + + +

{{ $t('new.ingredients') }}

+ + + + + delete + + + + {{ ingredient }} + + + +

{{ $t('new.none') }}

+ +
+
{{ $t('new.add') }} @@ -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; + }); }, }, }; + + diff --git a/cuizin/js_src/components/Recipe.vue b/cuizin/js_src/components/Recipe.vue index 0a642d2..0bbe6e1 100644 --- a/cuizin/js_src/components/Recipe.vue +++ b/cuizin/js_src/components/Recipe.vue @@ -46,23 +46,26 @@

{{ recipe.nb_person }}

-

timelapse {{ $t('recipe.preparation') }} {{ $tc('misc.Nmins', recipe.preparation_time, { count: recipe.preparation_time }) }}

-

whatshot {{ $t('recipe.cooking') }} {{ $tc('misc.Nmins', recipe.cooking_time, { count: recipe.cooking_time }) }}

+

timelapse {{ $t('recipe.preparation') }} {{ $tc('misc.Nmins', recipe.preparation_time, { count: recipe.preparation_time ? recipe.preparation_time : '?' }) }}

+

whatshot {{ $t('recipe.cooking') }} {{ $tc('misc.Nmins', recipe.cooking_time, { count: recipe.cooking_time ? recipe.cooking_time : '?' }) }}

{{ recipe.short_description }}

+

{{ $t('recipe.ingredients') }}

-
    +
    • {{ ingredient }}
    +

    {{ $t('new.none') }}

    +

    {{ $t('recipe.instructions') }}

    {{ item }}

    -

    - +

    + fa-external-link @@ -156,10 +159,6 @@ img { width: 100%; } -.panel { - max-width: 600px; -} - .fa-icon { font-size: 20px; } diff --git a/cuizin/js_src/i18n/en/index.js b/cuizin/js_src/i18n/en/index.js index d2b24b4..25be549 100644 --- a/cuizin/js_src/i18n/en/index.js +++ b/cuizin/js_src/i18n/en/index.js @@ -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', diff --git a/cuizin/js_src/i18n/fr/index.js b/cuizin/js_src/i18n/fr/index.js index cffe56a..ed193cd 100644 --- a/cuizin/js_src/i18n/fr/index.js +++ b/cuizin/js_src/i18n/fr/index.js @@ -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', diff --git a/cuizin/web.py b/cuizin/web.py index 18dd342..2959dfc 100644 --- a/cuizin/web.py +++ b/cuizin/web.py @@ -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): """