diff --git a/.gitignore b/.gitignore index eb9e3ff..a8af265 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ *.swp +*.pyc package-lock.json yarn.lock +*.db .DS_Store node_modules/ diff --git a/cuizin/__init__.py b/cuizin/__init__.py index 819da74..4265ee7 100644 --- a/cuizin/__init__.py +++ b/cuizin/__init__.py @@ -11,9 +11,6 @@ BACKENDS = ['750g', 'allrecipes', 'cuisineaz', 'marmiton', 'supertoinette'] def add_recipe(url, modules_path=None): - db.database.connect() - db.database.create_tables([db.Recipe]) - webnip = WebNip(modules_path=modules_path) backends = [ @@ -25,9 +22,20 @@ def add_recipe(url, modules_path=None): for module in BACKENDS ] + recipe = None for backend in backends: browser = backend.browser if url.startswith(browser.BASEURL): browser.location(url) - db.Recipe.from_weboob(browser.page.get_recipe()).save() + recipe = db.Recipe.from_weboob(browser.page.get_recipe()) + # Ensure URL is set + recipe.url = url break + + if not recipe: + # TODO + recipe = db.Recipe() + recipe.url = url + + recipe.save() + return recipe diff --git a/cuizin/__main__.py b/cuizin/__main__.py index f19e359..eadb464 100644 --- a/cuizin/__main__.py +++ b/cuizin/__main__.py @@ -1,5 +1,8 @@ import os +import peewee + +from cuizin import db from cuizin import web @@ -9,4 +12,10 @@ if __name__ == '__main__': HOST = os.environ.get('CUIZIN_HOST', 'localhost') PORT = os.environ.get('CUIZIN_PORT', '8080') DEBUG = os.environ.get('CUIZIN_DEBUG', False) + + try: + db.database.create_tables([db.Recipe]) + except peewee.OperationalError: + pass + app.run(host=HOST, port=PORT, debug=DEBUG) diff --git a/cuizin/db.py b/cuizin/db.py index 6cfd5b2..804a870 100644 --- a/cuizin/db.py +++ b/cuizin/db.py @@ -1,4 +1,5 @@ import base64 +import mimetypes import requests from peewee import ( @@ -8,11 +9,13 @@ from peewee import ( from playhouse.shortcuts import model_to_dict -database = SqliteDatabase('recipes.db') +database = SqliteDatabase('recipes.db', threadlocals=True) +database.connect() class Recipe(Model): title = CharField() + url = CharField(null=True, unique=True) author = CharField(null=True) picture = BlobField(null=True) short_description = TextField(null=True) @@ -29,7 +32,7 @@ class Recipe(Model): @staticmethod def from_weboob(obj): recipe = Recipe() - for field in ['title', 'author', 'picture_url', 'short_description', + for field in ['title', 'url', 'author', 'picture_url', 'short_description', 'preparation_time', 'cooking_time', 'instructions']: value = getattr(obj, field) if value: @@ -39,7 +42,11 @@ class Recipe(Model): def to_dict(self): serialized = model_to_dict(self) - serialized['picture'] = base64.b64encode( - serialized['picture'] - ).decode('utf-8') + prepend_info = ( + 'data:%s;base64' % mimetypes.guess_type(serialized['picture'])[0] + ) + serialized['picture'] = '%s,%s' % ( + prepend_info, + base64.b64encode(serialized['picture']).decode('utf-8') + ) return serialized diff --git a/cuizin/js_src/App.vue b/cuizin/js_src/App.vue index 397f89a..1cd5a27 100644 --- a/cuizin/js_src/App.vue +++ b/cuizin/js_src/App.vue @@ -1,91 +1,31 @@ + + diff --git a/cuizin/js_src/components/HelloWorld.vue b/cuizin/js_src/components/HelloWorld.vue deleted file mode 100644 index 6bbe5cd..0000000 --- a/cuizin/js_src/components/HelloWorld.vue +++ /dev/null @@ -1,35 +0,0 @@ - - - - diff --git a/cuizin/js_src/components/Home.vue b/cuizin/js_src/components/Home.vue new file mode 100644 index 0000000..088c9a1 --- /dev/null +++ b/cuizin/js_src/components/Home.vue @@ -0,0 +1,61 @@ + + + diff --git a/cuizin/js_src/components/New.vue b/cuizin/js_src/components/New.vue new file mode 100644 index 0000000..4b6482d --- /dev/null +++ b/cuizin/js_src/components/New.vue @@ -0,0 +1,144 @@ + + + diff --git a/cuizin/js_src/components/Recipe.vue b/cuizin/js_src/components/Recipe.vue new file mode 100644 index 0000000..a0e9ec5 --- /dev/null +++ b/cuizin/js_src/components/Recipe.vue @@ -0,0 +1,45 @@ + + + diff --git a/cuizin/js_src/constants.js b/cuizin/js_src/constants.js new file mode 100644 index 0000000..86675c7 --- /dev/null +++ b/cuizin/js_src/constants.js @@ -0,0 +1,2 @@ +export const API_URL = 'http://localhost:8080'; +export const FOOBAR = 'TODO'; // TODO diff --git a/cuizin/js_src/main.js b/cuizin/js_src/main.js index 6ac070d..835a331 100644 --- a/cuizin/js_src/main.js +++ b/cuizin/js_src/main.js @@ -7,6 +7,10 @@ import 'vuetify/dist/vuetify.min.css'; import App from './App'; import router from './router'; +// Isomorphic fetch +require('es6-promise').polyfill(); +require('isomorphic-fetch'); + Vue.use(Vuetify); Vue.config.productionTip = false; diff --git a/cuizin/js_src/router/index.js b/cuizin/js_src/router/index.js index 50d7d36..292b2a8 100644 --- a/cuizin/js_src/router/index.js +++ b/cuizin/js_src/router/index.js @@ -1,6 +1,8 @@ import Vue from 'vue'; import Router from 'vue-router'; -import HelloWorld from '@/components/HelloWorld'; +import Home from '@/components/Home'; +import New from '@/components/New'; +import Recipe from '@/components/Recipe'; Vue.use(Router); @@ -8,8 +10,18 @@ export default new Router({ routes: [ { path: '/', - name: 'HelloWorld', - component: HelloWorld, + name: 'Home', + component: Home, + }, + { + path: '/new', + name: 'New', + component: New, + }, + { + path: '/recipe/:recipeId', + name: 'Recipe', + component: Recipe, }, ], }); diff --git a/cuizin/web.py b/cuizin/web.py index 06c67cb..d4d5a8d 100644 --- a/cuizin/web.py +++ b/cuizin/web.py @@ -1,20 +1,43 @@ -import bottle +import json +import bottle +import peewee + +from cuizin import add_recipe from cuizin import db -bottle.Bottle() +app = bottle.Bottle() + +@app.hook('after_request') +def enable_cors(): + """ + Add CORS headers at each request. + """ + # The str() call is required as we import unicode_literal and WSGI + # headers list should have plain str type. + bottle.response.headers[str('Access-Control-Allow-Origin')] = str('*') + bottle.response.headers[str('Access-Control-Allow-Methods')] = str( + 'PUT, GET, POST, DELETE, OPTIONS, PATCH' + ) + bottle.response.headers[str('Access-Control-Allow-Headers')] = str( + 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token' + ) -@app.get('/api/v1') +@app.route('/api/v1', ['GET', 'OPTIONS']) def api_v1_index(): return { 'recipes': '/api/v1/recipes' } -@app.get('/api/v1/recipes') +@app.route('/api/v1/recipes', ['GET', 'OPTIONS']) def api_v1_recipes(): + # CORS + if bottle.request.method == 'OPTIONS': + return '' + return { 'recipes': [ recipe.to_dict() for recipe in db.Recipe.select() @@ -22,8 +45,33 @@ def api_v1_recipes(): } -@app.get('/api/v1/recipe/:id') +@app.post('/api/v1/recipes') +def api_v1_recipes_post(): + data = json.load(bottle.request.body) + if 'url' not in data: + return { + 'error': 'No URL provided' + } + + recipes = [] + try: + recipes = [add_recipe(data['url']).to_dict()] + except peewee.IntegrityError: + recipes = [db.Recipe.select().where( + db.Recipe.url == data['url'] + ).first().to_dict()] + + return { + 'recipes': recipes + } + + +@app.route('/api/v1/recipe/:id', ['GET', 'OPTIONS']) def api_v1_recipe(id): + # CORS + if bottle.request.method == 'OPTIONS': + return '' + return { 'recipes': [ recipe.to_dict() for recipe in db.Recipe.select().where( @@ -36,4 +84,4 @@ def api_v1_recipe(id): @app.get('/static/') def get_static_files(filename): """Get Static files""" - return bottle.static_file(filename, root=) + return bottle.static_file(filename) # TODO: root= diff --git a/package.json b/package.json index 192efcc..7028588 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "build": "node build/build.js" }, "dependencies": { + "es6-promise": "^4.2.4", + "isomorphic-fetch": "^2.2.1", "vue": "^2.5.2", "vue-router": "^3.0.1", "vuetify": "^1.0.0" diff --git a/recipes.db b/recipes.db deleted file mode 100644 index 6b4f580..0000000 Binary files a/recipes.db and /dev/null differ