606 lines
30 KiB
HTML
606 lines
30 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
{% load crispy_forms_tags %}
|
|
{% load i18n %}
|
|
{% load l10n %}
|
|
{% load custom_tags %}
|
|
|
|
{% block title %}{{ recipe.name }}{% endblock %}
|
|
|
|
{% block extra_head %}
|
|
|
|
{% include 'include/vue_base.html' %}
|
|
<script src="{% static 'js/moment-with-locales.min.js' %}"></script>
|
|
|
|
<script src="{% static 'js/frac.js' %}"></script>
|
|
|
|
<link rel="stylesheet" href="{% static 'css/pretty-checkbox.min.css' %}">
|
|
<link rel="stylesheet" href="{% static 'custom/css/markdown_blockquote.css' %}">
|
|
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row">
|
|
<div class="col col-md-8">
|
|
{% recipe_rating recipe request.user as rating %}
|
|
<h3>{{ recipe.name }} {{ rating|safe }}</h3>
|
|
</div>
|
|
{% if request.user.is_authenticated %}
|
|
<div class="col col-md-4 d-print-none" style="text-align: right">
|
|
<div class="dropdown">
|
|
<a class="btn shadow-none" href="#" role="button" id="dropdownMenuLink"
|
|
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
|
<i class="fas fa-ellipsis-v"></i>
|
|
</a>
|
|
|
|
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuLink">
|
|
<a class="dropdown-item" href="{% url 'edit_recipe' recipe.pk %}"><i
|
|
class="fas fa-pencil-alt fa-fw"></i> {% trans 'Edit' %}</a>
|
|
<button class="dropdown-item" onclick="$('#bookmarkModal').modal({'show':true})">
|
|
<i class="fas fa-bookmark fa-fw"></i> {% trans 'Add to Book' %}</button>
|
|
|
|
<a class="dropdown-item" v-bind:href="getShoppingUrl()" v-if="has_ingredients">
|
|
<i class="fas fa-shopping-cart fa-fw"></i> {% trans 'Add to Shopping' %}</a>
|
|
|
|
<a class="dropdown-item" href="{% url 'new_meal_plan' %}?recipe={{ recipe.pk }}"><i
|
|
class="fas fa-calendar fa-fw"></i> {% trans 'Add to Plan' %}</a>
|
|
<button class="dropdown-item" onclick="openCookLogModal({{ recipe.pk }})"><i
|
|
class="fas fa-clipboard-list fa-fw"></i> {% trans 'Log Cooking' %}</button>
|
|
<button class="dropdown-item" onclick="window.print()"><i
|
|
class="fas fa-print fa-fw"></i> {% trans 'Print' %}</button>
|
|
<a class="dropdown-item" href="{% url 'view_export' %}?r={{ recipe.pk }}" target="_blank"
|
|
rel="noopener noreferrer"><i class="fas fa-file-export fa-fw"></i> {% trans 'Export' %}</a>
|
|
{% if recipe.internal %}
|
|
<a class="dropdown-item" href="{% url 'new_share_link' recipe.pk %}" target="_blank"
|
|
rel="noopener noreferrer"><i class="fas fa-share-alt fa-fw"></i> {% trans 'Share' %}</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if recipe.storage %}
|
|
<small>{% trans 'in' %} <a
|
|
href="{% url 'edit_storage' recipe.storage.pk %}">{{ recipe.storage.name }}</a></small><br/>
|
|
{% endif %}
|
|
|
|
{% if recipe.internal %}
|
|
<small>{% trans 'by' %} {{ recipe.created_by.get_user_name }}<br/></small>
|
|
{% endif %}
|
|
|
|
{% if recipe.keywords %}
|
|
{% for x in recipe.keywords.all %}
|
|
<span class="badge badge-pill badge-light">{{ x }}</span>
|
|
{% endfor %}
|
|
<br/>
|
|
<br/>
|
|
{% endif %}
|
|
|
|
{% if recipe.working_time and recipe.working_time != 0 %}
|
|
<span class="badge badge-secondary"><i
|
|
class="fas fa-user-clock"></i> {% trans 'Preparation time ca.' %} {{ recipe.working_time }} min </span>
|
|
{% endif %}
|
|
|
|
{% if recipe.waiting_time and recipe.waiting_time != 0 %}
|
|
<span
|
|
class="badge badge-secondary"><i
|
|
class="far fa-clock"></i> {% trans 'Waiting time ca.' %} {{ recipe.waiting_time }} min </span>
|
|
{% endif %}
|
|
{% recipe_last recipe request.user as last_cooked %}
|
|
{% if last_cooked %}
|
|
<span class="badge badge-primary">{% trans 'Last cooked' %} {{ last_cooked|date }}</span>
|
|
{% endif %}
|
|
|
|
{% if recipe.waiting_time and recipe.waiting_time != 0 or recipe.working_time and recipe.working_time != 0 or last_cooked %}
|
|
<br/>
|
|
<br/>
|
|
{% endif %}
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 order-md-1 col-sm-12 order-sm-2 col-12 order-2" v-if="recipe && has_ingredients">
|
|
<!-- TODO duplicate code remove -->
|
|
<div class="card border-primary">
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col col-md-9">
|
|
<h4 class="card-title">{% trans 'Ingredients' %}</h4>
|
|
</div>
|
|
<div class="col col-md-3">
|
|
|
|
<div class="input-group d-print-none">
|
|
<input type="number" value="1" maxlength="3" class="form-control"
|
|
v-model="ingredient_factor"/>
|
|
<div class="input-group-append">
|
|
<span class="input-group-text"><i class="fas fa-calculator"></i></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<br/>
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<table class="table table-sm">
|
|
<template v-for="s in recipe.steps">
|
|
<template v-if="s.name != '' && s.show_as_header && s.ingredients.length > 0">
|
|
<tr>
|
|
<td style="padding-top: 8px!important; ">
|
|
<b>[[s.name]]</b>
|
|
</td>
|
|
<td></td>
|
|
<td></td>
|
|
</tr>
|
|
</template>
|
|
<template v-for="i in s.ingredients">
|
|
<template v-if="i.is_header">
|
|
<tr>
|
|
<td style="padding-top: 8px!important; ">
|
|
<b>[[i.note]]</b>
|
|
</td>
|
|
<td></td>
|
|
<td></td>
|
|
</tr>
|
|
</template>
|
|
<template v-else>
|
|
<tr>
|
|
<td style="vertical-align: middle!important;">
|
|
<div class="pretty p-default p-curve">
|
|
<input type="checkbox" v-model="i.checked"/>
|
|
<div class="state p-success">
|
|
<label>
|
|
<template v-if="i.no_amount">
|
|
<span>⁣</span>
|
|
</template>
|
|
<template v-if="!i.no_amount">
|
|
<span v-html="calculateAmount(i.amount)"></span>
|
|
{# Allow for amounts without units, such as "2 eggs" #}
|
|
<template v-if="i.unit">
|
|
[[i.unit.name]]
|
|
</template>
|
|
</template>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
</td>
|
|
<td style="vertical-align: middle!important;">
|
|
<template v-if="i.food && i.food.recipe">
|
|
<a v-bind:href='"{% url 'view_recipe' 12345 %}".replace(/12345/, i.food.recipe)'
|
|
target="_blank" rel="noopener noreferrer">[[i.food.name]]</a>
|
|
</template>
|
|
<template v-else-if="i.food">
|
|
[[i.food.name]]
|
|
</template>
|
|
</td>
|
|
<td style="vertical-align: middle!important;">
|
|
<template v-if="i.note">
|
|
<b-button v-b-popover.hover="i.note"
|
|
class="btn btn-sm d-print-none"><i
|
|
class="fas fa-info"></i></b-button>
|
|
|
|
<div class="d-none d-print-block">
|
|
<i class="far fa-comment-alt"></i> [[i.note]]
|
|
</div>
|
|
</template>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</template>
|
|
</template>
|
|
|
|
<!-- Bottom border -->
|
|
<tr>
|
|
<td></td>
|
|
<td></td>
|
|
<td></td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% if recipe.image %}
|
|
<div class="col-12 order-1 col-sm-12 order-sm-1 col-md-6 order-md-2" style="text-align: center">
|
|
<img class="img img-fluid rounded" src="{{ recipe.image.url }}" style="max-height: 30vh;"
|
|
alt="{% trans 'Recipe Image' %}">
|
|
<br/>
|
|
<br/>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if recipe.nutrition %}
|
|
<div class="row mt-5">
|
|
<div class="col-md-6 order-md-1 col-sm-12 order-sm-2 col-12 order-2">
|
|
<div class="card border-primary">
|
|
<div class="card-body">
|
|
<h4 class="card-title">{% trans 'Nutrition' %}</h4>
|
|
<table class="table table-sm">
|
|
<tr>
|
|
<td style="padding-top: 8px!important; ">
|
|
<b>{% trans 'Calories' %}</b>
|
|
</td>
|
|
<td style="text-align: right">{{ recipe.nutrition.calories|floatformat:2 }}</td>
|
|
<td>kcal</td>
|
|
</tr>
|
|
<tr>
|
|
<tr>
|
|
<td style="padding-top: 8px!important; ">
|
|
<b>{% trans 'Carbohydrates' %}</b>
|
|
</td>
|
|
<td style="text-align: right">{{ recipe.nutrition.carbohydrates|floatformat:2 }}</td>
|
|
<td>g</td>
|
|
</tr>
|
|
<tr>
|
|
<tr>
|
|
<td style="padding-top: 8px!important; ">
|
|
<b>{% trans 'Fats' %}</b>
|
|
</td>
|
|
<td style="text-align: right">{{ recipe.nutrition.fats|floatformat:2 }}</td>
|
|
<td>g</td>
|
|
</tr>
|
|
<tr>
|
|
<tr>
|
|
<td style="padding-top: 8px!important; ">
|
|
<b>{% trans 'Proteins' %}</b>
|
|
</td>
|
|
<td style="text-align: right">{{ recipe.nutrition.proteins|floatformat:2 }}</td>
|
|
<td>g</td>
|
|
</tr>
|
|
<tr>
|
|
<td></td>
|
|
<td></td>
|
|
<td></td>
|
|
</tr>
|
|
</table>
|
|
{% if recipe.nutrition.source %}
|
|
Source: {{ recipe.nutrition.source }}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
|
|
<div v-if="recipe !== undefined && recipe.steps.length > 0">
|
|
<hr>
|
|
<h3>{% trans 'Instructions' %}</h3>
|
|
<hr>
|
|
</div>
|
|
|
|
<div style="font-size: large;" v-if="recipe">
|
|
{% for s in recipe.steps.all %}
|
|
<div style="margin-top: 1vh" class="card">
|
|
<div class="card-body">
|
|
{% if recipe.steps.all|length > 1 %}
|
|
<div class="card-title">
|
|
|
|
<div class="row d-flex">
|
|
<div class="col-md-8 text-muted ">
|
|
{% if s.type == 'TEXT' %}
|
|
<i class="fas fa-paragraph fa-fw"></i>
|
|
{% elif s.type == 'TIME' %}
|
|
<i class="fas fa-clock fa-fw"></i>
|
|
{% endif %}
|
|
{% if s.name %}{{ s.name }}{% else %}{% trans 'Step' %}
|
|
{{ forloop.counter }}{% endif %}
|
|
{% if s.time != 0 %}
|
|
- {{ s.time }} {% trans 'Minutes' %}
|
|
{% endif %}
|
|
</div>
|
|
<div class="col-md-4 col-12 justify-content-end" v-if="has_times">
|
|
<input type="datetime-local" class="form-control"
|
|
@change="updateTimes(recipe.steps[{{ forloop.counter0 }}])"
|
|
v-model="recipe.steps[{{ forloop.counter0 }}].time_finished">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row" v-if="recipe.steps[{{ forloop.counter0 }}].ingredients.length > 0"
|
|
style="margin-top: 1vh">
|
|
<div class="col-md-6">
|
|
<table class="table table-sm">
|
|
<template v-for="i in recipe.steps[{{ forloop.counter0 }}].ingredients">
|
|
<!-- TODO duplicate code remove -->
|
|
|
|
<template v-if="i.is_header">
|
|
<tr>
|
|
<td style="padding-top: 8px!important; ">
|
|
<b>[[i.note]]</b>
|
|
</td>
|
|
<td>
|
|
|
|
</td>
|
|
<td></td>
|
|
</tr>
|
|
</template>
|
|
<template v-else>
|
|
<tr>
|
|
<td style="vertical-align: middle!important;">
|
|
<div class="pretty p-default p-curve">
|
|
<input type="checkbox" v-model="i.checked"/>
|
|
<div class="state p-success">
|
|
<label>
|
|
<template v-if="i.no_amount">
|
|
<span>⁣</span>
|
|
</template>
|
|
<template v-if="!i.no_amount">
|
|
<span v-html="calculateAmount(i.amount)"></span>
|
|
{# Allow for amounts without units, such as "2 eggs" #}
|
|
<template v-if="i.unit">
|
|
[[i.unit.name]]
|
|
</template>
|
|
</template>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
</td>
|
|
<td style="vertical-align: middle!important;">
|
|
<template v-if="i.food && i.food.recipe">
|
|
<a v-bind:href='"{% url 'view_recipe' 12345 %}".replace(/12345/, i.food.recipe)'
|
|
target="_blank" rel="noopener noreferrer">[[i.food.name]]</a>
|
|
</template>
|
|
<template v-else-if="i.food">
|
|
[[i.food.name]]
|
|
</template>
|
|
</td>
|
|
<td style="vertical-align: middle!important;">
|
|
<template v-if="i.note">
|
|
<b-button v-b-popover.hover="i.note"
|
|
class="btn btn-sm d-print-none"><i
|
|
class="fas fa-info"></i></b-button>
|
|
<div class="d-none d-print-block">
|
|
<i class="far fa-comment-alt"></i> [[i.note]]
|
|
</div>
|
|
</template>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
|
|
</template>
|
|
<!-- Bottom border -->
|
|
<tr>
|
|
<td></td>
|
|
<td></td>
|
|
<td></td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{% endif %}
|
|
|
|
<div class="row" style="margin-top: 8px">
|
|
<div class="col-md-12">
|
|
{% if s.instruction %}
|
|
{{ s.instruction | markdown | safe }}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
{% if recipe.storage %}
|
|
<div class="row">
|
|
{% if recipe.internal %}
|
|
<div class="col col-12" style="margin-top: 2vh">
|
|
<a href='#' onClick='openRecipe({{ recipe.id }})'
|
|
class="d-print-none">{% trans 'View external recipe' %} <i class="fas fa-external-link-alt"></i></a>
|
|
</div>
|
|
{% else %}
|
|
|
|
{% if '.pdf' in recipe.file_path %}
|
|
<div class="col col-12">
|
|
<iframe src="{% static 'pdfjs/viewer.html' %}?file={% url 'api_get_recipe_file' recipe.id %}"
|
|
width="100%"
|
|
height="700px"
|
|
style="border: none;"></iframe>
|
|
</div>
|
|
{% endif %}
|
|
{% if '.jpg' in recipe.file_path or '.png' in recipe.file_path or '.jpeg' in recipe.file_path %}
|
|
<div class="col-md-12" style="text-align: center">
|
|
<img class="img img-fluid" src="{% url 'api_get_recipe_file' recipe.id %}"
|
|
alt="{% trans 'External recipe image' %}">
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="col col-12" style="margin-top: 1vh">
|
|
<div class="card border-info">
|
|
<div class="card-body text-info">
|
|
<h5 class="card-title">{% trans 'External recipe' %}</h5>
|
|
<p class="card-text">
|
|
{% blocktrans %}
|
|
This is an external recipe, which means you can only view it by opening the link
|
|
above.
|
|
You can convert this recipe to a fancy recipe by pressing the convert button. The
|
|
original
|
|
file
|
|
will still be accessible.
|
|
{% endblocktrans %}.
|
|
<br/>
|
|
<br/>
|
|
<a href="{% url 'edit_convert_recipe' recipe.pk %}"
|
|
class="card-link btn btn-info">{% trans 'Convert now!' %}</a>
|
|
<a href='#' onClick='openRecipe({{ recipe.id }})'
|
|
class="d-print-none btn btn-warning">{% trans 'View external recipe' %} <i
|
|
class="fas fa-external-link-alt"></i></a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if request.user.userpreference.comments %}
|
|
<br/>
|
|
<br/>
|
|
|
|
<h5 {% if not comments %}class="d-print-none" {% endif %}><i class="far fa-comments"></i> {% trans 'Comments' %}
|
|
</h5>
|
|
{% for c in comments %}
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<small class="card-title">{{ c.updated_at }} {% trans 'by' %} {{ c.created_by.username }}</small> <a
|
|
href="{% url 'edit_comment' c.pk %}" class="d-print-none"><i
|
|
class="fas fa-pencil-alt"></i></a><br/>
|
|
{{ c.text }}
|
|
</div>
|
|
</div>
|
|
<br/>
|
|
{% endfor %}
|
|
|
|
{% if request.user.is_authenticated %}
|
|
<div class="d-print-none">
|
|
|
|
<form method="POST" class="post-form">
|
|
{% csrf_token %}
|
|
<div class="input-group mb-3">
|
|
<textarea name="comment-text" cols="15" rows="2" class="textarea form-control" required
|
|
id="comment-id_text"></textarea>
|
|
<div class="input-group-append">
|
|
<input type="submit" value="{% trans 'Comment' %}" class="btn btn-success">
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
{% if recipe.storage %}
|
|
{% include 'include/recipe_open_modal.html' %}
|
|
{% endif %}
|
|
|
|
<!-- Bookmark Modal -->
|
|
<div class="modal fade" id="bookmarkModal" tabindex="-1" role="dialog" aria-labelledby="bookmarkModalLabel"
|
|
aria-hidden="true">
|
|
<div class="modal-dialog" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="bookmarkModalLabel">Modal title</h5>
|
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
<span aria-hidden="true">×</span>
|
|
</button>
|
|
</div>
|
|
<form method="POST" class="post-form">
|
|
<div class="modal-body">
|
|
|
|
{% csrf_token %}
|
|
{{ bookmark_form|crispy }}
|
|
|
|
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
|
<input type="submit" value="{% trans 'Save' %}" class="btn btn-success">
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% include 'include/log_cooking.html' %}
|
|
|
|
{% endblock %}
|
|
|
|
{% block script %}
|
|
<script type="application/javascript">
|
|
|
|
let csrftoken = Cookies.get('csrftoken');
|
|
Vue.http.headers.common['X-CSRFToken'] = csrftoken;
|
|
|
|
let app = new Vue({
|
|
delimiters: ['[[', ']]'],
|
|
el: '#id_base_container',
|
|
data: {
|
|
recipe: undefined,
|
|
has_ingredients: false,
|
|
has_times: false,
|
|
ingredient_factor: 1,
|
|
},
|
|
|
|
mounted: function () {
|
|
this.loadRecipe()
|
|
},
|
|
methods: {
|
|
loadRecipe: function () {
|
|
this.$http.get("{% url 'api:recipe-detail' recipe.pk %}" {% if share %}
|
|
+ "?share={{ share }}"{% endif %}).then((response) => {
|
|
this.recipe = response.data;
|
|
this.loading = false;
|
|
|
|
for (let step of this.recipe.steps) {
|
|
if (step.ingredients.length > 0) {
|
|
this.has_ingredients = true
|
|
}
|
|
if (step.time !== 0) {
|
|
this.has_times = true
|
|
}
|
|
this.$set(step, 'time_finished', undefined);
|
|
for (let i of step.ingredients) {
|
|
this.$set(i, 'checked', false)
|
|
}
|
|
}
|
|
|
|
}).catch((err) => {
|
|
this.error = err.data;
|
|
this.loading = false;
|
|
console.log(err)
|
|
})
|
|
},
|
|
roundDecimals: function (num) {
|
|
let decimals = {% if request.user.userpreference.ingredient_decimals %}
|
|
{{ request.user.userpreference.ingredient_decimals }} {% else %} 2; {% endif %}
|
|
return +(Math.round(num + `e+${decimals}`) + `e-${decimals}`);
|
|
},
|
|
updateTimes: function (step) {
|
|
let time_diff_first = 0;
|
|
for (let s of this.recipe.steps) {
|
|
if (this.recipe.steps.indexOf(s) < this.recipe.steps.indexOf(step)) {
|
|
time_diff_first += s.time
|
|
}
|
|
}
|
|
|
|
this.recipe.steps[0].time_finished = moment(step.time_finished).subtract(time_diff_first, 'minutes').format(moment.HTML5_FMT.DATETIME_LOCAL);
|
|
|
|
let time_diff = 0;
|
|
for (let s of this.recipe.steps) {
|
|
s.time_finished = moment(this.recipe.steps[0].time_finished).add(time_diff, 'minutes').format(moment.HTML5_FMT.DATETIME_LOCAL);
|
|
time_diff += s.time
|
|
}
|
|
|
|
},
|
|
getShoppingUrl: function () {
|
|
return `{% url 'view_shopping' %}?r=[${this.recipe.id},${this.ingredient_factor}]`
|
|
},
|
|
calculateAmount: function (amount) {
|
|
{% if request.user.userpreference.use_fractions %}
|
|
let return_string = ''
|
|
let fraction = frac.cont(amount, 9, true)
|
|
|
|
if (fraction[0] > 0) {
|
|
return_string += fraction[0]
|
|
}
|
|
|
|
if (fraction[1] > 0) {
|
|
return_string += ` <sup>${(fraction[1])}</sup>⁄<sub>${(fraction[2])}</sub>`
|
|
}
|
|
|
|
return return_string
|
|
{% else %}
|
|
return this.roundDecimals(amount * this.ingredient_factor)
|
|
{% endif %}
|
|
},
|
|
}
|
|
});
|
|
|
|
|
|
</script>
|
|
{% endblock %} |