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:
Hayden 2021-01-16 09:32:55 -09:00 committed by GitHub
commit d0f89956f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 373 additions and 122 deletions

1
.gitignore vendored
View file

@ -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/*

View file

@ -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

View file

@ -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

View file

@ -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;
}, },

View 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>

View 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>

View file

@ -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>

View file

@ -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)

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
}
}

View file

@ -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()

View file

@ -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 """

View file

@ -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)

View file

@ -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 """

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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")

View file

@ -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(

View file

@ -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)

View file

@ -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