From b5946ccc847547b5826ffd1ae965f97ba88f3d72 Mon Sep 17 00:00:00 2001 From: "Phyks (Lucas Verney)" Date: Sat, 3 Mar 2018 15:56:18 +0100 Subject: [PATCH] Few UI improvements + add refetch ability * Order by decreasing ids on the home page. * Confirmation modals when deleting / refetching items. * Spinners when loading. * Getting started message when there is no recipe. * Add the ability to refetch a recipe from the website (when a Weboob module has been improved for instance). --- .eslintrc.js | 5 + cuizin/db.py | 16 +- cuizin/js_src/App.vue | 38 ++-- cuizin/js_src/components/Home.vue | 108 ++++++------ cuizin/js_src/components/Loader.vue | 7 + cuizin/js_src/components/New.vue | 262 ++++++++++++++-------------- cuizin/js_src/components/Recipe.vue | 188 ++++++++++++-------- cuizin/js_src/constants.js | 2 +- cuizin/js_src/main.js | 10 +- cuizin/js_src/router/index.js | 34 ++-- cuizin/scraping.py | 19 +- cuizin/web.py | 32 +++- package.json | 1 - 13 files changed, 406 insertions(+), 316 deletions(-) create mode 100644 cuizin/js_src/components/Loader.vue diff --git a/.eslintrc.js b/.eslintrc.js index e319040..fef2087 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -24,6 +24,11 @@ module.exports = { }, // add your custom rules here rules: { + // Use 4 spaces indent + 'indent': ['error', 4], + 'import/prefer-default-export': 'off', + 'no-console': 'off', + 'no-underscore-dangle': 'off', // don't require .vue extension when importing 'import/extensions': ['error', 'always', { js: 'never', diff --git a/cuizin/db.py b/cuizin/db.py index 1027fd1..6f7b71b 100644 --- a/cuizin/db.py +++ b/cuizin/db.py @@ -56,21 +56,21 @@ class Recipe(Model): class Meta: database = database - @staticmethod - def from_weboob(obj): - recipe = Recipe() + def update_from_weboob(self, weboob_obj): + """ + Update fields taking values from the Weboob object. + """ # Set fields for field in ['title', 'url', 'author', 'picture_url', 'short_description', 'preparation_time', 'cooking_time', 'ingredients', 'instructions']: - value = getattr(obj, field) + value = getattr(weboob_obj, field) if value: - setattr(recipe, field, value) + setattr(self, field, value) # Serialize number of person - recipe.nb_person = '-'.join(str(num) for num in obj.nb_person) + self.nb_person = '-'.join(str(num) for num in weboob_obj.nb_person) # Download picture and save it as a blob - recipe.picture = requests.get(obj.picture_url).content - return recipe + self.picture = requests.get(weboob_obj.picture_url).content def to_dict(self): """ diff --git a/cuizin/js_src/App.vue b/cuizin/js_src/App.vue index 1cd5a27..b4a0488 100644 --- a/cuizin/js_src/App.vue +++ b/cuizin/js_src/App.vue @@ -1,31 +1,31 @@ diff --git a/cuizin/js_src/components/Home.vue b/cuizin/js_src/components/Home.vue index 18143bc..9054d05 100644 --- a/cuizin/js_src/components/Home.vue +++ b/cuizin/js_src/components/Home.vue @@ -1,64 +1,68 @@ diff --git a/cuizin/js_src/components/Loader.vue b/cuizin/js_src/components/Loader.vue new file mode 100644 index 0000000..518965f --- /dev/null +++ b/cuizin/js_src/components/Loader.vue @@ -0,0 +1,7 @@ + diff --git a/cuizin/js_src/components/New.vue b/cuizin/js_src/components/New.vue index d43e8b2..53a97b6 100644 --- a/cuizin/js_src/components/New.vue +++ b/cuizin/js_src/components/New.vue @@ -1,89 +1,89 @@ diff --git a/cuizin/js_src/components/Recipe.vue b/cuizin/js_src/components/Recipe.vue index e585277..a8c157a 100644 --- a/cuizin/js_src/components/Recipe.vue +++ b/cuizin/js_src/components/Recipe.vue @@ -1,81 +1,133 @@ diff --git a/cuizin/js_src/constants.js b/cuizin/js_src/constants.js index 4982031..8823376 100644 --- a/cuizin/js_src/constants.js +++ b/cuizin/js_src/constants.js @@ -1 +1 @@ -export const API_URL = process.env.API_URL || '/'; // eslint-disable-line import/prefer-default-export,no-use-before-define +export const API_URL = process.env.API_URL || '/'; // eslint-disable-line no-use-before-define diff --git a/cuizin/js_src/main.js b/cuizin/js_src/main.js index 0f30c8f..c0a7ef5 100644 --- a/cuizin/js_src/main.js +++ b/cuizin/js_src/main.js @@ -6,7 +6,6 @@ import 'roboto-fontface/css/roboto/roboto-fontface.css'; import 'font-awesome/css/font-awesome.css'; import 'material-design-icons/iconfont/material-icons.css'; import 'vuetify/dist/vuetify.min.css'; -import Nl2br from 'vue-nl2br'; import App from './App'; import router from './router'; @@ -15,15 +14,14 @@ import router from './router'; require('es6-promise').polyfill(); require('isomorphic-fetch'); -Vue.component('nl2br', Nl2br); Vue.use(Vuetify); Vue.config.productionTip = false; /* eslint-disable no-new */ new Vue({ - el: '#app', - router, - components: { App }, - template: '', + el: '#app', + router, + components: { App }, + template: '', }); diff --git a/cuizin/js_src/router/index.js b/cuizin/js_src/router/index.js index 292b2a8..a9daf15 100644 --- a/cuizin/js_src/router/index.js +++ b/cuizin/js_src/router/index.js @@ -7,21 +7,21 @@ import Recipe from '@/components/Recipe'; Vue.use(Router); export default new Router({ - routes: [ - { - path: '/', - name: 'Home', - component: Home, - }, - { - path: '/new', - name: 'New', - component: New, - }, - { - path: '/recipe/:recipeId', - name: 'Recipe', - component: Recipe, - }, - ], + routes: [ + { + path: '/', + name: 'Home', + component: Home, + }, + { + path: '/new', + name: 'New', + component: New, + }, + { + path: '/recipe/:recipeId', + name: 'Recipe', + component: Recipe, + }, + ], }); diff --git a/cuizin/scraping.py b/cuizin/scraping.py index c3a0c9b..95c62c3 100644 --- a/cuizin/scraping.py +++ b/cuizin/scraping.py @@ -15,11 +15,12 @@ from cuizin import db BACKENDS = ['750g', 'allrecipes', 'cuisineaz', 'marmiton', 'supertoinette'] -def add_recipe(url): +def fetch_recipe(url, recipe=None): """ Add a recipe, trying to scrape from a given URL. :param url: URL of the recipe. + :param recipe: An optional recipe object to update. :return: A ``cuizin.db.Recipe`` model. """ # Eventually load modules from a local clone @@ -36,22 +37,20 @@ def add_recipe(url): for module in BACKENDS ] + # Create a new Recipe object if none is given + if not recipe: + recipe = db.Recipe() + # Try to fetch the recipe with a Weboob backend - recipe = None for backend in backends: browser = backend.browser if url.startswith(browser.BASEURL): browser.location(url) - recipe = db.Recipe.from_weboob(browser.page.get_recipe()) - # Ensure URL is set - recipe.url = url + recipe.update_from_weboob(browser.page.get_recipe()) break - # If we could not scrape anything, simply create an empty recipe storing - # the URL. - if not recipe: - recipe = db.Recipe() - recipe.url = url + # Ensure URL is set + recipe.url = url recipe.save() return recipe diff --git a/cuizin/web.py b/cuizin/web.py index 7ff8e95..b156375 100644 --- a/cuizin/web.py +++ b/cuizin/web.py @@ -4,7 +4,7 @@ import os import bottle from cuizin import db -from cuizin.scraping import add_recipe +from cuizin.scraping import fetch_recipe MODULE_DIR = os.path.dirname(os.path.realpath(__file__)) @@ -49,7 +49,8 @@ def api_v1_recipes(): return { 'recipes': [ - recipe.to_dict() for recipe in db.Recipe.select() + recipe.to_dict() + for recipe in db.Recipe.select().order_by(db.Recipe.id.desc()) ] } @@ -73,7 +74,7 @@ def api_v1_recipes_post(): assert recipe recipes = [recipe.to_dict()] except AssertionError: - recipes = [add_recipe(data['url']).to_dict()] + recipes = [fetch_recipe(data['url']).to_dict()] return { 'recipes': recipes @@ -98,6 +99,31 @@ def api_v1_recipe(id): } +@app.route('/api/v1/recipe/:id/refetch', ['GET', 'OPTIONS']) +def api_v1_recipe_refetch(id): + """ + Refetch a given recipe. + """ + # CORS + if bottle.request.method == 'OPTIONS': + return '' + + recipe = db.Recipe.select().where( + db.Recipe.id == id + ).first() + if not recipe: + # TODO: Error + pass + + recipe = fetch_recipe(recipe.url, recipe=recipe) + + return { + 'recipes': [ + recipe.to_dict() + ] + } + + @app.delete('/api/v1/recipe/:id', ['DELETE', 'OPTIONS']) def api_v1_recipe_delete(id): """ diff --git a/package.json b/package.json index 91ba9ee..15dae3d 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "material-design-icons": "^3.0.1", "roboto-fontface": "^0.9.0", "vue": "^2.5.2", - "vue-nl2br": "^0.0.5", "vue-router": "^3.0.1", "vuetify": "^1.0.0" },