mealie importer options

This commit is contained in:
vabene1111
2025-09-16 08:00:22 +02:00
parent 13626ca11b
commit ea590f8e49
6 changed files with 87 additions and 52 deletions

View File

@@ -76,6 +76,11 @@ class ImportForm(ImportExportBase):
files = MultipleFileField(required=True)
duplicates = forms.BooleanField(help_text=_('To prevent duplicates recipes with the same name as existing ones are ignored. Check this box to import everything.'),
required=False)
meal_plans = forms.BooleanField(required=False)
shopping_lists = forms.BooleanField(required=False)
nutrition_per_serving = forms.BooleanField(required=False) # some managers (e.g. mealie) do not specify what the nutrition's relate to so we let the user choose
class ExportForm(ImportExportBase):
recipes = forms.ModelMultipleChoiceField(widget=MultiSelectWidget, queryset=Recipe.objects.none(), required=False)
all = forms.BooleanField(required=False)

View File

@@ -29,6 +29,10 @@ class Integration:
import_log = None
import_duplicates = False
import_meal_plans = True
import_shopping_lists = True
nutrition_per_servings = False
def __init__(self, request, export_type):
"""
Integration for importing and exporting recipes
@@ -60,7 +64,10 @@ class Integration:
space=request.space
)
def do_export(self, recipes, el):
def do_export(self, recipes, el, meal_plans=True, shopping_lists=True, nutrition_per_servings=False):
self.import_meal_plans = meal_plans
self.import_shopping_lists = shopping_lists
self.nutrition_per_servings = nutrition_per_servings
with scope(space=self.request.space):
el.total_recipes = len(recipes)

View File

@@ -85,6 +85,7 @@ class Mealie1(Integration):
units_dict[u['id']] = unit.pk
recipes_dict = {}
recipe_property_factor_dict = {}
recipes = []
recipe_keyword_relation = []
for r in mealie_database['recipes']:
@@ -102,6 +103,9 @@ class Mealie1(Integration):
created_by=self.request.user,
)
if not self.nutrition_per_servings:
recipe_property_factor_dict[r['id']] = recipe.servings
self.handle_duplicates(recipe, self.import_duplicates)
self.import_log.msg += self.get_recipe_processed_msg(recipe)
self.import_log.imported_recipes += 1
@@ -211,10 +215,11 @@ class Mealie1(Integration):
for r in mealie_database['recipe_nutrition']:
for key in property_types_dict:
if r[key]:
# the mealie UI does not communicate to the user if nutrition's are per serving or recipe, expect per serving by default
# TODO add option for user to choose between recipe and serving properties (use recipe_property_factor_dict with pre-calculated property factors)
properties_relation.append(
Property(property_type_id=property_types_dict[key].pk, property_amount=r[key], open_data_food_slug=r['recipe_id'], space=self.request.space))
Property(property_type_id=property_types_dict[key].pk,
property_amount=r[key] * recipe_property_factor_dict[r['recipe_id']] if r['recipe_id'] in recipe_property_factor_dict else 1,
open_data_food_slug=r['recipe_id'],
space=self.request.space))
properties = Property.objects.bulk_create(properties_relation)
property_ids = []
for p in properties:
@@ -255,54 +260,56 @@ class Mealie1(Integration):
CookLog.objects.bulk_create(cook_log_list)
self.import_log.msg += f"Importing {len(mealie_database["group_meal_plans"])} meal plans...\n"
self.import_log.save()
if self.import_meal_plans:
self.import_log.msg += f"Importing {len(mealie_database["group_meal_plans"])} meal plans...\n"
self.import_log.save()
meal_types_dict = {}
meal_plans = []
for m in mealie_database['group_meal_plans']:
if not m['entry_type'] in meal_types_dict:
meal_type = MealType.objects.get_or_create(name=m['entry_type'], created_by=self.request.user, space=self.request.space)[0]
meal_types_dict[m['entry_type']] = meal_type.pk
meal_plans.append(MealPlan(
recipe_id=recipes_dict[m['recipe_id']] if m['recipe_id'] else None,
title=m['title'] if m['title'] else "",
note=m['text'] if m['text'] else "",
from_date=m['date'],
to_date=m['date'],
meal_type_id=meal_types_dict[m['entry_type']],
created_by=self.request.user,
space=self.request.space,
))
meal_types_dict = {}
meal_plans = []
for m in mealie_database['group_meal_plans']:
if not m['entry_type'] in meal_types_dict:
meal_type = MealType.objects.get_or_create(name=m['entry_type'], created_by=self.request.user, space=self.request.space)[0]
meal_types_dict[m['entry_type']] = meal_type.pk
meal_plans.append(MealPlan(
recipe_id=recipes_dict[m['recipe_id']] if m['recipe_id'] else None,
title=m['title'] if m['title'] else "",
note=m['text'] if m['text'] else "",
from_date=m['date'],
to_date=m['date'],
meal_type_id=meal_types_dict[m['entry_type']],
created_by=self.request.user,
space=self.request.space,
))
MealPlan.objects.bulk_create(meal_plans)
MealPlan.objects.bulk_create(meal_plans)
self.import_log.msg += f"Importing {len(mealie_database["shopping_list_items"])} shopping list items...\n"
self.import_log.save()
if self.import_shopping_lists:
self.import_log.msg += f"Importing {len(mealie_database["shopping_list_items"])} shopping list items...\n"
self.import_log.save()
shopping_list_items = []
for sli in mealie_database['shopping_list_items']:
if not sli['checked']:
if sli['food_id']:
shopping_list_items.append(ShoppingListEntry(
amount=sli['quantity'],
unit_id=units_dict[sli['unit_id']] if sli['unit_id'] else None,
food_id=foods_dict[sli['food_id']] if sli['food_id'] else None,
created_by=self.request.user,
space=self.request.space,
))
elif not sli['food_id'] and sli['note'].strip():
amount, unit, food, note = ingredient_parser.parse(sli['note'].strip())
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
shopping_list_items.append(ShoppingListEntry(
amount=amount,
unit=u,
food=f,
created_by=self.request.user,
space=self.request.space,
))
ShoppingListEntry.objects.bulk_create(shopping_list_items)
shopping_list_items = []
for sli in mealie_database['shopping_list_items']:
if not sli['checked']:
if sli['food_id']:
shopping_list_items.append(ShoppingListEntry(
amount=sli['quantity'],
unit_id=units_dict[sli['unit_id']] if sli['unit_id'] else None,
food_id=foods_dict[sli['food_id']] if sli['food_id'] else None,
created_by=self.request.user,
space=self.request.space,
))
elif not sli['food_id'] and sli['note'].strip():
amount, unit, food, note = ingredient_parser.parse(sli['note'].strip())
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
shopping_list_items.append(ShoppingListEntry(
amount=amount,
unit=u,
food=f,
created_by=self.request.user,
space=self.request.space,
))
ShoppingListEntry.objects.bulk_create(shopping_list_items)
return recipes

View File

@@ -2280,7 +2280,13 @@ class AppImportView(APIView):
files = []
for f in request.FILES.getlist('files'):
files.append({'file': io.BytesIO(f.read()), 'name': f.name})
t = threading.Thread(target=integration.do_import, args=[files, il, form.cleaned_data['duplicates']])
t = threading.Thread(target=integration.do_import,
args=[files, il, form.cleaned_data['duplicates']],
kwargs={'meal_plans': form.cleaned_data['meal_plans'],
'shopping_lists': form.cleaned_data['shopping_lists'],
'nutrition_per_serving': form.cleaned_data['nutrition_per_serving']
}
)
t.setDaemon(True)
t.start()

View File

@@ -2,6 +2,7 @@ import {useDjangoUrls} from "@/composables/useDjangoUrls";
import {ref} from "vue";
import {getCookie} from "@/utils/cookie";
import {AiProvider, RecipeFromSourceResponseFromJSON, RecipeImageFromJSON, ResponseError, UserFile, UserFileFromJSON} from "@/openapi";
import {tr} from "vuetify/locale";
/**
@@ -117,12 +118,18 @@ export function useFileApi() {
* @param files array to import
* @param app app to import
* @param includeDuplicates if recipes that were found as duplicates should be imported as well
* @param mealPlans if meal plans should be imported
* @param shoppingLists if shopping lists should be imported
* @param nutritionPerServing if nutrition information should be treated as per serving (if false its treated as per recipe)
* @returns Promise resolving to the import ID of the app import
*/
function doAppImport(files: File[], app: string, includeDuplicates: boolean) {
function doAppImport(files: File[], app: string, includeDuplicates: boolean, mealPlans: boolean = true, shoppingLists: boolean = true, nutritionPerServing: boolean = false,) {
let formData = new FormData()
formData.append('type', app);
formData.append('duplicates', includeDuplicates ? 'true' : 'false')
formData.append('meal_plans', mealPlans ? 'true' : 'false')
formData.append('shopping_lists', shoppingLists ? 'true' : 'false')
formData.append('nutrition_per_serving', nutritionPerServing ? 'true' : 'false')
files.forEach(file => {
formData.append('files', file)
})
@@ -141,4 +148,4 @@ export function useFileApi() {
}
return {fileApiLoading, createOrUpdateUserFile, updateRecipeImage, doAiImport, doAppImport}
}
}

View File

@@ -658,6 +658,9 @@ const urlListImportedRecipes = ref([] as Recipe[])
const sourceImportText = ref("")
const appImportFiles = ref<File[]>([])
const appImportDuplicates = ref(false)
const appImportMealPlans = ref(true)
const appImportShoppingLists = ref(true)
const appImportNutritionsPerServing = ref(false)
const appImportLog = ref<null | ImportLog>(null)
const image = ref<null | File>(null)
const aiMode = ref<'file' | 'text'>('file')
@@ -770,7 +773,7 @@ function loadRecipeFromAiImport() {
}
function appImport() {
doAppImport(appImportFiles.value, importApp.value, appImportDuplicates.value).then(r => {
doAppImport(appImportFiles.value, importApp.value, appImportDuplicates.value, appImportMealPlans.value, appImportShoppingLists.value, appImportNutritionsPerServing.value).then(r => {
stepper.value = 'import_log'
recLoadImportLog(r)
})