recipes as part of recipes/steps

This commit is contained in:
vabene1111
2021-07-13 16:47:39 +02:00
parent f8c1411e4d
commit 0c28e7e1b4
6 changed files with 148 additions and 11 deletions

View 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),
),
]

View File

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

View File

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

View File

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

View File

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

View File

@ -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'
}
/**