Update UI

This commit is contained in:
Lucas Verney 2018-02-27 17:47:32 +01:00
parent 1170bb39c0
commit 746f683cab
15 changed files with 378 additions and 129 deletions

2
.gitignore vendored
View File

@ -1,6 +1,8 @@
*.swp
*.pyc
package-lock.json
yarn.lock
*.db
.DS_Store
node_modules/

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -1,91 +1,31 @@
<template>
<v-app>
<v-navigation-drawer
persistent
:mini-variant="miniVariant"
:clipped="clipped"
v-model="drawer"
enable-resize-watcher
fixed
app
>
<v-list>
<v-list-tile
value="true"
v-for="(item, i) in items"
:key="i"
>
<v-list-tile-action>
<v-icon v-html="item.icon"></v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title v-text="item.title"></v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<v-toolbar
app
:clipped-left="clipped"
>
<v-toolbar-side-icon @click.stop="drawer = !drawer"></v-toolbar-side-icon>
<v-btn icon @click.stop="miniVariant = !miniVariant">
<v-icon v-html="miniVariant ? 'chevron_right' : 'chevron_left'"></v-icon>
</v-btn>
<v-btn icon @click.stop="clipped = !clipped">
<v-icon>web</v-icon>
</v-btn>
<v-btn icon @click.stop="fixed = !fixed">
<v-icon>remove</v-icon>
</v-btn>
<v-toolbar-title v-text="title"></v-toolbar-title>
<v-toolbar app>
<v-toolbar-title class="toolbar-title">
<router-link to="/">Cuizin</router-link>
</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn icon @click.stop="rightDrawer = !rightDrawer">
<v-icon>menu</v-icon>
</v-btn>
<router-link to="/new">
<v-btn icon>
<v-icon>add</v-icon>
</v-btn>
</router-link>
</v-toolbar>
<v-content>
<router-view/>
</v-content>
<v-navigation-drawer
temporary
:right="right"
v-model="rightDrawer"
fixed
app
>
<v-list>
<v-list-tile @click="right = !right">
<v-list-tile-action>
<v-icon>compare_arrows</v-icon>
</v-list-tile-action>
<v-list-tile-title>Switch drawer (click me)</v-list-tile-title>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<v-footer :fixed="fixed" app>
<span>&copy; 2017</span>
</v-footer>
</v-app>
</template>
<script>
export default {
data() {
return {
clipped: false,
drawer: true,
fixed: false,
items: [{
icon: 'bubble_chart',
title: 'Inspire',
}],
miniVariant: false,
right: true,
rightDrawer: false,
title: 'Vuetify.js',
};
},
name: 'App',
};
</script>
<style>
.toolbar-title a {
color: rgba(0,0,0,.87);
text-decoration: none;
}
</style>

View File

@ -1,35 +0,0 @@
<template>
<v-container fluid>
<v-slide-y-transition mode="out-in">
<v-layout column align-center>
<img src="@/assets/logo.png" alt="Vuetify.js" class="mb-5">
<blockquote>
&#8220;First, solve the problem. Then, write the code.&#8221;
<footer>
<small>
<em>&mdash;John Johnson</em>
</small>
</footer>
</blockquote>
</v-layout>
</v-slide-y-transition>
</v-container>
</template>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
font-weight: normal;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@ -0,0 +1,61 @@
<template>
<v-container fluid grid-list-md>
<v-layout row wrap>
<v-flex
v-for="recipe in recipes"
:key="recipe.title"
xs3
>
<v-card :to="{name: 'Recipe', params: { recipeId: recipe.id }}">
<v-card-media :src="recipe.picture" height="200px"></v-card-media>
<v-card-title primary-title>
<div>
<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> {{ recipe.preparation_time }} mins</p>
</v-flex>
<v-flex xs6>
<p><v-icon>whatshot</v-icon> {{ recipe.cooking_time }} mins</p>
</v-flex>
</v-layout>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import * as constants from '@/constants';
export default {
data() {
return {
isLoading: false,
recipes: [],
};
},
created() {
this.fetchRecipes();
},
watch: {
// call again the method if the route changes
$route: 'fetchRecipes',
},
methods: {
fetchRecipes() {
this.isLoading = true;
fetch(`${constants.API_URL}/api/v1/recipes`)
.then(response => response.json())
.then((response) => {
this.recipes = response.recipes;
this.isLoading = false;
});
},
},
};
</script>

View File

@ -0,0 +1,144 @@
<template>
<v-container text-xs-center>
<v-layout row wrap>
<v-flex xs12>
<h2>Import from URL</h2>
<v-form v-model="validImport">
<v-text-field
label="URL"
v-model="url"
required
:rules="urlRules"
></v-text-field>
<v-btn
@click="submitImport"
:disabled="!validImport || disabledImport"
>
Import
</v-btn>
</v-form>
</v-flex>
</v-layout>
<v-layout row wrap mt-5>
<v-flex xs12>
<h2>Add manually</h2>
<v-form v-model="validAdd">
<v-text-field
label="Title"
v-model="title"
required
></v-text-field>
<v-text-field
label="Picture"
v-model="picture"
></v-text-field>
<v-text-field
label="Short description"
v-model="short_description"
textarea
></v-text-field>
<v-layout row>
<v-flex xs4 mr-3>
<v-text-field
label="Number of persons"
v-model="nb_person"
type="number"
></v-text-field>
</v-flex>
<v-flex xs4 mx-3>
<v-text-field
label="Preparation time"
v-model="preparation_time"
type="number"
suffix="mins"
></v-text-field>
</v-flex>
<v-flex xs4 ml-3>
<v-text-field
label="Cooking time"
v-model="cooking_time"
type="number"
suffix="mins"
></v-text-field>
</v-flex>
</v-layout>
<v-text-field
label="Ingredients"
v-model="ingredients"
textarea
></v-text-field>
<v-text-field
label="Instructions"
v-model="instructions"
textarea
required
></v-text-field>
<v-btn
@click="submitAdd"
:disabled="!validAdd"
>
Add
</v-btn>
</v-form>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import * as constants from '@/constants';
export default {
data() {
return {
url: null,
validImport: false,
disabledImport: false,
validAdd: false,
title: null,
picture: null,
short_description: null,
nb_person: null,
preparation_time: null,
cooking_time: null,
ingredients: null,
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';
}
},
],
};
},
methods: {
submitImport() {
this.disabledImport = true;
fetch(`${constants.API_URL}/api/v1/recipes`, {
method: 'POST',
body: JSON.stringify({
url: this.url,
}),
})
.then(response => response.json())
.then(response => response.recipes[0])
.then(recipe => this.$router.push({
name: 'Recipe',
params: {
recipeId: recipe.id,
},
}));
},
submitAdd() {
// TODO
},
},
};
</script>

View File

@ -0,0 +1,45 @@
<template>
<v-container grid-list-md>
<v-layout row wrap>
<v-flex xs12 v-if="this.recipe">
<h1>{{ this.recipe.title }}</h1>
<p><img :src="this.recipe.picture" height="300px" /></p>
<p>{{ this.recipe.short_description }}</p>
<p>{{ this.recipe.ingredients }}</p>
<p>{{ this.recipe.instructions }}</p>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import * as constants from '@/constants';
export default {
data() {
return {
isLoading: false,
recipe: null,
};
},
created() {
this.fetchRecipe();
},
watch: {
// call again the method if the route changes
$route: 'fetchRecipe',
},
methods: {
fetchRecipe() {
this.isLoading = true;
fetch(`${constants.API_URL}/api/v1/recipe/${this.$route.params.recipeId}`)
.then(response => response.json())
.then((response) => {
this.recipe = response.recipes[0];
this.isLoading = false;
});
},
},
};
</script>

View File

@ -0,0 +1,2 @@
export const API_URL = 'http://localhost:8080';
export const FOOBAR = 'TODO'; // TODO

View File

@ -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;

View File

@ -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,
},
],
});

View File

@ -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/<filename:path>')
def get_static_files(filename):
"""Get Static files"""
return bottle.static_file(filename, root=)
return bottle.static_file(filename) # TODO: root=

View File

@ -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"

Binary file not shown.