mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-06 06:38:03 -05:00
Merge branch 'develop' into feature/importer_to_vue
# Conflicts: # vue/src/apps/RecipeEditView/RecipeEditView.vue # vue/src/utils/openapi/api.ts
This commit is contained in:
@@ -49,7 +49,7 @@ class UserPreferenceForm(forms.ModelForm):
|
||||
fields = (
|
||||
'default_unit', 'use_fractions', 'use_kj', 'theme', 'nav_color',
|
||||
'sticky_navbar', 'default_page', 'show_recent', 'search_style',
|
||||
'plan_share', 'ingredient_decimals', 'comments',
|
||||
'plan_share', 'ingredient_decimals', 'comments', 'left_handed',
|
||||
)
|
||||
|
||||
labels = {
|
||||
@@ -65,7 +65,8 @@ class UserPreferenceForm(forms.ModelForm):
|
||||
'plan_share': _('Plan sharing'),
|
||||
'ingredient_decimals': _('Ingredient decimal places'),
|
||||
'shopping_auto_sync': _('Shopping list auto sync period'),
|
||||
'comments': _('Comments')
|
||||
'comments': _('Comments'),
|
||||
'left_handed': _('Left-handed mode')
|
||||
}
|
||||
|
||||
help_texts = {
|
||||
@@ -89,6 +90,7 @@ class UserPreferenceForm(forms.ModelForm):
|
||||
'sticky_navbar': _('Makes the navbar stick to the top of the page.'), # noqa: E501
|
||||
'mealplan_autoadd_shopping': _('Automatically add meal plan ingredients to shopping list.'),
|
||||
'mealplan_autoexclude_onhand': _('Exclude ingredients that are on hand.'),
|
||||
'left_handed': _('Will optimize the UI for use with your left hand.')
|
||||
}
|
||||
|
||||
widgets = {
|
||||
|
||||
@@ -43,7 +43,8 @@ class NextcloudCookbook(Integration):
|
||||
if 'keywords' in recipe_json:
|
||||
try:
|
||||
for x in recipe_json['keywords'].split(','):
|
||||
recipe.keywords.add(Keyword.objects.get_or_create(space=self.request.space, name=x)[0])
|
||||
if x.strip() != '':
|
||||
recipe.keywords.add(Keyword.objects.get_or_create(space=self.request.space, name=x)[0])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@@ -77,14 +77,13 @@ class RecipeSage(Integration):
|
||||
}
|
||||
|
||||
for s in recipe.steps.all():
|
||||
if s.type != Step.TIME:
|
||||
data['recipeInstructions'].append({
|
||||
'@type': 'HowToStep',
|
||||
'text': s.instruction
|
||||
})
|
||||
data['recipeInstructions'].append({
|
||||
'@type': 'HowToStep',
|
||||
'text': s.instruction
|
||||
})
|
||||
|
||||
for i in s.ingredients.all():
|
||||
data['recipeIngredient'].append(f'{float(i.amount)} {i.unit} {i.food}')
|
||||
for i in s.ingredients.all():
|
||||
data['recipeIngredient'].append(f'{float(i.amount)} {i.unit} {i.food}')
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -12,33 +12,33 @@ class RezKonv(Integration):
|
||||
|
||||
ingredients = []
|
||||
directions = []
|
||||
for line in file.replace('\r', '').split('\n'):
|
||||
for line in file.replace('\r', '').replace('\n\n', '\n').split('\n'):
|
||||
if 'Titel:' in line:
|
||||
title = line.replace('Titel:', '').strip()
|
||||
if 'Kategorien:' in line:
|
||||
tags = line.replace('Kategorien:', '').strip()
|
||||
if ingredient_mode and ('quelle' in line.lower() or 'source' in line.lower()):
|
||||
if ingredient_mode and (
|
||||
'quelle' in line.lower() or 'source' in line.lower() or (line == '' and len(ingredients) > 0)):
|
||||
ingredient_mode = False
|
||||
direction_mode = True
|
||||
if ingredient_mode:
|
||||
if line != '' and '===' not in line and 'Zubereitung' not in line:
|
||||
ingredients.append(line.strip())
|
||||
if direction_mode:
|
||||
if line.strip() != '' and line.strip() != '=====':
|
||||
directions.append(line.strip())
|
||||
if 'Zutaten:' in line:
|
||||
if 'Zutaten:' in line or 'Ingredients' in line or 'Menge:' in line:
|
||||
ingredient_mode = True
|
||||
if 'Zubereitung:' in line:
|
||||
ingredient_mode = False
|
||||
direction_mode = True
|
||||
|
||||
recipe = Recipe.objects.create(name=title, created_by=self.request.user, internal=True, space=self.request.space)
|
||||
recipe = Recipe.objects.create(name=title, created_by=self.request.user, internal=True,
|
||||
space=self.request.space)
|
||||
|
||||
for k in tags.split(','):
|
||||
keyword, created = Keyword.objects.get_or_create(name=k.strip(), space=self.request.space)
|
||||
recipe.keywords.add(keyword)
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction='\n'.join(directions) + '\n\n', space=self.request.space,
|
||||
instruction=' \n'.join(directions) + '\n\n', space=self.request.space,
|
||||
)
|
||||
|
||||
ingredient_parser = IngredientParser(self.request, True)
|
||||
@@ -60,7 +60,8 @@ class RezKonv(Integration):
|
||||
def split_recipe_file(self, file):
|
||||
recipe_list = []
|
||||
current_recipe = ''
|
||||
encoding_list = ['windows-1250', 'latin-1'] #TODO build algorithm to try trough encodings and fail if none work, use for all importers
|
||||
encoding_list = ['windows-1250',
|
||||
'latin-1'] # TODO build algorithm to try trough encodings and fail if none work, use for all importers
|
||||
encoding = 'windows-1250'
|
||||
for fl in file.readlines():
|
||||
try:
|
||||
|
||||
@@ -71,11 +71,10 @@ class Saffron(Integration):
|
||||
recipeInstructions = []
|
||||
recipeIngredient = []
|
||||
for s in recipe.steps.all():
|
||||
if s.type != Step.TIME:
|
||||
recipeInstructions.append(s.instruction)
|
||||
recipeInstructions.append(s.instruction)
|
||||
|
||||
for i in s.ingredients.all():
|
||||
recipeIngredient.append(f'{float(i.amount)} {i.unit} {i.food}')
|
||||
for i in s.ingredients.all():
|
||||
recipeIngredient.append(f'{float(i.amount)} {i.unit} {i.food}')
|
||||
|
||||
data += "Ingredients: \n"
|
||||
for ingredient in recipeIngredient:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1469,10 +1469,10 @@ msgid ""
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Markdown ist eine Schreibweise mit der Text einfach formatiert "
|
||||
"werden kann. Diese Seite benutzt <a href=\"https\\://python-markdown.github."
|
||||
"werden kann. Diese Seite benutzt <a href=\"https://python-markdown.github."
|
||||
"io/\" target=\"_blank\">Python Markdown</a>, eine Bibliothek, die reinen "
|
||||
"Text in schönes HTML umwandelt. Die komplette Dokumentation befindet sich <a "
|
||||
"href=\"https\\://daringfireball.net/projects/markdown/syntax\" target="
|
||||
"href=\"https://daringfireball.net/projects/markdown/syntax\" target="
|
||||
"\"_blank\">hier</a>. Die wichtigsten Formatierungszeichen befinden sich hier "
|
||||
"auf dieser Seite.\n"
|
||||
" "
|
||||
|
||||
@@ -13,10 +13,10 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-02-11 08:52+0100\n"
|
||||
"PO-Revision-Date: 2022-02-09 01:31+0000\n"
|
||||
"PO-Revision-Date: 2022-04-05 10:31+0000\n"
|
||||
"Last-Translator: Jesse <jesse.kamps@pm.me>\n"
|
||||
"Language-Team: Dutch <http://translate.tandoor.dev/projects/tandoor/recipes-"
|
||||
"backend/nl/>\n"
|
||||
"Language-Team: Dutch <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/nl/>\n"
|
||||
"Language: nl\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -291,10 +291,6 @@ msgstr ""
|
||||
"(lage waarden betekenen bijvoorbeeld dat meer typefouten genegeerd worden)."
|
||||
|
||||
#: .\cookbook\forms.py:445
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Select type method of search. Click <a href=\"/docs/search/\">here</a> "
|
||||
#| "for full desciption of choices."
|
||||
msgid ""
|
||||
"Select type method of search. Click <a href=\"/docs/search/\">here</a> for "
|
||||
"full description of choices."
|
||||
@@ -368,8 +364,6 @@ msgid "Partial Match"
|
||||
msgstr "Gedeeltelijke overeenkomst"
|
||||
|
||||
#: .\cookbook\forms.py:464
|
||||
#, fuzzy
|
||||
#| msgid "Starts Wtih"
|
||||
msgid "Starts With"
|
||||
msgstr "Begint met"
|
||||
|
||||
@@ -529,10 +523,8 @@ msgid "One of queryset or hash_key must be provided"
|
||||
msgstr "Er moet een queryset of hash_key opgegeven worden"
|
||||
|
||||
#: .\cookbook\helper\shopping_helper.py:148
|
||||
#, fuzzy
|
||||
#| msgid "You must supply a created_by"
|
||||
msgid "You must supply a servings size"
|
||||
msgstr "Je moet een created_by aanleveren"
|
||||
msgstr "Je moet een portiegrootte aanleveren"
|
||||
|
||||
#: .\cookbook\helper\template_helper.py:61
|
||||
#: .\cookbook\helper\template_helper.py:63
|
||||
@@ -604,11 +596,9 @@ msgid "Rebuilds full text search index on Recipe"
|
||||
msgstr "Herbouwt de volledige tekst zoekindex van Recept"
|
||||
|
||||
#: .\cookbook\management\commands\rebuildindex.py:18
|
||||
#, fuzzy
|
||||
#| msgid "Only Postgress databases use full text search, no index to rebuild"
|
||||
msgid "Only Postgresql databases use full text search, no index to rebuild"
|
||||
msgstr ""
|
||||
"Alleen Postgress databases gebruiken volledige tekst zoekmethoden, geen "
|
||||
"Alleen Postgresql databases gebruiken volledige tekst zoekmethoden, geen "
|
||||
"index aanwezig om te herbouwen"
|
||||
|
||||
#: .\cookbook\management\commands\rebuildindex.py:29
|
||||
@@ -732,18 +722,22 @@ msgstr ""
|
||||
msgid ""
|
||||
"Providing a list_recipe ID and servings of 0 will delete that shopping list."
|
||||
msgstr ""
|
||||
"Als je een list_recipe ID en portiegrootte van 0 opgeeft wordt dat "
|
||||
"boodschappenlijstje verwijderd."
|
||||
|
||||
#: .\cookbook\serializer.py:988
|
||||
msgid "Amount of food to add to the shopping list"
|
||||
msgstr ""
|
||||
msgstr "Hoeveelheid eten om aan het boodschappenlijstje toe te voegen"
|
||||
|
||||
#: .\cookbook\serializer.py:989
|
||||
msgid "ID of unit to use for the shopping list"
|
||||
msgstr ""
|
||||
msgstr "ID of eenheid om te gebruik voor het boodschappenlijstje"
|
||||
|
||||
#: .\cookbook\serializer.py:990
|
||||
msgid "When set to true will delete all food from active shopping lists."
|
||||
msgstr ""
|
||||
"Wanneer ingesteld op waar, wordt al het voedsel van actieve "
|
||||
"boodschappenlijstjes verwijderd."
|
||||
|
||||
#: .\cookbook\tables.py:35 .\cookbook\templates\generic\edit_template.html:6
|
||||
#: .\cookbook\templates\generic\edit_template.html:14
|
||||
@@ -1480,8 +1474,6 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:57
|
||||
#: .\cookbook\templates\markdown_info.html:73
|
||||
#, fuzzy
|
||||
#| msgid "or by leaving a blank line inbetween."
|
||||
msgid "or by leaving a blank line in between."
|
||||
msgstr "of door een witregel te gebruiken."
|
||||
|
||||
@@ -1505,10 +1497,6 @@ msgid "Lists"
|
||||
msgstr "Lijsten"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:85
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Lists can ordered or unorderd. It is <b>important to leave a blank line "
|
||||
#| "before the list!</b>"
|
||||
msgid ""
|
||||
"Lists can ordered or unordered. It is <b>important to leave a blank line "
|
||||
"before the list!</b>"
|
||||
@@ -1806,15 +1794,6 @@ msgstr ""
|
||||
" "
|
||||
|
||||
#: .\cookbook\templates\search_info.html:29
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| " \n"
|
||||
#| " Simple searches ignore punctuation and common words such as "
|
||||
#| "'the', 'a', 'and'. And will treat seperate words as required.\n"
|
||||
#| " Searching for 'apple or flour' will return any recipe that "
|
||||
#| "includes both 'apple' and 'flour' anywhere in the fields that have been "
|
||||
#| "selected for a full text search.\n"
|
||||
#| " "
|
||||
msgid ""
|
||||
" \n"
|
||||
" Simple searches ignore punctuation and common words such as "
|
||||
@@ -1851,23 +1830,6 @@ msgstr ""
|
||||
" "
|
||||
|
||||
#: .\cookbook\templates\search_info.html:39
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| " \n"
|
||||
#| " Web searches simulate functionality found on many web search "
|
||||
#| "sites supporting special syntax.\n"
|
||||
#| " Placing quotes around several words will convert those words "
|
||||
#| "into a phrase.\n"
|
||||
#| " 'or' is recongized as searching for the word (or phrase) "
|
||||
#| "immediately before 'or' OR the word (or phrase) directly after.\n"
|
||||
#| " '-' is recognized as searching for recipes that do not "
|
||||
#| "include the word (or phrase) that comes immediately after. \n"
|
||||
#| " For example searching for 'apple pie' or cherry -butter will "
|
||||
#| "return any recipe that includes the phrase 'apple pie' or the word "
|
||||
#| "'cherry' \n"
|
||||
#| " in any field included in the full text search but exclude any "
|
||||
#| "recipe that has the word 'butter' in any field included.\n"
|
||||
#| " "
|
||||
msgid ""
|
||||
" \n"
|
||||
" Web searches simulate functionality found on many web search "
|
||||
@@ -1913,19 +1875,6 @@ msgstr ""
|
||||
" "
|
||||
|
||||
#: .\cookbook\templates\search_info.html:59
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| " \n"
|
||||
#| " Another approach to searching that also requires Postgresql "
|
||||
#| "is fuzzy search or trigram similarity. A trigram is a group of three "
|
||||
#| "consecutive characters.\n"
|
||||
#| " For example searching for 'apple' will create x trigrams "
|
||||
#| "'app', 'ppl', 'ple' and will create a score of how closely words match "
|
||||
#| "the generated trigrams.\n"
|
||||
#| " One benefit of searching trigams is that a search for "
|
||||
#| "'sandwich' will find mispelled words such as 'sandwhich' that would be "
|
||||
#| "missed by other methods.\n"
|
||||
#| " "
|
||||
msgid ""
|
||||
" \n"
|
||||
" Another approach to searching that also requires Postgresql is "
|
||||
@@ -2061,10 +2010,8 @@ msgid "Search-Settings"
|
||||
msgstr "Zoek instellingen"
|
||||
|
||||
#: .\cookbook\templates\settings.html:56
|
||||
#, fuzzy
|
||||
#| msgid "Search-Settings"
|
||||
msgid "Shopping-Settings"
|
||||
msgstr "Zoek instellingen"
|
||||
msgstr "Boodschappen instellingen"
|
||||
|
||||
#: .\cookbook\templates\settings.html:65
|
||||
msgid "Name Settings"
|
||||
@@ -2180,10 +2127,8 @@ msgid "Perfect for large Databases"
|
||||
msgstr "Perfect voor grote databases"
|
||||
|
||||
#: .\cookbook\templates\settings.html:207
|
||||
#, fuzzy
|
||||
#| msgid "Shopping List"
|
||||
msgid "Shopping Settings"
|
||||
msgstr "Boodschappenlijst"
|
||||
msgstr "Boodschappen instellingen"
|
||||
|
||||
#: .\cookbook\templates\setup.html:6 .\cookbook\templates\system.html:5
|
||||
msgid "Cookbook Setup"
|
||||
@@ -2263,8 +2208,6 @@ msgid "Finished"
|
||||
msgstr "Afgerond"
|
||||
|
||||
#: .\cookbook\templates\shopping_list.html:267
|
||||
#, fuzzy
|
||||
#| msgid "You are offline, shopping list might not syncronize."
|
||||
msgid "You are offline, shopping list might not synchronize."
|
||||
msgstr "Je bent offline, de boodschappenlijst synchroniseert mogelijk niet."
|
||||
|
||||
@@ -2804,93 +2747,113 @@ msgstr "{child.name} is succesvol verplaatst naar {parent.name}"
|
||||
#: .\cookbook\views\api.py:474
|
||||
#, python-brace-format
|
||||
msgid "{obj.name} was removed from the shopping list."
|
||||
msgstr ""
|
||||
msgstr "{obj.name} is verwijderd van het boodschappenlijstje."
|
||||
|
||||
#: .\cookbook\views\api.py:479 .\cookbook\views\api.py:729
|
||||
#: .\cookbook\views\api.py:742
|
||||
#, python-brace-format
|
||||
msgid "{obj.name} was added to the shopping list."
|
||||
msgstr ""
|
||||
msgstr "{obj.name} is toegevoegd aan het boodschappenlijstje."
|
||||
|
||||
#: .\cookbook\views\api.py:591
|
||||
msgid "ID of recipe a step is part of. For multiple repeat parameter."
|
||||
msgstr ""
|
||||
"ID van het recept waar de stap onderdeel van is. Herhaal parameter voor "
|
||||
"meerdere."
|
||||
|
||||
#: .\cookbook\views\api.py:592
|
||||
msgid "Query string matched (fuzzy) against object name."
|
||||
msgstr ""
|
||||
msgstr "Zoekterm komt overeen (fuzzy) met object naam."
|
||||
|
||||
#: .\cookbook\views\api.py:635
|
||||
msgid ""
|
||||
"Query string matched (fuzzy) against recipe name. In the future also "
|
||||
"fulltext search."
|
||||
msgstr ""
|
||||
"Zoekterm komt overeen (fuzzy) met recept naam. In de toekomst wordt zoeken "
|
||||
"op volledige tekst ondersteund."
|
||||
|
||||
#: .\cookbook\views\api.py:636
|
||||
msgid "ID of keyword a recipe should have. For multiple repeat parameter."
|
||||
msgstr ""
|
||||
"ID van etiket dat een recept moet hebben. Herhaal parameter voor meerdere."
|
||||
|
||||
#: .\cookbook\views\api.py:637
|
||||
msgid "ID of food a recipe should have. For multiple repeat parameter."
|
||||
msgstr ""
|
||||
"ID van ingrediënt dat een recept moet hebben. Herhaal parameter voor "
|
||||
"meerdere."
|
||||
|
||||
#: .\cookbook\views\api.py:638
|
||||
msgid "ID of unit a recipe should have."
|
||||
msgstr ""
|
||||
msgstr "ID van eenheid dat een recept moet hebben."
|
||||
|
||||
#: .\cookbook\views\api.py:639
|
||||
msgid "Rating a recipe should have. [0 - 5]"
|
||||
msgstr ""
|
||||
msgstr "Waardering die een recept moet hebben. [0 - 5]"
|
||||
|
||||
#: .\cookbook\views\api.py:640
|
||||
msgid "ID of book a recipe should be in. For multiple repeat parameter."
|
||||
msgstr ""
|
||||
"ID van boek dat een recept moet hebben. Herhaal parameter voor meerdere."
|
||||
|
||||
#: .\cookbook\views\api.py:641
|
||||
msgid ""
|
||||
"If recipe should have all (AND=false) or any (OR=<b>true</b>) of the "
|
||||
"provided keywords."
|
||||
msgstr ""
|
||||
"Als een recept alle moet hebben (AND=onwaar) of sommige (OR=<b>waar</b>) van "
|
||||
"de gegeven zoektermen."
|
||||
|
||||
#: .\cookbook\views\api.py:642
|
||||
msgid ""
|
||||
"If recipe should have all (AND=false) or any (OR=<b>true</b>) of the "
|
||||
"provided foods."
|
||||
msgstr ""
|
||||
"Als een recept alle moet hebben (AND=onwaar) of sommige (OR=<b>waar</b>) van "
|
||||
"de gegeven ingrediënten."
|
||||
|
||||
#: .\cookbook\views\api.py:643
|
||||
msgid ""
|
||||
"If recipe should be in all (AND=false) or any (OR=<b>true</b>) of the "
|
||||
"provided books."
|
||||
msgstr ""
|
||||
"Als een recept alle moet hebben (AND=onwaar) of sommige (OR=<b>waar</b>) van "
|
||||
"de gegeven boeken."
|
||||
|
||||
#: .\cookbook\views\api.py:644
|
||||
msgid "If only internal recipes should be returned. [true/<b>false</b>]"
|
||||
msgstr ""
|
||||
"Wanneer alleen interne recepten gevonden moeten worden. [waar/<b>onwaar</b>]"
|
||||
|
||||
#: .\cookbook\views\api.py:645
|
||||
msgid "Returns the results in randomized order. [true/<b>false</b>]"
|
||||
msgstr ""
|
||||
msgstr "Geeft de resultaten in willekeurige volgorde weer. [waar/<b>onwaar</b>]"
|
||||
|
||||
#: .\cookbook\views\api.py:646
|
||||
msgid "Returns new results first in search results. [true/<b>false</b>]"
|
||||
msgstr ""
|
||||
msgstr "Geeft nieuwe resultaten eerst weer. [waar/<b>onwaar</b>]"
|
||||
|
||||
#: .\cookbook\views\api.py:784
|
||||
msgid ""
|
||||
"Returns the shopping list entry with a primary key of id. Multiple values "
|
||||
"allowed."
|
||||
msgstr ""
|
||||
"Geeft het boodschappenlijstje item met een primaire sleutel van id. "
|
||||
"Meerdere waarden toegestaan."
|
||||
|
||||
#: .\cookbook\views\api.py:787
|
||||
msgid ""
|
||||
"Filter shopping list entries on checked. [true, false, both, <b>recent</"
|
||||
"b>]<br> - recent includes unchecked items and recently completed items."
|
||||
msgstr ""
|
||||
"Filter boodschappenlijstjes op aangevinkt. [waar,onwaar,beide,<b>recent</b>]"
|
||||
"<br> - recent bevat niet aangevinkte en recent voltooide items."
|
||||
|
||||
#: .\cookbook\views\api.py:789
|
||||
msgid "Returns the shopping list entries sorted by supermarket category order."
|
||||
msgstr ""
|
||||
"Geeft items op boodschappenlijstjes gesorteerd per supermarktcategorie weer."
|
||||
|
||||
#: .\cookbook\views\api.py:949 .\cookbook\views\data.py:42
|
||||
#: .\cookbook\views\edit.py:129 .\cookbook\views\new.py:95
|
||||
@@ -2929,11 +2892,9 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\views\api.py:1109
|
||||
msgid "Connection Refused."
|
||||
msgstr ""
|
||||
msgstr "Verbinding geweigerd."
|
||||
|
||||
#: .\cookbook\views\api.py:1118
|
||||
#, fuzzy
|
||||
#| msgid "No useable data could be found."
|
||||
msgid "No usable data could be found."
|
||||
msgstr "Er is geen bruikbare data gevonden."
|
||||
|
||||
@@ -3017,6 +2978,8 @@ msgid ""
|
||||
"The PDF Exporter is not enabled on this instance as it is still in an "
|
||||
"experimental state."
|
||||
msgstr ""
|
||||
"De PDF exporter is niet ingeschakeld op deze instantie gezien het in een "
|
||||
"experimentele staat is."
|
||||
|
||||
#: .\cookbook\views\lists.py:25
|
||||
msgid "Import Log"
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-09-13 22:40+0200\n"
|
||||
"PO-Revision-Date: 2021-10-23 09:06+0000\n"
|
||||
"Last-Translator: rustam <uzbekr@gmail.com>\n"
|
||||
"PO-Revision-Date: 2022-04-07 19:32+0000\n"
|
||||
"Last-Translator: Artem Aksenov <artemmillerr@gmail.com>\n"
|
||||
"Language-Team: Russian <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/ru/>\n"
|
||||
"Language: ru\n"
|
||||
@@ -18,14 +18,14 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
|
||||
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
|
||||
"X-Generator: Weblate 4.8\n"
|
||||
"X-Generator: Weblate 4.10.1\n"
|
||||
|
||||
#: .\cookbook\filters.py:23 .\cookbook\templates\base.html:125
|
||||
#: .\cookbook\templates\forms\ingredients.html:34
|
||||
#: .\cookbook\templates\space.html:43 .\cookbook\templates\stats.html:28
|
||||
#: .\cookbook\templates\url_import.html:270
|
||||
msgid "Ingredients"
|
||||
msgstr "ингредиенты"
|
||||
msgstr "Ингредиенты"
|
||||
|
||||
#: .\cookbook\forms.py:50
|
||||
msgid ""
|
||||
@@ -95,14 +95,14 @@ msgstr ""
|
||||
#: .\cookbook\forms.py:103 .\cookbook\forms.py:334
|
||||
#: .\cookbook\templates\url_import.html:154
|
||||
msgid "Name"
|
||||
msgstr "Имя"
|
||||
msgstr "Название"
|
||||
|
||||
#: .\cookbook\forms.py:104 .\cookbook\forms.py:335
|
||||
#: .\cookbook\templates\space.html:39 .\cookbook\templates\stats.html:24
|
||||
#: .\cookbook\templates\url_import.html:188
|
||||
#: .\cookbook\templates\url_import.html:573 .\cookbook\views\lists.py:112
|
||||
msgid "Keywords"
|
||||
msgstr "Ключевые поля"
|
||||
msgstr "Ключевые слова"
|
||||
|
||||
#: .\cookbook\forms.py:105
|
||||
msgid "Preparation time in minutes"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@ DICTIONARY = {
|
||||
'it': 'italian',
|
||||
# 'lv': 'Latvian',
|
||||
'es': 'spanish',
|
||||
'sv': 'swedish',
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -42,7 +42,8 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
|
||||
api_serializer = None
|
||||
# extended values are computationally expensive and not needed in normal circumstances
|
||||
try:
|
||||
if str2bool(self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
|
||||
if str2bool(
|
||||
self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
|
||||
return fields
|
||||
except (AttributeError, KeyError) as e:
|
||||
pass
|
||||
@@ -96,7 +97,8 @@ class CustomOnHandField(serializers.Field):
|
||||
shared_users = getattr(request, '_shared_users', None)
|
||||
if shared_users is None:
|
||||
try:
|
||||
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [self.context['request'].user.id]
|
||||
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [
|
||||
self.context['request'].user.id]
|
||||
except AttributeError: # Anonymous users (using share links) don't have shared users
|
||||
shared_users = []
|
||||
return obj.onhand_users.filter(id__in=shared_users).exists()
|
||||
@@ -170,7 +172,8 @@ class FoodInheritFieldSerializer(WritableNestedModelSerializer):
|
||||
|
||||
|
||||
class UserPreferenceSerializer(WritableNestedModelSerializer):
|
||||
food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', many=True, allow_null=True, required=False, read_only=True)
|
||||
food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', many=True, allow_null=True,
|
||||
required=False, read_only=True)
|
||||
plan_share = UserNameSerializer(many=True, allow_null=True, required=False, read_only=True)
|
||||
shopping_share = UserNameSerializer(many=True, allow_null=True, required=False)
|
||||
food_children_exist = serializers.SerializerMethodField('get_food_children_exist')
|
||||
@@ -189,9 +192,12 @@ class UserPreferenceSerializer(WritableNestedModelSerializer):
|
||||
class Meta:
|
||||
model = UserPreference
|
||||
fields = (
|
||||
'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_fractions', 'use_kj', 'search_style', 'show_recent', 'plan_share',
|
||||
'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_inherit_default', 'default_delay',
|
||||
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days', 'csv_delim', 'csv_prefix',
|
||||
'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_fractions', 'use_kj', 'search_style',
|
||||
'show_recent', 'plan_share',
|
||||
'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping',
|
||||
'food_inherit_default', 'default_delay',
|
||||
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days',
|
||||
'csv_delim', 'csv_prefix',
|
||||
'filter_to_supermarket', 'shopping_add_onhand', 'left_handed', 'food_children_exist'
|
||||
)
|
||||
|
||||
@@ -393,7 +399,6 @@ class FoodSimpleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Food
|
||||
fields = ('id', 'name')
|
||||
read_only_fields = ['id', 'name']
|
||||
|
||||
|
||||
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedRecipeMixin):
|
||||
@@ -416,7 +421,8 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
shared_users = getattr(request, '_shared_users', None)
|
||||
if shared_users is None:
|
||||
try:
|
||||
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [self.context['request'].user.id]
|
||||
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [
|
||||
self.context['request'].user.id]
|
||||
except AttributeError:
|
||||
shared_users = []
|
||||
filter = Q(id__in=obj.substitute.all())
|
||||
@@ -487,11 +493,15 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe')
|
||||
|
||||
|
||||
class IngredientSerializer(WritableNestedModelSerializer):
|
||||
food = FoodSerializer(allow_null=True)
|
||||
class IngredientSimpleSerializer(WritableNestedModelSerializer):
|
||||
food = FoodSimpleSerializer(allow_null=True)
|
||||
unit = UnitSerializer(allow_null=True)
|
||||
used_in_recipes = serializers.SerializerMethodField('get_used_in_recipes')
|
||||
amount = CustomDecimalField()
|
||||
|
||||
def get_used_in_recipes(self, obj):
|
||||
return list(Recipe.objects.filter(steps__ingredients=obj.id).values('id', 'name'))
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['space'] = self.context['request'].space
|
||||
return super().create(validated_data)
|
||||
@@ -504,10 +514,14 @@ class IngredientSerializer(WritableNestedModelSerializer):
|
||||
model = Ingredient
|
||||
fields = (
|
||||
'id', 'food', 'unit', 'amount', 'note', 'order',
|
||||
'is_header', 'no_amount', 'original_text'
|
||||
'is_header', 'no_amount', 'original_text', 'used_in_recipes',
|
||||
)
|
||||
|
||||
|
||||
class IngredientSerializer(IngredientSimpleSerializer):
|
||||
food = FoodSerializer(allow_null=True)
|
||||
|
||||
|
||||
class StepSerializer(WritableNestedModelSerializer, ExtendedRecipeMixin):
|
||||
ingredients = IngredientSerializer(many=True)
|
||||
ingredients_markdown = serializers.SerializerMethodField('get_ingredients_markdown')
|
||||
@@ -702,7 +716,8 @@ class RecipeBookEntrySerializer(serializers.ModelSerializer):
|
||||
def create(self, validated_data):
|
||||
book = validated_data['book']
|
||||
recipe = validated_data['recipe']
|
||||
if not book.get_owner() == self.context['request'].user and not self.context['request'].user in book.get_shared():
|
||||
if not book.get_owner() == self.context['request'].user and not self.context[
|
||||
'request'].user in book.get_shared():
|
||||
raise NotFound(detail=None, code=None)
|
||||
obj, created = RecipeBookEntry.objects.get_or_create(book=book, recipe=recipe)
|
||||
return obj
|
||||
@@ -731,7 +746,7 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
|
||||
def create(self, validated_data):
|
||||
validated_data['created_by'] = self.context['request'].user
|
||||
mealplan = super().create(validated_data)
|
||||
if self.context['request'].data.get('addshopping', False):
|
||||
if self.context['request'].data.get('addshopping', False) and self.context['request'].data.get('recipe', None):
|
||||
SLR = RecipeShoppingEditor(user=validated_data['created_by'], space=validated_data['space'])
|
||||
SLR.create(mealplan=mealplan, servings=validated_data['servings'])
|
||||
return mealplan
|
||||
@@ -755,13 +770,14 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
|
||||
def get_name(self, obj):
|
||||
if not isinstance(value := obj.servings, Decimal):
|
||||
value = Decimal(value)
|
||||
value = value.quantize(Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
|
||||
value = value.quantize(
|
||||
Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
|
||||
return (
|
||||
obj.name
|
||||
or getattr(obj.mealplan, 'title', None)
|
||||
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
|
||||
or obj.recipe.name
|
||||
) + f' ({value:.2g})'
|
||||
obj.name
|
||||
or getattr(obj.mealplan, 'title', None)
|
||||
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
|
||||
or obj.recipe.name
|
||||
) + f' ({value:.2g})'
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
# TODO remove once old shopping list
|
||||
@@ -832,7 +848,8 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
|
||||
class Meta:
|
||||
model = ShoppingListEntry
|
||||
fields = (
|
||||
'id', 'list_recipe', 'food', 'unit', 'ingredient', 'ingredient_note', 'amount', 'order', 'checked', 'recipe_mealplan',
|
||||
'id', 'list_recipe', 'food', 'unit', 'ingredient', 'ingredient_note', 'amount', 'order', 'checked',
|
||||
'recipe_mealplan',
|
||||
'created_by', 'created_at', 'completed_at', 'delay_until'
|
||||
)
|
||||
read_only_fields = ('id', 'created_by', 'created_at',)
|
||||
@@ -930,7 +947,10 @@ class ExportLogSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ExportLog
|
||||
fields = ('id', 'type', 'msg', 'running', 'total_recipes', 'exported_recipes', 'cache_duration', 'possibly_not_expired', 'created_by', 'created_at')
|
||||
fields = (
|
||||
'id', 'type', 'msg', 'running', 'total_recipes', 'exported_recipes', 'cache_duration',
|
||||
'possibly_not_expired',
|
||||
'created_by', 'created_at')
|
||||
read_only_fields = ('created_by',)
|
||||
|
||||
|
||||
@@ -1042,10 +1062,12 @@ class RecipeExportSerializer(WritableNestedModelSerializer):
|
||||
|
||||
|
||||
class RecipeShoppingUpdateSerializer(serializers.ModelSerializer):
|
||||
list_recipe = serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_("Existing shopping list to update"))
|
||||
list_recipe = serializers.IntegerField(write_only=True, allow_null=True, required=False,
|
||||
help_text=_("Existing shopping list to update"))
|
||||
ingredients = serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_(
|
||||
"List of ingredient IDs from the recipe to add, if not provided all ingredients will be added."))
|
||||
servings = serializers.IntegerField(default=1, write_only=True, allow_null=True, required=False, help_text=_("Providing a list_recipe ID and servings of 0 will delete that shopping list."))
|
||||
servings = serializers.IntegerField(default=1, write_only=True, allow_null=True, required=False, help_text=_(
|
||||
"Providing a list_recipe ID and servings of 0 will delete that shopping list."))
|
||||
|
||||
class Meta:
|
||||
model = Recipe
|
||||
@@ -1053,9 +1075,12 @@ class RecipeShoppingUpdateSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class FoodShoppingUpdateSerializer(serializers.ModelSerializer):
|
||||
amount = serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_("Amount of food to add to the shopping list"))
|
||||
unit = serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_("ID of unit to use for the shopping list"))
|
||||
delete = serializers.ChoiceField(choices=['true'], write_only=True, allow_null=True, allow_blank=True, help_text=_("When set to true will delete all food from active shopping lists."))
|
||||
amount = serializers.IntegerField(write_only=True, allow_null=True, required=False,
|
||||
help_text=_("Amount of food to add to the shopping list"))
|
||||
unit = serializers.IntegerField(write_only=True, allow_null=True, required=False,
|
||||
help_text=_("ID of unit to use for the shopping list"))
|
||||
delete = serializers.ChoiceField(choices=['true'], write_only=True, allow_null=True, allow_blank=True,
|
||||
help_text=_("When set to true will delete all food from active shopping lists."))
|
||||
|
||||
class Meta:
|
||||
model = Recipe
|
||||
|
||||
12
cookbook/static/css/app.min.css
vendored
12
cookbook/static/css/app.min.css
vendored
@@ -1,3 +1,15 @@
|
||||
.brand-icon {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.menu-dropdown-text {
|
||||
font-size: 14px;
|
||||
font-weight: 200;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.spinner-tandoor {
|
||||
animation: rotation 3s infinite linear;
|
||||
content: url("../assets/spinner.svg");
|
||||
|
||||
@@ -56,21 +56,46 @@
|
||||
{% block extra_head %} <!-- block for templates to put stuff into header -->
|
||||
{% endblock %}
|
||||
|
||||
<style>
|
||||
{% if request.user.userpreference.left_handed %}
|
||||
@media screen and (max-width: 600px) {
|
||||
#switcher .btn-circle {
|
||||
left: 80px !important;
|
||||
}
|
||||
}
|
||||
{% endif %}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-{% nav_color request %} bg-header" id="id_main_nav"
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-{% nav_color request %} bg-header"
|
||||
id="id_main_nav"
|
||||
style="{% sticky_nav request %}">
|
||||
|
||||
{% if not request.user.userpreference.left_handed %}
|
||||
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
|
||||
<a class="navbar-brand p-0 me-2 justify-content-center" href="{% base_path request 'base' %}"
|
||||
aria-label="Tandoor">
|
||||
<img class="brand-icon" src="{% static 'assets/brand_logo.svg' %}" alt="Logo">
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText"
|
||||
aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
|
||||
<a class="navbar-brand p-0 me-2 justify-content-center" href="{% base_path request 'base' %}" aria-label="Tandoor">
|
||||
<img class="brand-icon" src="{% static 'assets/brand_logo.png' %}" alt="" style="height: 5vh;">
|
||||
</a>
|
||||
{% if request.user.userpreference.left_handed %}
|
||||
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
|
||||
<a class="navbar-brand p-0 me-2 justify-content-center" href="{% base_path request 'base' %}"
|
||||
aria-label="Tandoor">
|
||||
<img class="brand-icon" src="{% static 'assets/brand_logo.svg' %}" alt="Logo">
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarText">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item {% if request.resolver_match.url_name in 'view_search' %}active{% endif %}">
|
||||
@@ -102,129 +127,140 @@
|
||||
<i class="fas fa-toolbox fa-lg"></i>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-center dropdown-menu-center-large">
|
||||
<div class="row m-0">
|
||||
<div class="row m-0 mt-2 mt-md-0">
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_keyword' %}" class="p-1">
|
||||
<a href="{% url 'list_keyword' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-tags fa-2x"></i>
|
||||
</div>
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted">
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
|
||||
{% trans 'Keyword' %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_food' %}" class="p-1">
|
||||
<a href="{% url 'list_food' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-leaf fa-2x"></i>
|
||||
</div>
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted">
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
|
||||
{% trans 'Foods' %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_unit' %}" class="p-1">
|
||||
<a href="{% url 'list_unit' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-balance-scale fa-2x"></i>
|
||||
</div>
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted">
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
|
||||
{% trans 'Units' %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-0">
|
||||
<div class="row m-0 mt-2 mt-md-0">
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_supermarket' %}" class="p-1">
|
||||
<a href="{% url 'list_supermarket' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-store-alt fa-2x"></i>
|
||||
</div>
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted">
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
|
||||
{% trans 'Supermarket' %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_supermarket_category' %}" class="p-1">
|
||||
<a href="{% url 'list_supermarket_category' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-cubes fa-2x"></i>
|
||||
</div>
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted">
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
|
||||
{% trans 'Supermarket Category' %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_automation' %}" class="p-1">
|
||||
<a href="{% url 'list_automation' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-robot fa-2x"></i>
|
||||
</div>
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted">
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
|
||||
{% trans 'Automations' %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-0">
|
||||
<div class="row m-0 mt-2 mt-md-0">
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_user_file' %}" class="p-1">
|
||||
<a href="{% url 'list_user_file' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-file fa-2x"></i>
|
||||
</div>
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted">
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
|
||||
{% trans 'Files' %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="{% url 'data_batch_edit' %}" class="p-1">
|
||||
<a href="{% url 'data_batch_edit' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-edit fa-2x"></i>
|
||||
</div>
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted">
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
|
||||
{% trans 'Batch Edit' %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="{% url 'view_history' %}" class="p-1">
|
||||
<a href="{% url 'view_history' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-history fa-2x"></i>
|
||||
</div>
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted">
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
|
||||
{% trans 'History' %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="row m-0">
|
||||
<div class="row m-0 mt-2 mt-md-0">
|
||||
<div class="col-4">
|
||||
<a href="{% url 'view_export' %}" class="p-1">
|
||||
<a href="{% url 'view_ingredient_editor' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-th-list fa-2x"></i>
|
||||
</div>
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
|
||||
{% trans 'Ingredient Editor' %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="{% url 'view_export' %}" class="p-0 p-md-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-file-export fa-2x"></i>
|
||||
</div>
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted">
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
|
||||
{% trans 'Export' %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -301,7 +337,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="container-fluid mt-2 mt-md-5 mt-xl-5 mt-lg-5" id="id_base_container">
|
||||
<div class="container-fluid mt-2 mt-md-5 mt-xl-5 mt-lg-5{% if request.user.userpreference.left_handed %} left-handed {% endif %}"
|
||||
id="id_base_container">
|
||||
<div class="row">
|
||||
<div class="col-xl-2 d-none d-xl-block">
|
||||
{% block content_xl_left %}
|
||||
@@ -336,7 +373,7 @@
|
||||
{% block content_fluid %}
|
||||
{% endblock %}
|
||||
|
||||
{% user_prefs request as prefs%}
|
||||
{% user_prefs request as prefs %}
|
||||
{{ prefs|json_script:'user_preference' }}
|
||||
|
||||
</div>
|
||||
|
||||
39
cookbook/templates/ingredient_editor.html
Normal file
39
cookbook/templates/ingredient_editor.html
Normal file
@@ -0,0 +1,39 @@
|
||||
{% extends "base.html" %}
|
||||
{% load render_bundle from webpack_loader %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
|
||||
{% block title %}{% trans 'Ingredient Editor' %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<h3>{% trans 'Ingredient Editor' %}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div id="app">
|
||||
|
||||
<ingredient-editor-view></ingredient-editor-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.DEFAULT_FOOD = {{ food_id }}
|
||||
window.DEFAULT_UNIT = {{ unit_id }}
|
||||
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
|
||||
</script>
|
||||
|
||||
{% render_bundle 'ingredient_editor_view' %}
|
||||
{% endblock %}
|
||||
@@ -20,4 +20,8 @@
|
||||
<button class="btn btn-success" type="submit"><i class="fas fa-save"></i> {% trans 'Create Superuser account' %}</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
$('#id_name').focus()
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -36,8 +36,7 @@
|
||||
<li class="list-group-item">
|
||||
{% trans 'Recipes' %} :
|
||||
<span class="badge badge-pill badge-info"
|
||||
>{{ counts.recipes }} / {% if request.space.max_recipes > 0 %} {{ request.space.max_recipes }}{%
|
||||
else %}∞{% endif %}</span
|
||||
>{{ counts.recipes }} / {% if request.space.max_recipes > 0 %} {{ request.space.max_recipes }}{% else %}∞{% endif %}</span
|
||||
>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
|
||||
@@ -47,7 +47,6 @@ router.register(r'user-name', api.UserNameViewSet, basename='username')
|
||||
router.register(r'user-preference', api.UserPreferenceViewSet)
|
||||
router.register(r'view-log', api.ViewLogViewSet)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.index, name='index'),
|
||||
path('setup/', views.setup, name='view_setup'),
|
||||
@@ -72,6 +71,7 @@ urlpatterns = [
|
||||
path('settings/', views.user_settings, name='view_settings'),
|
||||
path('history/', views.history, name='view_history'),
|
||||
path('supermarket/', views.supermarket, name='view_supermarket'),
|
||||
path('ingredient-editor/', views.ingredient_editor, name='view_ingredient_editor'),
|
||||
path('abuse/<slug:token>', views.report_share_abuse, name='view_report_share_abuse'),
|
||||
|
||||
path('import/', import_export.import_recipe, name='view_import'),
|
||||
@@ -116,7 +116,8 @@ urlpatterns = [
|
||||
path('api/share-link/<int:pk>', api.share_link, name='api_share_link'),
|
||||
path('api/get_facets/', api.get_facets, name='api_get_facets'),
|
||||
|
||||
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'), # TODO is this deprecated? not yet, some old forms remain, could likely be changed to generic API endpoints
|
||||
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'),
|
||||
# TODO is this deprecated? not yet, some old forms remain, could likely be changed to generic API endpoints
|
||||
path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'), # TODO is this deprecated?
|
||||
path('dal/unit/', dal.UnitAutocomplete.as_view(), name='dal_unit'), # TODO is this deprecated?
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ from cookbook.serializer import (AutomationSerializer, BookmarkletImportSerializ
|
||||
SupermarketCategorySerializer, SupermarketSerializer,
|
||||
SyncLogSerializer, SyncSerializer, UnitSerializer,
|
||||
UserFileSerializer, UserNameSerializer, UserPreferenceSerializer,
|
||||
ViewLogSerializer)
|
||||
ViewLogSerializer, IngredientSimpleSerializer)
|
||||
from recipes import settings
|
||||
|
||||
|
||||
@@ -121,14 +121,17 @@ class ExtendedRecipeMixin():
|
||||
|
||||
# add a recipe count annotation to the query
|
||||
# explanation on construction https://stackoverflow.com/a/43771738/15762829
|
||||
recipe_count = Recipe.objects.filter(**{recipe_filter: OuterRef('id')}, space=space).values(recipe_filter).annotate(count=Count('pk')).values('count')
|
||||
recipe_count = Recipe.objects.filter(**{recipe_filter: OuterRef('id')}, space=space).values(
|
||||
recipe_filter).annotate(count=Count('pk')).values('count')
|
||||
queryset = queryset.annotate(recipe_count=Coalesce(Subquery(recipe_count), 0))
|
||||
|
||||
# add a recipe image annotation to the query
|
||||
image_subquery = Recipe.objects.filter(**{recipe_filter: OuterRef('id')}, space=space).exclude(image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
|
||||
image_subquery = Recipe.objects.filter(**{recipe_filter: OuterRef('id')}, space=space).exclude(
|
||||
image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
|
||||
if tree:
|
||||
image_children_subquery = Recipe.objects.filter(**{f"{recipe_filter}__path__startswith": OuterRef('path')},
|
||||
space=space).exclude(image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
|
||||
image_children_subquery = Recipe.objects.filter(
|
||||
**{f"{recipe_filter}__path__startswith": OuterRef('path')},
|
||||
space=space).exclude(image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
|
||||
else:
|
||||
image_children_subquery = None
|
||||
if images:
|
||||
@@ -144,11 +147,14 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
|
||||
def get_queryset(self):
|
||||
self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
|
||||
query = self.request.query_params.get('query', None)
|
||||
fuzzy = self.request.user.searchpreference.lookup or any([self.model.__name__.lower() in x for x in self.request.user.searchpreference.trigram.values_list('field', flat=True)])
|
||||
fuzzy = self.request.user.searchpreference.lookup or any([self.model.__name__.lower() in x for x in
|
||||
self.request.user.searchpreference.trigram.values_list(
|
||||
'field', flat=True)])
|
||||
|
||||
if query is not None and query not in ["''", '']:
|
||||
if fuzzy:
|
||||
if any([self.model.__name__.lower() in x for x in self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
|
||||
if any([self.model.__name__.lower() in x for x in
|
||||
self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
|
||||
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name__unaccent', query))
|
||||
else:
|
||||
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name', query))
|
||||
@@ -156,7 +162,8 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
|
||||
else:
|
||||
# TODO have this check unaccent search settings or other search preferences?
|
||||
filter = Q(name__icontains=query)
|
||||
if any([self.model.__name__.lower() in x for x in self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
|
||||
if any([self.model.__name__.lower() in x for x in
|
||||
self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
|
||||
filter |= Q(name__unaccent__icontains=query)
|
||||
|
||||
self.queryset = (
|
||||
@@ -277,10 +284,12 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin, ExtendedRecipeMixin):
|
||||
except self.model.DoesNotExist:
|
||||
self.queryset = self.model.objects.none()
|
||||
else:
|
||||
return self.annotate_recipe(queryset=super().get_queryset(), request=self.request, serializer=self.serializer_class, tree=True)
|
||||
return self.annotate_recipe(queryset=super().get_queryset(), request=self.request,
|
||||
serializer=self.serializer_class, tree=True)
|
||||
self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
|
||||
|
||||
return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class, tree=True)
|
||||
return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class,
|
||||
tree=True)
|
||||
|
||||
@decorators.action(detail=True, url_path='move/(?P<parent>[^/.]+)', methods=['PUT'], )
|
||||
@decorators.renderer_classes((TemplateHTMLRenderer, JSONRenderer))
|
||||
@@ -456,12 +465,16 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
||||
pagination_class = DefaultPagination
|
||||
|
||||
def get_queryset(self):
|
||||
self.request._shared_users = [x.id for x in list(self.request.user.get_shopping_share())] + [self.request.user.id]
|
||||
self.request._shared_users = [x.id for x in list(self.request.user.get_shopping_share())] + [
|
||||
self.request.user.id]
|
||||
|
||||
self.queryset = super().get_queryset()
|
||||
shopping_status = ShoppingListEntry.objects.filter(space=self.request.space, food=OuterRef('id'), checked=False).values('id')
|
||||
shopping_status = ShoppingListEntry.objects.filter(space=self.request.space, food=OuterRef('id'),
|
||||
checked=False).values('id')
|
||||
# onhand_status = self.queryset.annotate(onhand_status=Exists(onhand_users_set__in=[shared_users]))
|
||||
return self.queryset.annotate(shopping_status=Exists(shopping_status)).prefetch_related('onhand_users', 'inherit_fields').select_related('recipe', 'supermarket_category')
|
||||
return self.queryset.annotate(shopping_status=Exists(shopping_status)).prefetch_related('onhand_users',
|
||||
'inherit_fields').select_related(
|
||||
'recipe', 'supermarket_category')
|
||||
|
||||
@decorators.action(detail=True, methods=['PUT'], serializer_class=FoodShoppingUpdateSerializer, )
|
||||
# TODO DRF only allows one action in a decorator action without overriding get_operation_id_base() this should be PUT and DELETE probably
|
||||
@@ -472,7 +485,8 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
||||
shared_users = list(self.request.user.get_shopping_share())
|
||||
shared_users.append(request.user)
|
||||
if request.data.get('_delete', False) == 'true':
|
||||
ShoppingListEntry.objects.filter(food=obj, checked=False, space=request.space, created_by__in=shared_users).delete()
|
||||
ShoppingListEntry.objects.filter(food=obj, checked=False, space=request.space,
|
||||
created_by__in=shared_users).delete()
|
||||
content = {'msg': _(f'{obj.name} was removed from the shopping list.')}
|
||||
return Response(content, status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -480,7 +494,8 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
||||
unit = request.data.get('unit', None)
|
||||
content = {'msg': _(f'{obj.name} was added to the shopping list.')}
|
||||
|
||||
ShoppingListEntry.objects.create(food=obj, amount=amount, unit=unit, space=request.space, created_by=request.user)
|
||||
ShoppingListEntry.objects.create(food=obj, amount=amount, unit=unit, space=request.space,
|
||||
created_by=request.user)
|
||||
return Response(content, status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def destroy(self, *args, **kwargs):
|
||||
@@ -579,8 +594,22 @@ class IngredientViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = IngredientSerializer
|
||||
permission_classes = [CustomIsUser]
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request and self.request.query_params.get('simple', False):
|
||||
return IngredientSimpleSerializer
|
||||
return IngredientSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return self.queryset.filter(step__recipe__space=self.request.space)
|
||||
queryset = self.queryset.filter(step__recipe__space=self.request.space)
|
||||
food = self.request.query_params.get('food', None)
|
||||
if food and re.match(r'^(\d)+$', food):
|
||||
queryset = queryset.filter(food_id=food)
|
||||
|
||||
unit = self.request.query_params.get('unit', None)
|
||||
if unit and re.match(r'^(\d)+$', unit):
|
||||
queryset = queryset.filter(unit_id=unit)
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class StepViewSet(viewsets.ModelViewSet):
|
||||
@@ -589,7 +618,8 @@ class StepViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [CustomIsUser]
|
||||
pagination_class = DefaultPagination
|
||||
query_params = [
|
||||
QueryParam(name='recipe', description=_('ID of recipe a step is part of. For multiple repeat parameter.'), qtype='int'),
|
||||
QueryParam(name='recipe', description=_('ID of recipe a step is part of. For multiple repeat parameter.'),
|
||||
qtype='int'),
|
||||
QueryParam(name='query', description=_('Query string matched (fuzzy) against object name.'), qtype='string'),
|
||||
]
|
||||
schema = QueryParamAutoSchema()
|
||||
@@ -633,33 +663,63 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
pagination_class = RecipePagination
|
||||
|
||||
query_params = [
|
||||
QueryParam(name='query', description=_('Query string matched (fuzzy) against recipe name. In the future also fulltext search.')),
|
||||
QueryParam(name='keywords', description=_('ID of keyword a recipe should have. For multiple repeat parameter. Equivalent to keywords_or'), qtype='int'),
|
||||
QueryParam(name='keywords_or', description=_('Keyword IDs, repeat for multiple. Return recipes with any of the keywords'), qtype='int'),
|
||||
QueryParam(name='keywords_and', description=_('Keyword IDs, repeat for multiple. Return recipes with all of the keywords.'), qtype='int'),
|
||||
QueryParam(name='keywords_or_not', description=_('Keyword IDs, repeat for multiple. Exclude recipes with any of the keywords.'), qtype='int'),
|
||||
QueryParam(name='keywords_and_not', description=_('Keyword IDs, repeat for multiple. Exclude recipes with all of the keywords.'), qtype='int'),
|
||||
QueryParam(name='foods', description=_('ID of food a recipe should have. For multiple repeat parameter.'), qtype='int'),
|
||||
QueryParam(name='foods_or', description=_('Food IDs, repeat for multiple. Return recipes with any of the foods'), qtype='int'),
|
||||
QueryParam(name='foods_and', description=_('Food IDs, repeat for multiple. Return recipes with all of the foods.'), qtype='int'),
|
||||
QueryParam(name='foods_or_not', description=_('Food IDs, repeat for multiple. Exclude recipes with any of the foods.'), qtype='int'),
|
||||
QueryParam(name='foods_and_not', description=_('Food IDs, repeat for multiple. Exclude recipes with all of the foods.'), qtype='int'),
|
||||
QueryParam(name='query', description=_(
|
||||
'Query string matched (fuzzy) against recipe name. In the future also fulltext search.')),
|
||||
QueryParam(name='keywords', description=_(
|
||||
'ID of keyword a recipe should have. For multiple repeat parameter. Equivalent to keywords_or'),
|
||||
qtype='int'),
|
||||
QueryParam(name='keywords_or',
|
||||
description=_('Keyword IDs, repeat for multiple. Return recipes with any of the keywords'),
|
||||
qtype='int'),
|
||||
QueryParam(name='keywords_and',
|
||||
description=_('Keyword IDs, repeat for multiple. Return recipes with all of the keywords.'),
|
||||
qtype='int'),
|
||||
QueryParam(name='keywords_or_not',
|
||||
description=_('Keyword IDs, repeat for multiple. Exclude recipes with any of the keywords.'),
|
||||
qtype='int'),
|
||||
QueryParam(name='keywords_and_not',
|
||||
description=_('Keyword IDs, repeat for multiple. Exclude recipes with all of the keywords.'),
|
||||
qtype='int'),
|
||||
QueryParam(name='foods', description=_('ID of food a recipe should have. For multiple repeat parameter.'),
|
||||
qtype='int'),
|
||||
QueryParam(name='foods_or',
|
||||
description=_('Food IDs, repeat for multiple. Return recipes with any of the foods'), qtype='int'),
|
||||
QueryParam(name='foods_and',
|
||||
description=_('Food IDs, repeat for multiple. Return recipes with all of the foods.'), qtype='int'),
|
||||
QueryParam(name='foods_or_not',
|
||||
description=_('Food IDs, repeat for multiple. Exclude recipes with any of the foods.'), qtype='int'),
|
||||
QueryParam(name='foods_and_not',
|
||||
description=_('Food IDs, repeat for multiple. Exclude recipes with all of the foods.'), qtype='int'),
|
||||
QueryParam(name='units', description=_('ID of unit a recipe should have.'), qtype='int'),
|
||||
QueryParam(name='rating', description=_('Rating a recipe should have or greater. [0 - 5] Negative value filters rating less than.'), qtype='int'),
|
||||
QueryParam(name='rating', description=_(
|
||||
'Rating a recipe should have or greater. [0 - 5] Negative value filters rating less than.'), qtype='int'),
|
||||
QueryParam(name='books', description=_('ID of book a recipe should be in. For multiple repeat parameter.')),
|
||||
QueryParam(name='books_or', description=_('Book IDs, repeat for multiple. Return recipes with any of the books'), qtype='int'),
|
||||
QueryParam(name='books_and', description=_('Book IDs, repeat for multiple. Return recipes with all of the books.'), qtype='int'),
|
||||
QueryParam(name='books_or_not', description=_('Book IDs, repeat for multiple. Exclude recipes with any of the books.'), qtype='int'),
|
||||
QueryParam(name='books_and_not', description=_('Book IDs, repeat for multiple. Exclude recipes with all of the books.'), qtype='int'),
|
||||
QueryParam(name='internal', description=_('If only internal recipes should be returned. [''true''/''<b>false</b>'']')),
|
||||
QueryParam(name='random', description=_('Returns the results in randomized order. [''true''/''<b>false</b>'']')),
|
||||
QueryParam(name='new', description=_('Returns new results first in search results. [''true''/''<b>false</b>'']')),
|
||||
QueryParam(name='timescooked', description=_('Filter recipes cooked X times or more. Negative values returns cooked less than X times'), qtype='int'),
|
||||
QueryParam(name='cookedon', description=_('Filter recipes last cooked on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='createdon', description=_('Filter recipes created on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='updatedon', description=_('Filter recipes updated on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='viewedon', description=_('Filter recipes lasts viewed on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='makenow', description=_('Filter recipes that can be made with OnHand food. [''true''/''<b>false</b>'']')),
|
||||
QueryParam(name='books_or',
|
||||
description=_('Book IDs, repeat for multiple. Return recipes with any of the books'), qtype='int'),
|
||||
QueryParam(name='books_and',
|
||||
description=_('Book IDs, repeat for multiple. Return recipes with all of the books.'), qtype='int'),
|
||||
QueryParam(name='books_or_not',
|
||||
description=_('Book IDs, repeat for multiple. Exclude recipes with any of the books.'), qtype='int'),
|
||||
QueryParam(name='books_and_not',
|
||||
description=_('Book IDs, repeat for multiple. Exclude recipes with all of the books.'), qtype='int'),
|
||||
QueryParam(name='internal',
|
||||
description=_('If only internal recipes should be returned. [''true''/''<b>false</b>'']')),
|
||||
QueryParam(name='random',
|
||||
description=_('Returns the results in randomized order. [''true''/''<b>false</b>'']')),
|
||||
QueryParam(name='new',
|
||||
description=_('Returns new results first in search results. [''true''/''<b>false</b>'']')),
|
||||
QueryParam(name='timescooked', description=_(
|
||||
'Filter recipes cooked X times or more. Negative values returns cooked less than X times'), qtype='int'),
|
||||
QueryParam(name='cookedon', description=_(
|
||||
'Filter recipes last cooked on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='createdon', description=_(
|
||||
'Filter recipes created on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='updatedon', description=_(
|
||||
'Filter recipes updated on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='viewedon', description=_(
|
||||
'Filter recipes lasts viewed on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='makenow',
|
||||
description=_('Filter recipes that can be made with OnHand food. [''true''/''<b>false</b>'']')),
|
||||
]
|
||||
schema = QueryParamAutoSchema()
|
||||
|
||||
@@ -674,7 +734,8 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
if not (share and self.detail):
|
||||
self.queryset = self.queryset.filter(space=self.request.space)
|
||||
|
||||
params = {x: self.request.GET.get(x) if len({**self.request.GET}[x]) == 1 else self.request.GET.getlist(x) for x in list(self.request.GET)}
|
||||
params = {x: self.request.GET.get(x) if len({**self.request.GET}[x]) == 1 else self.request.GET.getlist(x) for x
|
||||
in list(self.request.GET)}
|
||||
search = RecipeSearch(self.request, **params)
|
||||
self.queryset = search.get_queryset(self.queryset).prefetch_related('cooklog_set')
|
||||
return self.queryset
|
||||
@@ -785,7 +846,8 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
levels = int(request.query_params.get('levels', 1))
|
||||
except (ValueError, TypeError):
|
||||
levels = 1
|
||||
qs = obj.get_related_recipes(levels=levels) # TODO: make levels a user setting, included in request data?, keep solely in the backend?
|
||||
qs = obj.get_related_recipes(
|
||||
levels=levels) # TODO: make levels a user setting, included in request data?, keep solely in the backend?
|
||||
return Response(self.serializer_class(qs, many=True).data)
|
||||
|
||||
|
||||
@@ -795,7 +857,8 @@ class ShoppingListRecipeViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [CustomIsOwner | CustomIsShared]
|
||||
|
||||
def get_queryset(self):
|
||||
self.queryset = self.queryset.filter(Q(shoppinglist__space=self.request.space) | Q(entries__space=self.request.space))
|
||||
self.queryset = self.queryset.filter(
|
||||
Q(shoppinglist__space=self.request.space) | Q(entries__space=self.request.space))
|
||||
return self.queryset.filter(
|
||||
Q(shoppinglist__created_by=self.request.user)
|
||||
| Q(shoppinglist__shared=self.request.user)
|
||||
@@ -809,12 +872,17 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ShoppingListEntrySerializer
|
||||
permission_classes = [CustomIsOwner | CustomIsShared]
|
||||
query_params = [
|
||||
QueryParam(name='id', description=_('Returns the shopping list entry with a primary key of id. Multiple values allowed.'), qtype='int'),
|
||||
QueryParam(name='id',
|
||||
description=_('Returns the shopping list entry with a primary key of id. Multiple values allowed.'),
|
||||
qtype='int'),
|
||||
QueryParam(
|
||||
name='checked',
|
||||
description=_('Filter shopping list entries on checked. [''true'', ''false'', ''both'', ''<b>recent</b>'']<br> - ''recent'' includes unchecked items and recently completed items.')
|
||||
description=_(
|
||||
'Filter shopping list entries on checked. [''true'', ''false'', ''both'', ''<b>recent</b>'']<br> - ''recent'' includes unchecked items and recently completed items.')
|
||||
),
|
||||
QueryParam(name='supermarket', description=_('Returns the shopping list entries sorted by supermarket category order.'), qtype='int'),
|
||||
QueryParam(name='supermarket',
|
||||
description=_('Returns the shopping list entries sorted by supermarket category order.'),
|
||||
qtype='int'),
|
||||
]
|
||||
schema = QueryParamAutoSchema()
|
||||
|
||||
|
||||
@@ -61,7 +61,8 @@ def search(request):
|
||||
if request.user.userpreference.search_style == UserPreference.NEW:
|
||||
return search_v2(request)
|
||||
f = RecipeFilter(request.GET,
|
||||
queryset=Recipe.objects.filter(space=request.user.userpreference.space).all().order_by(Lower('name').asc()),
|
||||
queryset=Recipe.objects.filter(space=request.user.userpreference.space).all().order_by(
|
||||
Lower('name').asc()),
|
||||
space=request.space)
|
||||
if request.user.userpreference.search_style == UserPreference.LARGE:
|
||||
table = RecipeTable(f.qs)
|
||||
@@ -225,6 +226,19 @@ def supermarket(request):
|
||||
return render(request, 'supermarket.html', {})
|
||||
|
||||
|
||||
@group_required('user')
|
||||
def ingredient_editor(request):
|
||||
template_vars = {'food_id': -1, 'unit_id': -1}
|
||||
food_id = request.GET.get('food_id', None)
|
||||
if food_id and re.match(r'^(\d)+$', food_id):
|
||||
template_vars['food_id'] = food_id
|
||||
|
||||
unit_id = request.GET.get('unit_id', None)
|
||||
if unit_id and re.match(r'^(\d)+$', unit_id):
|
||||
template_vars['unit_id'] = unit_id
|
||||
return render(request, 'ingredient_editor.html', template_vars)
|
||||
|
||||
|
||||
@group_required('user')
|
||||
def meal_plan_entry(request, pk):
|
||||
plan = MealPlan.objects.filter(space=request.space).get(pk=pk)
|
||||
@@ -304,6 +318,7 @@ def user_settings(request):
|
||||
up.use_fractions = form.cleaned_data['use_fractions']
|
||||
up.use_kj = form.cleaned_data['use_kj']
|
||||
up.sticky_navbar = form.cleaned_data['sticky_navbar']
|
||||
up.left_handed = form.cleaned_data['left_handed']
|
||||
|
||||
up.save()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user