Merge branch 'develop' of https://github.com/vabene1111/recipes into develop

This commit is contained in:
Kaibu
2022-04-23 14:47:14 +02:00
13 changed files with 107 additions and 1134 deletions

View File

@ -6,7 +6,7 @@ from django.utils.translation import gettext as _
from django_scopes import scopes_disabled
from cookbook.forms import MultiSelectWidget
from cookbook.models import Food, Keyword, Recipe, ShoppingList
from cookbook.models import Food, Keyword, Recipe
with scopes_disabled():
class RecipeFilter(django_filters.FilterSet):
@ -60,22 +60,3 @@ with scopes_disabled():
class Meta:
model = Recipe
fields = ['name', 'keywords', 'foods', 'internal']
# class FoodFilter(django_filters.FilterSet):
# name = django_filters.CharFilter(lookup_expr='icontains')
# class Meta:
# model = Food
# fields = ['name']
class ShoppingListFilter(django_filters.FilterSet):
def __init__(self, data=None, *args, **kwargs):
if data is not None:
data = data.copy()
data.setdefault("finished", False)
super().__init__(data, *args, **kwargs)
class Meta:
model = ShoppingList
fields = ['finished']

View File

@ -4,7 +4,7 @@ import unicodedata
from django.core.cache import caches
from cookbook.models import Unit, Food, Automation
from cookbook.models import Unit, Food, Automation, Ingredient
class IngredientParser:
@ -46,7 +46,7 @@ class IngredientParser:
def apply_food_automation(self, food):
"""
Apply food alias automations to passed foood
Apply food alias automations to passed food
:param food: unit as string
:return: food as string (possibly changed by automation)
"""
@ -155,33 +155,36 @@ class IngredientParser:
except ValueError:
unit = x[end:]
if unit is not None and unit.strip() == '':
unit = None
if unit is not None and (unit.startswith('(') or unit.startswith('-')): # i dont know any unit that starts with ( or - so its likely an alternative like 1L (500ml) Water or 2-3
unit = ''
unit = None
note = x
return amount, unit, note
def parse_ingredient_with_comma(self, tokens):
ingredient = ''
def parse_food_with_comma(self, tokens):
food = ''
note = ''
start = 0
# search for first occurrence of an argument ending in a comma
while start < len(tokens) and not tokens[start].endswith(','):
start += 1
if start == len(tokens):
# no token ending in a comma found -> use everything as ingredient
ingredient = ' '.join(tokens)
# no token ending in a comma found -> use everything as food
food = ' '.join(tokens)
else:
ingredient = ' '.join(tokens[:start + 1])[:-1]
food = ' '.join(tokens[:start + 1])[:-1]
note = ' '.join(tokens[start + 1:])
return ingredient, note
return food, note
def parse_ingredient(self, tokens):
ingredient = ''
def parse_food(self, tokens):
food = ''
note = ''
if tokens[-1].endswith(')'):
# Check if the matching opening bracket is in the same token
if (not tokens[-1].startswith('(')) and ('(' in tokens[-1]):
return self.parse_ingredient_with_comma(tokens)
return self.parse_food_with_comma(tokens)
# last argument ends with closing bracket -> look for opening bracket
start = len(tokens) - 1
while not tokens[start].startswith('(') and not start == 0:
@ -191,36 +194,48 @@ class IngredientParser:
raise ValueError
elif start < 0:
# no opening bracket anywhere -> just ignore the last bracket
ingredient, note = self.parse_ingredient_with_comma(tokens)
food, note = self.parse_food_with_comma(tokens)
else:
# opening bracket found -> split in ingredient and note, remove brackets from note # noqa: E501
# opening bracket found -> split in food and note, remove brackets from note # noqa: E501
note = ' '.join(tokens[start:])[1:-1]
ingredient = ' '.join(tokens[:start])
food = ' '.join(tokens[:start])
else:
ingredient, note = self.parse_ingredient_with_comma(tokens)
return ingredient, note
food, note = self.parse_food_with_comma(tokens)
return food, note
def parse(self, x):
def parse(self, ingredient):
"""
Main parsing function, takes an ingredient string (e.g. '1 l Water') and extracts amount, unit, food, ...
:param ingredient: string ingredient
:return: amount, unit (can be None), food, note (can be empty)
"""
# initialize default values
amount = 0
unit = None
ingredient = ''
food = ''
note = ''
unit_note = ''
if len(x) == 0:
if len(ingredient) == 0:
raise ValueError('string to parse cannot be empty')
# some people/languages put amount and unit at the end of the ingredient string
# if something like this is detected move it to the beginning so the parser can handle it
if re.search(r'^([A-z])+(.)*[1-9](\d)*\s([A-z])+', ingredient):
match = re.search(r'[1-9](\d)*\s([A-z])+', ingredient)
print(f'reording from {ingredient} to {ingredient[match.start():match.end()] + " " + ingredient.replace(ingredient[match.start():match.end()], "")}')
ingredient = ingredient[match.start():match.end()] + ' ' + ingredient.replace(ingredient[match.start():match.end()], '')
# if the string contains parenthesis early on remove it and place it at the end
# because its likely some kind of note
if re.match('(.){1,6}\s\((.[^\(\)])+\)\s', x):
match = re.search('\((.[^\(])+\)', x)
x = x[:match.start()] + x[match.end():] + ' ' + x[match.start():match.end()]
if re.match('(.){1,6}\s\((.[^\(\)])+\)\s', ingredient):
match = re.search('\((.[^\(])+\)', ingredient)
ingredient = ingredient[:match.start()] + ingredient[match.end():] + ' ' + ingredient[match.start():match.end()]
tokens = x.split()
tokens = ingredient.split() # split at each space into tokens
if len(tokens) == 1:
# there only is one argument, that must be the ingredient
ingredient = tokens[0]
# there only is one argument, that must be the food
food = tokens[0]
else:
try:
# try to parse first argument as amount
@ -232,51 +247,60 @@ class IngredientParser:
try:
if unit is not None:
# a unit is already found, no need to try the second argument for a fraction
# probably not the best method to do it, but I didn't want to make an if check and paste the exact same thing in the else as already is in the except # noqa: E501
# probably not the best method to do it, but I didn't want to make an if check and paste the exact same thing in the else as already is in the except
raise ValueError
# try to parse second argument as amount and add that, in case of '2 1/2' or '2 ½'
amount += self.parse_fraction(tokens[1])
# assume that units can't end with a comma
if len(tokens) > 3 and not tokens[2].endswith(','):
# try to use third argument as unit and everything else as ingredient, use everything as ingredient if it fails # noqa: E501
# try to use third argument as unit and everything else as food, use everything as food if it fails
try:
ingredient, note = self.parse_ingredient(tokens[3:])
food, note = self.parse_food(tokens[3:])
unit = tokens[2]
except ValueError:
ingredient, note = self.parse_ingredient(tokens[2:])
food, note = self.parse_food(tokens[2:])
else:
ingredient, note = self.parse_ingredient(tokens[2:])
food, note = self.parse_food(tokens[2:])
except ValueError:
# assume that units can't end with a comma
if not tokens[1].endswith(','):
# try to use second argument as unit and everything else as ingredient, use everything as ingredient if it fails # noqa: E501
# try to use second argument as unit and everything else as food, use everything as food if it fails
try:
ingredient, note = self.parse_ingredient(tokens[2:])
food, note = self.parse_food(tokens[2:])
if unit is None:
unit = tokens[1]
else:
note = tokens[1]
except ValueError:
ingredient, note = self.parse_ingredient(tokens[1:])
food, note = self.parse_food(tokens[1:])
else:
ingredient, note = self.parse_ingredient(tokens[1:])
food, note = self.parse_food(tokens[1:])
else:
# only two arguments, first one is the amount
# which means this is the ingredient
ingredient = tokens[1]
# which means this is the food
food = tokens[1]
except ValueError:
try:
# can't parse first argument as amount
# -> no unit -> parse everything as ingredient
ingredient, note = self.parse_ingredient(tokens)
# -> no unit -> parse everything as food
food, note = self.parse_food(tokens)
except ValueError:
ingredient = ' '.join(tokens[1:])
food = ' '.join(tokens[1:])
if unit_note not in note:
note += ' ' + unit_note
try:
unit = self.apply_unit_automation(unit.strip())
except Exception:
pass
return amount, unit, self.apply_food_automation(ingredient.strip()), note.strip()
if unit:
unit = self.apply_unit_automation(unit.strip())
food = self.apply_food_automation(food.strip())
if len(food) > Food._meta.get_field('name').max_length: # test if food name is to long
# try splitting it at a space and taking only the first arg
if len(food.split()) > 1 and len(food.split()[0]) < Food._meta.get_field('name').max_length:
note = ' '.join(food.split()[1:]) + ' ' + note
food = food.split()[0]
else:
note = food + ' ' + note
food = food[:Food._meta.get_field('name').max_length]
return amount, unit, food, note[:Ingredient._meta.get_field('note').max_length].strip()

