mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-05-13 21:37:42 -04:00
Compare commits
5 Commits
fix/plan-t
...
l10n_meali
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f72724d2d | ||
|
|
5dc98e8e7e | ||
|
|
eaf3a1b9bd | ||
|
|
f7e4d5ad99 | ||
|
|
04b975b779 |
@@ -321,7 +321,7 @@ async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) {
|
||||
const householdsWithFood = subIng.food?.householdsWithIngredientFood || [];
|
||||
ownIngs.push({
|
||||
checked: !householdsWithFood.includes(currentHouseholdSlug.value),
|
||||
ingredient: subIng,
|
||||
ingredient: { ...subIng, quantity: (ing.quantity || 1) * (subIng.quantity || 1) },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,9 @@ describe("useStoreActions", () => {
|
||||
|
||||
const mockStore = ref([]);
|
||||
const mockLoading = ref(false);
|
||||
const mockInitialized = ref(false);
|
||||
|
||||
test("deleteMany calls deleteOne for each ID and refreshes once", async () => {
|
||||
const actions = useStoreActions("test-store", mockApi, mockStore, mockLoading, mockInitialized);
|
||||
const actions = useStoreActions("test-store", mockApi, mockStore, mockLoading);
|
||||
|
||||
mockApi.deleteOne = vi.fn().mockResolvedValue({ response: { data: {} } });
|
||||
mockApi.getAll = vi.fn().mockResolvedValue({ data: { items: [] } });
|
||||
@@ -33,7 +32,7 @@ describe("useStoreActions", () => {
|
||||
});
|
||||
|
||||
test("deleteMany handles empty array", async () => {
|
||||
const actions = useStoreActions("test-store", mockApi, mockStore, mockLoading, mockInitialized);
|
||||
const actions = useStoreActions("test-store", mockApi, mockStore, mockLoading);
|
||||
|
||||
mockApi.deleteOne = vi.fn();
|
||||
mockApi.getAll = vi.fn().mockResolvedValue({ data: { items: [] } });
|
||||
@@ -45,7 +44,7 @@ describe("useStoreActions", () => {
|
||||
});
|
||||
|
||||
test("deleteMany sets loading state", async () => {
|
||||
const actions = useStoreActions("test-store", mockApi, mockStore, mockLoading, mockInitialized);
|
||||
const actions = useStoreActions("test-store", mockApi, mockStore, mockLoading);
|
||||
|
||||
mockApi.deleteOne = vi.fn().mockResolvedValue({});
|
||||
mockApi.getAll = vi.fn().mockResolvedValue({ data: { items: [] } });
|
||||
@@ -56,25 +55,4 @@ describe("useStoreActions", () => {
|
||||
await promise;
|
||||
expect(mockLoading.value).toBe(false);
|
||||
});
|
||||
|
||||
test("refresh sets initialized to true even when store returns empty results", async () => {
|
||||
const localInitialized = ref(false);
|
||||
const actions = useStoreActions("test-store", mockApi, mockStore, mockLoading, localInitialized);
|
||||
|
||||
mockApi.getAll = vi.fn().mockResolvedValue({ data: { items: [] } });
|
||||
|
||||
expect(localInitialized.value).toBe(false);
|
||||
await actions.refresh();
|
||||
expect(localInitialized.value).toBe(true);
|
||||
});
|
||||
|
||||
test("refresh sets initialized to true when store returns items", async () => {
|
||||
const localInitialized = ref(false);
|
||||
const actions = useStoreActions("test-store", mockApi, mockStore, mockLoading, localInitialized);
|
||||
|
||||
mockApi.getAll = vi.fn().mockResolvedValue({ data: { items: [{ id: "1", name: "item" }] } });
|
||||
|
||||
await actions.refresh();
|
||||
expect(localInitialized.value).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,6 @@ export function useReadOnlyActions<T extends BoundT>(
|
||||
api: BaseCRUDAPIReadOnly<T>,
|
||||
allRef: Ref<T[] | null> | null,
|
||||
loading: Ref<boolean>,
|
||||
initialized: Ref<boolean>,
|
||||
defaultQueryParams: Record<string, QueryValue> = {},
|
||||
): ReadOnlyStoreActions<T> {
|
||||
function getAll(page = 1, perPage = -1, params = {} as Record<string, QueryValue>) {
|
||||
@@ -70,7 +69,6 @@ export function useReadOnlyActions<T extends BoundT>(
|
||||
allRef.value = data.items;
|
||||
}
|
||||
|
||||
initialized.value = true;
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -91,7 +89,6 @@ export function useStoreActions<T extends BoundT>(
|
||||
api: BaseCRUDAPI<unknown, T, unknown>,
|
||||
allRef: Ref<T[] | null> | null,
|
||||
loading: Ref<boolean>,
|
||||
initialized: Ref<boolean>,
|
||||
defaultQueryParams: Record<string, QueryValue> = {},
|
||||
): StoreActions<T> {
|
||||
function getAll(page = 1, perPage = -1, params = {} as Record<string, QueryValue>) {
|
||||
@@ -135,7 +132,6 @@ export function useStoreActions<T extends BoundT>(
|
||||
allRef.value = data.items;
|
||||
}
|
||||
|
||||
initialized.value = true;
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,11 +16,10 @@ export const useReadOnlyStore = function <T extends BoundT>(
|
||||
storeKey: string,
|
||||
store: Ref<T[]>,
|
||||
loading: Ref<boolean>,
|
||||
initialized: Ref<boolean>,
|
||||
api: BaseCRUDAPIReadOnly<T>,
|
||||
params = {} as Record<string, QueryValue>,
|
||||
) {
|
||||
const storeActions = useReadOnlyActions(`${storeKey}-store-readonly`, api, store, loading, initialized);
|
||||
const storeActions = useReadOnlyActions(`${storeKey}-store-readonly`, api, store, loading);
|
||||
const actions = {
|
||||
...storeActions,
|
||||
async refresh() {
|
||||
@@ -28,12 +27,11 @@ export const useReadOnlyStore = function <T extends BoundT>(
|
||||
},
|
||||
flushStore() {
|
||||
store.value = [];
|
||||
initialized.value = false;
|
||||
},
|
||||
};
|
||||
|
||||
// initial hydration
|
||||
if (!loading.value && !initialized.value) {
|
||||
if (!loading.value && !store.value.length) {
|
||||
actions.refresh();
|
||||
}
|
||||
|
||||
@@ -44,11 +42,10 @@ export const useStore = function <T extends BoundT>(
|
||||
storeKey: string,
|
||||
store: Ref<T[]>,
|
||||
loading: Ref<boolean>,
|
||||
initialized: Ref<boolean>,
|
||||
api: BaseCRUDAPI<unknown, T, unknown>,
|
||||
params = {} as Record<string, QueryValue>,
|
||||
) {
|
||||
const storeActions = useStoreActions(`${storeKey}-store`, api, store, loading, initialized);
|
||||
const storeActions = useStoreActions(`${storeKey}-store`, api, store, loading);
|
||||
const actions = {
|
||||
...storeActions,
|
||||
async refresh() {
|
||||
@@ -56,12 +53,11 @@ export const useStore = function <T extends BoundT>(
|
||||
},
|
||||
flushStore() {
|
||||
store.value = [];
|
||||
initialized.value = false;
|
||||
},
|
||||
};
|
||||
|
||||
// initial hydration
|
||||
if (!loading.value && !initialized.value) {
|
||||
if (!loading.value && !store.value.length) {
|
||||
actions.refresh();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,16 +5,12 @@ import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||
|
||||
const store: Ref<RecipeCategory[]> = ref([]);
|
||||
const loading = ref(false);
|
||||
const initialized = ref(false);
|
||||
const publicLoading = ref(false);
|
||||
const publicInitialized = ref(false);
|
||||
|
||||
export function resetCategoryStore() {
|
||||
store.value = [];
|
||||
loading.value = false;
|
||||
initialized.value = false;
|
||||
publicLoading.value = false;
|
||||
publicInitialized.value = false;
|
||||
}
|
||||
|
||||
export const useCategoryData = function () {
|
||||
@@ -27,10 +23,10 @@ export const useCategoryData = function () {
|
||||
|
||||
export const useCategoryStore = function (i18n?: Composer) {
|
||||
const api = useUserApi(i18n);
|
||||
return useStore<RecipeCategory>("category", store, loading, initialized, api.categories);
|
||||
return useStore<RecipeCategory>("category", store, loading, api.categories);
|
||||
};
|
||||
|
||||
export const usePublicCategoryStore = function (groupSlug: string, i18n?: Composer) {
|
||||
const api = usePublicExploreApi(groupSlug, i18n).explore;
|
||||
return useReadOnlyStore<RecipeCategory>("category", store, publicLoading, publicInitialized, api.categories);
|
||||
return useReadOnlyStore<RecipeCategory>("category", store, publicLoading, api.categories);
|
||||
};
|
||||
|
||||
@@ -5,21 +5,17 @@ import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||
|
||||
const cookbooks: Ref<ReadCookBook[]> = ref([]);
|
||||
const loading = ref(false);
|
||||
const initialized = ref(false);
|
||||
const publicLoading = ref(false);
|
||||
const publicInitialized = ref(false);
|
||||
|
||||
export function resetCookbookStore() {
|
||||
cookbooks.value = [];
|
||||
loading.value = false;
|
||||
initialized.value = false;
|
||||
publicLoading.value = false;
|
||||
publicInitialized.value = false;
|
||||
}
|
||||
|
||||
export const useCookbookStore = function (i18n?: Composer) {
|
||||
const api = useUserApi(i18n);
|
||||
const store = useStore<ReadCookBook>("cookbook", cookbooks, loading, initialized, api.cookbooks);
|
||||
const store = useStore<ReadCookBook>("cookbook", cookbooks, loading, api.cookbooks);
|
||||
|
||||
const updateAll = async function (updateData: UpdateCookBook[]) {
|
||||
loading.value = true;
|
||||
@@ -35,5 +31,5 @@ export const useCookbookStore = function (i18n?: Composer) {
|
||||
|
||||
export const usePublicCookbookStore = function (groupSlug: string, i18n?: Composer) {
|
||||
const api = usePublicExploreApi(groupSlug, i18n).explore;
|
||||
return useReadOnlyStore<ReadCookBook>("cookbook", cookbooks, publicLoading, publicInitialized, api.cookbooks);
|
||||
return useReadOnlyStore<ReadCookBook>("cookbook", cookbooks, publicLoading, api.cookbooks);
|
||||
};
|
||||
|
||||
@@ -5,16 +5,12 @@ import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||
|
||||
const store: Ref<IngredientFood[]> = ref([]);
|
||||
const loading = ref(false);
|
||||
const initialized = ref(false);
|
||||
const publicLoading = ref(false);
|
||||
const publicInitialized = ref(false);
|
||||
|
||||
export function resetFoodStore() {
|
||||
store.value = [];
|
||||
loading.value = false;
|
||||
initialized.value = false;
|
||||
publicLoading.value = false;
|
||||
publicInitialized.value = false;
|
||||
}
|
||||
|
||||
export const useFoodData = function () {
|
||||
@@ -28,10 +24,10 @@ export const useFoodData = function () {
|
||||
|
||||
export const useFoodStore = function (i18n?: Composer) {
|
||||
const api = useUserApi(i18n);
|
||||
return useStore<IngredientFood>("food", store, loading, initialized, api.foods);
|
||||
return useStore<IngredientFood>("food", store, loading, api.foods);
|
||||
};
|
||||
|
||||
export const usePublicFoodStore = function (groupSlug: string, i18n?: Composer) {
|
||||
const api = usePublicExploreApi(groupSlug, i18n).explore;
|
||||
return useReadOnlyStore<IngredientFood>("food", store, publicLoading, publicInitialized, api.foods);
|
||||
return useReadOnlyStore<IngredientFood>("food", store, publicLoading, api.foods);
|
||||
};
|
||||
|
||||
@@ -5,24 +5,20 @@ import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||
|
||||
const store: Ref<HouseholdSummary[]> = ref([]);
|
||||
const loading = ref(false);
|
||||
const initialized = ref(false);
|
||||
const publicLoading = ref(false);
|
||||
const publicInitialized = ref(false);
|
||||
|
||||
export function resetHouseholdStore() {
|
||||
store.value = [];
|
||||
loading.value = false;
|
||||
initialized.value = false;
|
||||
publicLoading.value = false;
|
||||
publicInitialized.value = false;
|
||||
}
|
||||
|
||||
export const useHouseholdStore = function (i18n?: Composer) {
|
||||
const api = useUserApi(i18n);
|
||||
return useReadOnlyStore<HouseholdSummary>("household", store, loading, initialized, api.households);
|
||||
return useReadOnlyStore<HouseholdSummary>("household", store, loading, api.households);
|
||||
};
|
||||
|
||||
export const usePublicHouseholdStore = function (groupSlug: string, i18n?: Composer) {
|
||||
const api = usePublicExploreApi(groupSlug, i18n).explore;
|
||||
return useReadOnlyStore<HouseholdSummary>("household-public", store, publicLoading, publicInitialized, api.households);
|
||||
return useReadOnlyStore<HouseholdSummary>("household-public", store, publicLoading, api.households);
|
||||
};
|
||||
|
||||
@@ -5,12 +5,10 @@ import { useUserApi } from "~/composables/api";
|
||||
|
||||
const store: Ref<MultiPurposeLabelOut[]> = ref([]);
|
||||
const loading = ref(false);
|
||||
const initialized = ref(false);
|
||||
|
||||
export function resetLabelStore() {
|
||||
store.value = [];
|
||||
loading.value = false;
|
||||
initialized.value = false;
|
||||
}
|
||||
|
||||
export const useLabelData = function () {
|
||||
@@ -24,5 +22,5 @@ export const useLabelData = function () {
|
||||
|
||||
export const useLabelStore = function (i18n?: Composer) {
|
||||
const api = useUserApi(i18n);
|
||||
return useStore<MultiPurposeLabelOut>("label", store, loading, initialized, api.multiPurposeLabels);
|
||||
return useStore<MultiPurposeLabelOut>("label", store, loading, api.multiPurposeLabels);
|
||||
};
|
||||
|
||||
@@ -5,16 +5,12 @@ import { usePublicExploreApi, useUserApi } from "~/composables/api";
|
||||
|
||||
const store: Ref<RecipeTag[]> = ref([]);
|
||||
const loading = ref(false);
|
||||
const initialized = ref(false);
|
||||
const publicLoading = ref(false);
|
||||
const publicInitialized = ref(false);
|
||||
|
||||
export function resetTagStore() {
|
||||
store.value = [];
|
||||
loading.value = false;
|
||||
initialized.value = false;
|
||||
publicLoading.value = false;
|
||||
publicInitialized.value = false;
|
||||
}
|
||||
|
||||
export const useTagData = function () {
|
||||
@@ -27,10 +23,10 @@ export const useTagData = function () {
|
||||
|
||||
export const useTagStore = function (i18n?: Composer) {
|
||||
const api = useUserApi(i18n);
|
||||
return useStore<RecipeTag>("tag", store, loading, initialized, api.tags);
|
||||
return useStore<RecipeTag>("tag", store, loading, api.tags);
|
||||
};
|
||||
|
||||
export const usePublicTagStore = function (groupSlug: string, i18n?: Composer) {
|
||||
const api = usePublicExploreApi(groupSlug, i18n).explore;
|
||||
return useReadOnlyStore<RecipeTag>("tag", store, publicLoading, publicInitialized, api.tags);
|
||||
return useReadOnlyStore<RecipeTag>("tag", store, publicLoading, api.tags);
|
||||
};
|
||||
|
||||
@@ -9,16 +9,12 @@ interface RecipeToolWithOnHand extends RecipeTool {
|
||||
|
||||
const store: Ref<RecipeTool[]> = ref([]);
|
||||
const loading = ref(false);
|
||||
const initialized = ref(false);
|
||||
const publicLoading = ref(false);
|
||||
const publicInitialized = ref(false);
|
||||
|
||||
export function resetToolStore() {
|
||||
store.value = [];
|
||||
loading.value = false;
|
||||
initialized.value = false;
|
||||
publicLoading.value = false;
|
||||
publicInitialized.value = false;
|
||||
}
|
||||
|
||||
export const useToolData = function () {
|
||||
@@ -33,10 +29,10 @@ export const useToolData = function () {
|
||||
|
||||
export const useToolStore = function (i18n?: Composer) {
|
||||
const api = useUserApi(i18n);
|
||||
return useStore<RecipeTool>("tool", store, loading, initialized, api.tools);
|
||||
return useStore<RecipeTool>("tool", store, loading, api.tools);
|
||||
};
|
||||
|
||||
export const usePublicToolStore = function (groupSlug: string, i18n?: Composer) {
|
||||
const api = usePublicExploreApi(groupSlug, i18n).explore;
|
||||
return useReadOnlyStore<RecipeTool>("tool", store, publicLoading, publicInitialized, api.tools);
|
||||
return useReadOnlyStore<RecipeTool>("tool", store, publicLoading, api.tools);
|
||||
};
|
||||
|
||||
@@ -5,12 +5,10 @@ import { useUserApi } from "~/composables/api";
|
||||
|
||||
const store: Ref<IngredientUnit[]> = ref([]);
|
||||
const loading = ref(false);
|
||||
const initialized = ref(false);
|
||||
|
||||
export function resetUnitStore() {
|
||||
store.value = [];
|
||||
loading.value = false;
|
||||
initialized.value = false;
|
||||
}
|
||||
|
||||
export const useUnitData = function () {
|
||||
@@ -25,5 +23,5 @@ export const useUnitData = function () {
|
||||
|
||||
export const useUnitStore = function (i18n?: Composer) {
|
||||
const api = useUserApi(i18n);
|
||||
return useStore<IngredientUnit>("unit", store, loading, initialized, api.units);
|
||||
return useStore<IngredientUnit>("unit", store, loading, api.units);
|
||||
};
|
||||
|
||||
@@ -6,12 +6,10 @@ import { BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
|
||||
|
||||
const store: Ref<UserSummary[]> = ref([]);
|
||||
const loading = ref(false);
|
||||
const initialized = ref(false);
|
||||
|
||||
export function resetUserStore() {
|
||||
store.value = [];
|
||||
loading.value = false;
|
||||
initialized.value = false;
|
||||
}
|
||||
|
||||
class GroupUserAPIReadOnly extends BaseCRUDAPIReadOnly<UserSummary> {
|
||||
@@ -23,5 +21,5 @@ export const useUserStore = function (i18n?: Composer) {
|
||||
const requests = useRequests(i18n);
|
||||
const api = new GroupUserAPIReadOnly(requests);
|
||||
|
||||
return useReadOnlyStore<UserSummary>("user", store, loading, initialized, api, { orderBy: "full_name" });
|
||||
return useReadOnlyStore<UserSummary>("user", store, loading, api, { orderBy: "full_name" });
|
||||
};
|
||||
|
||||
@@ -427,7 +427,7 @@
|
||||
"mealie-text": "Mealie can import recipes from the Mealie application from a pre v1.0 release. Export your recipes from your old instance, and upload the zip file below. Note that only recipes can be imported from the export.",
|
||||
"plantoeat": {
|
||||
"title": "Plan to Eat",
|
||||
"description-long": "Mealie can import recipes from Plan to Eat. Upload a ZIP archive, CSV, or TXT file exported from Plan to Eat."
|
||||
"description-long": "Mealie can import recipies from Plan to Eat."
|
||||
},
|
||||
"myrecipebox": {
|
||||
"title": "My Recipe Box",
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
"dashboard": "Kontrollpanel",
|
||||
"delete": "Slett",
|
||||
"disabled": "Deaktivert",
|
||||
"done": "Done",
|
||||
"done": "Ferdig",
|
||||
"download": "Last ned",
|
||||
"duplicate": "Dupliser",
|
||||
"edit": "Rediger",
|
||||
@@ -169,7 +169,7 @@
|
||||
"token": "Token",
|
||||
"tuesday": "Tirsdag",
|
||||
"type": "Type",
|
||||
"undo": "Undo",
|
||||
"undo": "Angre",
|
||||
"update": "Oppdater",
|
||||
"updated": "Oppdatert",
|
||||
"upload": "Last opp",
|
||||
@@ -334,7 +334,7 @@
|
||||
"no-meal-plan-defined-yet": "Ingen måltidsplan er definert ennå",
|
||||
"no-meal-planned-for-today": "Ingen måltid planlagt i dag",
|
||||
"numberOfDaysPast-hint": "Number of days in the past on page load",
|
||||
"numberOfDaysPast-label": "Default Days in the Past",
|
||||
"numberOfDaysPast-label": "Standard antall dager tilbake",
|
||||
"numberOfDays-hint": "Antall dager på sideinnlasting",
|
||||
"numberOfDays-label": "Standard antall dager",
|
||||
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Kun oppskrifter med disse kategoriene vil bli brukt i måltidsplaner",
|
||||
@@ -392,7 +392,7 @@
|
||||
"nextcloud": {
|
||||
"description": "Overfør data fra en Nextcloud Cookbook-instans",
|
||||
"description-long": "Oppskrifter fra Nextcloud kan importeres fra en zip-fil som inneholder dataene lagret i Nextcloud. Se eksempelet på mappestrukture nedenfor for å sikre at oppskriftene kan importeres.",
|
||||
"title": "Nextcloud Cookbook"
|
||||
"title": "Nextcloud kokebok"
|
||||
},
|
||||
"copymethat": {
|
||||
"description-long": "Mealie kan importere oppskrifter fra Copy Me That. Eksporter oppskrifter i HTML-format, last deretter opp .zip-filen under.",
|
||||
@@ -917,7 +917,7 @@
|
||||
"quantity": "Antall: {0}",
|
||||
"shopping-list": "Handleliste",
|
||||
"shopping-lists": "Handlelister",
|
||||
"add-item": "Add item",
|
||||
"add-item": "Legg til produkt",
|
||||
"food": "Matvare",
|
||||
"note": "Notat",
|
||||
"label": "Etikett",
|
||||
@@ -943,7 +943,7 @@
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Er du sikker på at du vil fjerne valg av alle elementer?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Er du sikker på at du vil slette alle valgte elementer?",
|
||||
"no-shopping-lists-found": "Ingen handlelister funnet",
|
||||
"item-checked-off": "Checked off {item}"
|
||||
"item-checked-off": "Avkrysset av {item}"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Alle oppskrifter",
|
||||
@@ -1478,10 +1478,10 @@
|
||||
"max-length": "Må være minst minst {max} tegn må bestå av maks {max} tegn"
|
||||
},
|
||||
"announcements": {
|
||||
"announcements": "Announcements",
|
||||
"all-announcements": "All announcements",
|
||||
"mark-all-as-read": "Mark All as Read",
|
||||
"show-announcements-from-mealie": "Show announcements from Mealie",
|
||||
"announcements": "Kunngjøringer",
|
||||
"all-announcements": "Alle kunngjøringer",
|
||||
"mark-all-as-read": "Marker alle som lest",
|
||||
"show-announcements-from-mealie": "Vis kunngjøringer fra Mealie",
|
||||
"show-announcements-setting-description": "Whether or not you want to allow users to see announcements from Mealie. When enabled users can still opt-out from seeing them in their user settings"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -943,7 +943,7 @@
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Ali res ne želite izbrati vseh elementov?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Ali ste prepričani, da želite izbrisati vse izbrane elemente?",
|
||||
"no-shopping-lists-found": "Ni nakupovalnih seznamov",
|
||||
"item-checked-off": "Checked off {item}"
|
||||
"item-checked-off": "Odkljukano {item}"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Vsi recepti",
|
||||
|
||||
@@ -337,8 +337,16 @@ const _content: Record<string, MigrationContent> = {
|
||||
},
|
||||
[MIGRATIONS.plantoeat]: {
|
||||
text: i18n.t("migration.plantoeat.description-long"),
|
||||
acceptedFileType: ".zip,.csv,.txt",
|
||||
tree: false,
|
||||
acceptedFileType: ".zip",
|
||||
tree: [
|
||||
{
|
||||
icon: $globals.icons.zip,
|
||||
title: "plantoeat-recipes-508318_10-13-2023.zip",
|
||||
children: [
|
||||
{ title: "plantoeat-recipes-508318_10-13-2023.csv", icon: $globals.icons.codeJson },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
[MIGRATIONS.recipekeeper]: {
|
||||
text: i18n.t("migration.recipekeeper.description-long"),
|
||||
|
||||
@@ -179,7 +179,7 @@ def validate_file_token(token: str | None = None) -> Path:
|
||||
@contextmanager
|
||||
def get_temporary_zip_path(auto_unlink=True) -> Generator[Path, None, None]:
|
||||
app_dirs.TEMP_DIR.mkdir(exist_ok=True, parents=True)
|
||||
temp_path = app_dirs.TEMP_DIR / f"{uuid4().hex}.zip"
|
||||
temp_path = app_dirs.TEMP_DIR.joinpath("my_zip_archive.zip")
|
||||
try:
|
||||
yield temp_path
|
||||
finally:
|
||||
|
||||
@@ -571,8 +571,8 @@
|
||||
"delicata squash": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "delicata squash",
|
||||
"plural_name": "delicata squashes"
|
||||
"name": "delikat squash",
|
||||
"plural_name": "delikate squasher"
|
||||
},
|
||||
"Frisée": {
|
||||
"aliases": [
|
||||
@@ -980,7 +980,7 @@
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "kastanjepuré",
|
||||
"plural_name": "chestnut purée"
|
||||
"plural_name": "kastanjepuré"
|
||||
},
|
||||
"prickly pear": {
|
||||
"aliases": [],
|
||||
@@ -1045,7 +1045,7 @@
|
||||
"sweet lime": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "sweet lime",
|
||||
"name": "søt lime",
|
||||
"plural_name": "sweet limes"
|
||||
},
|
||||
"custard-apple": {
|
||||
@@ -1873,8 +1873,8 @@
|
||||
"melon seed": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "melon seed",
|
||||
"plural_name": "melon seeds"
|
||||
"name": "melonfrø",
|
||||
"plural_name": "melonfrø"
|
||||
},
|
||||
"lotus seed": {
|
||||
"aliases": [],
|
||||
@@ -2003,48 +2003,48 @@
|
||||
"parmesan cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "parmesan cheese",
|
||||
"plural_name": "parmesan cheese"
|
||||
"name": "parmesanost",
|
||||
"plural_name": "parmesanost"
|
||||
},
|
||||
"cheddar cheese": {
|
||||
"aliases": [
|
||||
"cheddar cheese"
|
||||
"cheddarost"
|
||||
],
|
||||
"description": "",
|
||||
"name": "cheddar cheese",
|
||||
"plural_name": "cheddar cheese"
|
||||
"name": "cheddarost",
|
||||
"plural_name": "cheddarost"
|
||||
},
|
||||
"cream cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "cream cheese",
|
||||
"plural_name": "cream cheese"
|
||||
"name": "kremost",
|
||||
"plural_name": "kremost"
|
||||
},
|
||||
"sharp cheddar cheese": {
|
||||
"aliases": [
|
||||
"sharp cheddar"
|
||||
"skarp cheddarost"
|
||||
],
|
||||
"description": "",
|
||||
"name": "sharp cheddar cheese",
|
||||
"plural_name": "sharp cheddar cheese"
|
||||
"name": "skarp cheddarost",
|
||||
"plural_name": "skarp cheddarost"
|
||||
},
|
||||
"cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "cheese",
|
||||
"plural_name": "cheese"
|
||||
"name": "ost",
|
||||
"plural_name": "ost"
|
||||
},
|
||||
"mozzarella cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "mozzarella cheese",
|
||||
"plural_name": "mozzarella cheese"
|
||||
"name": "mozzarellaost",
|
||||
"plural_name": "mozzarellaost"
|
||||
},
|
||||
"feta cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "feta cheese",
|
||||
"plural_name": "feta cheese"
|
||||
"name": "fetaost",
|
||||
"plural_name": "fetaost"
|
||||
},
|
||||
"ricotta cheese": {
|
||||
"aliases": [],
|
||||
@@ -2073,14 +2073,14 @@
|
||||
"goat cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "goat cheese",
|
||||
"plural_name": "goat cheese"
|
||||
"name": "geitost",
|
||||
"plural_name": "geitost"
|
||||
},
|
||||
"fresh mozzarella cheese": {
|
||||
"aliases": [],
|
||||
"description": "",
|
||||
"name": "fresh mozzarella cheese",
|
||||
"plural_name": "fresh mozzarella cheese"
|
||||
"name": "fersk mozzarellaost",
|
||||
"plural_name": "fersk mozzarellaost"
|
||||
},
|
||||
"swis cheese": {
|
||||
"aliases": [],
|
||||
|
||||
@@ -7,7 +7,6 @@ from pathlib import Path
|
||||
from slugify import slugify
|
||||
|
||||
from mealie.pkgs.cache import cache_key
|
||||
from mealie.schema.reports.reports import ReportEntryCreate
|
||||
from mealie.services.scraper import cleaner
|
||||
|
||||
from ._migration_base import BaseMigrator
|
||||
@@ -16,23 +15,15 @@ from .utils.migration_helpers import scrape_image, split_by_comma
|
||||
|
||||
|
||||
def plantoeat_recipes(file: Path):
|
||||
"""Yields all recipes inside the export file as dict.
|
||||
"""Yields all recipes inside the export file as dict"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
with zipfile.ZipFile(file) as zip_file:
|
||||
zip_file.extractall(tmpdir)
|
||||
|
||||
Accepts a ZIP archive containing a CSV, or a raw CSV/TXT file.
|
||||
"""
|
||||
if zipfile.is_zipfile(file):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
with zipfile.ZipFile(file) as zip_file:
|
||||
zip_file.extractall(tmpdir)
|
||||
|
||||
for name in Path(tmpdir).glob("**/[!.]*.csv"):
|
||||
with open(name, newline="") as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
yield from reader
|
||||
else:
|
||||
with open(file, newline="", encoding="utf-8", errors="ignore") as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
yield from reader
|
||||
for name in Path(tmpdir).glob("**/[!.]*.csv"):
|
||||
with open(name, newline="") as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
yield from reader
|
||||
|
||||
|
||||
def get_value_as_string_or_none(dictionary: dict, key: str):
|
||||
@@ -121,32 +112,7 @@ class PlanToEatMigrator(BaseMigrator):
|
||||
|
||||
return recipe_dict
|
||||
|
||||
def _validate_archive(self) -> bool:
|
||||
"""Returns False and appends a failure report entry if the file is not a ZIP, CSV, or TXT."""
|
||||
if zipfile.is_zipfile(self.archive):
|
||||
return True
|
||||
|
||||
try:
|
||||
with open(self.archive, encoding="utf-8", errors="strict") as f:
|
||||
f.read(512)
|
||||
return True
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
|
||||
self.report_entries.append(
|
||||
ReportEntryCreate(
|
||||
report_id=self.report_id,
|
||||
success=False,
|
||||
message="Unsupported file format. Please upload a ZIP archive, CSV file, or TXT file.",
|
||||
exception="",
|
||||
)
|
||||
)
|
||||
return False
|
||||
|
||||
def _migrate(self) -> None:
|
||||
if not self._validate_archive():
|
||||
return
|
||||
|
||||
recipe_image_urls = {}
|
||||
|
||||
recipes = []
|
||||
|
||||
@@ -45,8 +45,6 @@ migrations_tandoor = CWD / "migrations/tandoor.zip"
|
||||
|
||||
migrations_plantoeat = CWD / "migrations/plantoeat.zip"
|
||||
|
||||
migrations_plantoeat_csv = CWD / "migrations/plantoeat.csv"
|
||||
|
||||
migrations_myrecipebox = CWD / "migrations/myrecipebox.csv"
|
||||
|
||||
migrations_recipekeeper = CWD / "migrations/recipekeeper.zip"
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
Title,Course,Cuisine,Main Ingredient,Description,Source,Url,Url Host,Prep Time,Cook Time,Total Time,Servings,Yield,Ingredients,Directions,Tags,Rating,Public Url,Photo Url,Private,Nutritional Score (generic),Calories,Fat,Saturated Fat,Cholesterol,Sodium,Sugar,Carbohydrate,Fiber,Protein,Cost,Created At,Updated At
|
||||
Test Recipe,Main Course,American,Beans,"This is a description.
|
||||
Here is new line.",Manually entered source,https://eatwithclarity.com/sushi-bowl-with-sesame-tofu/,,75,75,150,7,1 loaf,", Heading
|
||||
2 itm Test, note
|
||||
, Heading2
|
||||
3 pkg Two, note2
|
||||
|
||||
","Directions.
|
||||
Will go here.","Allergen-Friendly, Cheap, Test",3,https://app.plantoeat.com/recipes/38843883,https://plantoeat.s3.amazonaws.com/recipes/29516709/470292506c8d9b71582487a7879ab7b197d06490-large.jpg?1628205591,yes,,13,16,17,18,19,22,20,21,23,,2023-10-13 20:29:29,2023-10-13 20:32:48
|
||||
Test Recipe2,,,,,,,,,,,,,"2 itm Test, note
|
||||
3 pkg Two, note2
|
||||
","Directions.
|
||||
Will go here.",,,,,,,,,,,,,,,,,2023-10-13 20:29:29,2023-10-13 20:32:48
|
||||
|
@@ -94,15 +94,6 @@ test_cases = [
|
||||
"transFatContent",
|
||||
},
|
||||
),
|
||||
MigrationTestData(
|
||||
typ=SupportedMigrations.plantoeat,
|
||||
archive=test_data.migrations_plantoeat_csv,
|
||||
search_slug="test-recipe",
|
||||
nutrition_filter={
|
||||
"unsaturatedFatContent",
|
||||
"transFatContent",
|
||||
},
|
||||
),
|
||||
MigrationTestData(
|
||||
typ=SupportedMigrations.myrecipebox,
|
||||
archive=test_data.migrations_myrecipebox,
|
||||
@@ -133,7 +124,6 @@ test_ids = [
|
||||
"mealie_alpha_archive",
|
||||
"tandoor_archive",
|
||||
"plantoeat_archive",
|
||||
"plantoeat_csv",
|
||||
"myrecipebox_csv",
|
||||
"recipekeeper_archive",
|
||||
"cookn_archive",
|
||||
@@ -200,30 +190,6 @@ def test_recipe_migration(api_client: TestClient, unique_user_fn_scoped: TestUse
|
||||
# TODO: validate other types of content
|
||||
|
||||
|
||||
def test_plantoeat_rejects_invalid_file_type(api_client: TestClient, unique_user: TestUser) -> None:
|
||||
# Simulate uploading a binary file (e.g. PDF) that is neither ZIP nor CSV/TXT
|
||||
binary_content = bytes(range(256)) * 4 # arbitrary binary data that is not valid UTF-8
|
||||
payload = {"migration_type": SupportedMigrations.plantoeat.value}
|
||||
file_payload = {"archive": binary_content}
|
||||
|
||||
response = api_client.post(
|
||||
api_routes.groups_migrations,
|
||||
data=payload,
|
||||
files=file_payload,
|
||||
headers=unique_user.token,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
report_id = response.json()["id"]
|
||||
|
||||
response = api_client.get(api_routes.groups_reports_item_id(report_id), headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
report = response.json()
|
||||
assert report["entries"]
|
||||
assert not report["entries"][0]["success"]
|
||||
assert "ZIP" in report["entries"][0]["message"] or "CSV" in report["entries"][0]["message"]
|
||||
|
||||
|
||||
def test_bad_mealie_alpha_data_is_ignored(api_client: TestClient, unique_user: TestUser):
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
with ZipFile(test_data.migrations_mealie) as zf:
|
||||
|
||||
Reference in New Issue
Block a user