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 django_scopes import scopes_disabled
from cookbook.forms import MultiSelectWidget from cookbook.forms import MultiSelectWidget
from cookbook.models import Food, Keyword, Recipe, ShoppingList from cookbook.models import Food, Keyword, Recipe
with scopes_disabled(): with scopes_disabled():
class RecipeFilter(django_filters.FilterSet): class RecipeFilter(django_filters.FilterSet):
@ -60,22 +60,3 @@ with scopes_disabled():
class Meta: class Meta:
model = Recipe model = Recipe
fields = ['name', 'keywords', 'foods', 'internal'] 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 django.core.cache import caches
from cookbook.models import Unit, Food, Automation from cookbook.models import Unit, Food, Automation, Ingredient
class IngredientParser: class IngredientParser:
@ -46,7 +46,7 @@ class IngredientParser:
def apply_food_automation(self, food): 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 :param food: unit as string
:return: food as string (possibly changed by automation) :return: food as string (possibly changed by automation)
""" """
@ -155,33 +155,36 @@ class IngredientParser:
except ValueError: except ValueError:
unit = x[end:] 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 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 note = x
return amount, unit, note return amount, unit, note
def parse_ingredient_with_comma(self, tokens): def parse_food_with_comma(self, tokens):
ingredient = '' food = ''
note = '' note = ''
start = 0 start = 0
# search for first occurrence of an argument ending in a comma # search for first occurrence of an argument ending in a comma
while start < len(tokens) and not tokens[start].endswith(','): while start < len(tokens) and not tokens[start].endswith(','):
start += 1 start += 1
if start == len(tokens): if start == len(tokens):
# no token ending in a comma found -> use everything as ingredient # no token ending in a comma found -> use everything as food
ingredient = ' '.join(tokens) food = ' '.join(tokens)
else: else:
ingredient = ' '.join(tokens[:start + 1])[:-1] food = ' '.join(tokens[:start + 1])[:-1]
note = ' '.join(tokens[start + 1:]) note = ' '.join(tokens[start + 1:])
return ingredient, note return food, note
def parse_ingredient(self, tokens): def parse_food(self, tokens):
ingredient = '' food = ''
note = '' note = ''
if tokens[-1].endswith(')'): if tokens[-1].endswith(')'):
# Check if the matching opening bracket is in the same token # Check if the matching opening bracket is in the same token
if (not tokens[-1].startswith('(')) and ('(' in tokens[-1]): 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 # last argument ends with closing bracket -> look for opening bracket
start = len(tokens) - 1 start = len(tokens) - 1
while not tokens[start].startswith('(') and not start == 0: while not tokens[start].startswith('(') and not start == 0:
@ -191,36 +194,48 @@ class IngredientParser:
raise ValueError raise ValueError
elif start < 0: elif start < 0:
# no opening bracket anywhere -> just ignore the last bracket # 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: 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] note = ' '.join(tokens[start:])[1:-1]
ingredient = ' '.join(tokens[:start]) food = ' '.join(tokens[:start])
else: else:
ingredient, note = self.parse_ingredient_with_comma(tokens) food, note = self.parse_food_with_comma(tokens)
return ingredient, note 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 # initialize default values
amount = 0 amount = 0
unit = None unit = None
ingredient = '' food = ''
note = '' note = ''
unit_note = '' unit_note = ''
if len(x) == 0: if len(ingredient) == 0:
raise ValueError('string to parse cannot be empty') 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 # if the string contains parenthesis early on remove it and place it at the end
# because its likely some kind of note # because its likely some kind of note
if re.match('(.){1,6}\s\((.[^\(\)])+\)\s', x): if re.match('(.){1,6}\s\((.[^\(\)])+\)\s', ingredient):
match = re.search('\((.[^\(])+\)', x) match = re.search('\((.[^\(])+\)', ingredient)
x = x[:match.start()] + x[match.end():] + ' ' + x[match.start():match.end()] 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: if len(tokens) == 1:
# there only is one argument, that must be the ingredient # there only is one argument, that must be the food
ingredient = tokens[0] food = tokens[0]
else: else:
try: try:
# try to parse first argument as amount # try to parse first argument as amount
@ -232,51 +247,60 @@ class IngredientParser:
try: try:
if unit is not None: if unit is not None:
# a unit is already found, no need to try the second argument for a fraction # 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 raise ValueError
# try to parse second argument as amount and add that, in case of '2 1/2' or '2 ½' # try to parse second argument as amount and add that, in case of '2 1/2' or '2 ½'
amount += self.parse_fraction(tokens[1]) amount += self.parse_fraction(tokens[1])
# assume that units can't end with a comma # assume that units can't end with a comma
if len(tokens) > 3 and not tokens[2].endswith(','): 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: try:
ingredient, note = self.parse_ingredient(tokens[3:]) food, note = self.parse_food(tokens[3:])
unit = tokens[2] unit = tokens[2]
except ValueError: except ValueError:
ingredient, note = self.parse_ingredient(tokens[2:]) food, note = self.parse_food(tokens[2:])
else: else:
ingredient, note = self.parse_ingredient(tokens[2:]) food, note = self.parse_food(tokens[2:])
except ValueError: except ValueError:
# assume that units can't end with a comma # assume that units can't end with a comma
if not tokens[1].endswith(','): 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: try:
ingredient, note = self.parse_ingredient(tokens[2:]) food, note = self.parse_food(tokens[2:])
if unit is None: if unit is None:
unit = tokens[1] unit = tokens[1]
else: else:
note = tokens[1] note = tokens[1]
except ValueError: except ValueError:
ingredient, note = self.parse_ingredient(tokens[1:]) food, note = self.parse_food(tokens[1:])
else: else:
ingredient, note = self.parse_ingredient(tokens[1:]) food, note = self.parse_food(tokens[1:])
else: else:
# only two arguments, first one is the amount # only two arguments, first one is the amount
# which means this is the ingredient # which means this is the food
ingredient = tokens[1] food = tokens[1]
except ValueError: except ValueError:
try: try:
# can't parse first argument as amount # can't parse first argument as amount
# -> no unit -> parse everything as ingredient # -> no unit -> parse everything as food
ingredient, note = self.parse_ingredient(tokens) food, note = self.parse_food(tokens)
except ValueError: except ValueError:
ingredient = ' '.join(tokens[1:]) food = ' '.join(tokens[1:])
if unit_note not in note: if unit_note not in note:
note += ' ' + unit_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.utils.translation import gettext as _
from django_tables2.utils import A from django_tables2.utils import A
from .models import (CookLog, InviteLink, Keyword, Recipe, RecipeImport, from .models import (CookLog, InviteLink, Recipe, RecipeImport,
ShoppingList, Storage, Sync, SyncLog, ViewLog) Storage, Sync, SyncLog, ViewLog)
class ImageUrlColumn(tables.Column): class ImageUrlColumn(tables.Column):
@ -121,14 +121,6 @@ class RecipeImportTable(tables.Table):
fields = ('id', 'name', 'file_path') 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): class InviteLinkTable(tables.Table):
link = tables.TemplateColumn( link = tables.TemplateColumn(

View File

@ -26,15 +26,6 @@
{% endif %} {% endif %}
</h3> </h3>
</span> </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 %} {% if filter %}
<br/> <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 %} {% block content_fluid %}
<div id="app"> {{ data }}
<import-view></import-view>
</div>
{% endblock %} {% endblock %}
{% block 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">
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
window.API_TOKEN = '{{ api_token }}'
window.BOOKMARKLET_IMPORT_ID = {{ bookmarklet_import_id }}
</script>
{% render_bundle 'import_view' %}
{% endblock %} {% endblock %}

View File

@ -4,7 +4,7 @@ from cookbook.helper.ingredient_parser import IngredientParser
def test_ingredient_parser(): def test_ingredient_parser():
expectations = { expectations = {
"2¼ l Wasser": (2.25, "l", "Wasser", ""), "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", ""), "¼ l Wasser": (0.25, "l", "Wasser", ""),
"3l Wasser": (3, "l", "Wasser", ""), "3l Wasser": (3, "l", "Wasser", ""),
"4 l Wasser": (4, "l", "Wasser", ""), "4 l Wasser": (4, "l", "Wasser", ""),
@ -58,7 +58,15 @@ def test_ingredient_parser():
"2L Wasser": (2, "L", "Wasser", ""), "2L Wasser": (2, "L", "Wasser", ""),
"1 (16 ounce) package dry lentils, rinsed": (1, "package", "dry lentils, rinsed", "16 ounce"), "1 (16 ounce) package dry lentils, rinsed": (1, "package", "dry lentils, rinsed", "16 ounce"),
"2-3 c Water": (2, "c", "Water", "2-3"), "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 # for German you could say that if an ingredient does not have
# an amount # and it starts with a lowercase letter, then that # 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(): for key, val in expectations.items():
count += 1 count += 1
parsed = ingredient_parser.parse(key) parsed = ingredient_parser.parse(key)
print(f'testing if {key} becomes {val}')
assert parsed == val assert parsed == val

View File

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

View File

@ -1,14 +1,13 @@
from datetime import datetime from datetime import datetime
from django.db.models import Q, Sum from django.db.models import Sum
from django.shortcuts import render from django.shortcuts import render
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django_tables2 import RequestConfig from django_tables2 import RequestConfig
from cookbook.filters import ShoppingListFilter
from cookbook.helper.permission_helper import group_required from cookbook.helper.permission_helper import group_required
from cookbook.models import InviteLink, RecipeImport, ShoppingList, Storage, SyncLog, UserFile from cookbook.models import InviteLink, RecipeImport, Storage, SyncLog, UserFile
from cookbook.tables import (ImportLogTable, InviteLinkTable, RecipeImportTable, ShoppingListTable, from cookbook.tables import (ImportLogTable, InviteLinkTable, RecipeImportTable,
StorageTable) StorageTable)
@ -41,20 +40,12 @@ def recipe_import(request):
@group_required('user') @group_required('user')
def shopping_list(request): 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( return render(
request, request,
'generic/list_template.html', 'shoppinglist_template.html',
{ {
'title': _("Shopping Lists"), "title": _("Shopping List"),
'table': table,
'filter': f,
'create_url': 'view_shopping'
} }
) )
@ -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.models import Group
from django.contrib.auth.password_validation import validate_password from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError 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.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.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils import timezone from django.utils import timezone
@ -27,9 +27,9 @@ from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, ShoppingP
SpaceCreateForm, SpaceJoinForm, SpacePreferenceForm, User, SpaceCreateForm, SpaceJoinForm, SpacePreferenceForm, User,
UserCreateForm, UserNameForm, UserPreference, UserPreferenceForm) UserCreateForm, UserNameForm, UserPreference, UserPreferenceForm)
from cookbook.helper.permission_helper import group_required, has_group_permission, share_link_valid 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, MealPlan, RecipeImport, SearchFields, SearchPreference, ShareLink,
ShoppingList, Space, Unit, UserFile, ViewLog) Space, Unit, ViewLog)
from cookbook.tables import (CookLogTable, InviteLinkTable, RecipeTable, RecipeTableSmall, from cookbook.tables import (CookLogTable, InviteLinkTable, RecipeTable, RecipeTableSmall,
ViewLogTable) ViewLogTable)
from cookbook.views.data import Object 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}) 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') @group_required('guest')
def user_settings(request): def user_settings(request):
if request.space.demo: if request.space.demo:
@ -662,10 +633,15 @@ def test(request):
if not settings.DEBUG: if not settings.DEBUG:
return HttpResponseRedirect(reverse('index')) return HttpResponseRedirect(reverse('index'))
if (api_token := Token.objects.filter(user=request.user).first()) is None: from cookbook.helper.ingredient_parser import IngredientParser
api_token = Token.objects.create(user=request.user) 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): 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> <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>
<a class="dropdown-item" :href="`${resolveDjangoUrl('view_shopping')}?r=[${recipe.id},${servings_value}]`" v-if="recipe.internal" target="_blank" rel="noopener noreferrer"> <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>
<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" @click="createMealPlan" href="javascript:void(0);"><i class="fas fa-calendar fa-fw"></i> {{ $t("Add_to_Plan") }} </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.", "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_desc": "Days of recent shopping list entries to display.",
"shopping_recent_days": "Recent Days", "shopping_recent_days": "Recent Days",
"create_shopping_new": "Add to NEW Shopping List",
"download_pdf": "Download PDF", "download_pdf": "Download PDF",
"download_csv": "Download CSV", "download_csv": "Download CSV",
"csv_delim_help": "Delimiter to use for CSV exports.", "csv_delim_help": "Delimiter to use for CSV exports.",