Merge branch 'release/r2022-02-13'
# Conflicts: # recipe/views.py
This commit is contained in:
commit
056e18d294
@ -33,3 +33,4 @@ zipp==3.5.0
|
|||||||
redis==4.1.0
|
redis==4.1.0
|
||||||
black
|
black
|
||||||
instagram_private_api
|
instagram_private_api
|
||||||
|
drf-nested-routers
|
||||||
|
|||||||
@ -27,7 +27,6 @@ DEBUG = True
|
|||||||
|
|
||||||
ALLOWED_HOSTS = []
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
|||||||
@ -18,8 +18,8 @@ Including another URLconf
|
|||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from rest_framework import routers
|
|
||||||
from rest_framework.authtoken import views
|
from rest_framework.authtoken import views
|
||||||
|
from rest_framework_nested import routers
|
||||||
|
|
||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
# Wire up our API using automatic URL routing.
|
# Wire up our API using automatic URL routing.
|
||||||
|
|||||||
@ -138,7 +138,7 @@ export default {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
axios
|
axios
|
||||||
.put(config.publicPath + '/recipe/recipe/' + recipe_id, data)
|
.put(config.publicPath + '/recipe/recipe/' + recipe_id + '/', data)
|
||||||
.then(function () {
|
.then(function () {
|
||||||
ElMessage({
|
ElMessage({
|
||||||
message: '修改成功.',
|
message: '修改成功.',
|
||||||
@ -152,7 +152,7 @@ export default {
|
|||||||
},
|
},
|
||||||
onSubmitDelete(recipe_id) {
|
onSubmitDelete(recipe_id) {
|
||||||
axios
|
axios
|
||||||
.delete(config.publicPath + '/recipe/recipe/' + recipe_id)
|
.delete(config.publicPath + '/recipe/recipe/' + recipe_id + '/')
|
||||||
.then(function () {
|
.then(function () {
|
||||||
ElMessage.error('删除成功.');
|
ElMessage.error('删除成功.');
|
||||||
location.reload();
|
location.reload();
|
||||||
|
|||||||
@ -42,6 +42,94 @@
|
|||||||
type="textarea"
|
type="textarea"
|
||||||
/>
|
/>
|
||||||
</van-cell-group>
|
</van-cell-group>
|
||||||
|
|
||||||
|
<van-cell-group
|
||||||
|
class="recipe-create"
|
||||||
|
inset
|
||||||
|
border
|
||||||
|
v-if="form.recipe_ingredients && form.recipe_ingredients.length > 0"
|
||||||
|
title="数量为 0 的食材将被删除"
|
||||||
|
>
|
||||||
|
<van-cell
|
||||||
|
v-bind:title="recipe_ingredient.ingredient.name"
|
||||||
|
v-for="recipe_ingredient in form.recipe_ingredients"
|
||||||
|
:key="recipe_ingredient.id"
|
||||||
|
>
|
||||||
|
<template #value>
|
||||||
|
<van-stepper
|
||||||
|
v-model.number="recipe_ingredient.quantity"
|
||||||
|
default-value="recipe_ingredient.quantity"
|
||||||
|
:decimal-length="1"
|
||||||
|
allow-empty="true"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
{{ recipe_ingredient.ingredient.unit }}
|
||||||
|
</template>
|
||||||
|
</van-cell>
|
||||||
|
</van-cell-group>
|
||||||
|
|
||||||
|
<div class="recipe-create">
|
||||||
|
<van-row gutter="20">
|
||||||
|
<van-col span="8">
|
||||||
|
<van-button
|
||||||
|
icon="plus"
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
round
|
||||||
|
hairline
|
||||||
|
class="submit-button"
|
||||||
|
@click="showIngredientDialog = true"
|
||||||
|
>创建食材</van-button
|
||||||
|
>
|
||||||
|
</van-col>
|
||||||
|
<van-col span="16">
|
||||||
|
<van-button
|
||||||
|
icon="plus"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
round
|
||||||
|
hairline
|
||||||
|
class="submit-button"
|
||||||
|
@click="showIngredientPicker = true"
|
||||||
|
>添加食材</van-button
|
||||||
|
>
|
||||||
|
</van-col>
|
||||||
|
</van-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<van-popup v-model:show="showIngredientPicker" round position="bottom">
|
||||||
|
<van-picker
|
||||||
|
:columns="ingredient_columns"
|
||||||
|
:columns-field-names="customFieldName"
|
||||||
|
@cancel="showIngredientPicker = false"
|
||||||
|
@confirm="onConfirmIngredient"
|
||||||
|
/>
|
||||||
|
</van-popup>
|
||||||
|
|
||||||
|
<van-dialog
|
||||||
|
v-model:show="showIngredientDialog"
|
||||||
|
title="创建食材"
|
||||||
|
show-cancel-button
|
||||||
|
confirm-button-text="创建"
|
||||||
|
@confirm="onCreateIngredient"
|
||||||
|
>
|
||||||
|
<van-cell-group inset border>
|
||||||
|
<van-field
|
||||||
|
v-model="ingredient_name"
|
||||||
|
label="名称"
|
||||||
|
name="ingredient_name"
|
||||||
|
placeholder="葱、蒜、姜..."
|
||||||
|
:rules="[{ required: true, message: '请填写食材名称' }]"
|
||||||
|
/>
|
||||||
|
<van-field
|
||||||
|
v-model="ingredient_unit"
|
||||||
|
label="单位"
|
||||||
|
name="ingredient_unit"
|
||||||
|
placeholder="个、斤、千克..."
|
||||||
|
:rules="[{ required: true, message: '请填写单位' }]"
|
||||||
|
/>
|
||||||
|
</van-cell-group>
|
||||||
|
</van-dialog>
|
||||||
<div class="recipe-create">
|
<div class="recipe-create">
|
||||||
<van-row gutter="20">
|
<van-row gutter="20">
|
||||||
<van-col span="8" v-if="recipe_id">
|
<van-col span="8" v-if="recipe_id">
|
||||||
@ -49,7 +137,6 @@
|
|||||||
class="submit-button"
|
class="submit-button"
|
||||||
round
|
round
|
||||||
type="danger"
|
type="danger"
|
||||||
plain
|
|
||||||
hairline
|
hairline
|
||||||
:disabled="disable_submit"
|
:disabled="disable_submit"
|
||||||
@click="onSubmitDelete(recipe_id)"
|
@click="onSubmitDelete(recipe_id)"
|
||||||
@ -62,7 +149,6 @@
|
|||||||
class="submit-button"
|
class="submit-button"
|
||||||
round
|
round
|
||||||
type="primary"
|
type="primary"
|
||||||
plain
|
|
||||||
hairline
|
hairline
|
||||||
:disabled="disable_submit"
|
:disabled="disable_submit"
|
||||||
@click="onSubmit(recipe_id)"
|
@click="onSubmit(recipe_id)"
|
||||||
@ -75,7 +161,6 @@
|
|||||||
class="submit-button"
|
class="submit-button"
|
||||||
round
|
round
|
||||||
type="primary"
|
type="primary"
|
||||||
plain
|
|
||||||
hairline
|
hairline
|
||||||
:disabled="disable_submit"
|
:disabled="disable_submit"
|
||||||
@click="onSubmit(recipe_id)"
|
@click="onSubmit(recipe_id)"
|
||||||
@ -90,21 +175,27 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
Form,
|
Button,
|
||||||
Field,
|
Cell,
|
||||||
CellGroup,
|
CellGroup,
|
||||||
|
Col,
|
||||||
|
Dialog,
|
||||||
|
Field,
|
||||||
|
Form,
|
||||||
|
Picker,
|
||||||
|
Popup,
|
||||||
Radio,
|
Radio,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
Rate,
|
Rate,
|
||||||
Button,
|
|
||||||
Toast,
|
|
||||||
Col,
|
|
||||||
Row,
|
Row,
|
||||||
|
Stepper,
|
||||||
|
Toast,
|
||||||
} from 'vant';
|
} from 'vant';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import config from '@/config/index';
|
import config from '@/config/index';
|
||||||
import router from '@/router/index';
|
|
||||||
import constants from '@/utils/constants.js';
|
import constants from '@/utils/constants.js';
|
||||||
|
import router from '@/router/index';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['recipe_'],
|
props: ['recipe_'],
|
||||||
@ -115,17 +206,29 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
[Form.name]: Form,
|
[Button.name]: Button,
|
||||||
[Field.name]: Field,
|
[Cell.name]: Cell,
|
||||||
[CellGroup.name]: CellGroup,
|
[CellGroup.name]: CellGroup,
|
||||||
|
[Col.name]: Col,
|
||||||
|
[Dialog.Component.name]: Dialog.Component,
|
||||||
|
[Field.name]: Field,
|
||||||
|
[Form.name]: Form,
|
||||||
|
[Picker.name]: Picker,
|
||||||
|
[Popup.name]: Popup,
|
||||||
[Radio.name]: Radio,
|
[Radio.name]: Radio,
|
||||||
[RadioGroup.name]: RadioGroup,
|
[RadioGroup.name]: RadioGroup,
|
||||||
[Rate.name]: Rate,
|
[Rate.name]: Rate,
|
||||||
[Button.name]: Button,
|
|
||||||
[Col.name]: Col,
|
|
||||||
[Row.name]: Row,
|
[Row.name]: Row,
|
||||||
|
[Stepper.name]: Stepper,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
const customFieldName = {
|
||||||
|
text: 'name',
|
||||||
|
};
|
||||||
|
const showIngredientPicker = ref(false);
|
||||||
|
const showIngredientDialog = ref(false);
|
||||||
|
const disable_submit = ref(false);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
form: {
|
form: {
|
||||||
name: null,
|
name: null,
|
||||||
@ -133,12 +236,22 @@ export default {
|
|||||||
rate: 0,
|
rate: 0,
|
||||||
difficulty: 0,
|
difficulty: 0,
|
||||||
note: null,
|
note: null,
|
||||||
|
recipe_ingredients: null,
|
||||||
},
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
|
disable_submit,
|
||||||
recipe_id: null,
|
recipe_id: null,
|
||||||
|
ingredient_columns: null,
|
||||||
|
customFieldName,
|
||||||
|
showIngredientPicker,
|
||||||
|
showIngredientDialog,
|
||||||
|
ingredient_name: null,
|
||||||
|
ingredient_unit: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {},
|
mounted() {
|
||||||
|
this.getIngredients();
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onSubmit(recipe_id) {
|
onSubmit(recipe_id) {
|
||||||
if (!this.form.name) {
|
if (!this.form.name) {
|
||||||
@ -153,6 +266,9 @@ export default {
|
|||||||
difficulty: this.form.difficulty,
|
difficulty: this.form.difficulty,
|
||||||
rate: this.form.rate,
|
rate: this.form.rate,
|
||||||
note: this.form.note ? this.form.note : null,
|
note: this.form.note ? this.form.note : null,
|
||||||
|
recipe_ingredients: this.form.recipe_ingredients
|
||||||
|
? this.form.recipe_ingredients
|
||||||
|
: [],
|
||||||
};
|
};
|
||||||
if (!recipe_id) {
|
if (!recipe_id) {
|
||||||
axios
|
axios
|
||||||
@ -161,14 +277,19 @@ export default {
|
|||||||
(response) => (response, router.push({ name: 'RecipeMobileHome' }))
|
(response) => (response, router.push({ name: 'RecipeMobileHome' }))
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
axios.put(config.publicPath + '/recipe/recipe/' + recipe_id, data).then(
|
axios
|
||||||
(Toast.success({
|
.put(config.publicPath + '/recipe/recipe/' + recipe_id + '/', data)
|
||||||
message: '修改成功',
|
.then(
|
||||||
forbidClick: true,
|
(response) => (
|
||||||
duration: 500,
|
response,
|
||||||
}),
|
Toast.success({
|
||||||
(this.loading = false))
|
message: '修改成功',
|
||||||
);
|
forbidClick: true,
|
||||||
|
duration: 500,
|
||||||
|
}),
|
||||||
|
(this.loading = false)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSubmitDelete(recipe_id) {
|
onSubmitDelete(recipe_id) {
|
||||||
@ -176,14 +297,99 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
axios.delete(config.publicPath + '/recipe/recipe/' + recipe_id).then(
|
axios
|
||||||
(Toast.success({
|
.delete(config.publicPath + '/recipe/recipe/' + recipe_id + '/')
|
||||||
message: '删除成功',
|
.then(
|
||||||
|
(Toast.success({
|
||||||
|
message: '删除成功',
|
||||||
|
forbidClick: true,
|
||||||
|
duration: 500,
|
||||||
|
}),
|
||||||
|
(this.loading = false))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getIngredients() {
|
||||||
|
axios.get(config.publicPath + '/recipe/ingredient/').then((response) => {
|
||||||
|
this.ingredient_columns = response.data['results'];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onConfirmIngredient(value) {
|
||||||
|
var exists = false;
|
||||||
|
this.showIngredientPicker = false;
|
||||||
|
if (this.form.recipe_ingredients === null) {
|
||||||
|
this.form.recipe_ingredients = [];
|
||||||
|
}
|
||||||
|
for (let i = 0; i < this.form.recipe_ingredients.length; i++) {
|
||||||
|
if (this.form.recipe_ingredients[i].ingredient.id == value.id) {
|
||||||
|
exists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (exists) {
|
||||||
|
Toast.fail({
|
||||||
|
message: '该食材已存在',
|
||||||
forbidClick: true,
|
forbidClick: true,
|
||||||
duration: 500,
|
duration: 800,
|
||||||
}),
|
});
|
||||||
(this.loading = false))
|
} else {
|
||||||
|
this.form.recipe_ingredients.push({
|
||||||
|
id: 0,
|
||||||
|
ingredient: {
|
||||||
|
id: value.id,
|
||||||
|
name: value.name,
|
||||||
|
unit: value.unit,
|
||||||
|
}, // 添加的食材
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCreateIngredient() {
|
||||||
|
console.log(
|
||||||
|
'onCreateIngredient',
|
||||||
|
this.ingredient_name,
|
||||||
|
this.ingredient_unit
|
||||||
);
|
);
|
||||||
|
if (!this.ingredient_name || !this.ingredient_unit) {
|
||||||
|
Toast.fail({
|
||||||
|
message: '请输入食材名称和单位',
|
||||||
|
forbidClick: true,
|
||||||
|
duration: 800,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.showIngredientDialog = false;
|
||||||
|
for (let i = 0; i < this.ingredient_columns.length; i++) {
|
||||||
|
if (this.ingredient_columns[i].name == this.ingredient_name) {
|
||||||
|
Toast.fail({
|
||||||
|
message: '该食材已存在',
|
||||||
|
forbidClick: true,
|
||||||
|
duration: 800,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
axios
|
||||||
|
.post(config.publicPath + '/recipe/ingredient/', {
|
||||||
|
name: this.ingredient_name,
|
||||||
|
unit: this.ingredient_unit,
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
(response) => (
|
||||||
|
response,
|
||||||
|
this.ingredient_columns.unshift({
|
||||||
|
id: response.data.id,
|
||||||
|
name: this.ingredient_name,
|
||||||
|
unit: this.ingredient_unit,
|
||||||
|
}),
|
||||||
|
Toast.success({
|
||||||
|
message: '创建成功',
|
||||||
|
forbidClick: true,
|
||||||
|
duration: 800,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// this.ingredient_name = value.name;
|
||||||
|
// this.ingredient_unit = value.unit;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
axios
|
axios
|
||||||
.get(config.publicPath + '/recipe/recipe/' + this.$route.params.id)
|
.get(config.publicPath + '/recipe/recipe/' + this.$route.params.id + '/')
|
||||||
.then((response) => (this.recipe = response.data));
|
.then((response) => (this.recipe = response.data));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,12 +2,8 @@
|
|||||||
<el-container>
|
<el-container>
|
||||||
<el-header>
|
<el-header>
|
||||||
<el-menu mode="horizontal" default-active="#" router>
|
<el-menu mode="horizontal" default-active="#" router>
|
||||||
<el-menu-item index="/">
|
<el-menu-item index="/"> 首页 </el-menu-item>
|
||||||
首页
|
<el-menu-item index="/week-recipe/"> 每周菜谱 </el-menu-item>
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/week-recipe/">
|
|
||||||
每周菜谱
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="#">
|
<el-menu-item index="#">
|
||||||
{{ recipe.name }}
|
{{ recipe.name }}
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
@ -33,7 +29,7 @@ import config from '@/config/index';
|
|||||||
export default {
|
export default {
|
||||||
name: 'RecipeDetail',
|
name: 'RecipeDetail',
|
||||||
components: { input_recipe },
|
components: { input_recipe },
|
||||||
data: function() {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
recipe: {},
|
recipe: {},
|
||||||
constants: constants,
|
constants: constants,
|
||||||
@ -41,7 +37,7 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
axios
|
axios
|
||||||
.get(config.publicPath + '/recipe/recipe/' + this.$route.params.id)
|
.get(config.publicPath + '/recipe/recipe/' + this.$route.params.id + '/')
|
||||||
.then((response) => (this.recipe = response.data));
|
.then((response) => (this.recipe = response.data));
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
45
recipe/migrations/0004_ingredient_recipeingredient.py
Normal file
45
recipe/migrations/0004_ingredient_recipeingredient.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Generated by Django 3.2.6 on 2022-02-12 13:43
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [('recipe', '0003_recipe_status')]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Ingredient',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=128)),
|
||||||
|
('unit', models.CharField(max_length=32)),
|
||||||
|
('status', models.CharField(default='active', max_length=32)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RecipeIngredient',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('quantity', models.FloatField()),
|
||||||
|
(
|
||||||
|
'ingredient',
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='recipe_ingredients',
|
||||||
|
to='recipe.ingredient',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'recipe',
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='recipe_ingredients',
|
||||||
|
to='recipe.recipe',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
('status', models.CharField(default='active', max_length=32)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -6,6 +6,27 @@ from utils import const
|
|||||||
import utils
|
import utils
|
||||||
|
|
||||||
|
|
||||||
|
class Ingredient(models.Model):
|
||||||
|
name = models.CharField(max_length=128)
|
||||||
|
unit = models.CharField(max_length=32)
|
||||||
|
status = models.CharField(max_length=32, default=const.INGREDIENT_STATUS_ACTIVE)
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeIngredient(models.Model):
|
||||||
|
recipe = models.ForeignKey('Recipe', on_delete=models.CASCADE, related_name='recipe_ingredients')
|
||||||
|
ingredient = models.ForeignKey(Ingredient, on_delete=models.CASCADE, related_name='recipe_ingredients')
|
||||||
|
status = models.CharField(max_length=32, default=const.INGREDIENT_STATUS_ACTIVE)
|
||||||
|
quantity = models.FloatField()
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'recipe': {'id': self.recipe.id, 'name': self.recipe.name},
|
||||||
|
'ingredient': {'id': self.ingredient.id, 'name': self.ingredient.name, 'unit': self.ingredient.unit},
|
||||||
|
'quantity': self.quantity,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Recipe(models.Model):
|
class Recipe(models.Model):
|
||||||
name = models.CharField(max_length=128)
|
name = models.CharField(max_length=128)
|
||||||
recipe_type = models.CharField(max_length=32, default=const.RECIPE_TYPE_MEAT)
|
recipe_type = models.CharField(max_length=32, default=const.RECIPE_TYPE_MEAT)
|
||||||
@ -17,10 +38,20 @@ class Recipe(models.Model):
|
|||||||
def serialize(self, verbose=False):
|
def serialize(self, verbose=False):
|
||||||
data = {'id': self.id, 'name': self.name, 'recipe_type': self.recipe_type}
|
data = {'id': self.id, 'name': self.name, 'recipe_type': self.recipe_type}
|
||||||
if verbose:
|
if verbose:
|
||||||
data.update({'difficulty': self.difficulty, 'rate': self.rate, 'note': self.note})
|
data.update(
|
||||||
|
{
|
||||||
|
'difficulty': self.difficulty,
|
||||||
|
'rate': self.rate,
|
||||||
|
'note': self.note,
|
||||||
|
'recipe_ingredients': self.get_ingredients(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def get_ingredients(self):
|
||||||
|
return [i.serialize() for i in self.recipeingredient_set.order_by('id')]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_from_str(cls, content):
|
def create_from_str(cls, content):
|
||||||
content = content.strip()
|
content = content.strip()
|
||||||
|
|||||||
@ -1,16 +1,61 @@
|
|||||||
from os import read
|
|
||||||
from django.contrib.auth.models import User, Group
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
import recipe.models
|
import recipe.models
|
||||||
|
from utils import const
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientSerializer(serializers.ModelSerializer):
|
||||||
|
id = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = recipe.models.Ingredient
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeIngredientSerializer(serializers.ModelSerializer):
|
||||||
|
id = serializers.IntegerField(read_only=True)
|
||||||
|
ingredient = IngredientSerializer()
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
if 'ingredient' in validated_data:
|
||||||
|
validated_data.pop('ingredient')
|
||||||
|
if 'recipe' in validated_data:
|
||||||
|
validated_data.pop('recipe')
|
||||||
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = recipe.models.RecipeIngredient
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
class RecipeSerializer(serializers.ModelSerializer):
|
class RecipeSerializer(serializers.ModelSerializer):
|
||||||
id = serializers.IntegerField(read_only=True)
|
id = serializers.IntegerField(read_only=True)
|
||||||
|
recipe_ingredients = RecipeIngredientSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
if 'recipe_ingredients' in validated_data:
|
||||||
|
recipe_ingredients = validated_data.pop('recipe_ingredients')
|
||||||
|
instance = super().create(validated_data)
|
||||||
|
if recipe_ingredients:
|
||||||
|
for recipe_ingredient in recipe_ingredients:
|
||||||
|
recipe_ingredient['recipe'] = instance
|
||||||
|
RecipeIngredientSerializer.create(recipe_ingredient)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self):
|
||||||
|
# exclude deleted recipe_ingredients
|
||||||
|
data_ = super().data
|
||||||
|
data_['recipe_ingredients'] = [
|
||||||
|
ingredient
|
||||||
|
for ingredient in data_['recipe_ingredients']
|
||||||
|
if ingredient['status'] != const.INGREDIENT_STATUS_DELETED
|
||||||
|
]
|
||||||
|
return data_
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = recipe.models.Recipe
|
model = recipe.models.Recipe
|
||||||
fields = '__all__'
|
fields = ('recipe_ingredients', 'id', 'name', 'recipe_type', 'status', 'note', 'rate', 'difficulty')
|
||||||
|
|
||||||
|
|
||||||
class WeekRecipeSerializer(serializers.ModelSerializer):
|
class WeekRecipeSerializer(serializers.ModelSerializer):
|
||||||
|
|||||||
@ -1,16 +1,22 @@
|
|||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
|
|
||||||
# from django.core.urlresolvers import reverse
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from rest_framework import routers
|
|
||||||
from recipe import views
|
from recipe import views
|
||||||
|
|
||||||
|
|
||||||
# Wire up our API using automatic URL routing.
|
# Wire up our API using automatic URL routing.
|
||||||
# Additionally, we include login URLs for the browsable API.
|
# Additionally, we include login URLs for the browsable API.
|
||||||
|
from rest_framework_nested import routers
|
||||||
|
|
||||||
|
router = routers.DefaultRouter()
|
||||||
|
router.register(r'recipe', views.RecipeAPI)
|
||||||
|
router.register(r'ingredient', views.IngredientAPI)
|
||||||
|
recipe_nested_router = routers.NestedSimpleRouter(router, r'recipe', lookup='recipe')
|
||||||
|
recipe_nested_router.register(r'recipe-ingredient', views.RecipeIngredientAPI)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^recipe/(?P<pk>\d+)$', views.RecipeAPI.as_view(), name='recipe-detail'),
|
|
||||||
url(r'^recipe/$', views.RecipeListAPI.as_view()),
|
|
||||||
url(r'^week-recipe/$', views.WeekRecipeListAPI.as_view()),
|
url(r'^week-recipe/$', views.WeekRecipeListAPI.as_view()),
|
||||||
url(r'^daily-recipe/(?P<pk>\d+)$', views.DailyRecipeAPI.as_view(), name='dailyrecipe-detail'),
|
url(r'^daily-recipe/(?P<pk>\d+)$', views.DailyRecipeAPI.as_view(), name='dailyrecipe-detail'),
|
||||||
|
path(r'', include(router.urls)),
|
||||||
|
path(r'', include(recipe_nested_router.urls)),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import recipe.serializers
|
|||||||
from utils import const
|
from utils import const
|
||||||
|
|
||||||
|
|
||||||
class RecipeAPI(rest_framework.generics.RetrieveUpdateDestroyAPIView):
|
class RecipeAPI(viewsets.ModelViewSet):
|
||||||
|
|
||||||
# authentication_classes = (authentication.TokenAuthentication,
|
# authentication_classes = (authentication.TokenAuthentication,
|
||||||
# authentication.SessionAuthentication,
|
# authentication.SessionAuthentication,
|
||||||
@ -25,15 +25,38 @@ class RecipeAPI(rest_framework.generics.RetrieveUpdateDestroyAPIView):
|
|||||||
instance.status = const.RECIPE_STATUS_DELETED
|
instance.status = const.RECIPE_STATUS_DELETED
|
||||||
instance.save(update_fields=['status'])
|
instance.save(update_fields=['status'])
|
||||||
|
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
instance = self.get_object()
|
||||||
|
if 'recipe_ingredients' in self.request.data:
|
||||||
|
recipe_ingredients = self.request.data.pop('recipe_ingredients')
|
||||||
|
if recipe_ingredients:
|
||||||
|
for recipe_ingredient in recipe_ingredients:
|
||||||
|
# 没有 id 则新增食材
|
||||||
|
if not recipe_ingredient['id'] and recipe_ingredient['quantity']:
|
||||||
|
ingredient = recipe.models.Ingredient.objects.get(id=recipe_ingredient['ingredient']['id'])
|
||||||
|
del recipe_ingredient['ingredient']
|
||||||
|
del recipe_ingredient['id']
|
||||||
|
recipe.models.RecipeIngredient.objects.create(
|
||||||
|
recipe=instance, ingredient=ingredient, **recipe_ingredient
|
||||||
|
)
|
||||||
|
# 有 id 没数量则删除食材
|
||||||
|
elif recipe_ingredient['id'] and not recipe_ingredient['quantity']:
|
||||||
|
recipe.models.RecipeIngredient.objects.filter(id=recipe_ingredient['id']).update(
|
||||||
|
status=const.INGREDIENT_STATUS_DELETED
|
||||||
|
)
|
||||||
|
# 有 id 有数量则更新食材
|
||||||
|
elif recipe_ingredient['id'] and recipe_ingredient['quantity']:
|
||||||
|
del recipe_ingredient['ingredient']
|
||||||
|
recipe.models.RecipeIngredient.objects.filter(id=recipe_ingredient['id']).update(
|
||||||
|
**recipe_ingredient
|
||||||
|
)
|
||||||
|
|
||||||
class RecipeListAPI(rest_framework.generics.ListAPIView, rest_framework.generics.CreateAPIView):
|
return super().perform_update(serializer)
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
self.queryset = recipe.models.Recipe.objects.exclude(status=const.RECIPE_STATUS_DELETED)
|
||||||
|
return super().list(request, *args, **kwargs)
|
||||||
|
|
||||||
# authentication_classes = (authentication.TokenAuthentication,
|
|
||||||
# authentication.SessionAuthentication,
|
|
||||||
# authentication.BasicAuthentication)
|
|
||||||
# permission_classes = (permissions.IsAuthenticated,)
|
|
||||||
queryset = recipe.models.Recipe.objects.exclude(status=const.RECIPE_STATUS_DELETED).order_by('-id')
|
|
||||||
serializer_class = recipe.serializers.RecipeSerializer
|
|
||||||
filterset_fields = {
|
filterset_fields = {
|
||||||
'recipe_type': const.FILTER_EXACT,
|
'recipe_type': const.FILTER_EXACT,
|
||||||
'difficulty': const.FILTER_GTE_LTE,
|
'difficulty': const.FILTER_GTE_LTE,
|
||||||
@ -41,7 +64,7 @@ class RecipeListAPI(rest_framework.generics.ListAPIView, rest_framework.generics
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class WeekRecipeListAPI(rest_framework.generics.ListAPIView, rest_framework.generics.CreateAPIView):
|
class WeekRecipeListAPI(rest_framework.generics.ListCreateAPIView):
|
||||||
|
|
||||||
queryset = recipe.models.DailyRecipe.objects.all()
|
queryset = recipe.models.DailyRecipe.objects.all()
|
||||||
serializer_class = recipe.serializers.WeekRecipeSerializer
|
serializer_class = recipe.serializers.WeekRecipeSerializer
|
||||||
@ -78,3 +101,44 @@ class DailyRecipeAPI(rest_framework.generics.RetrieveUpdateAPIView):
|
|||||||
recipes.extend(request.data.get('soup', []))
|
recipes.extend(request.data.get('soup', []))
|
||||||
daily_recipe.recipes.set(recipe.models.Recipe.objects.filter(id__in=recipes))
|
daily_recipe.recipes.set(recipe.models.Recipe.objects.filter(id__in=recipes))
|
||||||
return Response(daily_recipe.serialize(), status=status.HTTP_201_CREATED, headers={})
|
return Response(daily_recipe.serialize(), status=status.HTTP_201_CREATED, headers={})
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientAPI(viewsets.ModelViewSet):
|
||||||
|
|
||||||
|
# authentication_classes = (authentication.TokenAuthentication,
|
||||||
|
# authentication.SessionAuthentication,
|
||||||
|
# authentication.BasicAuthentication)
|
||||||
|
# permission_classes = (permissions.IsAuthenticated,)
|
||||||
|
queryset = recipe.models.Ingredient.objects.all()
|
||||||
|
serializer_class = recipe.serializers.IngredientSerializer
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
self.queryset = recipe.models.Ingredient.objects.exclude(status=const.INGREDIENT_STATUS_DELETED).order_by('-id')
|
||||||
|
return super().list(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
instance.status = const.INGREDIENT_STATUS_DELETED
|
||||||
|
instance.save(update_fields=['status'])
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeIngredientAPI(viewsets.ModelViewSet):
|
||||||
|
|
||||||
|
# authentication_classes = (authentication.TokenAuthentication,
|
||||||
|
# authentication.SessionAuthentication,
|
||||||
|
# authentication.BasicAuthentication)
|
||||||
|
# permission_classes = (permissions.IsAuthenticated,)
|
||||||
|
queryset = recipe.models.RecipeIngredient.objects.exclude(status=const.INGREDIENT_STATUS_DELETED)
|
||||||
|
serializer_class = recipe.serializers.RecipeIngredientSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.queryset.filter(recipe=self.kwargs['recipe_pk'])
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
instance.status = const.INGREDIENT_STATUS_DELETED
|
||||||
|
instance.save(update_fields=['status'])
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
recipe_ingredient = recipe.models.RecipeIngredient.objects.create(
|
||||||
|
recipe_id=kwargs['recipe_pk'], ingredient_id=request.data['ingredient'], quantity=request.data['quantity']
|
||||||
|
)
|
||||||
|
return Response(recipe_ingredient.serialize(), status=status.HTTP_201_CREATED, headers={})
|
||||||
|
|||||||
@ -14,6 +14,9 @@ RECIPE_TYPE_SOUP = 'soup'
|
|||||||
RECIPE_STATUS_ACTIVE = 'active'
|
RECIPE_STATUS_ACTIVE = 'active'
|
||||||
RECIPE_STATUS_DELETED = 'deleted'
|
RECIPE_STATUS_DELETED = 'deleted'
|
||||||
|
|
||||||
|
INGREDIENT_STATUS_ACTIVE = 'active'
|
||||||
|
INGREDIENT_STATUS_DELETED = 'deleted'
|
||||||
|
|
||||||
RECIPE_TYPE_CHOICE = [RECIPE_TYPE_MEAT, RECIPE_TYPE_VEGETABLE, RECIPE_TYPE_SOUP]
|
RECIPE_TYPE_CHOICE = [RECIPE_TYPE_MEAT, RECIPE_TYPE_VEGETABLE, RECIPE_TYPE_SOUP]
|
||||||
|
|
||||||
FILTER_EXACT = ['exact']
|
FILTER_EXACT = ['exact']
|
||||||
|
|||||||
@ -10,7 +10,6 @@ reload(sys)
|
|||||||
|
|
||||||
import utils.lark
|
import utils.lark
|
||||||
import git
|
import git
|
||||||
import ipdb
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# def notify():
|
# def notify():
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user