View File

@ -3,8 +3,8 @@ from django.utils.html import format_html
from django.utils.translation import gettext as _
from django_tables2.utils import A
from .models import (CookLog, InviteLink, Keyword, Recipe, RecipeImport,
ShoppingList, Storage, Sync, SyncLog, ViewLog)
from .models import (CookLog, InviteLink, Recipe, RecipeImport,
Storage, Sync, SyncLog, ViewLog)
class ImageUrlColumn(tables.Column):
@ -121,14 +121,6 @@ class RecipeImportTable(tables.Table):
fields = ('id', 'name', 'file_path')
class ShoppingListTable(tables.Table):
id = tables.LinkColumn('view_shopping', args=[A('id')])
class Meta:
model = ShoppingList
template_name = 'generic/table_template.html'
fields = ('id', 'finished', 'created_by', 'created_at')
class InviteLinkTable(tables.Table):
link = tables.TemplateColumn(

View File

@ -26,15 +26,6 @@
{% endif %}
</h3>
</span>
{% if request.resolver_match.url_name in 'list_shopping_list' %}
<span class="col-md-3">
<a href="{% url 'view_shopping_new' %}" class="float-right">
<button class="btn btn-outline-secondary shadow-none">
<i class="fas fa-star"></i> {% trans 'Try the new shopping list' %}
</button>
</a>
</span>
{% endif %}
{% if filter %}
<br/>

View File

@ -1,36 +0,0 @@
{% extends "base.html" %}
{% load render_bundle from webpack_loader %}
{% load static %}
{% load i18n %}
{% load l10n %}
{% block title %}{% trans 'Meal-Plan' %}{% endblock %}
{% block content_fluid %}
<div id="app">
<meal-plan-view></meal-plan-view>
</div>
{% endblock %}
{% 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">
window.IMAGE_PLACEHOLDER = "{% static 'assets/recipe_no_image.svg' %}"
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
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

@ -1,921 +0,0 @@
{% extends "base.html" %}
{% comment %} TODO: Deprecate {% endcomment %}
{% load django_tables2 %}
{% load crispy_forms_tags %}
{% load static %}
{% load i18n %}
{% block title %}{% trans "Shopping List" %}{% endblock %}
{% block extra_head %}
{% include 'include/vue_base.html' %}
<link rel="stylesheet" href="{% static 'css/vue-multiselect-bs4.min.css' %}" />
<script src="{% static 'js/vue-multiselect.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>
<link rel="stylesheet" href="{% static 'css/pretty-checkbox.min.css' %}" />
{% endblock %}
{% block content %}
<div class="row">
<span class="col col-md-9">
<h2>{% trans 'Shopping List' %}</h2>
</span>
<span class="col-md-3">
<a href="{% url 'view_shopping_new' %}" class="float-right">
<button class="btn btn-outline-secondary shadow-none"><i class="fas fa-star"></i> {% trans 'Try the new shopping list' %}</button>
</a>
</span>
<div class="col col-mdd-3 text-right">
<b-form-checkbox switch size="lg" v-model="edit_mode" @change="$forceUpdate()">{% trans 'Edit' %}</b-form-checkbox>
</div>
</div>
<template v-if="shopping_list !== undefined">
<div class="text-center" v-if="loading">
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
<img class="spinner-tandoor" />
{% else %}
<i class="fas fa-spinner fa-spin fa-8x"></i>
{% endif %}
</div>
<div v-else-if="edit_mode">
<div class="row">
<div class="col col-md-6">
<div class="card">
<div class="card-header"><i class="fa fa-search"></i> {% trans 'Search' %}</div>
<div class="card-body">
<input type="text" class="form-control" v-model="recipe_query" @keyup="getRecipes" placeholder="{% trans 'Search Recipe' %}" />
<ul class="list-group" style="margin-top: 8px">
<li class="list-group-item" v-for="x in recipes">
<div class="row flex-row" style="padding-left: 0.5vw; padding-right: 0.5vw">
<div class="flex-column flex-fill my-auto"><a v-bind:href="getRecipeUrl(x.id)" target="_blank" rel="nofollow norefferer">[[x.name]]</a></div>
<div class="flex-column align-self-end">
<button class="btn btn-outline-primary shadow-none" @click="addRecipeToList(x)"><i class="fa fa-plus"></i></button>
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
<div class="col col-md-6">
<div class="card">
<div class="card-header"><i class="fa fa-shopping-cart"></i> {% trans 'Shopping Recipes' %}</div>
<div class="card-body">
<template v-if="shopping_list.recipes.length < 1"> {% trans 'No recipes selected' %} </template>
<template v-else>
<div class="row flex-row my-auto" v-for="x in shopping_list.recipes" style="margin-top: 1vh !important">
<div class="flex-column align-self-start" style="margin-right: 0.4vw">
<button class="btn btn-outline-danger" @click="removeRecipeFromList(x)"><i class="fa fa-trash"></i></button>
</div>
<div class="flex-grow-1 flex-column my-auto">
<a v-bind:href="getRecipeUrl(x.recipe)" target="_blank" rel="nofollow norefferer">[[x.recipe_name]]</a>
</div>
<div class="flex-column align-self-end">
<div class="input-group input-group-sm my-auto">
<div class="input-group-prepend">
<button class="text-muted btn btn-outline-primary shadow-none" @click="((x.servings - 1) > 0) ? x.servings -= 1 : 1">-</button>
</div>
<input class="form-control" type="number" v-model="x.servings" />
<div class="input-group-append">
<button class="text-muted btn btn-outline-primary shadow-none" @click="x.servings += 1">+</button>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
</div>
<table class="table table-sm" style="margin-top: 1vh">
<template v-for="c in display_categories">
<thead>
<tr>
<th colspan="5">[[c.name]]</th>
</tr>
</thead>
<tbody is="draggable" :list="c.entries" tag="tbody" group="people" @sort="sortEntries" @change="dragChanged(c, $event)" handle=".handle">
<tr v-for="(element, index) in c.entries" :key="element.id" v-bind:class="{ 'text-muted': element.checked }">
<td class="handle"><i class="fas fa-sort"></i></td>
<td>[[element.amount.toFixed(2)]]</td>
<td>[[element.unit.name]]</td>
<td>[[element.food.name]]</td>
<td>
<button
class="btn btn-sm btn-outline-danger"
v-if="element.list_recipe === null"
@click="shopping_list.entries = shopping_list.entries.filter(item => item.id !== element.id)"
>
<i class="fa fa-trash"></i>
</button>
</td>
</tr>
</tbody>
</template>
</table>
<div class="row" style="text-align: right">
<div class="col">
<b-form-checkbox switch v-model="entry_mode_simple" @change="$cookies.set('shopping_entry_mode_simple',!entry_mode_simple, -1)"
>{% trans 'Entry Mode' %}</b-form-checkbox
>
</div>
</div>
<div class="row" v-if="entry_mode_simple" style="margin-top: 2vh">
<div class="col-12">
<form v-on:submit.prevent="addSimpleEntry()">
<label for="id_simple_entry">{% trans 'Add Entry' %}</label>
<div class="input-group">
<input id="id_simple_entry" class="form-control" v-model="simple_entry" />
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" @click="addSimpleEntry()"><i class="fa fa-plus"></i></button>
</div>
</div>
</form>
</div>
</div>
<div class="row" v-if="!entry_mode_simple" style="margin-top: 2vh">
<div class="col-12 col-lg-3">
<input id="id_advanced_entry" class="form-control" type="number" placeholder="{% trans 'Amount' %}" v-model="new_entry.amount" ref="new_entry_amount" />
</div>
<div class="col-12 col-lg-4">
<multiselect
v-tabindex
ref="unit"
v-model="new_entry.unit"
:options="units"
:close-on-select="true"
:clear-on-select="true"
:allow-empty="true"
:preserve-search="true"
placeholder="{% trans 'Select Unit' %}"
tag-placeholder="{% trans 'Create' %}"
select-label="{% trans 'Select' %}"
:taggable="true"
@tag="addUnitType"
label="name"
track-by="name"
:multiple="false"
:loading="units_loading"
@search-change="searchUnits"
>
</multiselect>
</div>
<div class="col-12 col-lg-4">
<multiselect
v-tabindex
ref="food"
v-model="new_entry.food"
:options="foods"
:close-on-select="true"
:clear-on-select="true"
:allow-empty="true"
:preserve-search="true"
placeholder="{% trans 'Select Food' %}"
tag-placeholder="{% trans 'Create' %}"
select-label="{% trans 'Select' %}"
:taggable="true"
@tag="addFoodType"
label="name"
track-by="name"
:multiple="false"
:loading="foods_loading"
@search-change="searchFoods"
>
</multiselect>
</div>
<div class="col-12 col-lg-1 my-auto text-right">
<button class="btn btn-success btn-lg" @click="addEntry()"><i class="fa fa-plus"></i></button>
</div>
</div>
<div class="row">
<div class="col" style="margin-top: 1vh">
<label for="id_supermarket">{% trans 'Supermarket' %}</label>
<multiselect
id="id_supermarket"
v-tabindex
v-model="shopping_list.supermarket"
:options="supermarkets"
:close-on-select="true"
:clear-on-select="true"
:allow-empty="true"
:preserve-search="true"
placeholder="{% trans 'Select Supermarket' %}"
select-label="{% trans 'Select' %}"
label="name"
track-by="id"
:multiple="false"
:loading="supermarkets_loading"
@search-change="searchSupermarket"
>
</multiselect>
</div>
</div>
<div class="row">
<div class="col" style="margin-top: 1vh">
<label for="id_select_shared">{% trans 'Shared with' %}</label>
<multiselect
id="id_select_shared"
v-tabindex
v-model="shopping_list.shared"
:options="users"
:close-on-select="true"
:clear-on-select="true"
:allow-empty="true"
:preserve-search="true"
placeholder="{% trans 'Select User' %}"
select-label="{% trans 'Select' %}"
label="username"
track-by="id"
:multiple="true"
:loading="users_loading"
@search-change="searchUsers"
>
</multiselect>
</div>
</div>
<div class="row">
<div class="col" style="text-align: right; margin-top: 1vh">
<div class="form-group form-check form-group-lg">
<input class="form-check-input" style="zoom: 1.3" type="checkbox" v-model="shopping_list.finished" id="id_finished" />
<label class="form-check-label" style="zoom: 1.3" for="id_finished"> {% trans 'Finished' %}</label>
</div>
</div>
</div>
</div>
<div v-else>
{% if request.user.userpreference.shopping_auto_sync > 0 %}
<div class="row" v-if="!onLine">
<div class="col col-md-12">
<div class="alert alert-warning" role="alert">{% trans 'You are offline, shopping list might not synchronize.' %}</div>
</div>
</div>
{% endif %}
<div class="row" style="margin-top: 8px">
<div class="col col-md-12">
<table class="table">
<template v-for="c in display_categories">
<template v-if="c.entries.filter(item => item.checked === false).length > 0">
<tr>
<td colspan="4">[[c.name]]</td>
</tr>
<tr v-for="x in c.entries">
<template v-if="!x.checked">
<td><input type="checkbox" style="zoom: 1.4" v-model="x.checked" @change="entryChecked(x)" /></td>
<td>[[x.amount.toFixed(2)]]</td>
<td>[[x.unit.name]]</td>
<td>[[x.food.name]] <span class="text-muted" v-if="x.recipes.length > 0">([[x.recipes.join(', ')]])</span></td>
</template>
</tr>
</template>
</template>
<tr>
<td colspan="4"></td>
</tr>
<template v-for="c in display_categories">
<tr v-for="x in c.entries" class="text-muted">
<template v-if="x.checked">
<td><input type="checkbox" style="zoom: 1.4" v-model="x.checked" @change="entryChecked(x)" /></td>
<td>[[x.amount]]</td>
<td>[[x.unit.name]]</td>
<td>[[x.food.name]]</td>
</template>
</tr>
</template>
</table>
</div>
</div>
</div>
<div class="row" style="margin-top: 2vh">
<div class="col" style="text-align: right">
<b-button class="btn btn-info" v-b-modal.id_modal_export><i class="fas fa-file-export"></i> {% trans 'Export' %}</b-button>
<button class="btn btn-success" @click="updateShoppingList()" v-if="edit_mode"><i class="fas fa-save"></i> {% trans 'Save' %}</button>
</div>
</div>
<br />
<br />
<b-modal id="id_modal_export" title="{% trans 'Copy/Export' %}">
<div class="row">
<div class="col col-12">
<label>
{% trans 'List Prefix' %}
<input class="form-control" v-model="export_text_prefix" />
</label>
</div>
</div>
<div class="row">
<div class="col col-12">
<b-form-textarea class="form-control" max-rows="8" v-model="export_text"> </b-form-textarea>
</div>
</div>
</b-modal>
</template>
{% endblock %} {% block script %}
<script src="{% url 'javascript-catalog' %}"></script>
<script type="application/javascript">
let csrftoken = Cookies.get('csrftoken');
Vue.http.headers.common['X-CSRFToken'] = csrftoken;
Vue.component('vue-multiselect', window.VueMultiselect.default)
let app = new Vue({
components: {
Multiselect: window.VueMultiselect.default
},
delimiters: ['[[', ']]'],
el: '#id_base_container',
data: {
shopping_list_id: {% if shopping_list_id %}{{ shopping_list_id }}{% else %}null{% endif %},
loading: true,
{% if edit %}
edit_mode: true,
{% else %}
edit_mode: false,
{% endif %}
export_text_prefix: '',
recipe_query: '',
recipes: [],
shopping_list: undefined,
new_entry: {
unit: undefined,
amount: undefined,
food: undefined,
},
unchecked_entries: 0,
foods: [],
foods_loading: false,
units: [],
units_loading: false,
supermarkets: [],
supermarkets_loading: false,
users: [],
users_loading: false,
onLine: navigator.onLine,
simple_entry: '',
auto_sync_blocked: false,
auto_sync_running: false,
entry_mode_simple: $cookies.isKey('shopping_entry_mode_simple') ? ($cookies.get('shopping_entry_mode_simple') === 'true') : true,
},
directives: {
tabindex: {
inserted(el) {
el.setAttribute('tabindex', 0);
}
}
},
computed: {
servings_cache() {
let cache = {}
this.shopping_list.recipes.forEach((r) => {
cache[r.id] = r.servings;
})
return cache
},
recipe_cache() {
let cache = {}
this.shopping_list.recipes.forEach((r) => {
cache[r.id] = r.recipe_name;
})
return cache
},
display_categories() {
this.unchecked_entries = 0
let categories = {
no_category: {
name: gettext('Uncategorized'),
id: -1,
entries: [],
order: -1
}
}
this.shopping_list.entries.forEach((e) => {
if (e.food.supermarket_category !== null) {
categories[e.food.supermarket_category.id] = {
name: e.food.supermarket_category.name,
id: e.food.supermarket_category.id,
order: 0,
entries: []
};
}
})
if (this.shopping_list.supermarket !== null) {
this.shopping_list.supermarket.category_to_supermarket.forEach(el => {
categories[el.category.id] = {
name: el.category.name,
id: el.category.id,
order: el.order,
entries: []
};
})
}
this.shopping_list.entries.forEach(element => {
let item = {}
Object.assign(item, element);
item.recipes = []
let entry = this.findMergeEntry(categories, item)
if (entry !== undefined) {
let servings = 1
if (item.list_recipe in this.servings_cache) {
servings = this.servings_cache[item.list_recipe]
}
entry.amount += item.amount * servings
if (item.list_recipe !== null && entry.recipes.indexOf(this.recipe_cache[item.list_recipe]) === -1) {
entry.recipes.push(this.recipe_cache[item.list_recipe])
}
entry.entries.push(item.id)
} else {
if (item.list_recipe !== null) {
item.amount = item.amount * this.servings_cache[item.list_recipe]
}
item.unit = ((element.unit !== undefined && element.unit !== null) ? element.unit : {'name': ''})
item.entries = [element.id]
if (element.list_recipe !== null) {
item.recipes.push(this.recipe_cache[element.list_recipe])
}
if (item.food.supermarket_category !== null) {
categories[item.food.supermarket_category.id].entries.push(item)
} else {
categories['no_category'].entries.push(item)
}
}
if (!item.checked) {
this.unchecked_entries += 1
}
});
let ordered_categories = []
for (let [i, v] of Object.entries(categories)) {
ordered_categories.push(v)
}
ordered_categories.sort(function (a, b) {
if (a.order < b.order) {
return -1
} else if (a.order > b.order) {
return 1
} else {
return 0
}
})
return ordered_categories
},
export_text() {
let text = ''
for (let c of this.display_categories) {
for (let e of c.entries.filter(item => item.checked === false)) {
text += `${this.export_text_prefix}${e.amount} ${e.unit.name} ${e.food.name} \n`
}
}
return text
}
},
mounted: function () {
this.loadShoppingList()
{% if recipes %}
this.loading = true
this.edit_mode = true
let loadingRecipes = []
{% for r in recipes %}
loadingRecipes.push(this.loadInitialRecipe({{ r.recipe }}, {{ r.servings }}))
{% endfor %}
Promise.allSettled(loadingRecipes).then(() => {
this.loading = false
})
{% endif %}
{% if request.user.userpreference.shopping_auto_sync > 0 %}
setInterval(() => {
if ((this.shopping_list_id !== null) && !this.edit_mode && window.navigator.onLine && !this.auto_sync_blocked && !this.auto_sync_running) {
this.auto_sync_running = true
this.loadShoppingList(true)
}
}, {% widthratio request.user.userpreference.shopping_auto_sync 1 1000 %})
window.addEventListener('online', this.updateOnlineStatus);
window.addEventListener('offline', this.updateOnlineStatus);
{% endif %}
this.searchUsers('')
this.searchSupermarket('')
this.searchUnits('')
this.searchFoods('')
},
methods: {
findMergeEntry: function (categories, entry) {
for (let [i, e] of Object.entries(categories)) {
let found_entry = e.entries.find(item => {
if (entry.food.id === item.food.id && entry.food.name === item.food.name) {
if (entry.unit === null && item.unit === null) {
return true
} else if (entry.unit !== null && item.unit !== null && entry.unit.id === item.unit.id && entry.unit.name === item.unit.name) {
return true
}
}
})
if (found_entry !== undefined) {
return found_entry
}
}
return undefined
},
updateOnlineStatus(e) {
const {
type
} = e;
this.onLine = type === 'online';
},
makeToast: function (title, message, variant = null) {
this.$bvToast.toast(message, {
title: title,
variant: variant,
toaster: 'b-toaster-top-center',
solid: true
})
},
loadInitialRecipe: function (recipe, servings) {
servings = 1
return this.$http.get('{% url 'api:recipe-detail' 123456 %}'.replace('123456', recipe)).then((response) => {
this.addRecipeToList(response.data, servings)
}).catch((err) => {
console.log("getRecipes error: ", err);
this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
})
},
loadShoppingList: function (autosync = false) {
if (this.shopping_list_id) {
this.$http.get("{% url 'api:shoppinglist-detail' 123456 %}".replace('123456', this.shopping_list_id) + ((autosync) ? '?autosync=true' : '')).then((response) => {
if (!autosync) {
this.shopping_list = response.body
this.loading = false
} else {
if (!this.auto_sync_blocked) {
let check_map = {}
for (let e of response.body.entries) {
check_map[e.id] = {checked: e.checked}
}
for (let se of this.shopping_list.entries) {
if (check_map[se.id] !== undefined) {
se.checked = check_map[se.id].checked
}
}
}
this.auto_sync_running = false
}
if (this.shopping_list.entries.length === 0) {
this.edit_mode = true
}
console.log(response.data)
}).catch((err) => {
console.log(err)
this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
})
} else {
this.shopping_list = {
"recipes": [],
"entries": [],
"entries_display": [],
"shared": [{% for u in request.user.userpreference.plan_share.all %}
{'id': {{ u.pk }}, 'username': '{{ u.get_user_name }}'},
{% endfor %}],
"created_by": {{ request.user.pk }},
"supermarket": null
}
this.loading = false
if (this.shopping_list.entries.length === 0) {
this.edit_mode = true
}
}
},
updateShoppingList: function () {
this.loading = true
let recipe_promises = []
for (let i in this.shopping_list.recipes) {
if (this.shopping_list.recipes[i].created) {
console.log('updating recipe', this.shopping_list.recipes[i])
recipe_promises.push(this.$http.post("{% url 'api:shoppinglistrecipe-list' %}", this.shopping_list.recipes[i], {}).then((response) => {
let old_id = this.shopping_list.recipes[i].id
console.log("list recipe create response ", response.body)
this.$set(this.shopping_list.recipes, i, response.body)
for (let e of this.shopping_list.entries.filter(item => item.list_recipe === old_id)) {
console.log("found recipe updating ID")
e.list_recipe = this.shopping_list.recipes[i].id
}
}).catch((err) => {
console.log(err)
this.makeToast(gettext('Error'), gettext("There was an error updating a resource!") + err.bodyText, 'danger')
}))
}
}
Promise.allSettled(recipe_promises).then(() => {
console.log("proceeding to update shopping list", this.shopping_list)
if (this.shopping_list.id === undefined) {
return this.$http.post("{% url 'api:shoppinglist-list' %}", this.shopping_list, {}).then((response) => {
console.log(response)
this.makeToast(gettext('Updated'), gettext("Object created successfully!"), 'success')
this.loading = false
this.shopping_list = response.body
this.shopping_list_id = this.shopping_list.id
window.history.pushState('shopping_list', '{% trans 'Shopping List' %}', "{% url 'view_shopping' 123456 %}".replace('123456', this.shopping_list_id));
}).catch((err) => {
console.log(err)
this.makeToast(gettext('Error'), gettext("There was an error creating a resource!") + err.bodyText, 'danger')
this.loading = false
})
} else {
return this.$http.put("{% url 'api:shoppinglist-detail' 123456 %}".replace('123456', this.shopping_list.id), this.shopping_list, {}).then((response) => {
console.log(response)
this.shopping_list = response.body
this.makeToast(gettext('Updated'), gettext("Changes saved successfully!"), 'success')
this.loading = false
}).catch((err) => {
console.log(err)
this.makeToast(gettext('Error'), gettext("There was an error updating a resource!") + err.bodyText, 'danger')
this.loading = false
})
}
})
},
sortEntries: function (a, b) {
},
dragChanged: function (category, evt) {
if (evt.added !== undefined) {
if (evt.added.element.id === undefined) {
this.makeToast(gettext('Warning'), gettext('This feature is only available after saving the shopping list'), 'warning')
} else {
this.shopping_list.entries.forEach(entry => {
if (entry.id === evt.added.element.id) {
if (category.id === -1) {
entry.food.supermarket_category = null
} else {
entry.food.supermarket_category = {
name: category.name,
id: category.id
}
}
this.$http.put(("{% url 'api:food-detail' 123456 %}").replace('123456', entry.food.id), entry.food).then((response) => {
}).catch((err) => {
this.makeToast(gettext('Error'), gettext('There was an error updating a resource!') + err.bodyText, 'danger')
})
}
})
}
}
},
entryChecked: function (entry) {
this.auto_sync_blocked = true
let updates = []
this.shopping_list.entries.forEach((item) => {
if (entry.entries.includes(item.id)) {
item.checked = entry.checked
updates.push(this.$http.put("{% url 'api:shoppinglistentry-detail' 123456 %}".replace('123456', item.id), item, {}).then((response) => {
}).catch((err) => {
console.log(err)
this.makeToast(gettext('Error'), gettext('There was an error updating a resource!') + err.bodyText, 'danger')
this.loading = false
}))
}
})
Promise.allSettled(updates).then(() => {
this.auto_sync_blocked = false
if (this.unchecked_entries < 1) {
this.shopping_list.finished = true
this.$http.put("{% url 'api:shoppinglist-detail' 123456 %}".replace('123456', this.shopping_list.id), this.shopping_list, {}).then((response) => {
this.makeToast(gettext('Finished'), gettext('Shopping list finished!'), 'success')
}).catch((err) => {
console.log(err)
this.makeToast(gettext('Error'), gettext("There was an error updating a resource!") + err.bodyText, 'danger')
})
} else {
if (this.unchecked_entries > 0 && this.shopping_list.finished) {
this.shopping_list.finished = false
this.$http.put("{% url 'api:shoppinglist-detail' 123456 %}".replace('123456', this.shopping_list.id), this.shopping_list, {}).then((response) => {
this.makeToast(gettext('Open'), gettext('Shopping list reopened!'), 'success')
}).catch((err) => {
console.log(err)
this.makeToast(gettext('Error'), gettext("There was an error updating a resource!") + err.bodyText, 'danger')
})
}
}
})
},
addEntry: function () {
if (this.new_entry.food !== undefined) {
this.shopping_list.entries.push({
'list_recipe': null,
'food': this.new_entry.food,
'unit': this.new_entry.unit,
'amount': parseFloat(this.new_entry.amount),
'order': 0,
'checked': false,
})
this.new_entry = {
unit: undefined,
amount: undefined,
food: undefined,
}
this.$refs.new_entry_amount.focus();
} else {
this.makeToast(gettext('Error'), gettext('Please enter a valid food'), 'danger')
}
},
addSimpleEntry: function () {
if (this.simple_entry !== '') {
this.$http.post('{% url 'api_ingredient_from_string' %}', {text: this.simple_entry}, {emulateJSON: true}).then((response) => {
console.log(response)
let unit = null
if (response.body.unit !== '') {
unit = {'name': response.body.unit}
}
this.shopping_list.entries.push({
'list_recipe': null,
'food': {'name': response.body.food, supermarket_category: null},
'unit': unit,
'amount': response.body.amount,
'order': 0,
'checked': false,
})
this.simple_entry = ''
}).catch((err) => {
console.log(err)
this.makeToast(gettext('Error'), gettext('Something went wrong while trying to add the simple entry.'), 'danger')
})
}
},
getRecipes: function () {
let url = "{% url 'api:recipe-list' %}?page_size=5&internal=true"
if (this.recipe_query !== '') {
url += '&query=' + this.recipe_query;
} else {
this.recipes = []
return
}
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')
})
},
getRecipeUrl: function (id) {
return '{% url 'view_recipe' 123456 %}'.replace('123456', id)
},
addRecipeToList: function (recipe, servings = 1) {
let slr = {
"created": true,
"id": Math.random() * 1000,
"recipe": recipe.id,
"recipe_name": recipe.name,
"servings": servings,
}
this.shopping_list.recipes.push(slr)
this.$http.get('{% url 'api:recipe-detail' 123456 %}'.replace('123456', recipe.id)).then((response) => {
for (let s of response.data.steps) {
for (let i of s.ingredients) {
if (!i.is_header && i.food !== null && !i.food.ignore_food) {
this.shopping_list.entries.push({
'list_recipe': slr.id,
'food': i.food,
'unit': i.unit,
'amount': i.amount,
'order': 0
})
}
}
}
}).catch((err) => {
this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
})
},
removeRecipeFromList: function (slr) {
this.shopping_list.entries = this.shopping_list.entries.filter(item => item.list_recipe !== slr.id)
this.shopping_list.recipes = this.shopping_list.recipes.filter(item => item !== slr)
},
searchKeywords: function (query) {
this.keywords_loading = true
this.$http.get("{% url 'api:keyword-list' %}" + '?query=' + query + '&limit=10').then((response) => {
this.keywords = response.data.results;
this.keywords_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) => {
this.units = response.data.results;
this.units_loading = false
}).catch((err) => {
this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
})
},
searchFoods: function (query) {
this.foods_loading = true
this.$http.get("{% url 'api:food-list' %}" + '?query=' + query + '&limit=10').then((response) => {
this.foods = response.data.results
this.foods_loading = false
}).catch((err) => {
this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
})
},
addFoodType: function (tag, index) {
let new_food = {'name': tag, supermarket_category: null}
this.foods.push(new_food)
this.new_entry.food = new_food
},
addUnitType: function (tag, index) {
let new_unit = {'name': tag}
this.units.push(new_unit)
this.new_entry.unit = new_unit
},
searchUsers: function (query) {
this.users_loading = true
this.$http.get("{% url 'api:username-list' %}" + '?query=' + query + '&limit=10').then((response) => {
this.users = response.data
this.users_loading = false
}).catch((err) => {
this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
})
},
searchSupermarket: function (query) {
this.supermarkets_loading = true
this.$http.get("{% url 'api:supermarket-list' %}" + '?query=' + query + '&limit=10').then((response) => {
this.supermarkets = response.data
this.supermarkets_loading = false
}).catch((err) => {
this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
})
},
},
beforeDestroy() {
window.removeEventListener('online', this.updateOnlineStatus);
window.removeEventListener('offline', this.updateOnlineStatus);
}
});
</script>
{% endblock %}

