init
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,2 +1,2 @@
|
||||
__pycache__
|
||||
db.sqlite3
|
||||
__pycache__
|
||||
db.sqlite3
|
||||
|
||||
@@ -1,131 +1,136 @@
|
||||
"""
|
||||
Django settings for cookAssistant project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 2.2.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/2.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/2.2/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
TEMPLATES_DIR = os.path.join(BASE_DIR, 'templates')
|
||||
FRONTEND_DIR = os.path.join(BASE_DIR, 'frontend')
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'g3dfrq+4px+4v(*7l+o$y--)z4gwxxovewwgdu#5#_@428l@ry'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'webpack_loader',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'cookAssistant.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [TEMPLATES_DIR, ],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'cookAssistant.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
WEBPACK_LOADER = {
|
||||
'DEFAULT': {
|
||||
'CACHE': not DEBUG,
|
||||
'BUNDLE_DIR_NAME': '/bundles/', # must end with slash
|
||||
'STATS_FILE': os.path.join(FRONTEND_DIR, 'webpack-stats.json'),
|
||||
}
|
||||
}
|
||||
"""
|
||||
Django settings for cookAssistant project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 2.2.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/2.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/2.2/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
TEMPLATES_DIR = os.path.join(BASE_DIR, 'templates')
|
||||
FRONTEND_DIR = os.path.join(BASE_DIR, 'frontend')
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'g3dfrq+4px+4v(*7l+o$y--)z4gwxxovewwgdu#5#_@428l@ry'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
# Custom settings
|
||||
'webpack_loader',
|
||||
'rest_framework',
|
||||
'rest_framework_nested',
|
||||
'recipe_book',
|
||||
'planning',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'cookAssistant.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [TEMPLATES_DIR, ],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'cookAssistant.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
WEBPACK_LOADER = {
|
||||
'DEFAULT': {
|
||||
'CACHE': not DEBUG,
|
||||
'BUNDLE_DIR_NAME': '/bundles/', # must end with slash
|
||||
'STATS_FILE': os.path.join(FRONTEND_DIR, 'webpack-stats.json'),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,46 @@
|
||||
"""cookAssistant URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/2.2/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('',
|
||||
TemplateView.as_view(
|
||||
template_name="application.html"
|
||||
),
|
||||
name="app"
|
||||
),
|
||||
]
|
||||
"""cookAssistant URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/2.2/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from recipe_book import views
|
||||
from rest_framework_nested import routers
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r'recips', views.RecipesViewSet)
|
||||
router.register(r'ingdts', views.IngredientViewSet)
|
||||
|
||||
amounts_router = routers.NestedDefaultRouter(
|
||||
router, r'recips', lookup="recipe"
|
||||
)
|
||||
amounts_router.register(
|
||||
r'amounts', views.IngredientWAmountViewSet,
|
||||
base_name="recipe-ingdt-amounts"
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/', include(router.urls)),
|
||||
path('api/', include(amounts_router.urls)),
|
||||
path('',
|
||||
TemplateView.as_view(
|
||||
template_name="application.html"
|
||||
),
|
||||
name="app"
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
"""
|
||||
WSGI config for cookAssistant project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cookAssistant.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
"""
|
||||
WSGI config for cookAssistant project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cookAssistant.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
|
||||
657
frontend/package-lock.json
generated
657
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,12 +10,13 @@
|
||||
"dependencies": {
|
||||
"core-js": "^2.6.5",
|
||||
"vue": "^2.6.6",
|
||||
"vue-router": "^3.0.2",
|
||||
"vuetify": "^1.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^3.5.0",
|
||||
"@vue/cli-plugin-eslint": "^3.5.0",
|
||||
"@vue/cli-service": "^3.5.0",
|
||||
"@vue/cli-service": "^3.6.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"eslint": "^5.8.0",
|
||||
"eslint-plugin-vue": "^5.0.0",
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<title>frontend</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
|
||||
@@ -1,27 +1,119 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<ToolBar/>
|
||||
<v-toolbar app tabs>
|
||||
<v-toolbar-side-icon
|
||||
@click.stop="showNav = !showNav"
|
||||
></v-toolbar-side-icon>
|
||||
<v-toolbar-title>{{ title }}</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn icon
|
||||
v-for="(tool, idx) in tools"
|
||||
:key="`tool-${idx}`"
|
||||
>
|
||||
<v-icon
|
||||
:color="tool.color ? tool.color : 'dark'"
|
||||
v-html="tool.icon"
|
||||
></v-icon>
|
||||
</v-btn>
|
||||
|
||||
<template v-slot:extension>
|
||||
<router-view name="extension"></router-view>
|
||||
</template>
|
||||
</v-toolbar>
|
||||
<v-navigation-drawer
|
||||
fixed
|
||||
app
|
||||
clipped
|
||||
v-model="showNav"
|
||||
>
|
||||
<v-list dense>
|
||||
<v-list-tile
|
||||
v-for="link in links"
|
||||
:key="link.path"
|
||||
:to="link.path"
|
||||
>
|
||||
<v-list-tile-avatar>
|
||||
<v-icon v-html="link.icon"
|
||||
></v-icon>
|
||||
</v-list-tile-avatar>
|
||||
<v-list-tile-title v-html="link.text"
|
||||
></v-list-tile-title>
|
||||
</v-list-tile>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
<v-content>
|
||||
<HelloWorld/>
|
||||
<router-view></router-view>
|
||||
</v-content>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HelloWorld from './components/HelloWorld'
|
||||
import ToolBar from './components/ToolBar'
|
||||
|
||||
import NotFound from './routes/NotFound'
|
||||
|
||||
export const RecipeCategories = {
|
||||
0: "Petit-déjeuner",
|
||||
1: "Entrée",
|
||||
2: "Plat",
|
||||
3: "Dessert"
|
||||
}
|
||||
// *** TOOLS ***
|
||||
//
|
||||
// * create_recipe: go to creation page
|
||||
// * import_recipe: go to import page
|
||||
// ---- Recipe list
|
||||
// * category_tabs: navigate through categories
|
||||
// * search: show input inside extension
|
||||
// ---- Recipe details
|
||||
// * go_back: go back to previous page
|
||||
// * edit: edit fields of model details
|
||||
// * add_to_menu: show menu slot picker in bottom sheet
|
||||
// ---- Planning
|
||||
// * complete_menu: ask for automatic completion of menu
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
HelloWorld,
|
||||
ToolBar
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
//
|
||||
links: [],
|
||||
categories: RecipeCategories,
|
||||
showNav: null,//
|
||||
}
|
||||
},
|
||||
created () {
|
||||
// Build navigation links from router.js config
|
||||
for (var idx in this.$router.options.routes) {
|
||||
var route = this.$router.options.routes[idx];
|
||||
this.links.push({
|
||||
path: route.path,
|
||||
text: route.meta.title,
|
||||
icon: route.meta.icon,
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
tools: function() {
|
||||
return [{icon: 'add'},]
|
||||
},
|
||||
// TODO: move this inside routing events
|
||||
title: function() {
|
||||
var title = "CookAssistant"
|
||||
for (var key in this.$route.matched) {
|
||||
var route = this.$route.matched[key];
|
||||
if (route.hasOwnProperty('meta')) {
|
||||
if (route.meta.title != undefined) {
|
||||
title = route.meta.title
|
||||
}
|
||||
}
|
||||
}
|
||||
return title;
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html { overflow-y: auto }
|
||||
</style>
|
||||
|
||||
153
frontend/src/api.js
Normal file
153
frontend/src/api.js
Normal file
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
|
||||
TODO:
|
||||
Implement 'create'/'edit'/'read' modes with generic
|
||||
configuration retrieved from OPTIONS request (using api.js)
|
||||
|
||||
'read' would use GET methods
|
||||
'edit' would use PUT actions
|
||||
'create' would use POST action
|
||||
|
||||
*/
|
||||
const baseUrl = "http://localhost:8000/api"
|
||||
|
||||
/*
|
||||
Check that the response status is what's expected.
|
||||
Returns an object with :
|
||||
* success: bool
|
||||
* data: response data
|
||||
*/
|
||||
const processStatusCode = function(expected_code) {
|
||||
return function(response) {
|
||||
const status_code = response.status;
|
||||
let data;
|
||||
if (expected_code != 204) {
|
||||
data = response.json();
|
||||
} else {
|
||||
data = null
|
||||
}
|
||||
return Promise.all([status_code, data])
|
||||
.then(ret => ({
|
||||
success: ret[0] == expected_code,
|
||||
data: ret[1]
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Returns data on success or throw an error with data.
|
||||
*/
|
||||
const processSuccess = function(result) {
|
||||
if (!result.success) {
|
||||
throw result.data;
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
const _headers = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
const _request = {
|
||||
get (url) {
|
||||
return fetch(
|
||||
url,
|
||||
{ method: 'GET',
|
||||
})
|
||||
.then(processStatusCode(200))
|
||||
.then(processSuccess);
|
||||
},
|
||||
post (url, data) {
|
||||
return fetch(
|
||||
url,
|
||||
{ method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: _headers,
|
||||
})
|
||||
.then(processStatusCode(201))
|
||||
.then(processSuccess)
|
||||
},
|
||||
put (url, data) {
|
||||
return fetch(
|
||||
url,
|
||||
{ method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
headers: _headers,
|
||||
})
|
||||
.then(processStatusCode(200))
|
||||
.then(processSuccess)
|
||||
},
|
||||
delete(url) {
|
||||
return fetch(
|
||||
url,
|
||||
{ method: 'DELETE',
|
||||
headers: _headers, })
|
||||
.then(processStatusCode(204))
|
||||
.then(processSuccess)
|
||||
},
|
||||
options (url) {
|
||||
return fetch(
|
||||
url,
|
||||
{ method: 'OPTIONS',
|
||||
})
|
||||
.then(processStatusCode(200))
|
||||
.then(processSuccess)
|
||||
}
|
||||
}
|
||||
|
||||
const mountEndPoint = function(url) {
|
||||
return {
|
||||
read (id) {
|
||||
return _request.get(url + id + '/')
|
||||
},
|
||||
create (data) {
|
||||
return _request.post(url, data)
|
||||
},
|
||||
edit (id, data) {
|
||||
return _request.put(url + id + '/', data)
|
||||
},
|
||||
delete (id) {
|
||||
return _request.delete(url + id + '/')
|
||||
},
|
||||
options() { return _request.options(url) }
|
||||
}
|
||||
}
|
||||
|
||||
const mountNestedEndPoint = function(url) {
|
||||
const getUrl = function(parent_id) {
|
||||
// TODO: Make it safe against url injection !
|
||||
return url.replace('*', parent_id)
|
||||
}
|
||||
return {
|
||||
read (parent_id, id) {
|
||||
return _request.get(getUrl(parent_id) + id + '/')
|
||||
},
|
||||
create (parent_id, data) {
|
||||
return _request.post(
|
||||
getUrl(parent_id),
|
||||
data,
|
||||
)
|
||||
},
|
||||
edit (parent_id, id, data) {
|
||||
return _request.put(
|
||||
getUrl(parent_id) + id + '/',
|
||||
data,
|
||||
)
|
||||
},
|
||||
delete (parent_id, id) {
|
||||
return _request.delete( getUrl(parent_id) + id + '/' )
|
||||
},
|
||||
options () {
|
||||
return _request.options( getUrl(1) )
|
||||
},
|
||||
list (parent_id) {
|
||||
return _request.get( getUrl(parent_id) )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
recipes: mountEndPoint(`${baseUrl}/recips/`),
|
||||
ingredients: mountNestedEndPoint(
|
||||
`${baseUrl}/recips/*/amounts/`
|
||||
),
|
||||
}
|
||||
40
frontend/src/components/CategoryTabs.vue
Normal file
40
frontend/src/components/CategoryTabs.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<!-- Categories list -->
|
||||
<span>
|
||||
<v-tabs v-if="showTabs" :mandatory="false">
|
||||
<v-tab v-for="key in Object.keys(categories)"
|
||||
:key="`cat-${key}`"
|
||||
:to="`/recipes/category/${key}`">
|
||||
{{ categories[key] }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<template v-else>
|
||||
<v-toolbar-title>
|
||||
Détails
|
||||
</v-toolbar-title>
|
||||
<v-btn v-show="!showTabs"
|
||||
icon
|
||||
@click="$router.go(-1)"
|
||||
><v-icon>arrow_back</v-icon></v-btn>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { RecipeCategories } from '../App'
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
categories: RecipeCategories,
|
||||
}),
|
||||
computed: {
|
||||
showTabs: function() {
|
||||
return !(this.$route.path.startsWith("/recipes/id"))
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -1,147 +0,0 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-layout
|
||||
text-xs-center
|
||||
wrap
|
||||
>
|
||||
<v-flex xs12>
|
||||
<v-img
|
||||
:src="require('../assets/logo.svg')"
|
||||
class="my-3"
|
||||
contain
|
||||
height="200"
|
||||
></v-img>
|
||||
</v-flex>
|
||||
|
||||
<v-flex mb-4>
|
||||
<h1 class="display-2 font-weight-bold mb-3">
|
||||
Welcome to Vuetify
|
||||
</h1>
|
||||
<p class="subheading font-weight-regular">
|
||||
For help and collaboration with other Vuetify developers,
|
||||
<br>please join our online
|
||||
<a href="https://community.vuetifyjs.com" target="_blank">Discord Community</a>
|
||||
</p>
|
||||
</v-flex>
|
||||
|
||||
<v-flex
|
||||
mb-5
|
||||
xs12
|
||||
>
|
||||
<h2 class="headline font-weight-bold mb-3">What's next?</h2>
|
||||
|
||||
<v-layout justify-center>
|
||||
<a
|
||||
v-for="(next, i) in whatsNext"
|
||||
:key="i"
|
||||
:href="next.href"
|
||||
class="subheading mx-3"
|
||||
target="_blank"
|
||||
>
|
||||
{{ next.text }}
|
||||
</a>
|
||||
</v-layout>
|
||||
</v-flex>
|
||||
|
||||
<v-flex
|
||||
xs12
|
||||
mb-5
|
||||
>
|
||||
<h2 class="headline font-weight-bold mb-3">Important Links</h2>
|
||||
|
||||
<v-layout justify-center>
|
||||
<a
|
||||
v-for="(link, i) in importantLinks"
|
||||
:key="i"
|
||||
:href="link.href"
|
||||
class="subheading mx-3"
|
||||
target="_blank"
|
||||
>
|
||||
{{ link.text }}
|
||||
</a>
|
||||
</v-layout>
|
||||
</v-flex>
|
||||
|
||||
<v-flex
|
||||
xs12
|
||||
mb-5
|
||||
>
|
||||
<h2 class="headline font-weight-bold mb-3">Ecosystem</h2>
|
||||
|
||||
<v-layout justify-center>
|
||||
<a
|
||||
v-for="(eco, i) in ecosystem"
|
||||
:key="i"
|
||||
:href="eco.href"
|
||||
class="subheading mx-3"
|
||||
target="_blank"
|
||||
>
|
||||
{{ eco.text }}
|
||||
</a>
|
||||
</v-layout>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({
|
||||
ecosystem: [
|
||||
{
|
||||
text: 'vuetify-loader',
|
||||
href: 'https://github.com/vuetifyjs/vuetify-loader'
|
||||
},
|
||||
{
|
||||
text: 'github',
|
||||
href: 'https://github.com/vuetifyjs/vuetify'
|
||||
},
|
||||
{
|
||||
text: 'awesome-vuetify',
|
||||
href: 'https://github.com/vuetifyjs/awesome-vuetify'
|
||||
}
|
||||
],
|
||||
importantLinks: [
|
||||
{
|
||||
text: 'Documentation',
|
||||
href: 'https://vuetifyjs.com'
|
||||
},
|
||||
{
|
||||
text: 'Chat',
|
||||
href: 'https://community.vuetifyjs.com'
|
||||
},
|
||||
{
|
||||
text: 'Made with Vuetify',
|
||||
href: 'https://madewithvuetifyjs.com'
|
||||
},
|
||||
{
|
||||
text: 'Twitter',
|
||||
href: 'https://twitter.com/vuetifyjs'
|
||||
},
|
||||
{
|
||||
text: 'Articles',
|
||||
href: 'https://medium.com/vuetify'
|
||||
}
|
||||
],
|
||||
whatsNext: [
|
||||
{
|
||||
text: 'Explore components',
|
||||
href: 'https://vuetifyjs.com/components/api-explorer'
|
||||
},
|
||||
{
|
||||
text: 'Select a layout',
|
||||
href: 'https://vuetifyjs.com/layout/pre-defined'
|
||||
},
|
||||
{
|
||||
text: 'Frequently Asked Questions',
|
||||
href: 'https://vuetifyjs.com/getting-started/frequently-asked-questions'
|
||||
}
|
||||
|
||||
]
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
26
frontend/src/components/RessourceField.vue
Normal file
26
frontend/src/components/RessourceField.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<v-select v-if="type == 'choice'"
|
||||
:label="label"
|
||||
placeholder="----"
|
||||
:items="choices"
|
||||
item-text="display_name"
|
||||
:value="value"
|
||||
@change="val => $emit('input', val)"
|
||||
/>
|
||||
<v-text-field v-else
|
||||
:label="label"
|
||||
:type="type"
|
||||
:value="value"
|
||||
@input="val => $emit('input', val)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//TODO: work with nested object fields...
|
||||
export default {
|
||||
props: ['value', 'label', 'read_only', 'required', 'type', 'choices'],
|
||||
data () {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,37 +0,0 @@
|
||||
<template>
|
||||
<v-toolbar dark color="primary">
|
||||
<!-- <v-toolbar-side-icon></v-toolbar-side-icon> -->
|
||||
|
||||
<v-toolbar-title class="white--text">
|
||||
Cook Assistant
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn icon>
|
||||
<v-icon>search</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon>
|
||||
<v-icon>apps</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon>
|
||||
<v-icon>refresh</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon>
|
||||
<v-icon>more_vert</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
87
frontend/src/components/recipes/Ingredient.vue
Normal file
87
frontend/src/components/recipes/Ingredient.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<v-card-text class="py-1" v-if="!editing">
|
||||
{{ item.display }}
|
||||
</v-card-text>
|
||||
<v-input v-else
|
||||
:append-icon="append_icon"
|
||||
@click:append="appendIconAction"
|
||||
:messages="errors"
|
||||
>
|
||||
<v-container fluid class="pa-0 ma-0">
|
||||
<v-layout>
|
||||
<v-flex xs3>
|
||||
<RField
|
||||
v-bind="fields.amount"
|
||||
v-model="item.amount"
|
||||
@input="markUpdated"
|
||||
/>
|
||||
</v-flex>
|
||||
<v-flex xs3>
|
||||
<RField
|
||||
v-bind="fields.unit"
|
||||
v-model="item.unit"
|
||||
@input="markUpdated"
|
||||
/>
|
||||
</v-flex>
|
||||
<v-flex xs6>
|
||||
<RField
|
||||
v-bind="fields.ingredient.children.name"
|
||||
v-model="item.ingredient.name"
|
||||
@input="markUpdated"
|
||||
/>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-input>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '../../api.js'
|
||||
import RField from '../RessourceField'
|
||||
|
||||
|
||||
|
||||
export default {
|
||||
components: { RField },
|
||||
props: ["item", "fields", "editing"],
|
||||
data() {
|
||||
return {
|
||||
errors: [],
|
||||
updated: false,
|
||||
append_icon: "delete"
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isNew () {
|
||||
return !this.item.ingredient.id
|
||||
},
|
||||
},
|
||||
created () {
|
||||
if (this.isNew) {
|
||||
this.append_icon = "add"
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
appendIconAction () {
|
||||
if (this.isNew) {
|
||||
this.$emit('create', this)
|
||||
} else {
|
||||
// TODO: should ask for confirmation
|
||||
this.$emit('delete', this)
|
||||
}
|
||||
},
|
||||
markUpdated () {
|
||||
this.updated = true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
editing(newValue) {
|
||||
if (newValue == false && this.updated) {
|
||||
// TODO: check if there has been updates
|
||||
this.$emit('save', this)
|
||||
this.updated = false
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
103
frontend/src/components/recipes/IngredientList.vue
Normal file
103
frontend/src/components/recipes/IngredientList.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<v-card class="pa-1">
|
||||
<v-card-title>
|
||||
<strong>Ingrédients</strong>
|
||||
</v-card-title>
|
||||
<ingredient-details
|
||||
v-for="ingdt in edited_ingdts"
|
||||
:item="ingdt"
|
||||
:fields="fields"
|
||||
:editing="editing"
|
||||
@delete="deleteItem"
|
||||
@save="saveItem"
|
||||
/>
|
||||
<!-- New ingredient in editing mode -->
|
||||
<p v-if="editing">
|
||||
(+) <ingredient-details
|
||||
:item="newItem()"
|
||||
:fields="fields"
|
||||
:editing="editing"
|
||||
@create="createItem"
|
||||
/>
|
||||
</p>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '../../api.js'
|
||||
import Ingredient from './Ingredient'
|
||||
function NullIngredient (recipe_id) {
|
||||
this.recipe = recipe_id
|
||||
this.ingredient = { name: "" }
|
||||
this.amount = 0
|
||||
this.unit = ""
|
||||
this.display = ""
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'ingredient-details': Ingredient,
|
||||
},
|
||||
props: ['ingredients', 'editing', 'recipe'],
|
||||
data () {
|
||||
return {
|
||||
fields: {},
|
||||
edited_ingdts: [],
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
// Copy the property to mutate it
|
||||
api.ingredients.options()
|
||||
.then(opts => {
|
||||
this.fields = opts.actions.POST
|
||||
})
|
||||
console.log("get list:", this.recipe)
|
||||
api.ingredients.list(this.recipe)
|
||||
.then(data => this.edited_ingdts = data)
|
||||
.catch(err => console.error("error reading ingdt:", err))
|
||||
},
|
||||
methods: {
|
||||
newItem () {
|
||||
return new NullIngredient(this.recipe)
|
||||
},
|
||||
createItem (comp) {
|
||||
let item = comp.item
|
||||
item.recipe = this.recipe
|
||||
api.ingredients.create(
|
||||
this.recipe,
|
||||
item
|
||||
)
|
||||
.then(data => {
|
||||
this.edited_ingdts.push(data)
|
||||
//TODO: reset additionnal Ingredient
|
||||
})
|
||||
.catch(err => comp.errors.push(JSON.stringify(err)))
|
||||
},
|
||||
saveItem(comp) {
|
||||
let item = comp.item
|
||||
item.recipe = this.recipe // Inject recipe's id into data
|
||||
api.ingredients.edit(
|
||||
item.recipe,
|
||||
item.ingredient.id,
|
||||
item,
|
||||
)
|
||||
.then(data => {
|
||||
// TODO: better identification for ingredients
|
||||
// BUG: will not work if ingredient changes!!
|
||||
let idx = this.edited_ingdts.indexOf(it => it.ingredient == data.ingredient)
|
||||
this.edited_ingdts[idx] = data
|
||||
})
|
||||
.catch(err => comp.errors.push(JSON.stringify(err)))
|
||||
},
|
||||
deleteItem(comp) {
|
||||
let item = comp.item
|
||||
api.ingredients.delete(this.recipe, item.ingredient.id)
|
||||
.then(() => {
|
||||
var idx = this.edited_ingdts.indexOf(item)
|
||||
this.edited_ingdts.splice(idx, 1)
|
||||
})
|
||||
.catch(err => comp.errors.push(JSON.stringify(err)))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
194
frontend/src/components/recipes/RecipeDetails.vue
Normal file
194
frontend/src/components/recipes/RecipeDetails.vue
Normal file
@@ -0,0 +1,194 @@
|
||||
<!-- Details view of a Recipe
|
||||
|
||||
Has read/edit modes
|
||||
|
||||
-->
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-layout wrap>
|
||||
<v-flex xs10> <!-- Heading -->
|
||||
<v-alert v-for="(error,idx) in errors"
|
||||
:key="`alert-${idx}`"
|
||||
:value="true">
|
||||
Error: {{ error }}
|
||||
</v-alert>
|
||||
<v-card-text v-if="!editing">
|
||||
<h6 class="title pb-3">
|
||||
{{item.name}}
|
||||
</h6>
|
||||
<p class="subheading pb-3">
|
||||
{{categories[item.category]}}
|
||||
</p>
|
||||
</v-card-text>
|
||||
<template v-else>
|
||||
<RField
|
||||
v-bind="fields.name"
|
||||
v-model="item.name"
|
||||
/>
|
||||
<RField
|
||||
v-bind="fields.category"
|
||||
v-model="item.category"
|
||||
/>
|
||||
</template>
|
||||
</v-flex>
|
||||
<v-flex xs2> <!-- Buttons -->
|
||||
<!-- Add to planning -->
|
||||
<v-bottom-sheet v-show="!editing"
|
||||
v-model="sheetSelectSlot"
|
||||
>
|
||||
<template v-slot:activator>
|
||||
<v-btn
|
||||
fab dark
|
||||
color="success"
|
||||
><v-icon>add</v-icon></v-btn>
|
||||
</template>
|
||||
<!-- Put a meal picker here... -->
|
||||
<v-card elevation="10" style="min-height: 300px;">
|
||||
<v-card-text>
|
||||
Content of bottom sheet
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-bottom-sheet>
|
||||
<!-- Edit mode -->
|
||||
<v-fab-transition>
|
||||
<v-btn
|
||||
fab dark
|
||||
:small="!editing"
|
||||
color="error"
|
||||
@click="switchEdit"
|
||||
>
|
||||
<v-icon v-if="editing">save</v-icon>
|
||||
<v-icon v-else>edit</v-icon>
|
||||
</v-btn>
|
||||
</v-fab-transition>
|
||||
</v-flex>
|
||||
<v-flex xs12 md4>
|
||||
<IngredientList v-if="!isLoading"
|
||||
:editing="editing"
|
||||
:recipe="item.id"
|
||||
:ingredients="item.ingredients"
|
||||
/>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RField from '../RessourceField'
|
||||
import { RecipeCategories } from '../../App'
|
||||
import api from '../../api.js'
|
||||
import NotFound from '../../routes/NotFound'
|
||||
const IngredientList = import('./IngredientList')
|
||||
|
||||
function NullRecipe() {
|
||||
this.name = String();
|
||||
this.category = Number();
|
||||
this.ingredients = [];
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
IngredientList: () => ({
|
||||
component: IngredientList,
|
||||
loading: NotFound, //TODO: actual loading view
|
||||
error: NotFound,
|
||||
delay: 100,
|
||||
timeout: 3000,
|
||||
}),
|
||||
RField,
|
||||
},
|
||||
data() {
|
||||
// Transform categories for v-select component
|
||||
var select_items = []
|
||||
Object.keys(RecipeCategories)
|
||||
.map(function(key) {
|
||||
select_items.push({
|
||||
text: RecipeCategories[key],
|
||||
value: key,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
isLoading: true,
|
||||
sheetSelectSlot: false,
|
||||
editing: false,
|
||||
select_items,
|
||||
categories: RecipeCategories,
|
||||
errors: [],
|
||||
// Will be populated by initRecipeItem method
|
||||
item: new NullRecipe,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
switchEdit: function() {
|
||||
if (this.editing) {
|
||||
var data = {
|
||||
name: this.item.name,
|
||||
category: this.item.category,
|
||||
}
|
||||
if (this.item.id) {
|
||||
// Edit recipe
|
||||
api.recipes.edit(this.item.id, data)
|
||||
.then(data => {
|
||||
this.item = data
|
||||
this.editing = false
|
||||
})
|
||||
.catch(err => this.errors.push(err))
|
||||
} else {
|
||||
// Create recipe
|
||||
api.recipes.create(data)
|
||||
.then(data => {
|
||||
this.item = data
|
||||
this.editing = false
|
||||
})
|
||||
.catch(err => this.errors.push(err))
|
||||
}
|
||||
} else {
|
||||
this.editing = true
|
||||
}
|
||||
},
|
||||
initRecipeItem: function(id) {
|
||||
// An id of 0 means we want a creation page
|
||||
if (id == 0) {
|
||||
this.item = new NullRecipe
|
||||
this.editing = true
|
||||
} else {
|
||||
api.recipes.read(id)
|
||||
.then(data => {
|
||||
this.errors = []
|
||||
this.item = data
|
||||
})
|
||||
.catch(err => {
|
||||
this.errors.push(err)
|
||||
this.item = new NullRecipe
|
||||
})
|
||||
.then(() => this.isLoading = false)
|
||||
}
|
||||
},
|
||||
updateRecip: function(form_data) {
|
||||
console.log(form_data);
|
||||
var messages = [];
|
||||
return messages;
|
||||
}
|
||||
},
|
||||
created () {
|
||||
api.recipes.options()
|
||||
.then(data => this.fields = data.actions.POST)
|
||||
.then(() => console.log(this.fields))
|
||||
},
|
||||
beforeRouteEnter (to, from, next) {
|
||||
next(vm => {
|
||||
const id = to.params.id;
|
||||
vm.initRecipeItem(id);
|
||||
})
|
||||
},
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
const id = to.params.id;
|
||||
this.initRecipeItem(id);
|
||||
next()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
48
frontend/src/components/recipes/RecipeList.vue
Normal file
48
frontend/src/components/recipes/RecipeList.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
/* List view of recipes */
|
||||
<template>
|
||||
<v-list>
|
||||
<template v-for="(item, idx) in items">
|
||||
<v-list-tile
|
||||
:key="item.title"
|
||||
:to="`/recipes/id/${item.id}`">
|
||||
<v-list-tile-content>
|
||||
{{item.name}}
|
||||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
<v-divider v-if="idx < (items.length - 1)"
|
||||
:key="idx"></v-divider>
|
||||
</template>
|
||||
</v-list>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
// Recipes from db grouped by categories id
|
||||
content: {
|
||||
0: [], 1: [], 2: [], 3: []
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
items: function() {
|
||||
return this.content[this.$route.params.cat];
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
const vm = this;
|
||||
fetch('http://localhost:8000/api/recips/')
|
||||
.then((response) => response.json())
|
||||
.then(function(data) {
|
||||
for (var idx in data) {
|
||||
var obj = data[idx];
|
||||
vm.content[parseInt(obj.category)].push(obj);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -1,9 +1,11 @@
|
||||
import Vue from 'vue'
|
||||
import './plugins/vuetify'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
render: h => h(App),
|
||||
}).$mount('#app')
|
||||
|
||||
46
frontend/src/router.js
Normal file
46
frontend/src/router.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
|
||||
import Index from './routes/Index'
|
||||
import NotFound from './routes/NotFound'
|
||||
import Recipes from './routes/Recipes'
|
||||
import Planning from './routes/Planning'
|
||||
|
||||
import RecipeDetails from './components/recipes/RecipeDetails'
|
||||
import RecipeList from './components/recipes/RecipeList'
|
||||
import CategoryTabs from './components/CategoryTabs'
|
||||
|
||||
Vue.use(Router)
|
||||
|
||||
const router = new Router({
|
||||
routes: [
|
||||
{ path: '/',
|
||||
component: Index,
|
||||
meta: { title: "Home", icon: "home" }
|
||||
},
|
||||
|
||||
{ path: '/recipes',
|
||||
components: { default: Recipes,
|
||||
extension: CategoryTabs, },
|
||||
meta: { title: "Recettes",
|
||||
icon:"book" },
|
||||
children: [
|
||||
{ path: 'id/:id',
|
||||
component: RecipeDetails,
|
||||
meta: { subtitle: "Détails", } },
|
||||
{ path: 'category/:cat',
|
||||
component: RecipeList,
|
||||
meta: { subtitle: "Liste", } },
|
||||
{ path: '*',
|
||||
component: NotFound }
|
||||
]
|
||||
},
|
||||
|
||||
{ path: '/planning',
|
||||
component: Planning,
|
||||
meta: { title: "Menu", icon: "calendar_today" },
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export default router
|
||||
21
frontend/src/routes/Index.vue
Normal file
21
frontend/src/routes/Index.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<section>
|
||||
<v-img
|
||||
:src="require('../assets/logo.svg')"
|
||||
class="ma-3"
|
||||
contain
|
||||
height="200"
|
||||
></v-img>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
9
frontend/src/routes/NotFound.vue
Normal file
9
frontend/src/routes/NotFound.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<v-alert value="true">Not found</v-alert>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({})
|
||||
}
|
||||
</script>
|
||||
91
frontend/src/routes/Planning.vue
Normal file
91
frontend/src/routes/Planning.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<v-list two-line overflow-y>
|
||||
<template v-for="(item, index) in items">
|
||||
<v-subheader v-if="item.header"
|
||||
:key="item.header"
|
||||
>{{item.header}}</v-subheader>
|
||||
<v-divider v-else-if="item.divider"></v-divider>
|
||||
<v-list-tile v-else
|
||||
:key="index">
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-sub-title
|
||||
>{{ item.subtitle }}</v-list-tile-sub-title>
|
||||
<v-list-tile-title
|
||||
>{{ item.title }}</v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
<v-list-tile-action v-show="item.title">
|
||||
<v-btn icon color="primary" small
|
||||
:to="`/recipes/id/${item.id}`"
|
||||
><v-icon>remove_red_eye</v-icon></v-btn>
|
||||
</v-list-tile-action>
|
||||
<v-list-tile-action>
|
||||
<v-btn icon color="warning" small>
|
||||
<v-icon>
|
||||
{{ item.title ? 'delete' : 'add' }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-list-tile-action>
|
||||
</v-list-tile>
|
||||
</template>
|
||||
</v-list>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
const MockPlanning = {
|
||||
"Lundi": {
|
||||
"Midi": "Raclette",
|
||||
"Soir": "Soupe",
|
||||
},
|
||||
"Mardi": {
|
||||
"Midi": null,
|
||||
"Soir": null,
|
||||
},
|
||||
"Mercredi": {
|
||||
"Midi": null,
|
||||
"Soir": null,
|
||||
},
|
||||
"Jeudi": {
|
||||
"Midi": null,
|
||||
"Soir": null,
|
||||
},
|
||||
"Vendredi": {
|
||||
"Midi": null,
|
||||
"Soir": null,
|
||||
},
|
||||
"Samedi": {
|
||||
"Midi": null,
|
||||
"Soir": null,
|
||||
},
|
||||
"Dimanche": {
|
||||
"Midi": null,
|
||||
"Soir": null,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
data() {
|
||||
var items = [];
|
||||
|
||||
for (var day in MockPlanning) {
|
||||
items.push({ divider: true })
|
||||
items.push({ header: day });
|
||||
for (var meal in MockPlanning[day]) {
|
||||
items.push({ id: 0, subtitle: meal, title: MockPlanning[day][meal] });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
toolActions: [
|
||||
{ icon: "find_replace", color: "success" },
|
||||
],
|
||||
planning: MockPlanning,
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
14
frontend/src/routes/Recipes.vue
Normal file
14
frontend/src/routes/Recipes.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
}),
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
13
frontend/src/views/Home.vue
Normal file
13
frontend/src/views/Home.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<HelloWorld />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HelloWorld from '../components/HelloWorld'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HelloWorld
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
const BundleTracker = require("webpack-bundle-tracker");
|
||||
|
||||
module.exports = {
|
||||
baseUrl: "http://127.0.0.1:8080/",
|
||||
publicPath: "http://localhost:8080/",
|
||||
outputDir: './dist/',
|
||||
|
||||
chainWebpack: config => {
|
||||
@@ -17,8 +17,8 @@ module.exports = {
|
||||
.set('__STATIC__', 'static')
|
||||
|
||||
config.devServer
|
||||
.public('http://0.0.0.0:8080')
|
||||
.host('0.0.0.0')
|
||||
.public('http://localhost:8080')
|
||||
.host('localhost')
|
||||
.port(8080)
|
||||
.hotOnly(true)
|
||||
.watchOptions({poll: 1000})
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"status":"done","publicPath":"http://127.0.0.1:8080/","chunks":{"app":[{"name":"app.js","publicPath":"http://127.0.0.1:8080/app.js","path":"C:\\Users\\lecor\\Documents\\Dev\\Python\\cookAssistant\\frontend\\dist\\app.js"},{"name":"app.a2bf8f46c2ce5a8648c5.hot-update.js","publicPath":"http://127.0.0.1:8080/app.a2bf8f46c2ce5a8648c5.hot-update.js","path":"C:\\Users\\lecor\\Documents\\Dev\\Python\\cookAssistant\\frontend\\dist\\app.a2bf8f46c2ce5a8648c5.hot-update.js"}]},"error":"ModuleError","message":"Module Error (from ./node_modules/vue-loader/lib/loaders/templateLoader.js):\n(Emitted value instead of an instance of Error) \n\n Errors compiling template:\n\n tag <v-toolbar-items> has no matching end tag.\n\n 7 | </v-toolbar-title>\n 8 | <v-spacer></v-spacer>\n 9 | <v-toolbar-items>\n | ^^^^^^^^^^^^^^^^^\n 10 | <v-btn\n 11 | flat\n"}
|
||||
{"status":"done","publicPath":"http://localhost:8080/","chunks":{"null":[{"name":"0.js","publicPath":"http://localhost:8080/0.js","path":"/home/artus/Documents/cookAssistant/frontend/dist/0.js"},{"name":"0.880af34e5f0e1c2d5443.hot-update.js","publicPath":"http://localhost:8080/0.880af34e5f0e1c2d5443.hot-update.js","path":"/home/artus/Documents/cookAssistant/frontend/dist/0.880af34e5f0e1c2d5443.hot-update.js"}],"app":[{"name":"app.js","publicPath":"http://localhost:8080/app.js","path":"/home/artus/Documents/cookAssistant/frontend/dist/app.js"}]},"error":"ModuleBuildError","message":"Module build failed (from ./node_modules/babel-loader/lib/index.js):\nSyntaxError: /home/artus/Documents/cookAssistant/frontend/src/components/recipes/IngredientList.vue: Unexpected token (85:70)\n\n 83 | )\n 84 | .then(data => {\n> 85 | var idx = this.edited_ingdts.indexOf(i => i.ingredient.id ==)\n | ^\n 86 | })\n 87 | .catch(err => comp.errors.push(JSON.stringify(err)))\n 88 | },\n at Object.raise (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:3851:17)\n at Object.unexpected (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:5167:16)\n at Object.parseExprAtom (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:6328:20)\n at Object.parseExprAtom (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:3570:20)\n at Object.parseExprSubscripts (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:5914:23)\n at Object.parseMaybeUnary (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:5894:21)\n at Object.parseExprOpBaseRightExpr (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:5854:34)\n at Object.parseExprOpRightExpr (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:5847:21)\n at Object.parseExprOp (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:5826:27)\n at Object.parseExprOps (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:5791:17)\n at Object.parseMaybeConditional (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:5754:23)\n at Object.parseMaybeAssign (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:5701:21)\n at Object.parseFunctionBody (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:6891:24)\n at Object.parseArrowExpression (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:6851:10)\n at Object.parseExprAtom (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:6213:18)\n at Object.parseExprAtom (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:3570:20)"}
|
||||
42
manage.py
42
manage.py
@@ -1,21 +1,21 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cookAssistant.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cookAssistant.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
0
planning/__init__.py
Normal file
0
planning/__init__.py
Normal file
3
planning/admin.py
Normal file
3
planning/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
5
planning/apps.py
Normal file
5
planning/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PlanningConfig(AppConfig):
|
||||
name = 'planning'
|
||||
0
planning/migrations/__init__.py
Normal file
0
planning/migrations/__init__.py
Normal file
12
planning/models.py
Normal file
12
planning/models.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django.db import models
|
||||
from recipe_book.models import Recipe
|
||||
# Create your models here.
|
||||
|
||||
class WeekPlanning(models.Model):
|
||||
pass
|
||||
|
||||
|
||||
class DayMeals(models.Model):
|
||||
midi = models.ForeignKey(
|
||||
Recipe,
|
||||
on_delete=models.CASCADE )
|
||||
3
planning/tests.py
Normal file
3
planning/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
planning/views.py
Normal file
3
planning/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
0
recipe_book/__init__.py
Normal file
0
recipe_book/__init__.py
Normal file
13
recipe_book/admin.py
Normal file
13
recipe_book/admin.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Recipe,IngredientWithAmount, Ingredient
|
||||
# Register your models here.
|
||||
|
||||
class IngredientWithAmountInline(admin.TabularInline):
|
||||
model=IngredientWithAmount
|
||||
|
||||
class RecipeAdmin(admin.ModelAdmin):
|
||||
inlines = [ IngredientWithAmountInline, ]
|
||||
admin.site.register(Recipe, RecipeAdmin)
|
||||
|
||||
admin.site.register(Ingredient)
|
||||
5
recipe_book/apps.py
Normal file
5
recipe_book/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class RecipeBookConfig(AppConfig):
|
||||
name = 'recipe_book'
|
||||
45
recipe_book/migrations/0001_initial.py
Normal file
45
recipe_book/migrations/0001_initial.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# Generated by Django 2.2 on 2019-04-11 13:20
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Ingredient',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=256)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IngredientWithAmount',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=6)),
|
||||
('unit', models.CharField(choices=[('gr', 'Grammes'), ('u', 'Units'), ('l', 'Litres')], max_length=2)),
|
||||
('ingredients', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='recipe_book.Ingredient')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Recipe',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=512)),
|
||||
('category', models.CharField(choices=[('0', 'Petit-déjeuner'), ('1', 'Entrée'), ('2', 'Plat'), ('3', 'Dessert')], max_length=2)),
|
||||
('ingredients', models.ManyToManyField(through='recipe_book.IngredientWithAmount', to='recipe_book.Ingredient')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ingredientwithamount',
|
||||
name='recipe',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='recipe_book.Recipe'),
|
||||
),
|
||||
]
|
||||
18
recipe_book/migrations/0002_auto_20190411_1556.py
Normal file
18
recipe_book/migrations/0002_auto_20190411_1556.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2 on 2019-04-11 13:56
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recipe_book', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='ingredientwithamount',
|
||||
old_name='ingredients',
|
||||
new_name='ingredient',
|
||||
),
|
||||
]
|
||||
0
recipe_book/migrations/__init__.py
Normal file
0
recipe_book/migrations/__init__.py
Normal file
103
recipe_book/models.py
Normal file
103
recipe_book/models.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
|
||||
"""
|
||||
Recipe's creation workflow :
|
||||
|
||||
1. Create recipe with required info, optional preparation
|
||||
2. Create recipe's list of ingredients
|
||||
a. Create new ingredients instances
|
||||
b. Create IngredientWithAmount instances
|
||||
3. Add preparation steps if necessary
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class Recipe(models.Model):
|
||||
name = models.CharField(max_length=512)
|
||||
category = models.CharField(
|
||||
max_length=2,
|
||||
choices=(
|
||||
('0', 'Petit-déjeuner'),
|
||||
('1', 'Entrée'),
|
||||
('2', 'Plat'),
|
||||
('3', 'Dessert'),
|
||||
))
|
||||
ingredients = models.ManyToManyField(
|
||||
'Ingredient',
|
||||
through='IngredientWithAmount'
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "{}".format(self.name)
|
||||
|
||||
|
||||
class Ingredient(models.Model):
|
||||
name = models.CharField(max_length=256)
|
||||
|
||||
def __str__(self):
|
||||
return "{}".format(self.name)
|
||||
|
||||
|
||||
class IngredientWithAmount(models.Model):
|
||||
""" A recipe-ingredient relation associated with amounts
|
||||
"""
|
||||
|
||||
recipe = models.ForeignKey(
|
||||
'Recipe',
|
||||
models.CASCADE)
|
||||
ingredient = models.ForeignKey(
|
||||
'Ingredient',
|
||||
models.CASCADE )
|
||||
amount = models.DecimalField(
|
||||
max_digits=6,
|
||||
decimal_places=2,
|
||||
)
|
||||
unit = models.CharField(
|
||||
max_length=2,
|
||||
choices=(
|
||||
('gr', 'Grammes'),
|
||||
('u', 'Units'),
|
||||
('l', 'Litres'),
|
||||
))
|
||||
|
||||
def __repr__(self):
|
||||
return "<{}>".format(self.display())
|
||||
|
||||
def display(self):
|
||||
""" Print info in a human friendly way.
|
||||
|
||||
* Correct grammar
|
||||
* Apropriate units according to amount
|
||||
"""
|
||||
# Note: amount has a .is_integer() method :)
|
||||
amount = self.amount
|
||||
name = self.ingredient.name.lower()
|
||||
particule = ""
|
||||
unit = ""
|
||||
if self.unit == 'u':
|
||||
integer = int(amount)
|
||||
fraction = amount - integer
|
||||
# Print subfractions of one as ratio
|
||||
if fraction: #TODO: if is not integer...
|
||||
fraction = "{}/{}".format(
|
||||
*amount.as_integer_ratio()
|
||||
)
|
||||
else:
|
||||
fraction = ""
|
||||
if integer:
|
||||
# Plural
|
||||
if integer >= 2:
|
||||
name += "s"
|
||||
integer = str(integer)
|
||||
else:
|
||||
integer = ""
|
||||
amount = "{}{}".format(integer, fraction)
|
||||
else:
|
||||
particule = "de "
|
||||
unit = self.unit
|
||||
|
||||
return "{}{} {}{}".format(
|
||||
amount, unit, particule, name
|
||||
)
|
||||
56
recipe_book/serializers.py
Normal file
56
recipe_book/serializers.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from rest_framework import serializers as ser
|
||||
from .models import Recipe, Ingredient, IngredientWithAmount
|
||||
|
||||
class RecipeSerializer(ser.ModelSerializer):
|
||||
class Meta:
|
||||
model = Recipe
|
||||
fields = ('id', 'name', 'category', 'ingredients')
|
||||
|
||||
|
||||
|
||||
class IngredientSerializer(ser.ModelSerializer):
|
||||
class Meta:
|
||||
model = Ingredient
|
||||
fields = ('id', 'name')
|
||||
|
||||
|
||||
class IngredientWithAmountSerializer(ser.ModelSerializer):
|
||||
ingredient = IngredientSerializer()
|
||||
|
||||
class Meta:
|
||||
model = IngredientWithAmount
|
||||
fields = ('recipe', 'ingredient', 'amount', 'unit', 'display')
|
||||
extra_kwargs = {
|
||||
'recipe': { 'write_only': True },
|
||||
}
|
||||
|
||||
def create(self, validated_data):
|
||||
# TODO: better management of ingredient finding
|
||||
ingredient, created = Ingredient.objects.get_or_create(
|
||||
**validated_data.pop('ingredient')
|
||||
)
|
||||
if created:
|
||||
ingredient.save()
|
||||
validated_data['ingredient'] = ingredient
|
||||
return IngredientWithAmount.objects.create(
|
||||
**validated_data
|
||||
)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
for attr in ('recipe', 'amount', 'unit'):
|
||||
setattr(
|
||||
instance,
|
||||
attr,
|
||||
validated_data.get(attr,
|
||||
getattr(instance, attr))
|
||||
)
|
||||
if 'ingredient' in validated_data:
|
||||
ingdt, created = Ingredient.objects.get_or_create(
|
||||
**validated_data['ingredient']
|
||||
)
|
||||
if created:
|
||||
ingdt.save()
|
||||
instance.ingredient = ingdt
|
||||
instance.save()
|
||||
|
||||
return instance
|
||||
3
recipe_book/tests.py
Normal file
3
recipe_book/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
33
recipe_book/views.py
Normal file
33
recipe_book/views.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from django.shortcuts import render
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from recipe_book.serializers import (
|
||||
RecipeSerializer,
|
||||
IngredientSerializer,
|
||||
IngredientWithAmountSerializer,
|
||||
)
|
||||
from recipe_book.models import (
|
||||
Recipe,
|
||||
Ingredient,
|
||||
IngredientWithAmount,
|
||||
)
|
||||
|
||||
class RecipesViewSet(viewsets.ModelViewSet):
|
||||
queryset = Recipe.objects.all()
|
||||
serializer_class = RecipeSerializer
|
||||
|
||||
|
||||
class IngredientViewSet(viewsets.ModelViewSet):
|
||||
queryset = Ingredient.objects.all()
|
||||
serializer_class = IngredientSerializer
|
||||
|
||||
|
||||
class IngredientWAmountViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = IngredientWithAmountSerializer
|
||||
lookup_field = 'ingredient'
|
||||
|
||||
def get_queryset(self):
|
||||
return IngredientWithAmount.objects.filter(
|
||||
recipe=self.kwargs['recipe_pk']
|
||||
)
|
||||
@@ -1,2 +1,4 @@
|
||||
django==2.2
|
||||
django-webpack-loader==0.6
|
||||
django==2.2
|
||||
django-webpack-loader==0.6
|
||||
djangorestframework
|
||||
drf-nested-routers
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
{% load render_bundle from webpack_loader %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<title>Django Vue Integration</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but frontend doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
|
||||
<div id="app">
|
||||
<app></app>
|
||||
</div>
|
||||
|
||||
{% render_bundle 'app' %}
|
||||
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
{% load render_bundle from webpack_loader %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<title>Django Vue Integration</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons">
|
||||
</head>
|
||||
<body class="overflow-hidden">
|
||||
<noscript>
|
||||
<strong>We're sorry but frontend doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
|
||||
<div id="app">
|
||||
<app></app>
|
||||
</div>
|
||||
|
||||
{% render_bundle 'app' %}
|
||||
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user