Merge branch 'develop' into feature/kilojoules

This commit is contained in:
vabene1111
2021-11-01 09:46:04 +01:00
committed by GitHub
44 changed files with 1284 additions and 1655 deletions

View File

@@ -14,10 +14,10 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-13 22:40+0200\n"
"PO-Revision-Date: 2021-09-07 16:06+0000\n"
"Last-Translator: Afaren <Afaren@outlook.com>\n"
"Language-Team: French <http://translate.tandoor.dev/projects/tandoor/recipes-"
"backend/fr/>\n"
"PO-Revision-Date: 2021-10-26 10:06+0000\n"
"Last-Translator: tarek EL SOL <tarek.elsol@gmail.com>\n"
"Language-Team: French <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/fr/>\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -305,7 +305,7 @@ msgstr ""
#: .\cookbook\forms.py:500
msgid "Partial Match"
msgstr ""
msgstr "correspondance partielle"
#: .\cookbook\forms.py:501
msgid "Starts Wtih"

View File

@@ -13,7 +13,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-13 22:40+0200\n"
"PO-Revision-Date: 2021-10-02 12:25+0000\n"
"PO-Revision-Date: 2021-10-26 10:06+0000\n"
"Last-Translator: Jesse <jesse.kamps@pm.me>\n"
"Language-Team: Dutch <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/nl/>\n"
@@ -58,7 +58,7 @@ msgid ""
"Users with whom newly created meal plan/shopping list entries should be "
"shared by default."
msgstr ""
"Gebruikers waarmee nieuwe maaltijdplannen/boodschappenlijstjes standaard "
"Gebruikers waarmee een nieuw maaltijdplan/boodschappenlijst standaard "
"gedeeld moeten worden."
#: .\cookbook\forms.py:59
@@ -71,7 +71,7 @@ msgstr "Aantal decimalen om ingrediënten op af te ronden."
#: .\cookbook\forms.py:61
msgid "If you want to be able to create and see comments underneath recipes."
msgstr "Als je opmerkingen bij recepten wil kunnen maken en zien."
msgstr "Als je opmerkingen onder recepten wil kunnen maken en zien."
#: .\cookbook\forms.py:63
msgid ""
@@ -842,7 +842,7 @@ msgstr "Winkelen"
#: .\cookbook\templates\base.html:113
msgid "Keyword"
msgstr "Etiket"
msgstr "Etiketten"
#: .\cookbook\templates\base.html:137
#: .\cookbook\templates\forms\ingredients.html:24
@@ -874,7 +874,7 @@ msgstr "Recept importeren"
#: .\cookbook\templates\shopping_list.html:188
#: .\cookbook\templates\shopping_list.html:210
msgid "Create"
msgstr "Maak"
msgstr "Nieuw recept"
#: .\cookbook\templates\base.html:207 .\cookbook\templates\space.html:7
#: .\cookbook\templates\space.html:19
@@ -1028,11 +1028,11 @@ msgid ""
" "
msgstr ""
"\n"
" Het volgende formulier kan worden gebruikt wanneer per ongeluk twee "
"(of meer) eenheden of ingrediënten zijn gemaakt die eigenlijk hetzelfde "
"zijn.\n"
" Het voegt de twee eenheden of ingrediënten samen en past alle bijbehorende "
"recepten aan.\n"
" Het volgende formulier kan worden gebruikt wanneer per ongeluk twee ("
"of meer) eenheden of ingrediënten zijn gemaakt die eigenlijk\n"
" hetzelfde zijn\n"
" Het voegt de twee eenheden of ingrediënten samen en past alle "
"bijbehorende recepten aan.\n"
" "
#: .\cookbook\templates\forms\ingredients.html:26
@@ -1068,7 +1068,7 @@ msgstr "Origineel bestand verwijderen"
#: .\cookbook\templates\generic\list_template.html:6
#: .\cookbook\templates\generic\list_template.html:21
msgid "List"
msgstr "Lijst"
msgstr " "
#: .\cookbook\templates\generic\list_template.html:34
msgid "Filter"
@@ -1201,13 +1201,14 @@ msgstr ""
"\n"
" Markdown is een lichtgewicht opmaak taal die gebruikt kan worden om "
"tekst eenvoudig op te maken.\n"
" Deze site gebruikt de <a href=\"https://python-markdown.github.io/\" target="
"\"_blank\">Python Markdown</a> bibliotheek\n"
" om je tekst in mooi uitziende HTML om te zetten. De volledige documentatie "
"kan \n"
" <a href=\"https://daringfireball.net/projects/markdown/syntax\" target="
"\"_blank\">hier</a>gevonden worden.\n"
" Onvolledige, maar waarschijnlijk voldoende, informatie staat hieronder.\n"
" Deze site gebruikt de <a href=\"https://python-markdown.github.io/\" "
"target=\"_blank\">Python Markdown</a> bibliotheek\n"
" om je tekst in mooi uitziende HTML om te zetten. De volledige "
"documentatie kan \n"
" <a href=\"https://daringfireball.net/projects/markdown/syntax\" "
"target=\"_blank\">hier</a>gevonden worden.\n"
" Onvolledige, maar waarschijnlijk voldoende, informatie staat "
"hieronder.\n"
" "
#: .\cookbook\templates\markdown_info.html:25
@@ -1216,7 +1217,7 @@ msgstr "Koppen"
#: .\cookbook\templates\markdown_info.html:54
msgid "Formatting"
msgstr "Formattering"
msgstr "Opmaak"
#: .\cookbook\templates\markdown_info.html:56
#: .\cookbook\templates\markdown_info.html:72
@@ -1299,7 +1300,7 @@ msgstr ""
#: .\cookbook\templates\markdown_info.html:132
#: .\cookbook\templates\markdown_info.html:145
msgid "This will become an image"
msgstr "Dit wordt een plaatje"
msgstr "Dit wordt een afbeelding"
#: .\cookbook\templates\markdown_info.html:152
msgid "Tables"
@@ -1372,7 +1373,7 @@ msgstr "Maak alleen een notitie"
#: .\cookbook\templates\shopping_list.html:29
#: .\cookbook\templates\shopping_list.html:714
msgid "Shopping List"
msgstr "Boodschappenlijstje"
msgstr "Boodschappenlijst"
#: .\cookbook\templates\meal_plan.html:172
msgid "Shopping list currently empty"
@@ -1380,7 +1381,7 @@ msgstr "Boodschappenlijst is momenteel leeg"
#: .\cookbook\templates\meal_plan.html:175
msgid "Open Shopping List"
msgstr "Open boodschappenlijstje"
msgstr "Open boodschappenlijst"
#: .\cookbook\templates\meal_plan.html:189
msgid "Plan"
@@ -1405,15 +1406,15 @@ msgstr ""
#: .\cookbook\templates\meal_plan.html:217
#: .\cookbook\templates\meal_plan.html:294
msgid "Edit plan types"
msgstr "Bewerk plan soorten"
msgstr "Bewerk maaltijdplan types"
#: .\cookbook\templates\meal_plan.html:219
msgid "Show help"
msgstr "Toon help"
msgstr "Help"
#: .\cookbook\templates\meal_plan.html:220
msgid "Week iCal export"
msgstr "Week iCal export"
msgstr "Exporteer week als iCal"
#: .\cookbook\templates\meal_plan.html:256
#: .\cookbook\templates\url_import.html:542
@@ -1433,7 +1434,7 @@ msgstr "Gedeeld met"
#: .\cookbook\templates\meal_plan.html:280
msgid "Add to Shopping"
msgstr "Voeg toe aan Boodschappen"
msgstr "Voeg toe aan boodschappenlijst"
#: .\cookbook\templates\meal_plan.html:323
msgid "New meal type"
@@ -1556,7 +1557,7 @@ msgid ""
"Recipes, foods, shopping lists and more are organized in spaces of one or "
"more people."
msgstr ""
"Recepten, ingrediënten, boodschappenlijstjes en meer zijn georganiseerd in "
"Recepten, ingrediënten, boodschappenlijsten en meer zijn georganiseerd in "
"ruimtes van één of meer personen."
#: .\cookbook\templates\no_space_info.html:18
@@ -1995,7 +1996,7 @@ msgstr "Maak Superuser acount"
#: .\cookbook\templates\shopping_list.html:79
msgid "Shopping Recipes"
msgstr "Boodschappen recepten"
msgstr "Recepten op boodschappenlijst"
#: .\cookbook\templates\shopping_list.html:83
msgid "No recipes selected"
@@ -2007,7 +2008,7 @@ msgstr "Invoermodus"
#: .\cookbook\templates\shopping_list.html:158
msgid "Add Entry"
msgstr "Zet op lijst"
msgstr "Voeg toe aan boodschappenlijst"
#: .\cookbook\templates\shopping_list.html:174
msgid "Amount"
@@ -2044,7 +2045,7 @@ msgstr "Afgerond"
#: .\cookbook\templates\shopping_list.html:296
msgid "You are offline, shopping list might not syncronize."
msgstr "Je bent offline, boodschappenlijst synchroniseert mogelijk niet."
msgstr "Je bent offline, de boodschappenlijst synchroniseert mogelijk niet."
#: .\cookbook\templates\shopping_list.html:361
msgid "Copy/Export"
@@ -2721,7 +2722,7 @@ msgstr "Supermarkten"
#: .\cookbook\views\lists.py:179
msgid "Shopping Categories"
msgstr "Boodschappen categorieën"
msgstr "Boodschappencategorieën"
#: .\cookbook\views\new.py:122
msgid "Imported new recipe!"

