Merge branch 'develop' of https://github.com/vabene1111/recipes into develop
This commit is contained in:
@ -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']
|
|
||||||
|
@ -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()
|
||||||
|
@ -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(
|
||||||
|
@ -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/>
|
||||||
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
@ -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", ""),
|
||||||
@ -32,7 +32,7 @@ def test_ingredient_parser():
|
|||||||
"1 Ei(er)": (1, None, "Ei(er)", ""),
|
"1 Ei(er)": (1, None, "Ei(er)", ""),
|
||||||
"1 Prise(n) Salz": (1, "Prise(n)", "Salz", ""),
|
"1 Prise(n) Salz": (1, "Prise(n)", "Salz", ""),
|
||||||
"etwas Wasser, lauwarmes": (0, None, "etwas Wasser", "lauwarmes"),
|
"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", ""),
|
"barrel-aged Bourbon": (0, None, "barrel-aged Bourbon", ""),
|
||||||
"golden syrup": (0, None, "golden syrup", ""),
|
"golden syrup": (0, None, "golden syrup", ""),
|
||||||
"unsalted butter, for greasing": (0, None, "unsalted butter", "for greasing"),
|
"unsalted butter, for greasing": (0, None, "unsalted butter", "for greasing"),
|
||||||
@ -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
|
||||||
|
@ -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'),
|
||||||
|
@ -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"),
|
|
||||||
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
@ -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):
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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.",
|
||||||
|
Reference in New Issue
Block a user