View File

@ -10,27 +10,11 @@
{% block content_fluid %}
<div id="app">
<import-view></import-view>
</div>
{{ data }}
{% endblock %}
{% 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">
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
window.API_TOKEN = '{{ api_token }}'
window.BOOKMARKLET_IMPORT_ID = {{ bookmarklet_import_id }}
</script>
{% render_bundle 'import_view' %}
{% endblock %}

View File

@ -4,7 +4,7 @@ from cookbook.helper.ingredient_parser import IngredientParser
def test_ingredient_parser():
expectations = {
"2¼ l Wasser": (2.25, "l", "Wasser", ""),
"2¼l Wasser": (2.25, "l", "Wasser", ""),
"3¼l Wasser": (3.25, "l", "Wasser", ""),
"¼ l Wasser": (0.25, "l", "Wasser", ""),
"3l Wasser": (3, "l", "Wasser", ""),
"4 l Wasser": (4, "l", "Wasser", ""),
@ -32,7 +32,7 @@ def test_ingredient_parser():
"1 Ei(er)": (1, None, "Ei(er)", ""),
"1 Prise(n) Salz": (1, "Prise(n)", "Salz", ""),
"etwas Wasser, lauwarmes": (0, None, "etwas Wasser", "lauwarmes"),
"Strudelblätter, fertige, für zwei Strudel": (0,None, "Strudelblätter", "fertige, für zwei Strudel"),
"Strudelblätter, fertige, für zwei Strudel": (0, None, "Strudelblätter", "fertige, für zwei Strudel"),
"barrel-aged Bourbon": (0, None, "barrel-aged Bourbon", ""),
"golden syrup": (0, None, "golden syrup", ""),
"unsalted butter, for greasing": (0, None, "unsalted butter", "for greasing"),
@ -58,7 +58,15 @@ def test_ingredient_parser():
"2L Wasser": (2, "L", "Wasser", ""),
"1 (16 ounce) package dry lentils, rinsed": (1, "package", "dry lentils, rinsed", "16 ounce"),
"2-3 c Water": (2, "c", "Water", "2-3"),
"Pane (raffermo o secco) 80 g": (0, "", "Pane 80 g", "raffermo o secco"), # TODO this is actually not a good result but currently expected
"Pane (raffermo o secco) 80 g": (80, "g", "Pane", "raffermo o secco"),
"1 Knoblauchzehe(n), gehackt oder gepresst": (1.0, None, 'Knoblauchzehe(n)', 'gehackt oder gepresst'),
# test for over long food entries to get properly split into the note field
"1 Lorem ipsum dolor sit amet consetetur sadipscing elitr sed diam nonumy eirmod tempor invidunt ut l Lorem ipsum dolor sit amet consetetur sadipscing elitr sed diam nonumy eirmod tempor invidunt ut l": (
1.0, 'Lorem', 'ipsum', 'dolor sit amet consetetur sadipscing elitr sed diam nonumy eirmod tempor invidunt ut l Lorem ipsum dolor sit amet consetetur sadipscing elitr sed diam nonumy eirmod tempor invidunt ut l'),
"1 LoremipsumdolorsitametconsetetursadipscingelitrseddiamnonumyeirmodtemporinviduntutlLoremipsumdolorsitametconsetetursadipscingelitrseddiamnonumyeirmodtemporinviduntutl": (
1.0, None, 'LoremipsumdolorsitametconsetetursadipscingelitrseddiamnonumyeirmodtemporinviduntutlLoremipsumdolorsitametconsetetursadipscingeli',
'LoremipsumdolorsitametconsetetursadipscingelitrseddiamnonumyeirmodtemporinviduntutlLoremipsumdolorsitametconsetetursadipscingelitrseddiamnonumyeirmodtemporinviduntutl')
}
# for German you could say that if an ingredient does not have
# an amount # and it starts with a lowercase letter, then that
@ -70,4 +78,5 @@ def test_ingredient_parser():
for key, val in expectations.items():
count += 1
parsed = ingredient_parser.parse(key)
print(f'testing if {key} becomes {val}')
assert parsed == val

View File

@ -64,10 +64,8 @@ urlpatterns = [
path('books/', views.books, name='view_books'),
path('plan/', views.meal_plan, name='view_plan'),
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'),
path('shopping/latest/', views.latest_shopping_list, name='view_shopping_latest'),
path('shopping/new/', lists.shopping_list_new, name='view_shopping_new'),
path('shopping/latest/', lists.shopping_list, name='view_shopping_latest'),
path('shopping/', lists.shopping_list, name='view_shopping'),
path('settings/', views.user_settings, name='view_settings'),
path('history/', views.history, name='view_history'),
path('supermarket/', views.supermarket, name='view_supermarket'),

View File

@ -1,14 +1,13 @@
from datetime import datetime
from django.db.models import Q, Sum
from django.db.models import Sum
from django.shortcuts import render
from django.utils.translation import gettext as _
from django_tables2 import RequestConfig
from cookbook.filters import ShoppingListFilter
from cookbook.helper.permission_helper import group_required
from cookbook.models import InviteLink, RecipeImport, ShoppingList, Storage, SyncLog, UserFile
from cookbook.tables import (ImportLogTable, InviteLinkTable, RecipeImportTable, ShoppingListTable,
from cookbook.models import InviteLink, RecipeImport, Storage, SyncLog, UserFile
from cookbook.tables import (ImportLogTable, InviteLinkTable, RecipeImportTable,
StorageTable)
@ -41,20 +40,12 @@ def recipe_import(request):
@group_required('user')
def shopping_list(request):
f = ShoppingListFilter(request.GET, queryset=ShoppingList.objects.filter(space=request.space).filter(
Q(created_by=request.user) | Q(shared=request.user)).distinct().all().order_by('finished', 'created_at'))
table = ShoppingListTable(f.qs)
RequestConfig(request, paginate={'per_page': 25}).configure(table)
return render(
request,
'generic/list_template.html',
'shoppinglist_template.html',
{
'title': _("Shopping Lists"),
'table': table,
'filter': f,
'create_url': 'view_shopping'
"title": _("Shopping List"),
}
)
@ -205,7 +196,7 @@ def custom_filter(request):
def user_file(request):
try:
current_file_size_mb = UserFile.objects.filter(space=request.space).aggregate(Sum('file_size_kb'))[
'file_size_kb__sum'] / 1000
'file_size_kb__sum'] / 1000
except TypeError:
current_file_size_mb = 0
@ -237,15 +228,3 @@ def step(request):
}
}
)
@group_required('user')
def shopping_list_new(request):
return render(
request,
'shoppinglist_template.html',
{
"title": _("New Shopping List"),
}
)

