mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -04:00
Feature/import export single recipe (#576)
* remove duplicate keys * show context menu when not logged in * remove console.log * hide menu when printing * add response to event * add type definitions * show context menu always * add image name enums * upload/download single recipe * cleanup menu views+ localization Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
parent
3220595a83
commit
f5faff66d3
@ -4,8 +4,6 @@ import i18n from "@/i18n.js";
|
|||||||
export const utilsAPI = {
|
export const utilsAPI = {
|
||||||
// import { api } from "@/api";
|
// import { api } from "@/api";
|
||||||
uploadFile(url, fileObject) {
|
uploadFile(url, fileObject) {
|
||||||
console.log("API Called");
|
|
||||||
|
|
||||||
return apiReq.post(
|
return apiReq.post(
|
||||||
url,
|
url,
|
||||||
fileObject,
|
fileObject,
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
allow-overflow
|
allow-overflow
|
||||||
close-delay="125"
|
close-delay="125"
|
||||||
open-on-hover
|
open-on-hover
|
||||||
|
content-class="d-print-none"
|
||||||
>
|
>
|
||||||
<template v-slot:activator="{ on, attrs }">
|
<template v-slot:activator="{ on, attrs }">
|
||||||
<v-btn :fab="fab" :small="fab" :color="color" :icon="!fab" dark v-bind="attrs" v-on="on" @click.prevent>
|
<v-btn :fab="fab" :small="fab" :color="color" :icon="!fab" dark v-bind="attrs" v-on="on" @click.prevent>
|
||||||
@ -25,11 +26,7 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<v-list dense>
|
<v-list dense>
|
||||||
<v-list-item
|
<v-list-item v-for="(item, index) in displayedMenu" :key="index" @click="menuAction(item.action)">
|
||||||
v-for="(item, index) in loggedIn && cardMenu ? userMenu : defaultMenu"
|
|
||||||
:key="index"
|
|
||||||
@click="menuAction(item.action)"
|
|
||||||
>
|
|
||||||
<v-list-item-icon>
|
<v-list-item-icon>
|
||||||
<v-icon v-text="item.icon" :color="item.color"></v-icon>
|
<v-icon v-text="item.icon" :color="item.color"></v-icon>
|
||||||
</v-list-item-icon>
|
</v-list-item-icon>
|
||||||
@ -53,6 +50,10 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
showPrint: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
fab: {
|
fab: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
@ -88,20 +89,28 @@ export default {
|
|||||||
recipeURL() {
|
recipeURL() {
|
||||||
return `${this.baseURL}/recipe/${this.slug}`;
|
return `${this.baseURL}/recipe/${this.slug}`;
|
||||||
},
|
},
|
||||||
|
printerMenu() {
|
||||||
|
return {
|
||||||
|
title: this.$t("general.print"),
|
||||||
|
icon: this.$globals.icons.printer,
|
||||||
|
color: "accent",
|
||||||
|
action: "print",
|
||||||
|
};
|
||||||
|
},
|
||||||
defaultMenu() {
|
defaultMenu() {
|
||||||
return [
|
return [
|
||||||
{
|
|
||||||
title: this.$t("general.print"),
|
|
||||||
icon: this.$globals.icons.printer,
|
|
||||||
color: "accent",
|
|
||||||
action: "print",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: this.$t("general.share"),
|
title: this.$t("general.share"),
|
||||||
icon: this.$globals.icons.shareVariant,
|
icon: this.$globals.icons.shareVariant,
|
||||||
color: "accent",
|
color: "accent",
|
||||||
action: "share",
|
action: "share",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: this.$t("general.download"),
|
||||||
|
icon: this.$globals.icons.download,
|
||||||
|
color: "accent",
|
||||||
|
action: "download",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
userMenu() {
|
userMenu() {
|
||||||
@ -118,9 +127,18 @@ export default {
|
|||||||
color: "accent",
|
color: "accent",
|
||||||
action: "edit",
|
action: "edit",
|
||||||
},
|
},
|
||||||
...this.defaultMenu,
|
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
displayedMenu() {
|
||||||
|
let menu = this.defaultMenu;
|
||||||
|
if (this.loggedIn && this.cardMenu) {
|
||||||
|
menu = [...this.userMenu, ...menu];
|
||||||
|
}
|
||||||
|
if (this.showPrint) {
|
||||||
|
menu = [this.printerMenu, ...menu];
|
||||||
|
}
|
||||||
|
return menu;
|
||||||
|
},
|
||||||
recipeText() {
|
recipeText() {
|
||||||
return this.$t("recipe.share-recipe-message", [this.name]);
|
return this.$t("recipe.share-recipe-message", [this.name]);
|
||||||
},
|
},
|
||||||
@ -159,6 +177,9 @@ export default {
|
|||||||
case "print":
|
case "print":
|
||||||
this.$router.push(`/recipe/${this.slug}` + "?print=true");
|
this.$router.push(`/recipe/${this.slug}` + "?print=true");
|
||||||
break;
|
break;
|
||||||
|
case "download":
|
||||||
|
window.open(`/api/recipes/${this.slug}/zip`);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -20,13 +20,23 @@
|
|||||||
<div v-if="!value" class="custom-btn-group ma-1">
|
<div v-if="!value" class="custom-btn-group ma-1">
|
||||||
<v-tooltip bottom color="info">
|
<v-tooltip bottom color="info">
|
||||||
<template v-slot:activator="{ on, attrs }">
|
<template v-slot:activator="{ on, attrs }">
|
||||||
<v-btn fab small class="mx-1" color="info" v-bind="attrs" v-on="on" @click="$emit('input', true)">
|
<v-btn
|
||||||
|
v-if="loggedIn"
|
||||||
|
fab
|
||||||
|
small
|
||||||
|
class="mx-1"
|
||||||
|
color="info"
|
||||||
|
v-bind="attrs"
|
||||||
|
v-on="on"
|
||||||
|
@click="$emit('input', true)"
|
||||||
|
>
|
||||||
<v-icon> {{ $globals.icons.edit }} </v-icon>
|
<v-icon> {{ $globals.icons.edit }} </v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<span>{{ $t("general.edit") }}</span>
|
<span>{{ $t("general.edit") }}</span>
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
|
show-print
|
||||||
:menu-top="false"
|
:menu-top="false"
|
||||||
:slug="slug"
|
:slug="slug"
|
||||||
:name="name"
|
:name="name"
|
||||||
@ -74,6 +84,10 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
loggedIn: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -58,8 +58,10 @@ export default {
|
|||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
formData.append(this.fileName, this.file);
|
formData.append(this.fileName, this.file);
|
||||||
|
|
||||||
if (await api.utils.uploadFile(this.url, formData)) {
|
const response = await api.utils.uploadFile(this.url, formData);
|
||||||
this.$emit(UPLOAD_EVENT);
|
|
||||||
|
if (response) {
|
||||||
|
this.$emit(UPLOAD_EVENT, response);
|
||||||
}
|
}
|
||||||
this.isSelecting = false;
|
this.isSelecting = false;
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import i18n from "@/i18n";
|
import i18n from "@/i18n.js";
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
color: {
|
color: {
|
||||||
@ -65,12 +65,14 @@ export default {
|
|||||||
},
|
},
|
||||||
top: {
|
top: {
|
||||||
default: null,
|
default: null,
|
||||||
|
type: Boolean,
|
||||||
},
|
},
|
||||||
submitText: {
|
submitText: {
|
||||||
default: () => i18n.t("general.create"),
|
default: () => i18n.t("general.create"),
|
||||||
},
|
},
|
||||||
keepOpen: {
|
keepOpen: {
|
||||||
default: false,
|
default: false,
|
||||||
|
type: Boolean,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -101,16 +103,17 @@ export default {
|
|||||||
this.$emit("submit");
|
this.$emit("submit");
|
||||||
this.submitted = true;
|
this.submitted = true;
|
||||||
},
|
},
|
||||||
|
deleteEvent() {
|
||||||
|
this.$emit("delete");
|
||||||
|
this.submitted = true;
|
||||||
|
},
|
||||||
open() {
|
open() {
|
||||||
|
console.log("Open Dialog");
|
||||||
this.dialog = true;
|
this.dialog = true;
|
||||||
},
|
},
|
||||||
close() {
|
close() {
|
||||||
this.dialog = false;
|
this.dialog = false;
|
||||||
},
|
},
|
||||||
deleteEvent() {
|
|
||||||
this.$emit("delete");
|
|
||||||
this.submitted = true;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -84,6 +84,26 @@
|
|||||||
</v-form>
|
</v-form>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
<BaseDialog
|
||||||
|
title="Upload a Recipe"
|
||||||
|
:titleIcon="$globals.icons.zip"
|
||||||
|
:submit-text="$t('general.import')"
|
||||||
|
ref="uploadZipDialog"
|
||||||
|
@submit="uploadZip"
|
||||||
|
:loading="processing"
|
||||||
|
>
|
||||||
|
<v-card-text class="mt-1 pb-0">
|
||||||
|
{{ $t("new-recipe.upload-individual-zip-file") }}
|
||||||
|
|
||||||
|
<div class="headline mx-auto mb-0 pb-0 text-center">
|
||||||
|
{{ this.fileName }}
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<TheUploadBtn class="mx-auto" :text-btn="false" @uploaded="setFile" :post="false"> </TheUploadBtn>
|
||||||
|
</v-card-actions>
|
||||||
|
</BaseDialog>
|
||||||
<v-speed-dial v-model="fab" :open-on-hover="absolute" :fixed="absolute" :bottom="absolute" :right="absolute">
|
<v-speed-dial v-model="fab" :open-on-hover="absolute" :fixed="absolute" :bottom="absolute" :right="absolute">
|
||||||
<template v-slot:activator>
|
<template v-slot:activator>
|
||||||
<v-btn v-model="fab" :color="absolute ? 'accent' : 'white'" dark :icon="!absolute" :fab="absolute">
|
<v-btn v-model="fab" :color="absolute ? 'accent' : 'white'" dark :icon="!absolute" :fab="absolute">
|
||||||
@ -106,13 +126,27 @@
|
|||||||
</template>
|
</template>
|
||||||
<span>{{ $t("general.new") }}</span>
|
<span>{{ $t("general.new") }}</span>
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
|
<v-tooltip left dark color="info">
|
||||||
|
<template v-slot:activator="{ on, attrs }">
|
||||||
|
<v-btn fab dark small color="info" v-bind="attrs" v-on="on" @click="openZipUploader">
|
||||||
|
<v-icon>{{ $globals.icons.zip }}</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<span>{{ $t("general.upload") }}</span>
|
||||||
|
</v-tooltip>
|
||||||
</v-speed-dial>
|
</v-speed-dial>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
|
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn.vue";
|
||||||
|
import BaseDialog from "@/components/UI/Dialogs/BaseDialog.vue";
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
TheUploadBtn,
|
||||||
|
BaseDialog,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
absolute: {
|
absolute: {
|
||||||
default: false,
|
default: false,
|
||||||
@ -124,6 +158,10 @@ export default {
|
|||||||
fab: false,
|
fab: false,
|
||||||
addRecipe: false,
|
addRecipe: false,
|
||||||
processing: false,
|
processing: false,
|
||||||
|
uploadData: {
|
||||||
|
fileName: "archive",
|
||||||
|
file: null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -143,9 +181,34 @@ export default {
|
|||||||
return this.$route.query.recipe_import_url || "";
|
return this.$route.query.recipe_import_url || "";
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
fileName() {
|
||||||
|
return this.uploadData.file?.name || "";
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
resetVars() {
|
||||||
|
this.uploadData = {
|
||||||
|
fileName: "archive",
|
||||||
|
file: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
setFile(file) {
|
||||||
|
this.uploadData.file = file;
|
||||||
|
console.log("Uploaded");
|
||||||
|
},
|
||||||
|
openZipUploader() {
|
||||||
|
this.resetVars();
|
||||||
|
this.$refs.uploadZipDialog.open();
|
||||||
|
},
|
||||||
|
async uploadZip() {
|
||||||
|
let formData = new FormData();
|
||||||
|
formData.append(this.uploadData.fileName, this.uploadData.file);
|
||||||
|
|
||||||
|
const response = await api.utils.uploadFile("/api/recipes/create-from-zip", formData);
|
||||||
|
|
||||||
|
this.$router.push(`/recipe/${response.data.slug}`);
|
||||||
|
},
|
||||||
async createRecipe() {
|
async createRecipe() {
|
||||||
this.error = false;
|
this.error = false;
|
||||||
if (this.$refs.urlForm === undefined || this.$refs.urlForm.validate()) {
|
if (this.$refs.urlForm === undefined || this.$refs.urlForm.validate()) {
|
||||||
@ -161,7 +224,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.fab = false;
|
this.fab = false;
|
||||||
this.error = false;
|
this.error = false;
|
||||||
@ -173,10 +235,6 @@ export default {
|
|||||||
let regEx = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,256}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/gm;
|
let regEx = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,256}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/gm;
|
||||||
return regEx.test(url) ? true : "Must be a Valid URL";
|
return regEx.test(url) ? true : "Must be a Valid URL";
|
||||||
},
|
},
|
||||||
|
|
||||||
bookmark() {
|
|
||||||
return `javascript:(function()%7Bvar url %3D document.URL %3B%0Avar mealie %3D "http%3A%2F%2Flocalhost%3A8080%2F%23"%0Avar dest %3D mealie %2B "%2F%3Frecipe_import_url%3D" %2B url%0Awindow.open(dest%2C '_blank')%7D)()%3B`;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -37,9 +37,7 @@
|
|||||||
"apprise-url": "Apprise URL",
|
"apprise-url": "Apprise URL",
|
||||||
"database": "Database",
|
"database": "Database",
|
||||||
"new-notification-form-description": "Mealie uses the Apprise library to generate notifications. They offer many options for services to use for notifications. Refer to their wiki for a comprehensive guide on how to create the URL for your service. If available, selecting the type of your notification may include extra features.",
|
"new-notification-form-description": "Mealie uses the Apprise library to generate notifications. They offer many options for services to use for notifications. Refer to their wiki for a comprehensive guide on how to create the URL for your service. If available, selecting the type of your notification may include extra features.",
|
||||||
"new-version": "New version available!",
|
|
||||||
"notification": "Notification",
|
"notification": "Notification",
|
||||||
"refresh": "Refresh",
|
|
||||||
"scheduled": "Scheduled",
|
"scheduled": "Scheduled",
|
||||||
"something-went-wrong": "Something Went Wrong!",
|
"something-went-wrong": "Something Went Wrong!",
|
||||||
"subscribed-events": "Subscribed Events",
|
"subscribed-events": "Subscribed Events",
|
||||||
@ -191,6 +189,7 @@
|
|||||||
"from-url": "Import a Recipe",
|
"from-url": "Import a Recipe",
|
||||||
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Paste in your recipe data. Each line will be treated as an item in a list",
|
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Paste in your recipe data. Each line will be treated as an item in a list",
|
||||||
"recipe-url": "Recipe URL",
|
"recipe-url": "Recipe URL",
|
||||||
|
"upload-individual-zip-file": "Upload an individual .zip file exported from another Mealie instance.",
|
||||||
"url-form-hint": "Copy and paste a link from your favorite recipe website"
|
"url-form-hint": "Copy and paste a link from your favorite recipe website"
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
|
@ -10,7 +10,13 @@
|
|||||||
</v-img>
|
</v-img>
|
||||||
<br v-else />
|
<br v-else />
|
||||||
|
|
||||||
<RecipePageActionMenu :value="true" @json="jsonEditor = true" @edit="jsonEditor = false" @save="createRecipe" />
|
<RecipePageActionMenu
|
||||||
|
logged-in
|
||||||
|
:value="true"
|
||||||
|
@json="jsonEditor = true"
|
||||||
|
@edit="jsonEditor = false"
|
||||||
|
@save="createRecipe"
|
||||||
|
/>
|
||||||
|
|
||||||
<div v-if="jsonEditor">
|
<div v-if="jsonEditor">
|
||||||
<!-- Probably not the best way, but it works! -->
|
<!-- Probably not the best way, but it works! -->
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
:slug="recipeDetails.slug"
|
:slug="recipeDetails.slug"
|
||||||
:name="recipeDetails.name"
|
:name="recipeDetails.name"
|
||||||
v-model="form"
|
v-model="form"
|
||||||
v-if="loggedIn"
|
:logged-in="loggedIn"
|
||||||
:open="showIcons"
|
:open="showIcons"
|
||||||
@close="form = false"
|
@close="form = false"
|
||||||
@json="jsonEditor = !jsonEditor"
|
@json="jsonEditor = !jsonEditor"
|
||||||
|
@ -6,7 +6,7 @@ import { mealRoutes } from "./meal";
|
|||||||
import { generalRoutes } from "./general";
|
import { generalRoutes } from "./general";
|
||||||
import { store } from "@/store";
|
import { store } from "@/store";
|
||||||
import VueRouter from "vue-router";
|
import VueRouter from "vue-router";
|
||||||
import { loadLanguageAsync } from "@/i18n"
|
import { loadLanguageAsync } from "@/i18n";
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import i18n from "@/i18n.js";
|
import i18n from "@/i18n.js";
|
||||||
|
|
||||||
|
@ -91,6 +91,7 @@ import {
|
|||||||
mdiArrowLeftBold,
|
mdiArrowLeftBold,
|
||||||
mdiMinus,
|
mdiMinus,
|
||||||
mdiWindowClose,
|
mdiWindowClose,
|
||||||
|
mdiFolderZipOutline,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
@ -176,6 +177,7 @@ const icons = {
|
|||||||
weatherSunny: mdiWeatherSunny,
|
weatherSunny: mdiWeatherSunny,
|
||||||
webhook: mdiWebhook,
|
webhook: mdiWebhook,
|
||||||
windowClose: mdiWindowClose,
|
windowClose: mdiWindowClose,
|
||||||
|
zip: mdiFolderZipOutline,
|
||||||
|
|
||||||
// Crud
|
// Crud
|
||||||
createAlt: mdiPlus,
|
createAlt: mdiPlus,
|
||||||
|
@ -4,7 +4,7 @@ from typing import Optional
|
|||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
from jose import JWTError, jwt
|
from jose import JWTError, jwt
|
||||||
from mealie.core.config import settings
|
from mealie.core.config import app_dirs, settings
|
||||||
from mealie.db.database import db
|
from mealie.db.database import db
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
from mealie.schema.auth import TokenData
|
from mealie.schema.auth import TokenData
|
||||||
@ -100,3 +100,13 @@ def validate_file_token(token: Optional[str] = None) -> Path:
|
|||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
|
|
||||||
return file_path
|
return file_path
|
||||||
|
|
||||||
|
|
||||||
|
async def temporary_zip_path() -> Path:
|
||||||
|
temp_path = app_dirs.TEMP_DIR.mkdir(exist_ok=True, parents=True)
|
||||||
|
temp_path = app_dirs.TEMP_DIR.joinpath("my_zip_archive.zip")
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield temp_path
|
||||||
|
finally:
|
||||||
|
temp_path.unlink(missing_ok=True)
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
|
import json
|
||||||
|
import shutil
|
||||||
from shutil import copyfileobj
|
from shutil import copyfileobj
|
||||||
|
from zipfile import ZipFile
|
||||||
|
|
||||||
from fastapi import APIRouter, BackgroundTasks, Depends, File, Form, HTTPException, status
|
from fastapi import APIRouter, BackgroundTasks, Depends, File, Form, HTTPException, status
|
||||||
from fastapi.datastructures import UploadFile
|
from fastapi.datastructures import UploadFile
|
||||||
@ -6,8 +9,8 @@ from mealie.core.config import settings
|
|||||||
from mealie.core.root_logger import get_logger
|
from mealie.core.root_logger import get_logger
|
||||||
from mealie.db.database import db
|
from mealie.db.database import db
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
from mealie.routes.deps import get_current_user, is_logged_in
|
from mealie.routes.deps import get_current_user, is_logged_in, temporary_zip_path
|
||||||
from mealie.schema.recipe import Recipe, RecipeAsset, RecipeURLIn
|
from mealie.schema.recipe import Recipe, RecipeAsset, RecipeImageTypes, RecipeURLIn
|
||||||
from mealie.schema.user import UserInDB
|
from mealie.schema.user import UserInDB
|
||||||
from mealie.services.events import create_recipe_event
|
from mealie.services.events import create_recipe_event
|
||||||
from mealie.services.image.image import scrape_image, write_image
|
from mealie.services.image.image import scrape_image, write_image
|
||||||
@ -16,6 +19,7 @@ from mealie.services.scraper.scraper import create_from_url
|
|||||||
from scrape_schema_recipe import scrape_url
|
from scrape_schema_recipe import scrape_url
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
from starlette.responses import FileResponse
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/recipes", tags=["Recipe CRUD"])
|
router = APIRouter(prefix="/api/recipes", tags=["Recipe CRUD"])
|
||||||
logger = get_logger()
|
logger = get_logger()
|
||||||
@ -87,6 +91,54 @@ def get_recipe(recipe_slug: str, session: Session = Depends(generate_session), i
|
|||||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, {"details": "unauthorized"})
|
raise HTTPException(status.HTTP_401_UNAUTHORIZED, {"details": "unauthorized"})
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/create-from-zip", dependencies=[Depends(get_current_user)])
|
||||||
|
async def create_recipe_from_zip(
|
||||||
|
session: Session = Depends(generate_session),
|
||||||
|
temp_path=Depends(temporary_zip_path),
|
||||||
|
archive: UploadFile = File(...),
|
||||||
|
):
|
||||||
|
""" Create recipe from archive """
|
||||||
|
|
||||||
|
with temp_path.open("wb") as buffer:
|
||||||
|
shutil.copyfileobj(archive.file, buffer)
|
||||||
|
|
||||||
|
recipe_dict = None
|
||||||
|
recipe_image = None
|
||||||
|
|
||||||
|
with ZipFile(temp_path) as myzip:
|
||||||
|
for file in myzip.namelist():
|
||||||
|
if file.endswith(".json"):
|
||||||
|
with myzip.open(file) as myfile:
|
||||||
|
recipe_dict = json.loads(myfile.read())
|
||||||
|
elif file.endswith(".webp"):
|
||||||
|
with myzip.open(file) as myfile:
|
||||||
|
recipe_image = myfile.read()
|
||||||
|
|
||||||
|
recipe: Recipe = db.recipes.create(session, Recipe(**recipe_dict))
|
||||||
|
|
||||||
|
write_image(recipe.slug, recipe_image, "webp")
|
||||||
|
|
||||||
|
return recipe
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{recipe_slug}/zip")
|
||||||
|
async def get_recipe_as_zip(
|
||||||
|
recipe_slug: str, session: Session = Depends(generate_session), temp_path=Depends(temporary_zip_path)
|
||||||
|
):
|
||||||
|
""" Get a Recipe and It's Original Image as a Zip File """
|
||||||
|
recipe: Recipe = db.recipes.get(session, recipe_slug)
|
||||||
|
|
||||||
|
image_asset = recipe.image_dir.joinpath(RecipeImageTypes.original.value)
|
||||||
|
|
||||||
|
with ZipFile(temp_path, "w") as myzip:
|
||||||
|
myzip.writestr(f"{recipe_slug}.json", recipe.json())
|
||||||
|
|
||||||
|
if image_asset.is_file():
|
||||||
|
myzip.write(image_asset, arcname=image_asset.name)
|
||||||
|
|
||||||
|
return FileResponse(temp_path, filename=f"{recipe_slug}.zip")
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{recipe_slug}", dependencies=[Depends(get_current_user)])
|
@router.put("/{recipe_slug}", dependencies=[Depends(get_current_user)])
|
||||||
def update_recipe(
|
def update_recipe(
|
||||||
recipe_slug: str,
|
recipe_slug: str,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
@ -11,6 +12,12 @@ from pydantic.utils import GetterDict
|
|||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeImageTypes(str, Enum):
|
||||||
|
original = "original.webp"
|
||||||
|
min = "min-original.webp"
|
||||||
|
tiny = "tiny-original.webp"
|
||||||
|
|
||||||
|
|
||||||
class RecipeSettings(CamelModel):
|
class RecipeSettings(CamelModel):
|
||||||
public: bool = True
|
public: bool = True
|
||||||
show_nutrition: bool = True
|
show_nutrition: bool = True
|
||||||
|
Loading…
x
Reference in New Issue
Block a user