Binary file not shown.

View File

@@ -2,22 +2,21 @@ import random
from datetime import timedelta
from decimal import Decimal
from gettext import gettext as _
from django.contrib.auth.models import User
from django.db.models import Avg, QuerySet, Sum
from django.urls import reverse
from django.utils import timezone
from drf_writable_nested import (UniqueFieldsMixin,
WritableNestedModelSerializer)
from drf_writable_nested import UniqueFieldsMixin, WritableNestedModelSerializer
from rest_framework import serializers
from rest_framework.exceptions import ValidationError, NotFound
from rest_framework.exceptions import NotFound, ValidationError
from cookbook.models import (Comment, CookLog, Food, Ingredient, Keyword,
MealPlan, MealType, NutritionInformation, Recipe,
RecipeBook, RecipeBookEntry, RecipeImport,
ShareLink, ShoppingList, ShoppingListEntry,
ShoppingListRecipe, Step, Storage, Sync, SyncLog,
Unit, UserPreference, ViewLog, SupermarketCategory, Supermarket,
SupermarketCategoryRelation, ImportLog, BookmarkletImport, UserFile, Automation)
from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Food, ImportLog,
Ingredient, Keyword, MealPlan, MealType, NutritionInformation, Recipe,
RecipeBook, RecipeBookEntry, RecipeImport, ShareLink, ShoppingList,
ShoppingListEntry, ShoppingListRecipe, Step, Storage, Supermarket,
SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, Unit,
UserFile, UserPreference, ViewLog)
from cookbook.templatetags.custom_tags import markdown
@@ -34,12 +33,19 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
except KeyError:
api_serializer = None
# extended values are computationally expensive and not needed in normal circumstances
if self.context.get('request', False) and bool(int(self.context['request'].query_params.get('extended', False))) and self.__class__ == api_serializer:
return fields
else:
try:
if bool(int(self.context['request'].query_params.get('extended', False))) and self.__class__ == api_serializer:
return fields
except AttributeError:
pass
except KeyError:
pass
try:
del fields['image']
del fields['numrecipe']
return fields
except KeyError:
pass
return fields
def get_image(self, obj):
# TODO add caching
@@ -158,7 +164,7 @@ class UserFileSerializer(serializers.ModelSerializer):
current_file_size_mb = 0
if ((validated_data['file'].size / 1000 / 1000 + current_file_size_mb - 5)
> self.context['request'].space.max_file_storage_mb != 0):
> self.context['request'].space.max_file_storage_mb != 0):
raise ValidationError(_('You have reached your file upload limit.'))
def create(self, validated_data):