View File

@ -11,9 +11,9 @@ from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth.models import Group
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from django.db.models import Avg, Q, Sum
from django.db.models import Avg, Q
from django.db.models.functions import Lower
from django.http import HttpResponseRedirect, JsonResponse
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse, reverse_lazy
from django.utils import timezone
@ -27,9 +27,9 @@ from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, ShoppingP
SpaceCreateForm, SpaceJoinForm, SpacePreferenceForm, User,
UserCreateForm, UserNameForm, UserPreference, UserPreferenceForm)
from cookbook.helper.permission_helper import group_required, has_group_permission, share_link_valid
from cookbook.models import (Comment, CookLog, Food, FoodInheritField, InviteLink, Keyword,
from cookbook.models import (Comment, CookLog, Food, InviteLink, Keyword,
MealPlan, RecipeImport, SearchFields, SearchPreference, ShareLink,
ShoppingList, Space, Unit, UserFile, ViewLog)
Space, Unit, ViewLog)
from cookbook.tables import (CookLogTable, InviteLinkTable, RecipeTable, RecipeTableSmall,
ViewLogTable)
from cookbook.views.data import Object
@ -256,35 +256,6 @@ def meal_plan_entry(request, pk):
return render(request, 'meal_plan_entry.html', {'plan': plan, 'same_day_plan': same_day_plan})
@group_required('user')
def latest_shopping_list(request):
sl = ShoppingList.objects.filter(Q(created_by=request.user) | Q(shared=request.user)).filter(finished=False,
space=request.space).order_by(
'-created_at').first()
if sl:
return HttpResponseRedirect(reverse('view_shopping', kwargs={'pk': sl.pk}) + '?edit=true')
else:
return HttpResponseRedirect(reverse('view_shopping') + '?edit=true')
@group_required('user')
def shopping_list(request, pk=None): # TODO deprecate
html_list = request.GET.getlist('r')
recipes = []
for r in html_list:
r = r.replace('[', '').replace(']', '')
if len(r) < 10000 and re.match(r'^([0-9])+,([0-9])+[.]*([0-9])*$', r):
rid, multiplier = r.split(',')
if recipe := Recipe.objects.filter(pk=int(rid), space=request.space).first():
recipes.append({'recipe': recipe.id, 'multiplier': multiplier})
edit = True if 'edit' in request.GET and request.GET['edit'] == 'true' else False
return render(request, 'shopping_list.html', {'shopping_list_id': pk, 'recipes': recipes, 'edit': edit})
@group_required('guest')
def user_settings(request):
if request.space.demo:
@ -662,10 +633,15 @@ def test(request):
if not settings.DEBUG:
return HttpResponseRedirect(reverse('index'))
if (api_token := Token.objects.filter(user=request.user).first()) is None:
api_token = Token.objects.create(user=request.user)
from cookbook.helper.ingredient_parser import IngredientParser
parser = IngredientParser(request, False)
return render(request, 'test.html', {'api_token': api_token})
data = {
'original': '1 LoremipsumdolorsitametconsetetursadipscingelitrseddiamnonumyeirmodtemporinviduntutlLoremipsumdolorsitametconsetetursadipscingelitrseddiamnonumyeirmodtemporinviduntutl'
}
data['parsed'] = parser.parse(data['original'])
return render(request, 'test.html', {'data': data})
def test2(request):

