mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 06:23:34 -07:00
Backup card (#78)
* backup / import dialog * upgrade to new tag method * New import card * rename settings.py to app_config.py * migrate to poetry for development * fix failing test Co-authored-by: Hayden <hay-kot@pm.me>
This commit is contained in:
parent
25988836c0
commit
d0f89956f4
31 changed files with 373 additions and 122 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -7,6 +7,7 @@ __pycache__/
|
||||||
docs/site/
|
docs/site/
|
||||||
mealie/temp/*
|
mealie/temp/*
|
||||||
mealie/temp/api.html
|
mealie/temp/api.html
|
||||||
|
.temp/
|
||||||
|
|
||||||
|
|
||||||
mealie/data/backups/*
|
mealie/data/backups/*
|
||||||
|
|
|
@ -3,13 +3,16 @@ FROM python:3
|
||||||
RUN apt-get update -y && \
|
RUN apt-get update -y && \
|
||||||
apt-get install -y python-pip python-dev
|
apt-get install -y python-pip python-dev
|
||||||
|
|
||||||
|
RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | POETRY_HOME=/opt/poetry python && \
|
||||||
|
cd /usr/local/bin && \
|
||||||
|
ln -s /opt/poetry/bin/poetry && \
|
||||||
|
poetry config virtualenvs.create false
|
||||||
|
|
||||||
COPY ./requirements.txt /app/requirements.txt
|
COPY ./pyproject.toml ./app/poetry.lock* /app/
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN pip install -r requirements.txt
|
RUN poetry install --no-root
|
||||||
RUN pip install pytest
|
|
||||||
|
|
||||||
COPY ./mealie /app
|
COPY ./mealie /app
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ Documentation
|
||||||
- [ ] V0.1.0 Release Notes
|
- [ ] V0.1.0 Release Notes
|
||||||
- [ ] Nextcloud Migration How To
|
- [ ] Nextcloud Migration How To
|
||||||
- [ ] New Docker Setup with Sqlite
|
- [ ] New Docker Setup with Sqlite
|
||||||
|
- [ ] Update Env Variables
|
||||||
- [ ] New Roadmap / Milestones
|
- [ ] New Roadmap / Milestones
|
||||||
|
|
||||||
Frontend
|
Frontend
|
||||||
|
|
|
@ -18,8 +18,8 @@ export default {
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async import(fileName) {
|
async import(fileName, data) {
|
||||||
let response = await apiReq.post(backupURLs.importBackup(fileName));
|
let response = await apiReq.post(backupURLs.importBackup(fileName), data);
|
||||||
store.dispatch("requestRecentRecipes");
|
store.dispatch("requestRecentRecipes");
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
90
frontend/src/components/Settings/Backup/BackupCard.vue
Normal file
90
frontend/src/components/Settings/Backup/BackupCard.vue
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ImportDialog
|
||||||
|
:name="selectedName"
|
||||||
|
:date="selectedDate"
|
||||||
|
ref="import_dialog"
|
||||||
|
@import="importBackup"
|
||||||
|
@delete="deleteBackup"
|
||||||
|
/>
|
||||||
|
<v-row>
|
||||||
|
<v-col
|
||||||
|
:sm="6"
|
||||||
|
:md="6"
|
||||||
|
:lg="4"
|
||||||
|
:xl="4"
|
||||||
|
v-for="backup in backups"
|
||||||
|
:key="backup.name"
|
||||||
|
>
|
||||||
|
<v-card @click="openDialog(backup)">
|
||||||
|
<v-card-text>
|
||||||
|
<v-row align="center">
|
||||||
|
<v-col cols="12" sm="2">
|
||||||
|
<v-icon color="primary"> mdi-backup-restore </v-icon>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="10">
|
||||||
|
<div>
|
||||||
|
<strong>{{ backup.name }}</strong>
|
||||||
|
</div>
|
||||||
|
<div>{{ readableTime(backup.date) }}</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ImportDialog from "./ImportDialog";
|
||||||
|
import api from "../../../api";
|
||||||
|
import utils from "../../../utils";
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
backups: Array,
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
ImportDialog,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedName: "",
|
||||||
|
selectedDate: "",
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openDialog(backup) {
|
||||||
|
this.selectedDate = this.readableTime(backup.date);
|
||||||
|
this.selectedName = backup.name;
|
||||||
|
this.$refs.import_dialog.open();
|
||||||
|
},
|
||||||
|
readableTime(timestamp) {
|
||||||
|
let date = new Date(timestamp);
|
||||||
|
return utils.getDateAsText(date);
|
||||||
|
},
|
||||||
|
async importBackup(data) {
|
||||||
|
this.$emit("loading");
|
||||||
|
let response = await api.backups.import(data.name, data);
|
||||||
|
|
||||||
|
let failed = response.data.failed;
|
||||||
|
let succesful = response.data.successful;
|
||||||
|
|
||||||
|
this.$emit("finished", succesful, failed);
|
||||||
|
},
|
||||||
|
deleteBackup(data) {
|
||||||
|
this.$emit("loading");
|
||||||
|
|
||||||
|
api.backups.delete(data.name);
|
||||||
|
this.selectedBackup = null;
|
||||||
|
this.backupLoading = false;
|
||||||
|
|
||||||
|
this.$emit("finished");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
129
frontend/src/components/Settings/Backup/ImportDialog.vue
Normal file
129
frontend/src/components/Settings/Backup/ImportDialog.vue
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
<template>
|
||||||
|
<div class="text-center">
|
||||||
|
<v-dialog v-model="dialog" width="500">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title> {{ name }} </v-card-title>
|
||||||
|
<v-card-subtitle class="mb-n3"> {{ date }} </v-card-subtitle>
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<v-checkbox
|
||||||
|
class="mb-n4 mt-1"
|
||||||
|
dense
|
||||||
|
label="Import Recipes"
|
||||||
|
v-model="importRecipes"
|
||||||
|
></v-checkbox>
|
||||||
|
<v-checkbox
|
||||||
|
class="my-n4"
|
||||||
|
dense
|
||||||
|
label="Import Themes"
|
||||||
|
v-model="importThemes"
|
||||||
|
></v-checkbox>
|
||||||
|
<v-checkbox
|
||||||
|
class="my-n4"
|
||||||
|
dense
|
||||||
|
label="Import Settings"
|
||||||
|
v-model="importSettings"
|
||||||
|
></v-checkbox>
|
||||||
|
</v-col>
|
||||||
|
<!-- <v-col>
|
||||||
|
<v-tooltip top>
|
||||||
|
<template v-slot:activator="{ on, attrs }">
|
||||||
|
<span v-on="on" v-bind="attrs">
|
||||||
|
<v-checkbox
|
||||||
|
class="mb-n4 mt-1"
|
||||||
|
dense
|
||||||
|
label="Force"
|
||||||
|
v-model="forceImport"
|
||||||
|
></v-checkbox>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<span>Force update existing recipes</span>
|
||||||
|
</v-tooltip>
|
||||||
|
<v-tooltip top>
|
||||||
|
<template v-slot:activator="{ on, attrs }">
|
||||||
|
<span v-on="on" v-bind="attrs">
|
||||||
|
<v-checkbox
|
||||||
|
class="mb-n4 mt-1"
|
||||||
|
dense
|
||||||
|
label="Rebase"
|
||||||
|
v-model="rebaseImport"
|
||||||
|
></v-checkbox>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<span
|
||||||
|
>Removes all recipes, and then imports recipes from the
|
||||||
|
backup</span
|
||||||
|
>
|
||||||
|
</v-tooltip>
|
||||||
|
</v-col> -->
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-btn disabled color="success" text @click="raiseEvent('download')">
|
||||||
|
Download
|
||||||
|
</v-btn>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="error" text @click="raiseEvent('delete')">
|
||||||
|
Delete
|
||||||
|
</v-btn>
|
||||||
|
<v-btn color="success" text @click="raiseEvent('import')">
|
||||||
|
Import
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
name: {
|
||||||
|
default: "Backup Name",
|
||||||
|
},
|
||||||
|
date: {
|
||||||
|
default: "Backup Date",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dialog: false,
|
||||||
|
importRecipes: true,
|
||||||
|
forceImport: false,
|
||||||
|
rebaseImport: false,
|
||||||
|
importThemes: false,
|
||||||
|
importSettings: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
open() {
|
||||||
|
this.dialog = true;
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
this.dialog = false;
|
||||||
|
},
|
||||||
|
raiseEvent(event) {
|
||||||
|
let eventData = {
|
||||||
|
name: this.name,
|
||||||
|
recipes: this.importRecipes,
|
||||||
|
force: this.forceImport,
|
||||||
|
rebase: this.rebaseImport,
|
||||||
|
themes: this.importThemes,
|
||||||
|
settings: this.importSettings,
|
||||||
|
};
|
||||||
|
this.close();
|
||||||
|
this.$emit(event, eventData);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
|
@ -26,36 +26,16 @@
|
||||||
></v-combobox>
|
></v-combobox>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col dense cols="12" sm="12" md="2">
|
<v-col dense cols="12" sm="12" md="2">
|
||||||
<v-btn block color="accent" @click="createBackup" width="165">
|
<v-btn block text color="accent" @click="createBackup" width="165">
|
||||||
Backup Recipes
|
Backup Recipes
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
<BackupCard
|
||||||
<v-row dense align="center">
|
@loading="backupLoading = true"
|
||||||
<v-col dense cols="12" sm="12" md="4">
|
@finished="processFinished"
|
||||||
<v-form ref="form">
|
:backups="availableBackups"
|
||||||
<v-combobox
|
/>
|
||||||
auto-select-first
|
|
||||||
label="Select a Backup for Import"
|
|
||||||
:items="availableBackups"
|
|
||||||
v-model="selectedBackup"
|
|
||||||
:rules="[(v) => !!v || 'Backup Selection is Required']"
|
|
||||||
required
|
|
||||||
></v-combobox>
|
|
||||||
</v-form>
|
|
||||||
</v-col>
|
|
||||||
<v-col dense cols="12" sm="12" md="3" lg="2">
|
|
||||||
<v-btn block color="accent" @click="importBackup">
|
|
||||||
Import Backup
|
|
||||||
</v-btn>
|
|
||||||
</v-col>
|
|
||||||
<v-col dense cols="12" sm="12" md="2" lg="2">
|
|
||||||
<v-btn block color="error" @click="deleteBackup">
|
|
||||||
Delete Backup
|
|
||||||
</v-btn>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
<SuccessFailureAlert
|
<SuccessFailureAlert
|
||||||
success-header="Successfully Imported"
|
success-header="Successfully Imported"
|
||||||
:success="successfulImports"
|
:success="successfulImports"
|
||||||
|
@ -69,10 +49,12 @@
|
||||||
<script>
|
<script>
|
||||||
import api from "../../../api";
|
import api from "../../../api";
|
||||||
import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
|
import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
|
||||||
|
import BackupCard from "./BackupCard";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
SuccessFailureAlert,
|
SuccessFailureAlert,
|
||||||
|
BackupCard,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -95,18 +77,6 @@ export default {
|
||||||
this.availableBackups = response.imports;
|
this.availableBackups = response.imports;
|
||||||
this.availableTemplates = response.templates;
|
this.availableTemplates = response.templates;
|
||||||
},
|
},
|
||||||
async importBackup() {
|
|
||||||
if (this.$refs.form.validate()) {
|
|
||||||
this.backupLoading = true;
|
|
||||||
|
|
||||||
let response = await api.backups.import(this.selectedBackup);
|
|
||||||
console.log(response.data);
|
|
||||||
this.failedImports = response.data.failed;
|
|
||||||
this.successfulImports = response.data.successful;
|
|
||||||
|
|
||||||
this.backupLoading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deleteBackup() {
|
deleteBackup() {
|
||||||
if (this.$refs.form.validate()) {
|
if (this.$refs.form.validate()) {
|
||||||
this.backupLoading = true;
|
this.backupLoading = true;
|
||||||
|
@ -129,6 +99,12 @@ export default {
|
||||||
this.backupLoading = false;
|
this.backupLoading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
processFinished(successful = null, failed = null) {
|
||||||
|
this.getAvailableBackups();
|
||||||
|
this.backupLoading = false;
|
||||||
|
this.successfulImports = successful;
|
||||||
|
this.failedImports = failed;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -3,9 +3,16 @@ from fastapi import FastAPI
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
import utils.startup as startup
|
import utils.startup as startup
|
||||||
from routes import (backup_routes, meal_routes, migration_routes,
|
from app_config import PORT, PRODUCTION, WEB_PATH, docs_url, redoc_url
|
||||||
recipe_routes, setting_routes, static_routes, user_routes)
|
from routes import (
|
||||||
from settings import PORT, PRODUCTION, WEB_PATH, docs_url, redoc_url
|
backup_routes,
|
||||||
|
meal_routes,
|
||||||
|
migration_routes,
|
||||||
|
recipe_routes,
|
||||||
|
setting_routes,
|
||||||
|
static_routes,
|
||||||
|
user_routes,
|
||||||
|
)
|
||||||
from utils.api_docs import generate_api_docs
|
from utils.api_docs import generate_api_docs
|
||||||
from utils.logger import logger
|
from utils.logger import logger
|
||||||
|
|
||||||
|
@ -19,17 +26,25 @@ app = FastAPI(
|
||||||
redoc_url=redoc_url,
|
redoc_url=redoc_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mount Vue Frontend only in production
|
|
||||||
if PRODUCTION:
|
def mount_static_files():
|
||||||
app.mount("/static", StaticFiles(directory=WEB_PATH, html=True))
|
app.mount("/static", StaticFiles(directory=WEB_PATH, html=True))
|
||||||
|
|
||||||
# API Routes
|
|
||||||
app.include_router(recipe_routes.router)
|
def api_routers():
|
||||||
app.include_router(meal_routes.router)
|
# First
|
||||||
app.include_router(setting_routes.router)
|
app.include_router(recipe_routes.router)
|
||||||
app.include_router(backup_routes.router)
|
app.include_router(meal_routes.router)
|
||||||
app.include_router(user_routes.router)
|
app.include_router(setting_routes.router)
|
||||||
app.include_router(migration_routes.router)
|
app.include_router(backup_routes.router)
|
||||||
|
app.include_router(user_routes.router)
|
||||||
|
app.include_router(migration_routes.router)
|
||||||
|
|
||||||
|
|
||||||
|
if PRODUCTION:
|
||||||
|
mount_static_files()
|
||||||
|
|
||||||
|
api_routers()
|
||||||
|
|
||||||
# API 404 Catch all CALL AFTER ROUTERS
|
# API 404 Catch all CALL AFTER ROUTERS
|
||||||
@app.get("/api/{full_path:path}", status_code=404, include_in_schema=False)
|
@app.get("/api/{full_path:path}", status_code=404, include_in_schema=False)
|
||||||
|
|
|
@ -20,7 +20,7 @@ DEBUG_DIR = DATA_DIR.joinpath("debug")
|
||||||
MIGRATION_DIR = DATA_DIR.joinpath("migration")
|
MIGRATION_DIR = DATA_DIR.joinpath("migration")
|
||||||
TEMPLATE_DIR = DATA_DIR.joinpath("templates")
|
TEMPLATE_DIR = DATA_DIR.joinpath("templates")
|
||||||
TINYDB_DIR = DATA_DIR.joinpath("db")
|
TINYDB_DIR = DATA_DIR.joinpath("db")
|
||||||
TEMP_DIR = DATA_DIR.joinpath("temp")
|
TEMP_DIR = DATA_DIR.joinpath(".temp")
|
||||||
|
|
||||||
REQUIRED_DIRS = [
|
REQUIRED_DIRS = [
|
||||||
DATA_DIR,
|
DATA_DIR,
|
Binary file not shown.
|
@ -2,7 +2,7 @@ import json
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import mongoengine
|
import mongoengine
|
||||||
from settings import USE_MONGO, USE_SQL
|
from app_config import USE_MONGO, USE_SQL
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from db.sql.db_session import create_session
|
from db.sql.db_session import create_session
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from settings import USE_MONGO, USE_SQL
|
from app_config import USE_MONGO, USE_SQL
|
||||||
|
|
||||||
from db.db_base import BaseDocument
|
from db.db_base import BaseDocument
|
||||||
from db.db_setup import USE_MONGO, USE_SQL
|
from db.db_setup import USE_MONGO, USE_SQL
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from settings import USE_MONGO, USE_SQL
|
from app_config import USE_MONGO, USE_SQL
|
||||||
|
|
||||||
from db.db_base import BaseDocument
|
from db.db_base import BaseDocument
|
||||||
from db.mongo.recipe_models import RecipeDocument
|
from db.mongo.recipe_models import RecipeDocument
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from settings import USE_MONGO, USE_SQL
|
from app_config import USE_MONGO, USE_SQL
|
||||||
|
|
||||||
from db.db_base import BaseDocument
|
from db.db_base import BaseDocument
|
||||||
from db.db_setup import USE_MONGO, USE_SQL
|
from db.db_setup import USE_MONGO, USE_SQL
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from settings import DATA_DIR, USE_MONGO, USE_SQL
|
from app_config import DATA_DIR, USE_MONGO, USE_SQL
|
||||||
|
|
||||||
from db.sql.db_session import globa_init as sql_global_init
|
from db.sql.db_session import globa_init as sql_global_init
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from settings import USE_MONGO, USE_SQL
|
from app_config import USE_MONGO, USE_SQL
|
||||||
|
|
||||||
from db.db_base import BaseDocument
|
from db.db_base import BaseDocument
|
||||||
from db.db_setup import USE_MONGO, USE_SQL
|
from db.db_setup import USE_MONGO, USE_SQL
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import mongoengine
|
import mongoengine
|
||||||
from settings import DB_HOST, DB_PASSWORD, DB_PORT, DB_USERNAME, MEALIE_DB_NAME
|
from app_config import DB_HOST, DB_PASSWORD, DB_PORT, DB_USERNAME, MEALIE_DB_NAME
|
||||||
from utils.logger import logger
|
from utils.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from datetime import datetime
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
@ -16,14 +17,45 @@ class BackupJob(BaseModel):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class LocalBackup(BaseModel):
|
||||||
|
name: str
|
||||||
|
date: datetime
|
||||||
|
|
||||||
|
|
||||||
class Imports(BaseModel):
|
class Imports(BaseModel):
|
||||||
imports: List[str]
|
imports: List[LocalBackup]
|
||||||
templates: List[str]
|
templates: List[str]
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
schema_extra = {
|
schema_extra = {
|
||||||
"example": {
|
"example": {
|
||||||
"imports": ["sample_data.zip", "sampe_data2.zip"],
|
"imports": [
|
||||||
|
{
|
||||||
|
"name": "AutoBackup_12-1-2020.zip",
|
||||||
|
"date": datetime.now(),
|
||||||
|
}
|
||||||
|
],
|
||||||
"templates": ["recipes.md", "custom_template.md"],
|
"templates": ["recipes.md", "custom_template.md"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ImportJob(BaseModel):
|
||||||
|
name: str
|
||||||
|
recipes: bool
|
||||||
|
force: bool = False
|
||||||
|
rebase: bool = False
|
||||||
|
themes: bool = False
|
||||||
|
settings: bool = False
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"name": "my_local_backup.zip",
|
||||||
|
"recipes": True,
|
||||||
|
"force": False,
|
||||||
|
"rebase": False,
|
||||||
|
"themes": False,
|
||||||
|
"settings": False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,28 +1,33 @@
|
||||||
|
import operator
|
||||||
|
|
||||||
|
from app_config import BACKUP_DIR, TEMPLATE_DIR
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from models.backup_models import BackupJob, Imports
|
from models.backup_models import BackupJob, ImportJob, Imports, LocalBackup
|
||||||
from services.backups.exports import backup_all
|
from services.backups.exports import backup_all
|
||||||
from services.backups.imports import ImportDatabase
|
from services.backups.imports import ImportDatabase
|
||||||
from settings import BACKUP_DIR, TEMPLATE_DIR
|
|
||||||
from utils.snackbar import SnackResponse
|
from utils.snackbar import SnackResponse
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter(tags=["Import / Export"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/backups/available/", tags=["Import / Export"], response_model=Imports)
|
@router.get("/api/backups/available/", response_model=Imports)
|
||||||
def available_imports():
|
def available_imports():
|
||||||
"""Returns a list of avaiable .zip files for import into Mealie."""
|
"""Returns a list of avaiable .zip files for import into Mealie."""
|
||||||
imports = []
|
imports = []
|
||||||
templates = []
|
templates = []
|
||||||
for archive in BACKUP_DIR.glob("*.zip"):
|
for archive in BACKUP_DIR.glob("*.zip"):
|
||||||
imports.append(archive.name)
|
backup = LocalBackup(name=archive.name, date=archive.stat().st_ctime)
|
||||||
|
imports.append(backup)
|
||||||
|
|
||||||
for template in TEMPLATE_DIR.glob("*.md"):
|
for template in TEMPLATE_DIR.glob("*.md"):
|
||||||
templates.append(template.name)
|
templates.append(template.name)
|
||||||
|
|
||||||
|
imports.sort(key=operator.attrgetter("date"), reverse=True)
|
||||||
|
|
||||||
return Imports(imports=imports, templates=templates)
|
return Imports(imports=imports, templates=templates)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/backups/export/database/", tags=["Import / Export"], status_code=201)
|
@router.post("/api/backups/export/database/", status_code=201)
|
||||||
def export_database(data: BackupJob):
|
def export_database(data: BackupJob):
|
||||||
"""Generates a backup of the recipe database in json format."""
|
"""Generates a backup of the recipe database in json format."""
|
||||||
export_path = backup_all(data.tag, data.template)
|
export_path = backup_all(data.tag, data.template)
|
||||||
|
@ -35,17 +40,17 @@ def export_database(data: BackupJob):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post("/api/backups/{file_name}/import/", status_code=200)
|
||||||
"/api/backups/{file_name}/import/", tags=["Import / Export"], status_code=200
|
def import_database(file_name: str, import_data: ImportJob):
|
||||||
)
|
|
||||||
def import_database(file_name: str):
|
|
||||||
""" Import a database backup file generated from Mealie. """
|
""" Import a database backup file generated from Mealie. """
|
||||||
|
|
||||||
import_db = ImportDatabase(
|
import_db = ImportDatabase(
|
||||||
zip_archive=file_name,
|
zip_archive=import_data.name,
|
||||||
import_recipes=True,
|
import_recipes=import_data.recipes,
|
||||||
import_settings=False,
|
force_import=import_data.force,
|
||||||
import_themes=False,
|
rebase=import_data.rebase,
|
||||||
|
import_settings=import_data.settings,
|
||||||
|
import_themes=import_data.themes,
|
||||||
)
|
)
|
||||||
|
|
||||||
imported = import_db.run()
|
imported = import_db.run()
|
||||||
|
|
|
@ -1,21 +1,20 @@
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from models.recipe_models import SlugResponse
|
|
||||||
from services.meal_services import MealPlan
|
from services.meal_services import MealPlan
|
||||||
from utils.snackbar import SnackResponse
|
from utils.snackbar import SnackResponse
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter(tags=["Meal Plan"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/meal-plan/all/", tags=["Meal Plan"], response_model=List[MealPlan])
|
@router.get("/api/meal-plan/all/", response_model=List[MealPlan])
|
||||||
def get_all_meals():
|
def get_all_meals():
|
||||||
""" Returns a list of all available Meal Plan """
|
""" Returns a list of all available Meal Plan """
|
||||||
|
|
||||||
return MealPlan.get_all()
|
return MealPlan.get_all()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/meal-plan/create/", tags=["Meal Plan"])
|
@router.post("/api/meal-plan/create/")
|
||||||
def set_meal_plan(data: MealPlan):
|
def set_meal_plan(data: MealPlan):
|
||||||
""" Creates a meal plan database entry """
|
""" Creates a meal plan database entry """
|
||||||
data.process_meals()
|
data.process_meals()
|
||||||
|
@ -29,7 +28,7 @@ def set_meal_plan(data: MealPlan):
|
||||||
return SnackResponse.success("Mealplan Created")
|
return SnackResponse.success("Mealplan Created")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/meal-plan/{plan_id}/update/", tags=["Meal Plan"])
|
@router.post("/api/meal-plan/{plan_id}/update/")
|
||||||
def update_meal_plan(plan_id: str, meal_plan: MealPlan):
|
def update_meal_plan(plan_id: str, meal_plan: MealPlan):
|
||||||
""" Updates a meal plan based off ID """
|
""" Updates a meal plan based off ID """
|
||||||
meal_plan.process_meals()
|
meal_plan.process_meals()
|
||||||
|
@ -46,7 +45,7 @@ def update_meal_plan(plan_id: str, meal_plan: MealPlan):
|
||||||
return SnackResponse.success("Mealplan Updated")
|
return SnackResponse.success("Mealplan Updated")
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/api/meal-plan/{plan_id}/delete/", tags=["Meal Plan"])
|
@router.delete("/api/meal-plan/{plan_id}/delete/")
|
||||||
def delete_meal_plan(plan_id):
|
def delete_meal_plan(plan_id):
|
||||||
""" Removes a meal plan from the database """
|
""" Removes a meal plan from the database """
|
||||||
|
|
||||||
|
@ -68,7 +67,7 @@ def get_today():
|
||||||
return MealPlan.today()
|
return MealPlan.today()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/meal-plan/this-week/", tags=["Meal Plan"], response_model=MealPlan)
|
@router.get("/api/meal-plan/this-week/", response_model=MealPlan)
|
||||||
def get_this_week():
|
def get_this_week():
|
||||||
""" Returns the meal plan data for this week """
|
""" Returns the meal plan data for this week """
|
||||||
|
|
||||||
|
|
|
@ -4,14 +4,14 @@ from fastapi import APIRouter, File, HTTPException, UploadFile
|
||||||
from models.migration_models import ChowdownURL
|
from models.migration_models import ChowdownURL
|
||||||
from services.migrations.chowdown import chowdown_migrate as chowdow_migrate
|
from services.migrations.chowdown import chowdown_migrate as chowdow_migrate
|
||||||
from services.migrations.nextcloud import migrate as nextcloud_migrate
|
from services.migrations.nextcloud import migrate as nextcloud_migrate
|
||||||
from settings import MIGRATION_DIR
|
from app_config import MIGRATION_DIR
|
||||||
from utils.snackbar import SnackResponse
|
from utils.snackbar import SnackResponse
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter(tags=["Migration"])
|
||||||
|
|
||||||
|
|
||||||
# Chowdown
|
# Chowdown
|
||||||
@router.post("/api/migration/chowdown/repo/", tags=["Migration"])
|
@router.post("/api/migration/chowdown/repo/")
|
||||||
def import_chowdown_recipes(repo: ChowdownURL):
|
def import_chowdown_recipes(repo: ChowdownURL):
|
||||||
""" Import Chowsdown Recipes from Repo URL """
|
""" Import Chowsdown Recipes from Repo URL """
|
||||||
try:
|
try:
|
||||||
|
@ -30,7 +30,7 @@ def import_chowdown_recipes(repo: ChowdownURL):
|
||||||
|
|
||||||
|
|
||||||
# Nextcloud
|
# Nextcloud
|
||||||
@router.get("/api/migration/nextcloud/available/", tags=["Migration"])
|
@router.get("/api/migration/nextcloud/available/")
|
||||||
def get_avaiable_nextcloud_imports():
|
def get_avaiable_nextcloud_imports():
|
||||||
""" Returns a list of avaiable directories that can be imported into Mealie """
|
""" Returns a list of avaiable directories that can be imported into Mealie """
|
||||||
available = []
|
available = []
|
||||||
|
@ -43,14 +43,14 @@ def get_avaiable_nextcloud_imports():
|
||||||
return available
|
return available
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/migration/nextcloud/{selection}/import/", tags=["Migration"])
|
@router.post("/api/migration/nextcloud/{selection}/import/")
|
||||||
def import_nextcloud_directory(selection: str):
|
def import_nextcloud_directory(selection: str):
|
||||||
""" Imports all the recipes in a given directory """
|
""" Imports all the recipes in a given directory """
|
||||||
|
|
||||||
return nextcloud_migrate(selection)
|
return nextcloud_migrate(selection)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/api/migration/{file_folder_name}/delete/", tags=["Migration"])
|
@router.delete("/api/migration/{file_folder_name}/delete/")
|
||||||
def delete_migration_data(file_folder_name: str):
|
def delete_migration_data(file_folder_name: str):
|
||||||
""" Removes migration data from the file system """
|
""" Removes migration data from the file system """
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ def delete_migration_data(file_folder_name: str):
|
||||||
return SnackResponse.info(f"Migration Data Remove: {remove_path.absolute()}")
|
return SnackResponse.info(f"Migration Data Remove: {remove_path.absolute()}")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/migration/upload/", tags=["Migration"])
|
@router.post("/api/migration/upload/")
|
||||||
def upload_nextcloud_zipfile(archive: UploadFile = File(...)):
|
def upload_nextcloud_zipfile(archive: UploadFile = File(...)):
|
||||||
""" Upload a .zip File to later be imported into Mealie """
|
""" Upload a .zip File to later be imported into Mealie """
|
||||||
dest = MIGRATION_DIR.joinpath(archive.filename)
|
dest = MIGRATION_DIR.joinpath(archive.filename)
|
||||||
|
|
|
@ -8,10 +8,10 @@ from services.recipe_services import Recipe, read_requested_values
|
||||||
from services.scrape_services import create_from_url
|
from services.scrape_services import create_from_url
|
||||||
from utils.snackbar import SnackResponse
|
from utils.snackbar import SnackResponse
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter(tags=["Recipes"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/all-recipes/", tags=["Recipes"], response_model=List[dict])
|
@router.get("/api/all-recipes/", response_model=List[dict])
|
||||||
def get_all_recipes(keys: Optional[List[str]] = Query(...), num: Optional[int] = 100):
|
def get_all_recipes(keys: Optional[List[str]] = Query(...), num: Optional[int] = 100):
|
||||||
"""
|
"""
|
||||||
Returns key data for all recipes based off the query paramters provided.
|
Returns key data for all recipes based off the query paramters provided.
|
||||||
|
@ -28,7 +28,7 @@ def get_all_recipes(keys: Optional[List[str]] = Query(...), num: Optional[int] =
|
||||||
return all_recipes
|
return all_recipes
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/all-recipes/", tags=["Recipes"], response_model=List[dict])
|
@router.post("/api/all-recipes/", response_model=List[dict])
|
||||||
def get_all_recipes_post(body: AllRecipeRequest):
|
def get_all_recipes_post(body: AllRecipeRequest):
|
||||||
"""
|
"""
|
||||||
Returns key data for all recipes based off the body data provided.
|
Returns key data for all recipes based off the body data provided.
|
||||||
|
@ -44,7 +44,7 @@ def get_all_recipes_post(body: AllRecipeRequest):
|
||||||
return all_recipes
|
return all_recipes
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/recipe/{recipe_slug}/", tags=["Recipes"], response_model=Recipe)
|
@router.get("/api/recipe/{recipe_slug}/", response_model=Recipe)
|
||||||
def get_recipe(recipe_slug: str):
|
def get_recipe(recipe_slug: str):
|
||||||
""" Takes in a recipe slug, returns all data for a recipe """
|
""" Takes in a recipe slug, returns all data for a recipe """
|
||||||
recipe = Recipe.get_by_slug(recipe_slug)
|
recipe = Recipe.get_by_slug(recipe_slug)
|
||||||
|
@ -52,7 +52,7 @@ def get_recipe(recipe_slug: str):
|
||||||
return recipe
|
return recipe
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/recipe/image/{recipe_slug}/", tags=["Recipes"])
|
@router.get("/api/recipe/image/{recipe_slug}/")
|
||||||
def get_recipe_img(recipe_slug: str):
|
def get_recipe_img(recipe_slug: str):
|
||||||
""" Takes in a recipe slug, returns the static image """
|
""" Takes in a recipe slug, returns the static image """
|
||||||
recipe_image = read_image(recipe_slug)
|
recipe_image = read_image(recipe_slug)
|
||||||
|
@ -75,7 +75,7 @@ def parse_recipe_url(url: RecipeURLIn):
|
||||||
return slug
|
return slug
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/recipe/create/", tags=["Recipes"])
|
@router.post("/api/recipe/create/")
|
||||||
def create_from_json(data: Recipe) -> str:
|
def create_from_json(data: Recipe) -> str:
|
||||||
""" Takes in a JSON string and loads data into the database as a new entry"""
|
""" Takes in a JSON string and loads data into the database as a new entry"""
|
||||||
created_recipe = data.save_to_db()
|
created_recipe = data.save_to_db()
|
||||||
|
@ -83,7 +83,7 @@ def create_from_json(data: Recipe) -> str:
|
||||||
return created_recipe
|
return created_recipe
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/recipe/{recipe_slug}/update/image/", tags=["Recipes"])
|
@router.post("/api/recipe/{recipe_slug}/update/image/")
|
||||||
def update_recipe_image(
|
def update_recipe_image(
|
||||||
recipe_slug: str, image: bytes = File(...), extension: str = Form(...)
|
recipe_slug: str, image: bytes = File(...), extension: str = Form(...)
|
||||||
):
|
):
|
||||||
|
@ -94,7 +94,7 @@ def update_recipe_image(
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/recipe/{recipe_slug}/update/", tags=["Recipes"])
|
@router.post("/api/recipe/{recipe_slug}/update/")
|
||||||
def update_recipe(recipe_slug: str, data: Recipe):
|
def update_recipe(recipe_slug: str, data: Recipe):
|
||||||
""" Updates a recipe by existing slug and data. """
|
""" Updates a recipe by existing slug and data. """
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ def update_recipe(recipe_slug: str, data: Recipe):
|
||||||
return new_slug
|
return new_slug
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/api/recipe/{recipe_slug}/delete/", tags=["Recipes"])
|
@router.delete("/api/recipe/{recipe_slug}/delete/")
|
||||||
def delete_recipe(recipe_slug: str):
|
def delete_recipe(recipe_slug: str):
|
||||||
""" Deletes a recipe by slug """
|
""" Deletes a recipe by slug """
|
||||||
|
|
||||||
|
|
|
@ -4,24 +4,24 @@ from services.settings_services import SiteSettings, SiteTheme
|
||||||
from utils.global_scheduler import scheduler
|
from utils.global_scheduler import scheduler
|
||||||
from utils.snackbar import SnackResponse
|
from utils.snackbar import SnackResponse
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter(tags=["Settings"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/site-settings/", tags=["Settings"])
|
@router.get("/api/site-settings/")
|
||||||
def get_main_settings():
|
def get_main_settings():
|
||||||
""" Returns basic site settings """
|
""" Returns basic site settings """
|
||||||
|
|
||||||
return SiteSettings.get_site_settings()
|
return SiteSettings.get_site_settings()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/site-settings/webhooks/test/", tags=["Settings"])
|
@router.post("/api/site-settings/webhooks/test/")
|
||||||
def test_webhooks():
|
def test_webhooks():
|
||||||
""" Run the function to test your webhooks """
|
""" Run the function to test your webhooks """
|
||||||
|
|
||||||
return post_webhooks()
|
return post_webhooks()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/site-settings/update/", tags=["Settings"])
|
@router.post("/api/site-settings/update/")
|
||||||
def update_settings(data: SiteSettings):
|
def update_settings(data: SiteSettings):
|
||||||
""" Returns Site Settings """
|
""" Returns Site Settings """
|
||||||
data.update()
|
data.update()
|
||||||
|
|
|
@ -6,19 +6,19 @@ from fastapi.responses import FileResponse
|
||||||
CWD = Path(__file__).parent
|
CWD = Path(__file__).parent
|
||||||
WEB_PATH = CWD.parent.joinpath("dist")
|
WEB_PATH = CWD.parent.joinpath("dist")
|
||||||
BASE_HTML = WEB_PATH.joinpath("index.html")
|
BASE_HTML = WEB_PATH.joinpath("index.html")
|
||||||
router = APIRouter()
|
router = APIRouter(include_in_schema=False)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/favicon.ico", include_in_schema=False)
|
@router.get("/favicon.ico")
|
||||||
def facivon():
|
def facivon():
|
||||||
return responses.RedirectResponse(url="/mealie/favicon.ico")
|
return responses.RedirectResponse(url="/mealie/favicon.ico")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", include_in_schema=False)
|
@router.get("/")
|
||||||
def root():
|
def root():
|
||||||
return FileResponse(BASE_HTML)
|
return FileResponse(BASE_HTML)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{full_path:path}", include_in_schema=False)
|
@router.get("/{full_path:path}")
|
||||||
def root_plus(full_path):
|
def root_plus(full_path):
|
||||||
return FileResponse(BASE_HTML)
|
return FileResponse(BASE_HTML)
|
||||||
|
|
|
@ -7,7 +7,7 @@ from jinja2 import Template
|
||||||
from services.meal_services import MealPlan
|
from services.meal_services import MealPlan
|
||||||
from services.recipe_services import Recipe
|
from services.recipe_services import Recipe
|
||||||
from services.settings_services import SiteSettings, SiteTheme
|
from services.settings_services import SiteSettings, SiteTheme
|
||||||
from settings import BACKUP_DIR, IMG_DIR, TEMP_DIR, TEMPLATE_DIR
|
from app_config import BACKUP_DIR, IMG_DIR, TEMP_DIR, TEMPLATE_DIR
|
||||||
from utils.logger import logger
|
from utils.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ from typing import List
|
||||||
|
|
||||||
from services.recipe_services import Recipe
|
from services.recipe_services import Recipe
|
||||||
from services.settings_services import SiteSettings, SiteTheme
|
from services.settings_services import SiteSettings, SiteTheme
|
||||||
from settings import BACKUP_DIR, IMG_DIR, TEMP_DIR
|
from app_config import BACKUP_DIR, IMG_DIR, TEMP_DIR
|
||||||
from utils.logger import logger
|
from utils.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
@ -74,7 +74,6 @@ class ImportDatabase:
|
||||||
recipe_dict = json.loads(f.read())
|
recipe_dict = json.loads(f.read())
|
||||||
recipe_dict = ImportDatabase._recipe_migration(recipe_dict)
|
recipe_dict = ImportDatabase._recipe_migration(recipe_dict)
|
||||||
|
|
||||||
|
|
||||||
recipe_obj = Recipe(**recipe_dict)
|
recipe_obj = Recipe(**recipe_dict)
|
||||||
recipe_obj.save_to_db()
|
recipe_obj.save_to_db()
|
||||||
successful_imports.append(recipe.stem)
|
successful_imports.append(recipe.stem)
|
||||||
|
|
|
@ -4,7 +4,7 @@ from pathlib import Path
|
||||||
import git
|
import git
|
||||||
import yaml
|
import yaml
|
||||||
from services.recipe_services import Recipe
|
from services.recipe_services import Recipe
|
||||||
from settings import IMG_DIR
|
from app_config import IMG_DIR
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from yaml import CLoader as Loader
|
from yaml import CLoader as Loader
|
||||||
|
|
|
@ -6,7 +6,7 @@ from pathlib import Path
|
||||||
|
|
||||||
from services.recipe_services import Recipe
|
from services.recipe_services import Recipe
|
||||||
from services.scrape_services import normalize_data, process_recipe_data
|
from services.scrape_services import normalize_data, process_recipe_data
|
||||||
from settings import IMG_DIR, TEMP_DIR
|
from app_config import IMG_DIR, TEMP_DIR
|
||||||
|
|
||||||
CWD = Path(__file__).parent
|
CWD = Path(__file__).parent
|
||||||
MIGRTAION_DIR = CWD.parent.parent.joinpath("data", "migration")
|
MIGRTAION_DIR = CWD.parent.parent.joinpath("data", "migration")
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from app_config import TEMP_DIR
|
||||||
import pytest
|
import pytest
|
||||||
from services.image_services import IMG_DIR
|
from services.image_services import IMG_DIR
|
||||||
from services.migrations.nextcloud import (
|
from services.migrations.nextcloud import (
|
||||||
|
@ -12,7 +12,7 @@ from services.recipe_services import Recipe
|
||||||
|
|
||||||
CWD = Path(__file__).parent
|
CWD = Path(__file__).parent
|
||||||
NEXTCLOUD_DIR = CWD.parent.joinpath("data", "nextcloud_recipes")
|
NEXTCLOUD_DIR = CWD.parent.joinpath("data", "nextcloud_recipes")
|
||||||
TEMP_NEXTCLOUD = CWD.parent.parent.joinpath("data", "temp", "nextcloud")
|
TEMP_NEXTCLOUD = TEMP_DIR.joinpath("nextcloud")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from settings import BASE_DIR
|
from app_config import BASE_DIR
|
||||||
|
|
||||||
"""Script to export the ReDoc documentation page into a standalone HTML file."""
|
"""Script to export the ReDoc documentation page into a standalone HTML file."""
|
||||||
|
|
||||||
|
@ -31,10 +31,11 @@ HTML_TEMPLATE = """<!DOCTYPE html>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
out_path = BASE_DIR.joinpath("temp", "index.html")
|
|
||||||
|
|
||||||
|
|
||||||
def generate_api_docs(app):
|
def generate_api_docs(app):
|
||||||
|
out_dir = BASE_DIR.joinpath(".temp")
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
out_path = out_dir.joinpath("index.html")
|
||||||
with open(out_path, "w") as fd:
|
with open(out_path, "w") as fd:
|
||||||
out_path.parent.mkdir(exist_ok=True)
|
out_path.parent.mkdir(exist_ok=True)
|
||||||
print(HTML_TEMPLATE % json.dumps(app.openapi()), file=fd)
|
print(HTML_TEMPLATE % json.dumps(app.openapi()), file=fd)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from settings import REQUIRED_DIRS
|
from app_config import REQUIRED_DIRS
|
||||||
|
|
||||||
CWD = Path(__file__).parent
|
CWD = Path(__file__).parent
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue