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).
This commit is contained in:
parent
c873021500
commit
b5946ccc84
@ -24,6 +24,11 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
// add your custom rules here
|
// add your custom rules here
|
||||||
rules: {
|
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
|
// don't require .vue extension when importing
|
||||||
'import/extensions': ['error', 'always', {
|
'import/extensions': ['error', 'always', {
|
||||||
js: 'never',
|
js: 'never',
|
||||||
|
16
cuizin/db.py
16
cuizin/db.py
@ -56,21 +56,21 @@ class Recipe(Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
database = database
|
database = database
|
||||||
|
|
||||||
@staticmethod
|
def update_from_weboob(self, weboob_obj):
|
||||||
def from_weboob(obj):
|
"""
|
||||||
recipe = Recipe()
|
Update fields taking values from the Weboob object.
|
||||||
|
"""
|
||||||
# Set fields
|
# Set fields
|
||||||
for field in ['title', 'url', 'author', 'picture_url',
|
for field in ['title', 'url', 'author', 'picture_url',
|
||||||
'short_description', 'preparation_time', 'cooking_time',
|
'short_description', 'preparation_time', 'cooking_time',
|
||||||
'ingredients', 'instructions']:
|
'ingredients', 'instructions']:
|
||||||
value = getattr(obj, field)
|
value = getattr(weboob_obj, field)
|
||||||
if value:
|
if value:
|
||||||
setattr(recipe, field, value)
|
setattr(self, field, value)
|
||||||
# Serialize number of person
|
# 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
|
# Download picture and save it as a blob
|
||||||
recipe.picture = requests.get(obj.picture_url).content
|
self.picture = requests.get(weboob_obj.picture_url).content
|
||||||
return recipe
|
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"""
|
"""
|
||||||
|
@ -1,31 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-app>
|
<v-app>
|
||||||
<v-toolbar app>
|
<v-toolbar app>
|
||||||
<v-toolbar-title class="toolbar-title">
|
<v-toolbar-title class="toolbar-title">
|
||||||
<router-link to="/">Cuizin</router-link>
|
<router-link to="/">Cuizin</router-link>
|
||||||
</v-toolbar-title>
|
</v-toolbar-title>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<router-link to="/new">
|
<router-link to="/new">
|
||||||
<v-btn icon>
|
<v-btn icon>
|
||||||
<v-icon>add</v-icon>
|
<v-icon>add</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</router-link>
|
</router-link>
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
<v-content>
|
<v-content>
|
||||||
<router-view/>
|
<router-view></router-view>
|
||||||
</v-content>
|
</v-content>
|
||||||
</v-app>
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.toolbar-title a {
|
.toolbar-title a {
|
||||||
color: rgba(0,0,0,.87);
|
color: rgba(0,0,0,.87);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,64 +1,68 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container fluid grid-list-md>
|
<v-container fluid grid-list-md>
|
||||||
<v-layout row wrap>
|
<Loader v-if="isLoading"></Loader>
|
||||||
<v-flex
|
<v-layout row wrap v-else>
|
||||||
v-for="recipe in recipes"
|
<v-flex xs12 v-if="!recipes.length" class="text-xs-center">
|
||||||
:key="recipe.title"
|
<p>Start by adding a recipe with the "+" button on the top right corner!</p>
|
||||||
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-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>
|
||||||
<v-flex xs6>
|
<v-flex
|
||||||
<p><v-icon>whatshot</v-icon> {{ recipe.cooking_time }} mins</p>
|
v-for="recipe in recipes"
|
||||||
|
: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-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-flex>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
</v-card>
|
</v-container>
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import * as constants from '@/constants';
|
import * as constants from '@/constants';
|
||||||
|
import Loader from '@/components/Loader';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
components: {
|
||||||
return {
|
Loader,
|
||||||
isLoading: false,
|
},
|
||||||
recipes: [],
|
data() {
|
||||||
};
|
return {
|
||||||
},
|
isLoading: false,
|
||||||
created() {
|
recipes: [],
|
||||||
this.fetchRecipes();
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
created() {
|
||||||
// call again the method if the route changes
|
this.fetchRecipes();
|
||||||
$route: 'fetchRecipes',
|
},
|
||||||
},
|
watch: {
|
||||||
methods: {
|
// call again the method if the route changes
|
||||||
fetchRecipes() {
|
$route: 'fetchRecipes',
|
||||||
this.isLoading = true;
|
},
|
||||||
|
methods: {
|
||||||
fetch(`${constants.API_URL}api/v1/recipes`)
|
fetchRecipes() {
|
||||||
.then(response => response.json())
|
this.isLoading = true;
|
||||||
.then((response) => {
|
|
||||||
this.recipes = response.recipes;
|
fetch(`${constants.API_URL}api/v1/recipes`)
|
||||||
this.isLoading = false;
|
.then(response => response.json())
|
||||||
});
|
.then((response) => {
|
||||||
|
this.recipes = response.recipes;
|
||||||
|
this.isLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
7
cuizin/js_src/components/Loader.vue
Normal file
7
cuizin/js_src/components/Loader.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<v-layout row>
|
||||||
|
<v-flex xs12 class="text-xs-center">
|
||||||
|
<v-progress-circular indeterminate :size="70" :width="7"></v-progress-circular>
|
||||||
|
</v-flex>
|
||||||
|
</v-layout>
|
||||||
|
</template>
|
@ -1,89 +1,89 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container text-xs-center>
|
<v-container text-xs-center>
|
||||||
<v-layout row wrap>
|
<v-layout row wrap>
|
||||||
<v-flex xs12>
|
<v-flex xs12>
|
||||||
<h2>Import from URL</h2>
|
<h2>Import from URL</h2>
|
||||||
<v-form v-model="validImport">
|
<v-form v-model="validImport">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="URL"
|
label="URL"
|
||||||
v-model="url"
|
v-model="url"
|
||||||
required
|
required
|
||||||
:rules="urlRules"
|
:rules="urlRules"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
<v-btn
|
<v-btn
|
||||||
@click="submitImport"
|
@click="submitImport"
|
||||||
:disabled="!validImport || disabledImport"
|
:disabled="!validImport || disabledImport"
|
||||||
>
|
>
|
||||||
Import
|
Import
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-form>
|
</v-form>
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
<v-layout row wrap mt-5 v-if="featureAddManually">
|
|
||||||
<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>
|
||||||
<v-flex xs4 mx-3>
|
</v-layout>
|
||||||
<v-text-field
|
<v-layout row wrap mt-5 v-if="featureAddManually">
|
||||||
label="Preparation time"
|
<v-flex xs12>
|
||||||
v-model="preparation_time"
|
<h2>Add manually</h2>
|
||||||
type="number"
|
<v-form v-model="validAdd">
|
||||||
suffix="mins"
|
<v-text-field
|
||||||
></v-text-field>
|
label="Title"
|
||||||
</v-flex>
|
v-model="title"
|
||||||
<v-flex xs4 ml-3>
|
required
|
||||||
<v-text-field
|
></v-text-field>
|
||||||
label="Cooking time"
|
<v-text-field
|
||||||
v-model="cooking_time"
|
label="Picture"
|
||||||
type="number"
|
v-model="picture"
|
||||||
suffix="mins"
|
></v-text-field>
|
||||||
></v-text-field>
|
<v-text-field
|
||||||
</v-flex>
|
label="Short description"
|
||||||
</v-layout>
|
v-model="short_description"
|
||||||
<v-text-field
|
textarea
|
||||||
label="Ingredients"
|
></v-text-field>
|
||||||
v-model="ingredients"
|
<v-layout row>
|
||||||
textarea
|
<v-flex xs4 mr-3>
|
||||||
></v-text-field>
|
<v-text-field
|
||||||
<v-text-field
|
label="Number of persons"
|
||||||
label="Instructions"
|
v-model="nb_person"
|
||||||
v-model="instructions"
|
type="number"
|
||||||
textarea
|
></v-text-field>
|
||||||
required
|
</v-flex>
|
||||||
></v-text-field>
|
<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
|
<v-btn
|
||||||
@click="submitAdd"
|
@click="submitAdd"
|
||||||
:disabled="!validAdd"
|
:disabled="!validAdd"
|
||||||
>
|
>
|
||||||
Add
|
Add
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-form>
|
</v-form>
|
||||||
</v-flex>
|
</v-flex>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -91,55 +91,55 @@ import * as constants from '@/constants';
|
|||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
url: null,
|
url: null,
|
||||||
validImport: false,
|
validImport: false,
|
||||||
disabledImport: false,
|
disabledImport: false,
|
||||||
validAdd: false,
|
validAdd: false,
|
||||||
title: null,
|
title: null,
|
||||||
picture: null,
|
picture: null,
|
||||||
short_description: null,
|
short_description: null,
|
||||||
nb_person: null,
|
nb_person: null,
|
||||||
preparation_time: null,
|
preparation_time: null,
|
||||||
cooking_time: null,
|
cooking_time: null,
|
||||||
ingredients: null,
|
ingredients: null,
|
||||||
instructions: null,
|
instructions: null,
|
||||||
urlRules: [
|
urlRules: [
|
||||||
v => !!v || 'URL is required',
|
v => !!v || 'URL is required',
|
||||||
(v) => {
|
(v) => {
|
||||||
try {
|
try {
|
||||||
new URL(v); // eslint-disable-line no-new
|
new URL(v); // eslint-disable-line no-new
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 'URL must be valid';
|
return 'URL must be valid';
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
featureAddManually: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
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
|
||||||
},
|
},
|
||||||
],
|
|
||||||
featureAddManually: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
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>
|
</script>
|
||||||
|
@ -1,81 +1,133 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container grid-list-md class="panel">
|
<v-container grid-list-md class="panel">
|
||||||
<v-layout row>
|
<Loader v-if="isLoading"></Loader>
|
||||||
<v-flex xs12 v-if="recipe">
|
<v-layout row v-else>
|
||||||
<h1 class="text-xs-center mt-3 mb-3">
|
<v-dialog v-model="refetchConfirm" max-width="500px">
|
||||||
{{ recipe.title }}
|
<v-card>
|
||||||
</h1>
|
<v-card-title class="headline">Refetch recipe</v-card-title>
|
||||||
<p>
|
<v-card-text>
|
||||||
</p>
|
This will refetch the recipe from the website and replace all current data with newly fetched ones. Are you sure?
|
||||||
<p class="text-xs-center">
|
</v-card-text>
|
||||||
<img :src="recipe.picture" />
|
<v-card-actions>
|
||||||
</p>
|
<v-spacer></v-spacer>
|
||||||
<v-layout row class="text-xs-center">
|
<v-btn color="secondary" flat @click.stop="refetchConfirm=false">Cancel</v-btn>
|
||||||
<v-flex xs6>
|
<v-btn color="error" flat @click.stop="handleRefetch">Refetch</v-btn>
|
||||||
<p>{{ recipe.nb_person }}</p>
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<v-dialog v-model="deleteConfirm" max-width="500px">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="headline">Delete recipe</v-card-title>
|
||||||
|
<v-card-text>This will delete this recipe. Are you sure?</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="secondary" flat @click.stop="deleteConfirm=false">Cancel</v-btn>
|
||||||
|
<v-btn color="error" flat @click.stop="handleDelete">Delete</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<v-flex xs12 v-if="recipe">
|
||||||
|
<h1 class="text-xs-center mt-3 mb-3">
|
||||||
|
{{ recipe.title }}
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
</p>
|
||||||
|
<p class="text-xs-center">
|
||||||
|
<img :src="recipe.picture" />
|
||||||
|
</p>
|
||||||
|
<v-layout row class="text-xs-center">
|
||||||
|
<v-flex xs6>
|
||||||
|
<p>{{ recipe.nb_person }}</p>
|
||||||
|
</v-flex>
|
||||||
|
<v-flex xs6>
|
||||||
|
<p><v-icon>timelapse</v-icon> Preparation: {{ recipe.preparation_time }} mins</p>
|
||||||
|
<p><v-icon>whatshot</v-icon> Cooking: {{ recipe.cooking_time }} mins</p>
|
||||||
|
</v-flex>
|
||||||
|
</v-layout>
|
||||||
|
<p>{{ recipe.short_description }}</p>
|
||||||
|
<h2>Ingredients</h2>
|
||||||
|
<ul class="ml-5">
|
||||||
|
<li v-for="ingredient in recipe.ingredients">
|
||||||
|
{{ ingredient }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<h2 class="mt-3">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">
|
||||||
|
<v-icon class="fa-icon">fa-external-link</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn @click.stop="deleteConfirm = true">
|
||||||
|
<v-icon>delete</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn @click.stop="refetchConfirm = true">
|
||||||
|
<v-icon>autorenew</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</p>
|
||||||
</v-flex>
|
</v-flex>
|
||||||
<v-flex xs6>
|
</v-layout>
|
||||||
<p><v-icon>timelapse</v-icon> Preparation: {{ recipe.preparation_time }} mins</p>
|
</v-container>
|
||||||
<p><v-icon>whatshot</v-icon> Cooking: {{ recipe.cooking_time }} mins</p>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
<p>{{ recipe.short_description }}</p>
|
|
||||||
<h2>Ingredients</h2>
|
|
||||||
<ul class="ml-5">
|
|
||||||
<li v-for="ingredient in recipe.ingredients">
|
|
||||||
{{ ingredient }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<h2 class="mt-3">Instructions</h2>
|
|
||||||
<nl2br tag="p" :text="recipe.instructions"></nl2br>
|
|
||||||
<p v-if="recipe.url" class="text-xs-center">
|
|
||||||
<v-btn :href="recipe.url">
|
|
||||||
<v-icon class="fa-icon">fa-external-link</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn @click="handleDelete">
|
|
||||||
<v-icon>delete</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</p>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import * as constants from '@/constants';
|
import * as constants from '@/constants';
|
||||||
|
import Loader from '@/components/Loader';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
components: {
|
||||||
return {
|
Loader,
|
||||||
isLoading: false,
|
},
|
||||||
recipe: null,
|
data() {
|
||||||
};
|
return {
|
||||||
},
|
isLoading: false,
|
||||||
created() {
|
recipe: null,
|
||||||
this.fetchRecipe();
|
deleteConfirm: false,
|
||||||
},
|
refetchConfirm: false,
|
||||||
watch: {
|
};
|
||||||
// call again the method if the route changes
|
},
|
||||||
$route: 'fetchRecipe',
|
created() {
|
||||||
},
|
this.fetchRecipe();
|
||||||
methods: {
|
},
|
||||||
fetchRecipe() {
|
watch: {
|
||||||
this.isLoading = true;
|
// call again the method if the route changes
|
||||||
|
$route: 'fetchRecipe',
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleRecipesResponse(response) {
|
||||||
|
this.recipe = response.recipes[0];
|
||||||
|
this.recipe.instructions = this.recipe.instructions.split(/\r\n/).map(item => item.trim());
|
||||||
|
this.isLoading = false;
|
||||||
|
},
|
||||||
|
fetchRecipe() {
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
fetch(`${constants.API_URL}api/v1/recipe/${this.$route.params.recipeId}`)
|
fetch(`${constants.API_URL}api/v1/recipe/${this.$route.params.recipeId}`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then((response) => {
|
.then(this.handleRecipesResponse);
|
||||||
this.recipe = response.recipes[0];
|
},
|
||||||
this.isLoading = false;
|
handleDelete() {
|
||||||
});
|
this.isLoading = true;
|
||||||
|
this.deleteConfirm = false;
|
||||||
|
fetch(`${constants.API_URL}api/v1/recipe/${this.$route.params.recipeId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
.then(() => this.$router.replace('/'));
|
||||||
|
},
|
||||||
|
handleRefetch() {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.refetchConfirm = false;
|
||||||
|
|
||||||
|
fetch(`${constants.API_URL}api/v1/recipe/${this.$route.params.recipeId}/refetch`, {
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(this.handleRecipesResponse);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
handleDelete() {
|
|
||||||
fetch(`${constants.API_URL}api/v1/recipe/${this.$route.params.recipeId}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
||||||
.then(() => this.$router.replace('/'));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -6,7 +6,6 @@ import 'roboto-fontface/css/roboto/roboto-fontface.css';
|
|||||||
import 'font-awesome/css/font-awesome.css';
|
import 'font-awesome/css/font-awesome.css';
|
||||||
import 'material-design-icons/iconfont/material-icons.css';
|
import 'material-design-icons/iconfont/material-icons.css';
|
||||||
import 'vuetify/dist/vuetify.min.css';
|
import 'vuetify/dist/vuetify.min.css';
|
||||||
import Nl2br from 'vue-nl2br';
|
|
||||||
|
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import router from './router';
|
import router from './router';
|
||||||
@ -15,15 +14,14 @@ import router from './router';
|
|||||||
require('es6-promise').polyfill();
|
require('es6-promise').polyfill();
|
||||||
require('isomorphic-fetch');
|
require('isomorphic-fetch');
|
||||||
|
|
||||||
Vue.component('nl2br', Nl2br);
|
|
||||||
Vue.use(Vuetify);
|
Vue.use(Vuetify);
|
||||||
|
|
||||||
Vue.config.productionTip = false;
|
Vue.config.productionTip = false;
|
||||||
|
|
||||||
/* eslint-disable no-new */
|
/* eslint-disable no-new */
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
router,
|
router,
|
||||||
components: { App },
|
components: { App },
|
||||||
template: '<App/>',
|
template: '<App/>',
|
||||||
});
|
});
|
||||||
|
@ -7,21 +7,21 @@ import Recipe from '@/components/Recipe';
|
|||||||
Vue.use(Router);
|
Vue.use(Router);
|
||||||
|
|
||||||
export default new Router({
|
export default new Router({
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
component: Home,
|
component: Home,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/new',
|
path: '/new',
|
||||||
name: 'New',
|
name: 'New',
|
||||||
component: New,
|
component: New,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/recipe/:recipeId',
|
path: '/recipe/:recipeId',
|
||||||
name: 'Recipe',
|
name: 'Recipe',
|
||||||
component: Recipe,
|
component: Recipe,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -15,11 +15,12 @@ from cuizin import db
|
|||||||
BACKENDS = ['750g', 'allrecipes', 'cuisineaz', 'marmiton', 'supertoinette']
|
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.
|
Add a recipe, trying to scrape from a given URL.
|
||||||
|
|
||||||
:param url: URL of the recipe.
|
:param url: URL of the recipe.
|
||||||
|
:param recipe: An optional recipe object to update.
|
||||||
:return: A ``cuizin.db.Recipe`` model.
|
:return: A ``cuizin.db.Recipe`` model.
|
||||||
"""
|
"""
|
||||||
# Eventually load modules from a local clone
|
# Eventually load modules from a local clone
|
||||||
@ -36,22 +37,20 @@ def add_recipe(url):
|
|||||||
for module in BACKENDS
|
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
|
# Try to fetch the recipe with a Weboob backend
|
||||||
recipe = None
|
|
||||||
for backend in backends:
|
for backend in backends:
|
||||||
browser = backend.browser
|
browser = backend.browser
|
||||||
if url.startswith(browser.BASEURL):
|
if url.startswith(browser.BASEURL):
|
||||||
browser.location(url)
|
browser.location(url)
|
||||||
recipe = db.Recipe.from_weboob(browser.page.get_recipe())
|
recipe.update_from_weboob(browser.page.get_recipe())
|
||||||
# Ensure URL is set
|
|
||||||
recipe.url = url
|
|
||||||
break
|
break
|
||||||
|
|
||||||
# If we could not scrape anything, simply create an empty recipe storing
|
# Ensure URL is set
|
||||||
# the URL.
|
recipe.url = url
|
||||||
if not recipe:
|
|
||||||
recipe = db.Recipe()
|
|
||||||
recipe.url = url
|
|
||||||
|
|
||||||
recipe.save()
|
recipe.save()
|
||||||
return recipe
|
return recipe
|
||||||
|
@ -4,7 +4,7 @@ import os
|
|||||||
import bottle
|
import bottle
|
||||||
|
|
||||||
from cuizin import db
|
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__))
|
MODULE_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
|
||||||
@ -49,7 +49,8 @@ def api_v1_recipes():
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'recipes': [
|
'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
|
assert recipe
|
||||||
recipes = [recipe.to_dict()]
|
recipes = [recipe.to_dict()]
|
||||||
except AssertionError:
|
except AssertionError:
|
||||||
recipes = [add_recipe(data['url']).to_dict()]
|
recipes = [fetch_recipe(data['url']).to_dict()]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'recipes': recipes
|
'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'])
|
@app.delete('/api/v1/recipe/:id', ['DELETE', 'OPTIONS'])
|
||||||
def api_v1_recipe_delete(id):
|
def api_v1_recipe_delete(id):
|
||||||
"""
|
"""
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
"material-design-icons": "^3.0.1",
|
"material-design-icons": "^3.0.1",
|
||||||
"roboto-fontface": "^0.9.0",
|
"roboto-fontface": "^0.9.0",
|
||||||
"vue": "^2.5.2",
|
"vue": "^2.5.2",
|
||||||
"vue-nl2br": "^0.0.5",
|
|
||||||
"vue-router": "^3.0.1",
|
"vue-router": "^3.0.1",
|
||||||
"vuetify": "^1.0.0"
|
"vuetify": "^1.0.0"
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user