View File

@ -14,10 +14,7 @@
<button class="dropdown-item" @click="$bvModal.show(`id_modal_add_book_${modal_id}`)"><i class="fas fa-bookmark fa-fw"></i> {{ $t("Manage_Books") }}</button>
</a>
<a class="dropdown-item" :href="`${resolveDjangoUrl('view_shopping')}?r=[${recipe.id},${servings_value}]`" v-if="recipe.internal" target="_blank" rel="noopener noreferrer">
<i class="fas fa-shopping-cart fa-fw"></i> {{ $t("Add_to_Shopping") }}
</a>
<a class="dropdown-item" v-if="recipe.internal" @click="addToShopping" href="#"> <i class="fas fa-shopping-cart fa-fw"></i> {{ $t("create_shopping_new") }} </a>
<a class="dropdown-item" v-if="recipe.internal" @click="addToShopping" href="#"> <i class="fas fa-shopping-cart fa-fw"></i> {{ $t("Add_to_Shopping") }} </a>
<a class="dropdown-item" @click="createMealPlan" href="javascript:void(0);"><i class="fas fa-calendar fa-fw"></i> {{ $t("Add_to_Plan") }} </a>

View File

@ -270,7 +270,6 @@
"CategoryInstruction": "Drag categories to change the order categories appear in shopping list.",
"shopping_recent_days_desc": "Days of recent shopping list entries to display.",
"shopping_recent_days": "Recent Days",
"create_shopping_new": "Add to NEW Shopping List",
"download_pdf": "Download PDF",
"download_csv": "Download CSV",
"csv_delim_help": "Delimiter to use for CSV exports.",