View File

@@ -344,6 +344,7 @@
<script type="application/javascript">
localStorage.setItem('SCRIPT_NAME', "{% base_path request 'script' %}")
localStorage.setItem('BASE_PATH', "{% base_path request 'base' %}")
localStorage.setItem('STATIC_URL', "{% base_path request 'static_base' %}")
window.addEventListener("load", () => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("{% url 'service_worker' %}", {scope: "{% base_path request 'base' %}" + '/'}).then(function (reg) {

View File

@@ -1,742 +1,36 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% load render_bundle from webpack_loader %}
{% load static %}
{% load i18n %}
{% load l10n %}
{% block title %}{% trans 'Meal-Plan' %}{% endblock %}
{% block extra_head %}
{% block content_fluid %}
{% include 'include/vue_base.html' %}
<div id="app">
<meal-plan-view></meal-plan-view>
</div>
<script src="{% static 'js/moment-with-locales.min.js' %}"></script>
<script src="{% static 'js/Sortable.min.js' %}"></script>
<script src="{% static 'js/vuedraggable.umd.min.js' %}"></script>
<script src="{% static 'js/vue-cookies.js' %}"></script>
<script src="{% static 'js/js.cookie.min.js' %}"></script>
{% endblock %}
{% block content %}
<div id="app">
<div class="row mt-2 mb-1">
<div class="col-md-4 offset-md-4">
<div class="input-group">
<div class="input-group-prepend">
<button class="btn btn-outline-secondary shadow-none"
@click="changeStartDate(number_of_days * -1)">
<i class="fas fa-arrow-left"></i>
</button>
</div>
<input name="date" id="id_date" class="form-control" type="date" v-model="start_date"
@change="updatePlan()">
<div class="input-group-append">
<button class="btn btn-outline-secondary shadow-none" @click="changeStartDate(number_of_days)">
<i class="fas fa-arrow-right"></i>
</button>
</div>
</div>
</div>
<div class="col-md-4">
<a href="{% url 'view_plan_new' %}" class="float-right">
<button class="btn btn-outline-secondary shadow-none">
<i class="fas fa-star"></i> {% trans 'Try the new meal planner' %}
</button>
</a>
</div>
</div>
<div class="row">
<div class="col-md-12">
<table class="table table-sm table-striped table-responsive-sm" style=" table-layout:fixed;">
<thead class="thead-dark" style="background-image: url({% static 'assets/header.svg' %});">
<tr>
<th class="thead-blank" v-for="d in dates" style="width: 14.2%; text-align: center;">
[[formatDateDayname(d)]]<br/>[[formatDateDay(d)]].
<button class="btn btn-sm btn-outline-secondary shadow-none" @click="addDayToShopping(d)"><i
class="fas fa-cart-plus fa-sm"></i></button>
</th>
</tr>
</thead>
<tbody v-for="t in meal_types">
<tr v-if="meal_plan[t.name] !== undefined">
<td :colspan="number_of_days" style="text-align: center">
[[ meal_plan[t.name].name]]
<template
v-if="t.created_by !== {{ request.user.pk }} && user_names[t.created_by] !== undefined">
([[ user_names[t.created_by] ]])
</template>
</td>
</tr>
<tr v-if="meal_plan[t.name] !== undefined">
<td v-for="d in meal_plan[t.name].days">
<draggable class="list-group" :list="d.items" group="plan" style="min-height: 40px"
@change="dragChanged(d.date, t, $event)"
:empty-insert-threshold="10" handle=".handle">
<div class="" v-for="(element, index) in d.items" :key="element.id">
<!-- small layout with handle -->
<div class="d-block d-md-none">
<div class="col-">
<i class="fas fa-arrows-alt handle input-group-text"
style="width: 100%"></i>
</div>
<div class="list-group-item" style="word-wrap: break-word;">
<a href="#" @click="plan_detail = element" data-toggle="modal"
data-target="#id_plan_detail_modal">[[ planElementName(element)]]</a>
</div>
</div>
<!-- big layout -->
<div class="list-group-item handle d-md-block d-none"
style="word-wrap: break-word; padding: 2;margin-bottom: 4">
<div class="col-md-12" style="padding: 0">
<a href="#" @click="plan_detail = element" data-toggle="modal"
data-target="#id_plan_detail_modal">[[ planElementName(element)]]</a>
</div>
</div>
</div>
</draggable>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="card">
<div class="card-header">
<i class="fas fa-calendar-plus"></i> {% trans 'New Entry' %} <a href="#" data-toggle="modal"
data-target="#id_plan_help_modal"><i
class="far fa-question-circle"></i></a>
</div>
<div class="row">
<div class="col-md-6">
<div class="card-body">
<div class="row">
<div class="col-md-12">
<div class="input-group mb-3">
<input type="text" class="form-control" v-model="recipe_query" @keyup="getRecipes"
placeholder="{% trans 'Search Recipe' %}">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button"
@click="getRandomRecipes">
<i class="fas fa-dice"></i>
</button>
</div>
</div>
</div>
</div>
<draggable class="list-group" :list="recipes"
:group="{ name: 'plan', pull: 'clone', put: false }" :clone="cloneRecipe">
<div class="list-group-item d-flex align-items-center justify-content-between"
v-for="(element, index) in recipes" :key="element.id">
<span>
<i class="fas fa-arrows-alt"></i> [[element.name]]
</span>
<span class="badge badge-light badge-pill">[[element.servings]]</span>
</div>
</draggable>
</div>
</div>
<div class="col-md-6">
<div>
<div class="card-body">
<input type="text" class="form-control" v-model="new_note_title"
placeholder="{% trans 'Title' %}" style="margin-bottom: 8px">
<textarea class="form-control" v-model="new_note_text"
placeholder="{% trans 'Note (optional)' %}"></textarea>
<small><span
class="text-muted">{% trans 'You can use markdown to format this field. See the <a href="/docs/markdown/" target="_blank" rel="noopener noreferrer">docs here</a>' %}</span></small>
<br/>
<br/>
<input type="number" class="form-control" v-model="new_note_servings"
placeholder="{% trans 'Serving Count' %}" style="margin-bottom: 8px">
<br/>
<draggable :list="pseudo_note_list"
:group="{ name: 'plan', pull: 'clone', put: false }" :clone="cloneNote">
<div class="list-group-item" v-for="(element, index) in pseudo_note_list"
:key="element.id">
<i class="fas fa-arrows-alt"></i> {% trans 'Create only note' %}
</div>
</draggable>
</div>
</div>
</div>
</div>
</div>
<br>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<i class="fas fa-shopping-cart"></i> {% trans 'Shopping List' %}
</div>
<div class="card-body">
<template v-if="shopping_list.length < 1">{% trans 'Shopping list currently empty' %}</template>
<template v-else>
<a v-bind:href="getShoppingUrl()" class="btn btn-success"
target="_blank">{% trans 'Open Shopping List' %}</a>
<br/>
<br/>
{% trans 'Recipes' %}
<ul class="list-group" style="margin-top: 8px">
<li class="list-group-item" v-for="item in shopping_list"> [[ item.recipe_name ]]</li>
</ul>
</template>
</div>
</div>
</div>
<div class="col-md-6" style="margin-top: 8px">
<div class="card">
<div class="card-header">
<i class="fas fa-shopping-cart"></i> {% trans 'Plan' %}
</div>
<div class="card-body">
<div class="row">
<div class="col">
<label>
{% trans 'Number of Days' %}
<input class="form-control" type="number" v-model="number_of_days"
@change="updatePlan(); $cookies.set('number_of_days',number_of_days, -1)">
</label>
</div>
</div>
<div class="row">
<div class="col">
<label>
{% trans 'Weekday offset' %}
<input class="form-control" type="number" v-model="start_offset"
@change="updatePlan(); $cookies.set('start_offset',start_offset, -1)">
<small class="text-muted">{% trans 'Number of days starting from the first day of the week to offset the default view.' %}</small>
</label>
</div>
</div>
<a href="#" data-toggle="modal"
data-target="#id_plan_types_modal">{% trans 'Edit plan types' %}</a> <br/>
<a href="#" data-toggle="modal"
data-target="#id_plan_help_modal">{% trans 'Show help' %}</a><br/>
<a v-bind:href="getIcalUrl()">{% trans 'Week iCal export' %}</a>
</div>
</div>
</div>
</div>
<br/>
<br/>
<div class="modal fade" id="id_plan_detail_modal" tabindex="-1" role="dialog"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<template v-if="plan_detail.title !==''">[[ plan_detail.title ]]</template>
<template v-else>[[ plan_detail.recipe_name ]]</template>
<small
class="text-muted"><br/>[[ plan_detail.meal_type_name ]] [[
formatLocalDate(plan_detail.date) ]]</small>
</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<template v-if="plan_detail.recipe_name !== undefined ">
<small class="text-muted">{% trans 'Recipe' %}</small><br/>
<a v-bind:href="planDetailRecipeUrl()" target="_blank">[[ plan_detail.recipe_name ]]</a>
<br/>
<br/>
<small class="text-muted">{% trans 'Serving Count' %}</small><br/>
<span>[[ plan_detail.servings ]]</span>
</template>
<template v-if="plan_detail.note !== ''">
<small class="text-muted">{% trans 'Note' %}</small><br/>
<span v-html="plan_detail.note_markdown"></span>
<br/>
</template>
<br/>
<br/>
<template v-if="plan_detail.created_by !== undefined ">
<small class="text-muted">{% trans 'Created by' %}</small><br/>
[[ user_names[plan_detail.created_by] ]]
<br/>
</template>
<template v-if="plan_detail.shared.length > 0">
<small class="text-muted">{% trans 'Shared with' %}</small><br/>
<span>[[ planDetailUserList() ]]</span>
<br/>
</template>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger"
@click="deleteEntry(plan_detail)">{% trans 'Delete' %}</button>
<button type="button" class="btn btn-success"
v-if="!shopping_list.includes(plan_detail) && plan_detail.recipe_name !== undefined"
@click="shopping_list.push(plan_detail)">{% trans 'Add to Shopping' %}</button>
<a class="btn btn-primary" v-bind:href="planDetailEditUrl()">{% trans 'Edit' %}</a>
<button type="button" class="btn btn-secondary"
data-dismiss="modal">{% trans 'Close' %}</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="id_plan_types_modal" tabindex="-1" role="dialog"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{% trans 'Edit plan types' %}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<draggable :list="meal_types_edit" handle=".handle"
:group="{ name: 'types'}">
<div v-for="(element, index) in meal_types_edit"
:key="element.id">
<template v-if="!element.delete">
<div class="input-group mb-3">
<div class="input-group-prepend handle">
<button tabindex="-1" class="btn btn-outline-secondary"><i
class="fas fa-arrows-alt-v"></i></button>
</div>
<input class="form-control" v-model="element.name">
<div class="input-group-append">
<button tabindex="-1" class="btn btn-outline-danger" type="button"
@click="markTypeDelete(element)"><i class="fas fa-trash-alt"></i>
</button>
</div>
</div>
</template>
</div>
</draggable>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary"
@click="meal_types_edit.push({name:'{% trans 'New meal type' %}', delete:false})">{% trans 'New' %}</button>
<button type="button" class="btn btn-success"
@click="updatePlanTypes()">{% trans 'Save' %}</button>
<button type="button" class="btn btn-secondary"
data-dismiss="modal">{% trans 'Close' %}</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="id_plan_help_modal" tabindex="-1" role="dialog"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{% trans 'Meal Plan Help' %}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{% blocktrans %}
<p>The meal plan module allows planning of meals both with recipes and notes.</p>
<p>Simply select a recipe from the list of recently viewed recipes or search the one you
want and drag it to the desired plan position. You can also add a note and a title and
then drag the recipe to create a plan entry with a custom title and note. Creating only
Notes is possible by dragging the create note box into the plan.</p>
<p>Click on a recipe in order to open the detailed view. There you can also add it to the
shopping list. You can also add all recipes of a day to the shopping list by
clicking the shopping cart at the top of the table.</p>
<p>Since a common use case is to plan meals together you can define
users you want to share your plan with in the settings.
</p>
<p>You can also edit the types of meals you want to plan. If you share your plan with
someone with
different meals, their meal types will appear in your list as well. To prevent
duplicates (e.g. Other and Misc.)
name your meal types the same as the users you share your meals with and they will be
merged.</p>
{% endblocktrans %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary"
data-dismiss="modal">{% trans 'Close' %}</button>
</div>
</div>
</div>
</div>
</div>
<script src="{% url 'javascript-catalog' %}"></script>
{% block script %}
{% if debug %}
<script src="{% url 'js_reverse' %}"></script>
{% else %}
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
{% endif %}
<script type="application/javascript">
moment.locale('{{request.LANGUAGE_CODE}}');
window.IMAGE_PLACEHOLDER = "{% static 'assets/recipe_no_image.svg' %}"
let csrftoken = Cookies.get('csrftoken');
Vue.http.headers.common['X-CSRFToken'] = csrftoken;
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
let app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
data: {
start_date: undefined,
start_offset: 0,
dates: [],
number_of_days: $cookies.isKey('number_of_days') ? $cookies.get('number_of_days') : 7,
plan_entries: [],
meal_types: [],
meal_types_edit: [],
meal_plan: {},
plan_detail: {shared: []},
recipes: [],
recipe_query: '',
pseudo_note_list: [
{id: 0, title: '', text: ''}
],
new_note_title: '',
new_note_text: '',
new_note_servings: '',
default_shared_users: [],
user_id_update: [],
user_names: {},
shopping: false,
shopping_list: [],
},
mounted: function () {
this.default_shared_users = [{% for u in request.user.userpreference.plan_share.all %}
{{ u.pk }},
{% endfor %}]
this.$set(this.user_names, {{ request.user.pk }}, '{{ request.user.get_user_name }}')
this.user_id_update = Array.from(this.default_shared_users)
this.start_offset = $cookies.isKey('start_offset') ? $cookies.get('start_offset') : 0;
this.start_date = moment().weekday(0).add(this.start_offset, 'days').format('YYYY-MM-DD')
this.updatePlan();
this.getRecipes();
//this.makeToast('success', 'this actually works', 'success')
},
methods: {
makeToast: function (title, message, variant = null) {
this.$bvToast.toast(message, {
title: title,
variant: variant,
toaster: 'b-toaster-top-center',
solid: true
})
},
updatePlan: function () {
this.dates = [];
for (var i = 0; i <= (this.number_of_days - 1); i++) {
this.dates.push(moment(this.start_date).add(i, 'days'));
}
let planEntryPromise = this.getPlanEntries();
let planTypePromise = this.getPlanTypes();
Promise.allSettled([planEntryPromise, planTypePromise]).then(() => {
this.buildGrid()
})
},
getPlanEntries: function () {
return this.$http.get("{% url 'api:mealplan-list' %}?from_date=" + this.dates[0].format('YYYY-MM-DD') + "&to_date=" + this.dates[this.dates.length - 1].format('YYYY-MM-DD')).then((response) => {
this.plan_entries = response.data;
}).catch((err) => {
console.log("getPlanEntries error: ", err);
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
getPlanTypes: function () {
return this.$http.get("{% url 'api:mealtype-list' %}").then((response) => {
this.meal_types = response.data;
this.meal_types_edit = jQuery.extend(true, [], response.data);
for (let mte of this.meal_types_edit) {
this.$set(mte, 'delete', false)
}
if (this.meal_types.length === 0) {
this.makeToast(gettext('Information'), gettext('To use the meal plan please first create at least one meal plan type.'), 'warning')
}
}).catch((err) => {
console.log("getPlanTypes error: ", err);
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
buildGrid: function () {
this.meal_plan = {}
for (let e of this.plan_entries) {
let new_type = {id: e.meal_type, name: e.meal_type_name, created_by: e.created_by}
if (this.meal_types.filter(el => el.name === new_type.name).length === 0) {
this.meal_types.push(new_type)
}
}
for (let t of this.meal_types) {
this.$set(this.meal_plan, t.name, {
name: t.name,
meal_type: t.id,
days: {}
})
for (let d of this.dates) {
this.$set(this.meal_plan[t.name].days, d.format('YYYY-MM-DD'), {
name: this.formatDateDayname(d),
date: d.format('YYYY-MM-DD'),
items: []
})
}
}
for (let e of this.plan_entries) {
this.meal_plan[e.meal_type_name].days[e.date].items.push(e)
for (let u of e.shared) {
if (!this.user_id_update.includes(parseInt(u))) {
this.user_id_update.push(parseInt(u))
}
}
}
this.updateUserNames()
},
getRandomRecipes: function () {
this.$set(this, 'recipe_query', '');
this.getRecipes();
},
getRecipes: function () {
let url = "{% url 'api:recipe-list' %}?page_size=5"
if (this.recipe_query !== '') {
url += '&query=' + this.recipe_query;
} else {
url += '&random=true'
}
this.$http.get(url).then((response) => {
this.recipes = this.removeDuplicates(response.data.results, recipe => recipe.id);
}).catch((err) => {
console.log("getRecipes error: ", err);
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
getMdNote: function () {
let url = "{% url 'api:recipe-list' %}?page_size=5"
if (this.recipe_query !== '') {
url += '&query=' + this.recipe_query;
}
this.$http.get(url).then((response) => {
this.recipes = response.data.results;
}).catch((err) => {
console.log("getRecipes error: ", err);
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
updateUserNames: function () {
return this.$http.get("{% url 'api:username-list' %}?filter_list=[" + this.user_id_update + ']').then((response) => {
for (let u of response.data) {
this.$set(this.user_names, u.id, u.username);
}
}).catch((err) => {
console.log("updateUserNames error: ", err);
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
dragChanged: function (date, meal_type, evt) {
if (evt.added !== undefined) {
let plan_entry = evt.added.element
plan_entry.date = date
plan_entry.meal_type = meal_type
plan_entry.meal_type_name = meal_type.name
if (plan_entry.is_new) { // its not a meal plan object
plan_entry.created_by = {{ request.user.id }};
plan_entry.shared = this.default_shared_users
this.$http.post(`{% url 'api:mealplan-list' %}`, plan_entry).then((response) => {
let entry = response.data
this.meal_plan[entry.meal_type_name].days[entry.date].items = this.meal_plan[entry.meal_type_name].days[entry.date].items.filter(item => !item.is_new)
this.meal_plan[entry.meal_type_name].days[entry.date].items.push(entry)
}).catch((err) => {
console.log("dragChanged create error", err);
})
} else {
this.$http.put(`{% url 'api:mealplan-list' %}${plan_entry.id}/`, plan_entry).then((response) => {
}).catch((err) => {
console.log("dragChanged update error", err);
this.makeToast(gettext('Error'), gettext('There was an error updating a resource!') + err.bodyText, 'danger')
})
}
}
},
deleteEntry: function (entry) {
$('#id_plan_detail_modal').modal('hide')
this.$http.delete(`{% url 'api:mealplan-list' %}${entry.id}/`, entry).then((response) => {
this.meal_plan[entry.meal_type_name].days[entry.date].items = this.meal_plan[entry.meal_type_name].days[entry.date].items.filter(item => item !== entry)
}).catch((err) => {
console.log("deleteEntry error: ", err);
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
removeDuplicates: function (data, key) {
return [
...new Map(data.map(item => [key(item), item])).values()
]
},
updatePlanTypes: function () {
let promise_list = []
let i = 0
for (let x of this.meal_types_edit) {
x.order = i
i++
if (x.id === undefined && !x.delete) {
x.created_by = {{ request.user.id }}
promise_list.push(this.$http.post("{% url 'api:mealtype-list' %}", x).then((response) => {
}).catch((err) => {
console.log("updatePlanTypes create error: ", err);
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
}))
} else if (x.delete) {
if (x.id !== undefined) {
promise_list.push(this.$http.delete(`{% url 'api:mealtype-list' %}${x.id}/`, x).then((response) => {
}).catch((err) => {
console.log("updatePlanTypes delete error: ", err);
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
}))
}
} else {
promise_list.push(this.$http.put(`{% url 'api:mealtype-list' %}${x.id}/`, x).then((response) => {
}).catch((err) => {
console.log("updatePlanTypes update error: ", err);
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
}))
}
}
Promise.allSettled(promise_list).then(() => {
this.updatePlan()
$('#id_plan_types_modal').modal('hide')
})
},
markTypeDelete: function (element) {
if (confirm(gettext('When deleting a meal type all entries using that type will be deleted as well. Deletion will apply when configuration is saved. Do you want to proceed?'))) {
element.delete = true
}
},
cloneRecipe: function (recipe) {
let r = {
id: Math.round(Math.random() * 1000) + 10000,
recipe: recipe,
recipe_name: recipe.name,
servings: (this.new_note_servings > 1) ? this.new_note_servings : recipe.servings,
title: this.new_note_title,
note: this.new_note_text,
is_new: true
}
console.log(recipe)
this.new_note_title = ''
this.new_note_text = ''
this.new_note_servings = ''
return r
},
cloneNote: function () {
let new_entry = {
id: Math.round(Math.random() * 1000) + 10000,
title: this.new_note_title,
note: this.new_note_text,
servings: 1,
is_new: true,
}
if (new_entry.title === '') {
new_entry.title = gettext('Title')
}
this.new_note_title = ''
this.new_note_text = ''
this.new_note_servings = ''
return new_entry
},
planElementName: function (element) {
if (element.title) {
return element.title
} else if (element.recipe_name) {
return element.recipe_name
} else {
return element.name
}
},
planDetailRecipeUrl: function () {
return "{% url 'view_recipe' 12345 %}".replace(/12345/, this.plan_detail.recipe.id);
},
planDetailEditUrl: function () {
return "{% url 'edit_meal_plan' 12345 %}".replace(/12345/, this.plan_detail.id);
},
planDetailUserList: function () {
let users = []
for (let u of this.plan_detail.shared) {
users.push(this.user_names[u])
}
return users.join(', ')
},
formatLocalDate: function (date) {
return moment(date).format('LL')
},
formatDateDay: function (date) {
return moment(date).format('D')
},
formatDateDayname: function (date) {
return moment(date).format('dddd')
},
changeStartDate: function (change) {
this.start_date = moment(this.start_date).add(change, 'days').format('YYYY-MM-DD')
this.updatePlan();
},
getShoppingUrl: function () {
let url = "{% url 'view_shopping' %}"
let first = true
for (let se of this.shopping_list) {
if (first) {
url += `?r=[${se.recipe.id},${se.servings}]`
first = false
} else {
url += `&r=[${se.recipe.id},${se.servings}]`
}
}
return url
},
getIcalUrl: function () {
if (this.dates.length === 0) {
return ""
}
return "{% url 'api_get_plan_ical' 12345 6789 %}".replace(/12345/, this.dates[0].format('YYYY-MM-DD')).replace(/6789/, this.dates[this.dates.length - 1].format('YYYY-MM-DD'));
},
addDayToShopping: function (date) {
for (let t of this.meal_types) {
for (let i of this.meal_plan[t.name].days[date.format('YYYY-MM-DD')].items) {
if (!this.shopping_list.includes(i)) {
this.shopping_list.push(i)
}
}
}
}
}
});
window.ICAL_URL = '{% url 'api_get_plan_ical' 12345 6789 %}'
window.SHOPPING_URL = '{% url 'view_shopping' %}'
</script>
{% render_bundle 'meal_plan_view' %}
{% endblock %}

View File

@@ -178,7 +178,7 @@
</div>
<div class="card">
<div class="card-body">
<h5 class="card-title">{% trans 'Percise' %}</h5>
<h5 class="card-title">{% trans 'Precise' %}</h5>
<p class="card-text">{% trans 'Allows fine control over search results but might not return results if too many spelling mistakes are made.' %}</p>
<p class="card-text"><small class="text-muted">{% trans 'Perfect for large Databases' %}</small></p>
<button class="btn btn-primary card-link" onclick="applyPreset('precise')">{% trans 'Apply' %}</button>

View File

@@ -142,7 +142,7 @@ def bookmarklet(request):
localStorage.setItem('redirectURL', '" + server + reverse('data_import_url') + "'); \
localStorage.setItem('token', '" + api_token.__str__() + "'); \
document.body.appendChild(document.createElement(\'script\')).src=\'" \
+ server + prefix + static('js/bookmarklet.js') + "? \
+ server + prefix + static('js/bookmarklet.js') + "? \
r=\'+Math.floor(Math.random()*999999999);}})();"
return re.sub(r"[\n\t\s]*", "", bookmark)
@@ -153,3 +153,5 @@ def base_path(request, path_type):
return request._current_scheme_host + request.META.get('HTTP_X_SCRIPT_NAME', '')
elif path_type == 'script':
return request.META.get('HTTP_X_SCRIPT_NAME', '')
elif path_type == 'static_base':
return static('vue/manifest.json').replace('vue/manifest.json', '')

View File

@@ -58,7 +58,6 @@ urlpatterns = [
path('search/v2/', views.search_v2, name='view_search_v2'),
path('books/', views.books, name='view_books'),
path('plan/', views.meal_plan, name='view_plan'),
path('plan_new/', views.meal_plan_new, name='view_plan_new'),
path('plan/entry/<int:pk>', views.meal_plan_entry, name='view_plan_entry'),
path('shopping/', views.shopping_list, name='view_shopping'),
path('shopping/<int:pk>', views.shopping_list, name='view_shopping'),

View File

@@ -5,6 +5,7 @@ from io import BytesIO
import requests
from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist
from django.core.files import File
from django.db.transaction import atomic
from django.http import HttpResponse, HttpResponseRedirect
@@ -150,8 +151,15 @@ def import_url(request):
recipe.steps.add(step)
for kw in data['keywords']:
k, created = Keyword.objects.get_or_create(name=kw['text'], space=request.space)
recipe.keywords.add(k)
if data['all_keywords']: # do not remove this check :) https://github.com/vabene1111/recipes/issues/645
k, created = Keyword.objects.get_or_create(name=kw['text'], space=request.space)
recipe.keywords.add(k)
else:
try:
k = Keyword.objects.get(name=kw['text'], space=request.space)
recipe.keywords.add(k)
except ObjectDoesNotExist:
pass
ingredient_parser = IngredientParser(request, True)
for ing in data['recipeIngredient']:

View File

@@ -220,11 +220,6 @@ def meal_plan(request):
return render(request, 'meal_plan.html', {})
@group_required('user')
def meal_plan_new(request):
return render(request, 'meal_plan_new.html', {})
@group_required('user')
def supermarket(request):
return render(request, 'supermarket.html', {})
@@ -344,10 +339,13 @@ def user_settings(request):
if fields_searched == 0:
search_form.add_error(None, _('You must select at least one field to search!'))
search_error = True
elif search_form.cleaned_data['search'] in ['websearch', 'raw'] and len(search_form.cleaned_data['fulltext']) == 0:
search_form.add_error('search', _('To use this search method you must select at least one full text search field!'))
elif search_form.cleaned_data['search'] in ['websearch', 'raw'] and len(
search_form.cleaned_data['fulltext']) == 0:
search_form.add_error('search',
_('To use this search method you must select at least one full text search field!'))
search_error = True
elif search_form.cleaned_data['search'] in ['websearch', 'raw'] and len(search_form.cleaned_data['trigram']) > 0:
elif search_form.cleaned_data['search'] in ['websearch', 'raw'] and len(
search_form.cleaned_data['trigram']) > 0:
search_form.add_error(None, _('Fuzzy search is not compatible with this search method!'))
search_error = True
else:
@@ -386,7 +384,8 @@ def user_settings(request):
else:
preference_form = UserPreferenceForm()
fields_searched = len(sp.icontains.all()) + len(sp.istartswith.all()) + len(sp.trigram.all()) + len(sp.fulltext.all())
fields_searched = len(sp.icontains.all()) + len(sp.istartswith.all()) + len(sp.trigram.all()) + len(
sp.fulltext.all())
if sp and not search_error and fields_searched > 0:
search_form = SearchPreferenceForm(instance=sp)
elif not search_error:
@@ -396,7 +395,8 @@ def user_settings(request):
api_token = Token.objects.create(user=request.user)
# these fields require postgress - just disable them if postgress isn't available
if not settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
if not settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
'django.db.backends.postgresql']:
search_form.fields['search'].disabled = True
search_form.fields['lookup'].disabled = True
search_form.fields['trigram'].disabled = True