recipes as part of recipes/steps
This commit is contained in:
24
cookbook/migrations/0141_auto_20210713_1042.py
Normal file
24
cookbook/migrations/0141_auto_20210713_1042.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.2.5 on 2021-07-13 08:42
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0140_userpreference_created_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='step',
|
||||
name='step_recipe',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='cookbook.recipe'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='step',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('TEXT', 'Text'), ('TIME', 'Time'), ('FILE', 'File'), ('RECIPE', 'Recipe')], default='TEXT', max_length=16),
|
||||
),
|
||||
]
|
@ -331,10 +331,11 @@ class Step(ExportModelOperationsMixin('step'), models.Model, PermissionModelMixi
|
||||
TEXT = 'TEXT'
|
||||
TIME = 'TIME'
|
||||
FILE = 'FILE'
|
||||
RECIPE = 'RECIPE'
|
||||
|
||||
name = models.CharField(max_length=128, default='', blank=True)
|
||||
type = models.CharField(
|
||||
choices=((TEXT, _('Text')), (TIME, _('Time')), (FILE, _('File')),),
|
||||
choices=((TEXT, _('Text')), (TIME, _('Time')), (FILE, _('File')), (RECIPE, _('Recipe')),),
|
||||
default=TEXT,
|
||||
max_length=16
|
||||
)
|
||||
@ -344,6 +345,7 @@ class Step(ExportModelOperationsMixin('step'), models.Model, PermissionModelMixi
|
||||
order = models.IntegerField(default=0)
|
||||
file = models.ForeignKey('UserFile', on_delete=models.PROTECT, null=True, blank=True)
|
||||
show_as_header = models.BooleanField(default=True)
|
||||
step_recipe = models.ForeignKey('Recipe', default=None, blank=True, null=True, on_delete=models.PROTECT)
|
||||
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
objects = ScopedManager(space='space')
|
||||
|
@ -300,6 +300,7 @@ class StepSerializer(WritableNestedModelSerializer):
|
||||
ingredients_markdown = serializers.SerializerMethodField('get_ingredients_markdown')
|
||||
ingredients_vue = serializers.SerializerMethodField('get_ingredients_vue')
|
||||
file = UserFileViewSerializer(allow_null=True, required=False)
|
||||
step_recipe_data = serializers.SerializerMethodField('get_step_recipe_data')
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['space'] = self.context['request'].space
|
||||
@ -311,11 +312,27 @@ class StepSerializer(WritableNestedModelSerializer):
|
||||
def get_ingredients_markdown(self, obj):
|
||||
return obj.get_instruction_render()
|
||||
|
||||
def get_step_recipe_data(self, obj):
|
||||
# check if root type is recipe to prevent infinite recursion
|
||||
# can be improved later to allow multi level embedding
|
||||
if obj.step_recipe and type(self.parent.root) == RecipeSerializer:
|
||||
return StepRecipeSerializer(obj.step_recipe).data
|
||||
|
||||
class Meta:
|
||||
model = Step
|
||||
fields = (
|
||||
'id', 'name', 'type', 'instruction', 'ingredients', 'ingredients_markdown',
|
||||
'ingredients_vue', 'time', 'order', 'show_as_header', 'file',
|
||||
'ingredients_vue', 'time', 'order', 'show_as_header', 'file', 'step_recipe', 'step_recipe_data'
|
||||
)
|
||||
|
||||
|
||||
class StepRecipeSerializer(WritableNestedModelSerializer):
|
||||
steps = StepSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = Recipe
|
||||
fields = (
|
||||
'id', 'name', 'steps',
|
||||
)
|
||||
|
||||
|
||||
|
@ -202,6 +202,7 @@
|
||||
<option value="TEXT">{% trans 'Text' %}</option>
|
||||
<option value="TIME">{% trans 'Time' %}</option>
|
||||
<option value="FILE">{% trans 'File' %}</option>
|
||||
<option value="RECIPE">{% trans 'Recipe' %}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@ -214,7 +215,7 @@
|
||||
:id="'id_step_' + step.id + '_time'">
|
||||
</div>
|
||||
|
||||
<div class="col-md-9">
|
||||
<div class="col-md-9" v-if="step.type === 'FILE'">
|
||||
<label :for="'id_step_' + step.id + '_file'">{% trans 'File' %}</label>
|
||||
<multiselect
|
||||
v-tabindex
|
||||
@ -235,6 +236,28 @@
|
||||
@search-change="searchFiles">
|
||||
</multiselect>
|
||||
</div>
|
||||
|
||||
<div class="col-md-9" v-if="step.type === 'RECIPE'">
|
||||
<label :for="'id_step_' + step.id + '_recipe'">{% trans 'Recipe' %}</label>
|
||||
<multiselect
|
||||
v-tabindex
|
||||
ref="step_recipe"
|
||||
v-model="step.step_recipe"
|
||||
:options="recipes.map(recipe => recipe.id)"
|
||||
:close-on-select="true"
|
||||
:clear-on-select="true"
|
||||
:allow-empty="true"
|
||||
:preserve-search="true"
|
||||
placeholder="{% trans 'Select Recipe' %}"
|
||||
select-label="{% trans 'Select' %}"
|
||||
:id="'id_step_' + step.id + '_recipe'"
|
||||
:custom-label="opt => recipes.find(x => x.id == opt).name"
|
||||
|
||||
:multiple="false"
|
||||
:loading="recipes_loading"
|
||||
@search-change="searchRecipes">
|
||||
</multiselect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="step.type == 'TEXT'">
|
||||
@ -523,6 +546,8 @@
|
||||
units_loading: false,
|
||||
files: [],
|
||||
files_loading: false,
|
||||
recipes: [],
|
||||
recipes_loading: false,
|
||||
message: '',
|
||||
},
|
||||
directives: {
|
||||
@ -549,6 +574,7 @@
|
||||
this.searchFoods('')
|
||||
this.searchKeywords('')
|
||||
this.searchFiles('')
|
||||
this.searchRecipes('')
|
||||
|
||||
this._keyListener = function (e) {
|
||||
if (e.code === "Space" && e.ctrlKey) {
|
||||
@ -720,6 +746,16 @@
|
||||
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
||||
})
|
||||
},
|
||||
searchRecipes: function (query) {
|
||||
this.recipes_loading = true
|
||||
this.$http.get("{% url 'api:recipe-list' %}" + '?query=' + query + '&limit=10').then((response) => {
|
||||
this.recipes = response.data.results
|
||||
this.recipes_loading = false
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
||||
})
|
||||
},
|
||||
searchUnits: function (query) {
|
||||
this.units_loading = true
|
||||
this.$http.get("{% url 'api:unit-list' %}" + '?query=' + query + '&limit=10').then((response) => {
|
||||
|
@ -3,7 +3,7 @@
|
||||
<div>
|
||||
<hr />
|
||||
|
||||
<template v-if="step.type === 'TEXT'">
|
||||
<template v-if="step.type === 'TEXT' || step.type === 'RECIPE'">
|
||||
<div class="row" v-if="recipe.steps.length > 1">
|
||||
<div class="col col-md-8">
|
||||
<h5 class="text-primary">
|
||||
@ -22,15 +22,21 @@
|
||||
</div>
|
||||
<div class="col col-md-4" style="text-align: right">
|
||||
<b-button @click="details_visible = !details_visible" style="border: none; background: none"
|
||||
class="shadow-none d-print-none" :class="{ 'text-primary': details_visible, 'text-success': !details_visible}">
|
||||
class="shadow-none d-print-none"
|
||||
:class="{ 'text-primary': details_visible, 'text-success': !details_visible}">
|
||||
<i class="far fa-check-circle"></i>
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<template v-if="step.type === 'TEXT'">
|
||||
|
||||
<b-collapse id="collapse-1" v-model="details_visible">
|
||||
<div class="row">
|
||||
<div class="col col-md-4" v-if="step.ingredients.length > 0 && recipe.steps.length > 1">
|
||||
<div class="col col-md-4"
|
||||
v-if="step.ingredients.length > 0 && (recipe.steps.length > 1 || force_ingredients)">
|
||||
<table class="table table-sm">
|
||||
<!-- eslint-disable vue/no-v-for-template-key-on-child -->
|
||||
<template v-for="i in step.ingredients">
|
||||
@ -65,7 +71,8 @@
|
||||
|
||||
<div class="col-md-2" style="text-align: right">
|
||||
<b-button @click="details_visible = !details_visible" style="border: none; background: none"
|
||||
class="shadow-none d-print-none" :class="{ 'text-primary': details_visible, 'text-success': !details_visible}">
|
||||
class="shadow-none d-print-none"
|
||||
:class="{ 'text-primary': details_visible, 'text-success': !details_visible}">
|
||||
<i class="far fa-check-circle"></i>
|
||||
</b-button>
|
||||
</div>
|
||||
@ -89,13 +96,31 @@
|
||||
<img :src="step.file.file" style="max-width: 50vw; max-height: 50vh">
|
||||
</div>
|
||||
<div v-else>
|
||||
<a :href="step.file.file" target="_blank" rel="noreferrer nofollow">{{ $t('Download') }} {{ $t('File') }}</a>
|
||||
<a :href="step.file.file" target="_blank" rel="noreferrer nofollow">{{ $t('Download') }} {{
|
||||
$t('File')
|
||||
}}</a>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card" v-if="step.type === 'RECIPE' && step.step_recipe_data !== null">
|
||||
<b-collapse id="collapse-1" v-model="details_visible">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">
|
||||
<a :href="resolveDjangoUrl('view_recipe',step.step_recipe_data.id)">{{ step.step_recipe_data.name }}</a>
|
||||
</h2>
|
||||
<div v-for="(sub_step, index) in step.step_recipe_data.steps" v-bind:key="`substep_${sub_step.id}`">
|
||||
<Step :recipe="step.step_recipe_data" :step="sub_step" :ingredient_factor="ingredient_factor" :index="index"
|
||||
:start_time="start_time" :force_ingredients="true"></Step>
|
||||
</div>
|
||||
</div>
|
||||
</b-collapse>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="start_time !== ''">
|
||||
<b-popover
|
||||
:target="`id_reactive_popover_${step.id}`"
|
||||
@ -139,6 +164,8 @@ import {GettextMixin} from "@/utils/utils";
|
||||
import CompileComponent from "@/components/CompileComponent";
|
||||
import Vue from "vue";
|
||||
import moment from "moment";
|
||||
import Keywords from "@/components/Keywords";
|
||||
import {ResolveUrlMixin} from "@/utils/utils";
|
||||
|
||||
Vue.prototype.moment = moment
|
||||
|
||||
@ -146,6 +173,7 @@ export default {
|
||||
name: 'Step',
|
||||
mixins: [
|
||||
GettextMixin,
|
||||
ResolveUrlMixin,
|
||||
],
|
||||
components: {
|
||||
Ingredient,
|
||||
@ -157,6 +185,10 @@ export default {
|
||||
index: Number,
|
||||
recipe: Object,
|
||||
start_time: String,
|
||||
force_ingredients: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -1085,6 +1085,18 @@ export interface RecipeSteps {
|
||||
* @memberof RecipeSteps
|
||||
*/
|
||||
file?: StepFile | null;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof RecipeSteps
|
||||
*/
|
||||
step_recipe?: number | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof RecipeSteps
|
||||
*/
|
||||
step_recipe_data?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1094,7 +1106,8 @@ export interface RecipeSteps {
|
||||
export enum RecipeStepsTypeEnum {
|
||||
Text = 'TEXT',
|
||||
Time = 'TIME',
|
||||
File = 'FILE'
|
||||
File = 'FILE',
|
||||
Recipe = 'RECIPE'
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1490,6 +1503,18 @@ export interface Step {
|
||||
* @memberof Step
|
||||
*/
|
||||
file?: StepFile | null;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof Step
|
||||
*/
|
||||
step_recipe?: number | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Step
|
||||
*/
|
||||
step_recipe_data?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1499,7 +1524,8 @@ export interface Step {
|
||||
export enum StepTypeEnum {
|
||||
Text = 'TEXT',
|
||||
Time = 'TIME',
|
||||
File = 'FILE'
|
||||
File = 'FILE',
|
||||
Recipe = 'RECIPE'
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user