Import/Export Overhall

* basic crud NOT SECURE

* refactor/database init on startup

* added scratch.py

* tests/user CRUD routes

* password hashing

* change app_config location

* bump python version

* formatting

* login ui starter

* change import from url design

* move components

* remove old snackbar

* refactor/Componenet folder structure rework

* refactor/remove old code

* refactor/rename componenets/js files

* remove console.logs

* refactor/ models to schema and sql to models

* new header styling for imports

* token request

* fix url scrapper

* refactor/rename schema files

* split routes file

* redesigned admin page

* enable relative imports for vue components

* refactor/switch to pages view

* add CamelCase package

* majors settings rework

* user management second pass

* super user CRUD

* refactor/consistent models names

* refactor/consistent model names

* password reset

* store refactor

* dependency update

* abstract button props

* profile page refactor

* basic password validation

* login form refactor/split v-container

* remo unused code

* hide editor buttons when not logged in

* mkdocs dev dependency

* v0.4.0 docs update

* profile image upload

* additional token routes

* Smaller recipe cards for smaller viewports

* fix admin sidebar

* add users

* change to outlined

* theme card starter

* code cleanup

* signups

* signup pages

* fix #194

* fix #193

* clarify mealie_port

* fix #184

* fixes #178

* fix blank card error on meal-plan creator

* admin signup

* formatting

* improved search bar

* improved search bar

* refresh token on page refresh

* allow mealplan with no categories

* fix card layout

* remove cdn dependencies

* start on groups

* Fixes #196

* recipe databse refactor

* changelog draft

* database refactoring

* refactor recipe schema/model

* site settings refactor

* continued model refactor

* merge docs changes from master

* site-settings work

* cleanup + tag models

* notes

* typo

* user table

* sign up data validation

* package updates

* group store init

* Fix home page settings

* group admin init

* group dashboard init

* update deps

* formatting

* bug / added libffi-dev

* pages refactor

* fix mealplan

* docs update

* multi group supporot for job scheduler

* formatting

* formatting

* home-page redesign

* set background for docs darkmode

* code cleanup

* docs refactor

* v0.4.0 image

* mkdocs port change

* formatting

* Fix Meal-Plan Today

* fix webhook bug

* fix meal plan this week

* export users

* 📦 Proper Package + Black Config

* formatting

* delete old files

* fix ci

* fix failing builds

* package/makefile docs update

* add docs server to tasks

* uncomment docker-compose

* reload in dev env

* move developer data

* fix upload issue

* run init_db before startup

* import groups and users

* fix themes

* fix theme

* potentially fixes #216

* unlink test db

* potentially fix #217

* localization

* fix import errors on no group

* fix hacky lxml error

* fix import error

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden 2021-03-21 17:09:29 -08:00 committed by GitHub
commit 6bb1e42026
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 574 additions and 508 deletions

22
.gitignore vendored
View file

@ -10,19 +10,19 @@ mealie/temp/api.html
.temp/
app_data/backups/*
app_data/debug/*
app_data/img/*
app_data/migration/*
app_data/users/*
dev/data/backups/*
dev/data/debug/*
dev/data/img/*
dev/data/migration/*
dev/data/users/*
#Exception to keep folders
!mealie/dist/.gitkeep
!app_data/backups/.gitkeep
!app_data/backups/dev_sample_data*
!app_data/debug/.gitkeep
!app_data/migration/.gitkeep
!app_data/img/.gitkeep
!dev/data/backups/.gitkeep
!dev/data/backups/dev_sample_data*
!dev/data/debug/.gitkeep
!dev/data/migration/.gitkeep
!dev/data/img/.gitkeep
.DS_Store
node_modules
@ -153,5 +153,5 @@ ENV/
node_modules/
mealie/data/debug/last_recipe.json
*.sqlite
app_data/db/test.db
dev/data/db/test.db
scratch.py

4
.vscode/tasks.json vendored
View file

@ -35,7 +35,7 @@
},
{
"label": "Dev: Start Frontend",
"command": "make vue",
"command": "make frontend",
"type": "shell",
"presentation": {
"reveal": "always",
@ -45,7 +45,7 @@
},
{
"label": "Dev: Start Docs Server",
"command": "make mdocs",
"command": "make docs",
"type": "shell",
"presentation": {
"reveal": "always",

View file

@ -31,8 +31,9 @@ RUN apk add --update --no-cache --virtual .build-deps \
COPY ./mealie /app/mealie
RUN poetry install --no-dev
COPY ./Caddyfile /app
COPY ./app_data/templates /app/data/templates
COPY ./dev/data/templates /app/data/templates
COPY --from=build-stage /app/dist /app/dist
VOLUME [ "/app/data/" ]

View file

@ -23,59 +23,15 @@
<v-divider></v-divider>
<v-card-text>
<v-row>
<v-col>
<v-checkbox
class="mb-n4 mt-1"
dense
:label="$t('settings.backup.import-recipes')"
v-model="importRecipes"
></v-checkbox>
<v-checkbox
class="my-n4"
dense
:label="$t('settings.backup.import-themes')"
v-model="importThemes"
></v-checkbox>
<v-checkbox
class="my-n4"
dense
:label="$t('settings.backup.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>
<ImportOptions @update-options="updateOptions" class="mt-5 mb-2" />
<v-divider></v-divider>
<v-checkbox
dense
label="Remove existing entries matching imported entries"
v-model="forceImport"
></v-checkbox>
</v-card-text>
<v-divider></v-divider>
@ -104,7 +60,9 @@
<script>
import ImportOptions from "@/components/Admin/Backup/ImportOptions";
export default {
components: { ImportOptions },
props: {
name: {
default: "Backup Name",
@ -115,15 +73,22 @@ export default {
},
data() {
return {
options: {
recipes: true,
settings: true,
themes: true,
users: true,
groups: true,
},
dialog: false,
importRecipes: true,
forceImport: false,
rebaseImport: false,
importThemes: false,
importSettings: false,
};
},
methods: {
updateOptions(options) {
this.options = options;
},
open() {
this.dialog = true;
},
@ -133,11 +98,13 @@ export default {
raiseEvent(event) {
let eventData = {
name: this.name,
recipes: this.importRecipes,
force: this.forceImport,
rebase: this.rebaseImport,
themes: this.importThemes,
settings: this.importSettings,
recipes: this.options.recipes,
settings: this.options.settings,
themes: this.options.themes,
users: this.options.users,
groups: this.options.groups,
};
this.close();
this.$emit(event, eventData);

View file

@ -0,0 +1,62 @@
<template>
<div>
<v-checkbox
v-for="option in options"
:key="option.text"
class="mb-n4 mt-n3"
dense
:label="option.text"
v-model="option.value"
@change="emitValue()"
></v-checkbox>
</div>
</template>
<script>
const UPDATE_EVENT = "update-options";
export default {
data() {
return {
options: {
recipes: {
value: true,
text: this.$t("general.recipes"),
},
settings: {
value: true,
text: this.$t("general.settings"),
},
themes: {
value: true,
text: this.$t("general.themes"),
},
users: {
value: true,
text: this.$t("general.users"),
},
groups: {
value: true,
text: this.$t("general.groups"),
},
},
};
},
mounted() {
this.emitValue();
},
methods: {
emitValue() {
this.$emit(UPDATE_EVENT, {
recipes: this.options.recipes.value,
settings: this.options.settings.value,
themes: this.options.themes.value,
users: this.options.users.value,
groups: this.options.groups.value,
});
},
},
};
</script>
<style lang="scss" scoped>
</style>

View file

@ -13,73 +13,55 @@
</v-app-bar>
<v-card-text class="mb-n4">
<v-row>
<div>
<div v-for="values in allNumbers" :key="values.title">
<v-card-text>
<div>
<h3>Recipes</h3>
</div>
<div class="success--text">
Success: {{ recipeNumbers.success }}
</div>
<div class="error--text">
Failed: {{ recipeNumbers.failure }}
</div>
</v-card-text>
</div>
<div>
<v-card-text>
<div>
<h3>Themes</h3>
</div>
<div class="success--text">
Success: {{ themeNumbers.success }}
</div>
<div class="error--text">
Failed: {{ themeNumbers.failure }}
</div>
</v-card-text>
</div>
<div>
<v-card-text>
<div>
<h3>Settings</h3>
</div>
<div class="success--text">
Success: {{ settingsNumbers.success }}
</div>
<div class="error--text">
Failed: {{ settingsNumbers.failure }}
<h3>{{ values.title }}</h3>
</div>
<div class="success--text">Success: {{ values.success }}</div>
<div class="error--text">Failed: {{ values.failure }}</div>
</v-card-text>
</div>
</v-row>
</v-card-text>
<v-tabs v-model="tab">
<v-tab>Recipes</v-tab>
<v-tab>Themes</v-tab>
<v-tab>Settings</v-tab>
<v-tab>{{ $t("general.recipes") }}</v-tab>
<v-tab>{{ $t("general.themes") }}</v-tab>
<v-tab>{{ $t("general.settings") }}</v-tab>
<v-tab>{{ $t("general.users") }}</v-tab>
<v-tab>{{ $t("general.groups") }}</v-tab>
</v-tabs>
<v-tabs-items v-model="tab">
<v-tab-item>
<v-card flat>
<DataTable :data-headers="recipeHeaders" :data-set="recipeData" />
<DataTable :data-headers="importHeaders" :data-set="recipeData" />
</v-card>
</v-tab-item>
<v-tab-item>
<v-card>
<DataTable
:data-headers="recipeHeaders"
:data-headers="importHeaders"
:data-set="themeData"
/> </v-card
></v-tab-item>
<v-tab-item>
<v-card
><DataTable
:data-headers="recipeHeaders"
:data-headers="importHeaders"
:data-set="settingsData"
/>
</v-card>
</v-tab-item>
<v-tab-item>
<v-card
><DataTable :data-headers="importHeaders" :data-set="userData" />
</v-card>
</v-tab-item>
<v-tab-item>
<v-card
><DataTable :data-headers="importHeaders" :data-set="groupData" />
</v-card>
</v-tab-item>
</v-tabs-items>
</v-card>
</v-dialog>
@ -98,7 +80,9 @@ export default {
recipeData: [],
themeData: [],
settingsData: [],
recipeHeaders: [
userData: [],
groupData: [],
importHeaders: [
{
text: "Status",
value: "status",
@ -117,39 +101,52 @@ export default {
computed: {
recipeNumbers() {
let numbers = { success: 0, failure: 0 };
this.recipeData.forEach(element => {
if (element.status) {
numbers.success++;
} else numbers.failure++;
});
return numbers;
return this.calculateNumbers(this.$t("general.recipes"), this.recipeData);
},
settingsNumbers() {
let numbers = { success: 0, failure: 0 };
this.settingsData.forEach(element => {
if (element.status) {
numbers.success++;
} else numbers.failure++;
});
return numbers;
return this.calculateNumbers(
this.$t("general.settings"),
this.settingsData
);
},
themeNumbers() {
let numbers = { success: 0, failure: 0 };
this.themeData.forEach(element => {
if (element.status) {
numbers.success++;
} else numbers.failure++;
});
return numbers;
return this.calculateNumbers(this.$t("general.themes"), this.themeData);
},
userNumbers() {
return this.calculateNumbers(this.$t("general.users"), this.userData);
},
groupNumbers() {
return this.calculateNumbers(this.$t("general.groups"), this.groupData);
},
allNumbers() {
return [
this.recipeNumbers,
this.settingsNumbers,
this.themeNumbers,
this.userNumbers,
this.groupNumbers,
];
},
},
methods: {
calculateNumbers(title, list_array) {
if (!list_array) return;
let numbers = { title: title, success: 0, failure: 0 };
list_array.forEach(element => {
if (element.status) {
numbers.success++;
} else numbers.failure++;
});
return numbers;
},
open(importData) {
console.log(importData);
this.recipeData = importData.recipeImports;
this.themeData = importData.themeReport;
this.settingsData = importData.settingsReport;
this.themeData = importData.themeImports;
this.settingsData = importData.settingsImports;
this.userData = importData.userImports;
this.groupData = importData.groupImports;
this.dialog = true;
},
},

View file

@ -15,57 +15,48 @@
{{ $t("general.create") }}
</v-btn>
</v-card-actions>
<v-card-text v-if="!fullBackup" class="mt-n6">
<v-row>
<v-col sm="4">
<p>{{ $t("general.options") }}:</p>
<v-checkbox
v-for="option in options"
:key="option.text"
class="mb-n4 mt-n3"
dense
:label="option.text"
v-model="option.value"
></v-checkbox>
</v-col>
<v-col>
<p>{{ $t("general.templates") }}:</p>
<v-checkbox
v-for="template in availableTemplates"
:key="template"
class="mb-n4 mt-n3"
dense
:label="template"
@click="appendTemplate(template)"
></v-checkbox>
</v-col>
</v-row>
</v-card-text>
<v-expand-transition>
<div v-if="!fullBackup">
<v-card-text class="mt-n4">
<v-row>
<v-col sm="4">
<p>{{ $t("general.options") }}:</p>
<ImportOptions @update-options="updateOptions" class="mt-5" />
</v-col>
<v-col>
<p>{{ $t("general.templates") }}:</p>
<v-checkbox
v-for="template in availableTemplates"
:key="template"
class="mb-n4 mt-n3"
dense
:label="template"
@click="appendTemplate(template)"
></v-checkbox>
</v-col>
</v-row>
</v-card-text>
</div>
</v-expand-transition>
</v-card>
</template>
<script>
import ImportOptions from "@/components/Admin/Backup/ImportOptions";
import api from "@/api";
export default {
components: { ImportOptions },
data() {
return {
tag: null,
fullBackup: true,
loading: false,
options: {
recipes: {
value: true,
text: this.$t("general.recipes"),
},
settings: {
value: true,
text: this.$t("general.settings"),
},
themes: {
value: true,
text: this.$t("general.themes"),
},
recipes: true,
settings: true,
themes: true,
users: true,
groups: true,
},
availableTemplates: [],
selectedTemplates: [],
@ -82,6 +73,9 @@ export default {
},
},
methods: {
updateOptions(options) {
this.options = options;
},
async getAvailableBackups() {
let response = await api.backups.requestAvailable();
response.templates.forEach(element => {
@ -94,9 +88,11 @@ export default {
let data = {
tag: this.tag,
options: {
recipes: this.options.recipes.value,
settings: this.options.settings.value,
themes: this.options.themes.value,
recipes: this.options.recipes,
settings: this.options.settings,
themes: this.options.themes,
users: this.options.users,
groups: this.options.groups,
},
templates: this.selectedTemplates,
};

View file

@ -9,13 +9,14 @@
</template>
<script>
const UPLOAD_EVENT = "uploaded";
import api from "@/api";
export default {
props: {
url: String,
text: { default: "Upload" },
icon: { default: "mdi-cloud-upload" },
fileName: { defaul: "archive" },
fileName: { default: "archive" },
},
data: () => ({
file: null,
@ -38,7 +39,7 @@ export default {
await api.utils.uploadFile(this.url, formData);
this.isSelecting = false;
this.$emit("uploaded");
this.$emit(UPLOAD_EVENT);
}
},
onButtonClick() {

View file

@ -46,7 +46,9 @@
"token": "Token",
"field-required": "Field Required",
"apply": "Apply",
"current-parenthesis": "(Current)"
"current-parenthesis": "(Current)",
"users": "Users",
"groups": "Groups"
},
"page": {
"home-page": "Home Page",

View file

@ -17,7 +17,7 @@ function inDarkMode(payload) {
const state = {
activeTheme: {},
darkMode: "system",
darkMode: "light",
isDark: false,
isLoggedIn: false,
token: "",

View file

@ -4,16 +4,17 @@ setup:
npm install && \
cd ..
backend:
source ./.venv/bin/activate && python mealie/app.py
backend:
poetry run python mealie/db/init_db.py && \
poetry run python mealie/app.py
vue:
.PHONY: frontend
frontend:
cd frontend && npm run serve
mdocs:
source ./.venv/bin/activate && \
cd docs && \
mkdocs serve
.PHONY: docs
docs:
cd docs && poetry run python -m mkdocs serve
docker-dev:
docker-compose -f docker-compose.dev.yml -p dev-mealie up --build

View file

@ -4,23 +4,10 @@ from fastapi.logger import logger
# import utils.startup as startup
from mealie.core.config import APP_VERSION, PORT, docs_url, redoc_url
from mealie.db.db_setup import sql_exists
from mealie.db.init_db import init_db
from mealie.routes import (
backup_routes,
debug_routes,
migration_routes,
setting_routes,
theme_routes,
)
from mealie.routes import backup_routes, debug_routes, migration_routes, setting_routes, theme_routes
from mealie.routes.groups import groups
from mealie.routes.mealplans import mealplans
from mealie.routes.recipe import (
all_recipe_routes,
category_routes,
recipe_crud_routes,
tag_routes,
)
from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_crud_routes, tag_routes
from mealie.routes.users import users
app = FastAPI(
@ -32,10 +19,6 @@ app = FastAPI(
)
def data_base_first_run():
init_db()
def start_scheduler():
import mealie.services.scheduler.scheduled_jobs
@ -62,9 +45,6 @@ def api_routers():
app.include_router(debug_routes.router)
if not sql_exists:
data_base_first_run()
api_routers()
start_scheduler()
@ -76,6 +56,7 @@ def main():
host="0.0.0.0",
port=PORT,
reload=True,
reload_dirs=["mealie"],
debug=True,
log_level="info",
workers=1,

View file

@ -34,7 +34,7 @@ else:
redoc_url = None
# Helpful Globals
DATA_DIR = CWD.parent.parent.joinpath("app_data")
DATA_DIR = CWD.parent.parent.joinpath("dev", "data")
if PRODUCTION:
DATA_DIR = Path("/app/data")

View file

@ -1,13 +1,3 @@
from mealie.schema.category import RecipeCategoryResponse, RecipeTagResponse
from mealie.schema.meal import MealPlanInDB
from mealie.schema.recipe import Recipe
from mealie.schema.settings import SiteSettings as SiteSettingsSchema
from mealie.schema.sign_up import SignUpOut
from mealie.schema.theme import SiteTheme
from mealie.schema.user import GroupInDB, UserInDB
from sqlalchemy.orm import load_only
from sqlalchemy.orm.session import Session
from mealie.db.db_base import BaseDocument
from mealie.db.models.group import Group
from mealie.db.models.mealplan import MealPlanModel
@ -16,6 +6,14 @@ from mealie.db.models.settings import SiteSettings
from mealie.db.models.sign_up import SignUp
from mealie.db.models.theme import SiteThemeModel
from mealie.db.models.users import User
from mealie.schema.category import RecipeCategoryResponse, RecipeTagResponse
from mealie.schema.meal import MealPlanInDB
from mealie.schema.recipe import Recipe
from mealie.schema.settings import SiteSettings as SiteSettingsSchema
from mealie.schema.sign_up import SignUpOut
from mealie.schema.theme import SiteTheme
from mealie.schema.user import GroupInDB, UserInDB
from sqlalchemy.orm.session import Session
class _Recipes(BaseDocument):

View file

@ -76,7 +76,7 @@ class BaseDocument:
return result
def get(self, session: Session, match_value: str, match_key: str = None, limit=1) -> dict or List[dict]:
def get(self, session: Session, match_value: str, match_key: str = None, limit=1) -> BaseModel or List[BaseModel]:
"""Retrieves an entry from the database by matching a key/value pair. If no
key is provided the class objects primary key will be used to match against.
@ -101,7 +101,7 @@ class BaseDocument:
return None
return [self.schema.from_orm(x) for x in result]
def create(self, session: Session, document: dict) -> dict:
def create(self, session: Session, document: dict) -> BaseModel:
"""Creates a new database entry for the given SQL Alchemy Model.
Args: \n
@ -121,7 +121,7 @@ class BaseDocument:
return_data = new_document.dict()
return return_data
def update(self, session: Session, match_value: str, new_data: str) -> dict:
def update(self, session: Session, match_value: str, new_data: str) -> BaseModel:
"""Update a database entry.
Args: \n

View file

@ -1,14 +1,13 @@
from fastapi.logger import logger
from mealie.core.config import DEFAULT_GROUP
from mealie.core.security import get_password_hash
from fastapi.logger import logger
from mealie.db.database import db
from mealie.db.db_setup import create_session, sql_exists
from mealie.schema.settings import SiteSettings
from mealie.schema.theme import SiteTheme
from sqlalchemy.orm import Session
from sqlalchemy.orm.session import Session
from mealie.db.database import db
from mealie.db.db_setup import create_session
def init_db(db: Session = None) -> None:
if not db:
@ -55,3 +54,12 @@ def default_user_init(session: Session):
logger.info("Generating Default User")
db.users.create(session, default_user)
if __name__ == "__main__":
if sql_exists:
print("Database Exists")
exit()
else:
print("Database Doesn't Exists, Initializing...")
init_db()

View file

@ -1,9 +1,9 @@
import sqlalchemy as sa
import sqlalchemy.orm as orm
from fastapi.logger import logger
from mealie.core.config import DEFAULT_GROUP
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
from mealie.db.models.recipe.category import Category, group2categories
from fastapi.logger import logger
from sqlalchemy.orm.session import Session
@ -30,7 +30,7 @@ class Group(SqlAlchemyBase, BaseMixins):
# Webhook Settings
webhook_enable = sa.Column(sa.Boolean, default=False)
webhook_time = sa.Column(sa.String, default="00:00")
webhook_urls = orm.relationship("WebhookURLModel", uselist=True, cascade="all, delete")
webhook_urls = orm.relationship("WebhookURLModel", uselist=True, cascade="all, delete-orphan")
def __init__(
self,
@ -52,13 +52,17 @@ class Group(SqlAlchemyBase, BaseMixins):
self.webhook_urls = [WebhookURLModel(url=x) for x in webhook_urls]
def update(self, session: Session, *args, **kwargs):
self._sql_remove_list(session, [WebhookURLModel], self.id)
self.__init__(session=session, *args, **kwargs)
@staticmethod
def get_ref(session: Session, name: str):
return session.query(Group).filter(Group.name == name).one()
item = session.query(Group).filter(Group.name == name).one()
if item:
return item
else:
return session.query(Group).filter(Group.id == 1).one()
@staticmethod
def create_if_not_exist(session, name: str = DEFAULT_GROUP):

View file

@ -32,19 +32,19 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
cookTime = sa.Column(sa.String)
recipeYield = sa.Column(sa.String)
recipeCuisine = sa.Column(sa.String)
tool: List[Tool] = orm.relationship("Tool", cascade="all, delete")
nutrition: Nutrition = orm.relationship("Nutrition", uselist=False, cascade="all, delete")
tool: List[Tool] = orm.relationship("Tool", cascade="all, delete-orphan")
nutrition: Nutrition = orm.relationship("Nutrition", uselist=False, cascade="all, delete-orphan")
recipeCategory: List = orm.relationship("Category", secondary=recipes2categories, back_populates="recipes")
recipeIngredient: List[RecipeIngredient] = orm.relationship(
"RecipeIngredient",
cascade="all, delete",
cascade="all, delete-orphan",
order_by="RecipeIngredient.position",
collection_class=ordering_list("position"),
)
recipeInstructions: List[RecipeInstruction] = orm.relationship(
"RecipeInstruction",
cascade="all, delete",
cascade="all, delete-orphan",
order_by="RecipeInstruction.position",
collection_class=ordering_list("position"),
)
@ -53,10 +53,10 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
slug = sa.Column(sa.String, index=True, unique=True)
tags: List[Tag] = orm.relationship("Tag", secondary=recipes2tags, back_populates="recipes")
dateAdded = sa.Column(sa.Date, default=date.today)
notes: List[Note] = orm.relationship("Note", cascade="all, delete")
notes: List[Note] = orm.relationship("Note", cascade="all, delete-orphan")
rating = sa.Column(sa.Integer)
orgURL = sa.Column(sa.String)
extras: List[ApiExtras] = orm.relationship("ApiExtras", cascade="all, delete")
extras: List[ApiExtras] = orm.relationship("ApiExtras", cascade="all, delete-orphan")
@validates("name")
def validate_name(self, key, name):
@ -145,8 +145,6 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
extras: dict = None,
):
"""Updated a database entry by removing nested rows and rebuilds the row through the __init__ functions"""
list_of_tables = [RecipeIngredient, RecipeInstruction, ApiExtras, Tool]
RecipeModel._sql_remove_list(session, list_of_tables, self.id)
self.__init__(
session=session,

View file

@ -28,20 +28,24 @@ class User(SqlAlchemyBase, BaseMixins):
password,
group: str = DEFAULT_GROUP,
admin=False,
id=None,
) -> None:
group = group if group else DEFAULT_GROUP
self.full_name = full_name
self.email = email
self.group = Group.create_if_not_exist(session, group)
self.group = Group.get_ref(session, group)
self.admin = admin
self.password = password
def update(self, full_name, email, group, admin, session=None):
def update(self, full_name, email, group, admin, session=None, id=None, password=None):
self.full_name = full_name
self.email = email
self.group = Group.create_if_not_exist(session, group)
self.group = Group.get_ref(session, group)
self.admin = admin
if password:
self.password = password
def update_password(self, password):
self.password = password

View file

@ -1,13 +1,13 @@
import operator
import shutil
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from mealie.core.config import BACKUP_DIR, TEMPLATE_DIR
from mealie.db.db_setup import generate_session
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from mealie.schema.backup import BackupJob, ImportJob, Imports, LocalBackup
from mealie.schema.snackbar import SnackResponse
from mealie.services.backups import imports
from mealie.services.backups.exports import backup_all
from mealie.services.backups.imports import ImportDatabase
from sqlalchemy.orm.session import Session
from starlette.responses import FileResponse
@ -41,6 +41,8 @@ def export_database(data: BackupJob, session: Session = Depends(generate_session
export_recipes=data.options.recipes,
export_settings=data.options.settings,
export_themes=data.options.themes,
export_users=data.options.users,
export_groups=data.options.groups,
)
try:
return SnackResponse.success("Backup Created at " + export_path)
@ -80,17 +82,18 @@ async def upload_nextcloud_zipfile(file_name: str):
def import_database(file_name: str, import_data: ImportJob, session: Session = Depends(generate_session)):
""" Import a database backup file generated from Mealie. """
import_db = ImportDatabase(
imported = imports.import_database(
session=session,
zip_archive=import_data.name,
archive=import_data.name,
import_recipes=import_data.recipes,
force_import=import_data.force,
rebase=import_data.rebase,
import_settings=import_data.settings,
import_themes=import_data.themes,
import_users=import_data.users,
import_groups=import_data.groups,
force_import=import_data.force,
rebase=import_data.rebase,
)
imported = import_db.run()
return imported

7
mealie/run.sh Normal file → Executable file
View file

@ -1,12 +1,13 @@
#!/bin/sh
# Initialize Database Prerun
python mealie/db/init_db.py
## Migrations
# TODO
# Database Init
## Web Server
caddy start --config ./Caddyfile
## Start API
# Start API
uvicorn mealie.app:app --host 0.0.0.0 --port 9000

View file

@ -23,6 +23,24 @@ class BackupOptions(BaseModel):
}
class ImportJob(BackupOptions):
name: str
force: bool = False
rebase: bool = False
class Config:
schema_extra = {
"example": {
"name": "my_local_backup.zip",
"recipes": True,
"force": False,
"rebase": False,
"themes": False,
"settings": False,
}
}
class BackupJob(BaseModel):
tag: Optional[str]
options: BackupOptions
@ -59,24 +77,3 @@ class Imports(BaseModel):
"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

@ -3,20 +3,27 @@ from typing import Optional
from pydantic.main import BaseModel
class RecipeImport(BaseModel):
name: Optional[str]
slug: str
status: bool
exception: Optional[str]
class ThemeImport(BaseModel):
class ImportBase(BaseModel):
name: str
status: bool
exception: Optional[str]
class SettingsImport(BaseModel):
name: str
status: bool
exception: Optional[str]
class RecipeImport(ImportBase):
slug: Optional[str]
class ThemeImport(ImportBase):
pass
class SettingsImport(ImportBase):
pass
class GroupImport(ImportBase):
pass
class UserImport(ImportBase):
pass

View file

@ -2,15 +2,16 @@ import json
import shutil
import zipfile
from pathlib import Path
from typing import List
from typing import Callable, List
from mealie.core.config import BACKUP_DIR, IMG_DIR, TEMP_DIR
from mealie.db.database import db
from mealie.db.db_setup import create_session
from fastapi.logger import logger
from mealie.schema.recipe import Recipe
from mealie.schema.restore import RecipeImport, SettingsImport, ThemeImport
from mealie.schema.restore import GroupImport, RecipeImport, SettingsImport, ThemeImport, UserImport
from mealie.schema.settings import SiteSettings
from mealie.schema.theme import SiteTheme
from mealie.schema.user import UpdateGroup, UserInDB
from pydantic.main import BaseModel
from sqlalchemy.orm.session import Session
@ -19,32 +20,21 @@ class ImportDatabase:
self,
session: Session,
zip_archive: str,
import_recipes: bool = True,
import_settings: bool = True,
import_themes: bool = True,
force_import: bool = False,
rebase: bool = False,
) -> None:
"""Import a database.zip file exported from mealie.
Args:
session (Session): SqlAlchemy Session
zip_archive (str): The filename contained in the backups directory
import_recipes (bool, optional): Import Recipes?. Defaults to True.
import_settings (bool, optional): Determines if settings are imported. Defaults to True.
import_themes (bool, optional): Determines if themes are imported. Defaults to True.
force_import (bool, optional): Force import will update all existing recipes. If False existing recipes are skipped. Defaults to False.
rebase (bool, optional): Rebase will first clear the database and then import Recipes. Defaults to False.
Raises:
Exception: If the zip file does not exists an exception raise.
"""
self.session = session
self.archive = BACKUP_DIR.joinpath(zip_archive)
self.imp_recipes = import_recipes
self.imp_settings = import_settings
self.imp_themes = import_themes
self.force_imports = force_import
self.force_rebase = rebase
if self.archive.is_file():
self.import_dir = TEMP_DIR.joinpath("active_import")
@ -56,58 +46,31 @@ class ImportDatabase:
else:
raise Exception("Import file does not exist")
def run(self):
recipe_report = []
settings_report = []
theme_report = []
if self.imp_recipes:
recipe_report = self.import_recipes()
if self.imp_settings:
settings_report = self.import_settings()
if self.imp_themes:
theme_report = self.import_themes()
self.clean_up()
return {
"recipeImports": recipe_report,
"settingsReport": settings_report,
"themeReport": theme_report,
}
def import_recipes(self):
session = create_session()
recipe_dir: Path = self.import_dir.joinpath("recipes")
imports = []
successful_imports = []
for recipe in recipe_dir.glob("*.json"):
with open(recipe, "r") as f:
recipe_dict = json.loads(f.read())
recipe_dict = ImportDatabase._recipe_migration(recipe_dict)
try:
if recipe_dict.get("categories", False):
recipe_dict["recipeCategory"] = recipe_dict.get("categories")
del recipe_dict["categories"]
recipes = ImportDatabase.read_models_file(
file_path=recipe_dir, model=Recipe, single_file=False, migrate=ImportDatabase._recipe_migration
)
recipe_obj = Recipe(**recipe_dict)
db.recipes.create(session, recipe_obj.dict())
import_status = RecipeImport(name=recipe_obj.name, slug=recipe_obj.slug, status=True)
imports.append(import_status)
successful_imports.append(recipe.stem)
logger.info(f"Imported: {recipe.stem}")
for recipe in recipes:
recipe: Recipe
except Exception as inst:
logger.error(inst)
logger.info(f"Failed Import: {recipe.stem}")
import_status = RecipeImport(
name=recipe.stem,
slug=recipe.stem,
status=False,
exception=str(inst),
)
imports.append(import_status)
import_status = self.import_model(
db_table=db.recipes,
model=recipe,
return_model=RecipeImport,
name_attr="name",
search_key="slug",
slug=recipe.slug,
)
if import_status.status:
successful_imports.append(recipe.slug)
imports.append(import_status)
self._import_images(successful_imports)
@ -115,6 +78,9 @@ class ImportDatabase:
@staticmethod
def _recipe_migration(recipe_dict: dict) -> dict:
if recipe_dict.get("categories", False):
recipe_dict["recipeCategory"] = recipe_dict.get("categories")
del recipe_dict["categories"]
try:
del recipe_dict["_id"]
del recipe_dict["dateAdded"]
@ -146,42 +112,201 @@ class ImportDatabase:
def import_themes(self):
themes_file = self.import_dir.joinpath("themes", "themes.json")
themes = ImportDatabase.read_models_file(themes_file, SiteTheme)
theme_imports = []
with open(themes_file, "r") as f:
themes: list[dict] = json.loads(f.read())
for theme in themes:
if theme.get("name") == "default":
continue
new_theme = SiteTheme(**theme)
try:
db.themes.create(self.session, new_theme.dict())
theme_imports.append(ThemeImport(name=new_theme.name, status=True))
except Exception as inst:
logger.info(f"Unable Import Theme {new_theme.name}")
theme_imports.append(ThemeImport(name=new_theme.name, status=False, exception=str(inst)))
for theme in themes:
if theme.name == "default":
continue
import_status = self.import_model(
db_table=db.themes,
model=theme,
return_model=ThemeImport,
name_attr="name",
search_key="name",
)
theme_imports.append(import_status)
return theme_imports
def import_settings(self):
def import_settings(self): #! Broken
settings_file = self.import_dir.joinpath("settings", "settings.json")
settings_imports = []
settings = ImportDatabase.read_models_file(settings_file, SiteSettings)
settings = settings[0]
with open(settings_file, "r") as f:
settings: dict = json.loads(f.read())
try:
db.settings.update(self.session, 1, settings.dict())
import_status = SettingsImport(name="Site Settings", status=True)
name = settings.get("name")
except Exception as inst:
self.session.rollback()
import_status = SettingsImport(name="Site Settings", status=False, exception=str(inst))
try:
db.settings.update(self.session, name, settings)
import_status = SettingsImport(name=name, status=True)
return [import_status]
except Exception as inst:
import_status = SettingsImport(name=name, status=False, exception=str(inst))
def import_groups(self):
groups_file = self.import_dir.joinpath("groups", "groups.json")
groups = ImportDatabase.read_models_file(groups_file, UpdateGroup)
group_imports = []
settings_imports.append(import_status)
for group in groups:
import_status = self.import_model(db.groups, group, GroupImport, search_key="name")
group_imports.append(import_status)
return settings_imports
return group_imports
def import_users(self):
users_file = self.import_dir.joinpath("users", "users.json")
users = ImportDatabase.read_models_file(users_file, UserInDB)
user_imports = []
for user in users:
if user.id == 1: # Update Default User
db.users.update(self.session, 1, user.dict())
import_status = UserImport(name=user.full_name, status=True)
user_imports.append(import_status)
continue
import_status = self.import_model(
db_table=db.users,
model=user,
return_model=UserImport,
name_attr="full_name",
search_key="email",
)
user_imports.append(import_status)
return user_imports
@staticmethod
def read_models_file(file_path: Path, model: BaseModel, single_file=True, migrate: Callable = None):
"""A general purpose function that is used to process a backup `.json` file created by mealie
note that if the file doesn't not exists the function will return any empty list
Args:
file_path (Path): The path to the .json file or directory
model (BaseModel): The pydantic model that will be created from the .json file entries
single_file (bool, optional): If true, the json data will be treated as list, if false it will use glob style matches and treat each file as its own entry. Defaults to True.
migrate (Callable, optional): A migrate function that will be called on the data prior to creating a model. Defaults to None.
Returns:
[type]: [description]
"""
if not file_path.exists():
return []
if single_file:
with open(file_path, "r") as f:
file_data = json.loads(f.read())
if migrate:
file_data = [migrate(x) for x in file_data]
return [model(**g) for g in file_data]
all_models = []
for file in file_path.glob("*.json"):
with open(file, "r") as f:
file_data = json.loads(f.read())
if migrate:
file_data = migrate(file_data)
all_models.append(model(**file_data))
return all_models
def import_model(self, db_table, model, return_model, name_attr="name", search_key="id", **kwargs):
"""A general purpose function used to insert a list of pydantic modelsi into the database.
The assumption at this point is that the models that are inserted. If self.force_imports is true
any existing entries will be removed prior to creation
Args:
db_table ([type]): A database table like `db.users`
model ([type]): The Pydantic model that matches the database
return_model ([type]): The return model that will be used for the 'report'
name_attr (str, optional): The name property on the return model. Defaults to "name".
search_key (str, optional): The key used to identify if an the entry already exists. Defaults to "id"
**kwargs (): Any kwargs passed will be used to set attributes on the `return_model`
Returns:
[type]: Returns the `return_model` specified.
"""
model_name = getattr(model, name_attr)
search_value = getattr(model, search_key)
item = db_table.get(self.session, search_value, search_key)
if item:
if self.force_imports:
primary_key = getattr(item, db_table.primary_key)
db_table.delete(self.session, primary_key)
else:
return return_model(
name=model_name,
status=False,
exception=f"Table entry with matching '{search_key}': '{search_value}' exists",
)
try:
db_table.create(self.session, model.dict())
import_status = return_model(name=model_name, status=True)
except Exception as inst:
self.session.rollback()
import_status = return_model(name=model_name, status=False, exception=str(inst))
for key, value in kwargs.items():
setattr(return_model, key, value)
return import_status
def clean_up(self):
shutil.rmtree(TEMP_DIR)
def import_database(
session: Session,
archive,
import_recipes=True,
import_settings=True,
import_themes=True,
import_users=True,
import_groups=True,
force_import: bool = False,
rebase: bool = False,
):
import_session = ImportDatabase(session, archive, force_import)
recipe_report = []
if import_recipes:
recipe_report = import_session.import_recipes()
settings_report = []
if import_settings:
settings_report = import_session.import_settings()
theme_report = []
if import_themes:
theme_report = import_session.import_themes()
group_report = []
if import_groups:
group_report = import_session.import_groups()
user_report = []
if import_users:
user_report = import_session.import_users()
import_session.clean_up()
data = {
"recipeImports": recipe_report,
"settingsImports": settings_report,
"themeImports": theme_report,
"groupImports": group_report,
"userImports": user_report,
}
return data

76
poetry.lock generated
View file

@ -358,7 +358,7 @@ six = "*"
[[package]]
name = "isort"
version = "5.7.0"
version = "5.8.0"
description = "A Python utility / library to sort Python imports."
category = "dev"
optional = false
@ -1366,8 +1366,8 @@ isodate = [
{file = "isodate-0.6.0.tar.gz", hash = "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8"},
]
isort = [
{file = "isort-5.7.0-py3-none-any.whl", hash = "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc"},
{file = "isort-5.7.0.tar.gz", hash = "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e"},
{file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"},
{file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"},
]
jinja2 = [
{file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"},
@ -1414,43 +1414,39 @@ lunr = [
{file = "lunr-0.5.8.tar.gz", hash = "sha256:c4fb063b98eff775dd638b3df380008ae85e6cb1d1a24d1cd81a10ef6391c26e"},
]
lxml = [
{file = "lxml-4.6.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a9d6bc8642e2c67db33f1247a77c53476f3a166e09067c0474facb045756087f"},
{file = "lxml-4.6.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:791394449e98243839fa822a637177dd42a95f4883ad3dec2a0ce6ac99fb0a9d"},
{file = "lxml-4.6.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:68a5d77e440df94011214b7db907ec8f19e439507a70c958f750c18d88f995d2"},
{file = "lxml-4.6.2-cp27-cp27m-win32.whl", hash = "sha256:fc37870d6716b137e80d19241d0e2cff7a7643b925dfa49b4c8ebd1295eb506e"},
{file = "lxml-4.6.2-cp27-cp27m-win_amd64.whl", hash = "sha256:69a63f83e88138ab7642d8f61418cf3180a4d8cd13995df87725cb8b893e950e"},
{file = "lxml-4.6.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:42ebca24ba2a21065fb546f3e6bd0c58c3fe9ac298f3a320147029a4850f51a2"},
{file = "lxml-4.6.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f83d281bb2a6217cd806f4cf0ddded436790e66f393e124dfe9731f6b3fb9afe"},
{file = "lxml-4.6.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:535f067002b0fd1a4e5296a8f1bf88193080ff992a195e66964ef2a6cfec5388"},
{file = "lxml-4.6.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:366cb750140f221523fa062d641393092813b81e15d0e25d9f7c6025f910ee80"},
{file = "lxml-4.6.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:97db258793d193c7b62d4e2586c6ed98d51086e93f9a3af2b2034af01450a74b"},
{file = "lxml-4.6.2-cp35-cp35m-win32.whl", hash = "sha256:648914abafe67f11be7d93c1a546068f8eff3c5fa938e1f94509e4a5d682b2d8"},
{file = "lxml-4.6.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4e751e77006da34643ab782e4a5cc21ea7b755551db202bc4d3a423b307db780"},
{file = "lxml-4.6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:681d75e1a38a69f1e64ab82fe4b1ed3fd758717bed735fb9aeaa124143f051af"},
{file = "lxml-4.6.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:127f76864468d6630e1b453d3ffbbd04b024c674f55cf0a30dc2595137892d37"},
{file = "lxml-4.6.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4fb85c447e288df535b17ebdebf0ec1cf3a3f1a8eba7e79169f4f37af43c6b98"},
{file = "lxml-4.6.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5be4a2e212bb6aa045e37f7d48e3e1e4b6fd259882ed5a00786f82e8c37ce77d"},
{file = "lxml-4.6.2-cp36-cp36m-win32.whl", hash = "sha256:8c88b599e226994ad4db29d93bc149aa1aff3dc3a4355dd5757569ba78632bdf"},
{file = "lxml-4.6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:6e4183800f16f3679076dfa8abf2db3083919d7e30764a069fb66b2b9eff9939"},
{file = "lxml-4.6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d8d3d4713f0c28bdc6c806a278d998546e8efc3498949e3ace6e117462ac0a5e"},
{file = "lxml-4.6.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:8246f30ca34dc712ab07e51dc34fea883c00b7ccb0e614651e49da2c49a30711"},
{file = "lxml-4.6.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:923963e989ffbceaa210ac37afc9b906acebe945d2723e9679b643513837b089"},
{file = "lxml-4.6.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:1471cee35eba321827d7d53d104e7b8c593ea3ad376aa2df89533ce8e1b24a01"},
{file = "lxml-4.6.2-cp37-cp37m-win32.whl", hash = "sha256:2363c35637d2d9d6f26f60a208819e7eafc4305ce39dc1d5005eccc4593331c2"},
{file = "lxml-4.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:f4822c0660c3754f1a41a655e37cb4dbbc9be3d35b125a37fab6f82d47674ebc"},
{file = "lxml-4.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0448576c148c129594d890265b1a83b9cd76fd1f0a6a04620753d9a6bcfd0a4d"},
{file = "lxml-4.6.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:60a20bfc3bd234d54d49c388950195d23a5583d4108e1a1d47c9eef8d8c042b3"},
{file = "lxml-4.6.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2e5cc908fe43fe1aa299e58046ad66981131a66aea3129aac7770c37f590a644"},
{file = "lxml-4.6.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:50c348995b47b5a4e330362cf39fc503b4a43b14a91c34c83b955e1805c8e308"},
{file = "lxml-4.6.2-cp38-cp38-win32.whl", hash = "sha256:94d55bd03d8671686e3f012577d9caa5421a07286dd351dfef64791cf7c6c505"},
{file = "lxml-4.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:7a7669ff50f41225ca5d6ee0a1ec8413f3a0d8aa2b109f86d540887b7ec0d72a"},
{file = "lxml-4.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e0bfe9bb028974a481410432dbe1b182e8191d5d40382e5b8ff39cdd2e5c5931"},
{file = "lxml-4.6.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:6fd8d5903c2e53f49e99359b063df27fdf7acb89a52b6a12494208bf61345a03"},
{file = "lxml-4.6.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7e9eac1e526386df7c70ef253b792a0a12dd86d833b1d329e038c7a235dfceb5"},
{file = "lxml-4.6.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:7ee8af0b9f7de635c61cdd5b8534b76c52cd03536f29f51151b377f76e214a1a"},
{file = "lxml-4.6.2-cp39-cp39-win32.whl", hash = "sha256:2e6fd1b8acd005bd71e6c94f30c055594bbd0aa02ef51a22bbfa961ab63b2d75"},
{file = "lxml-4.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:535332fe9d00c3cd455bd3dd7d4bacab86e2d564bdf7606079160fa6251caacf"},
{file = "lxml-4.6.2.tar.gz", hash = "sha256:cd11c7e8d21af997ee8079037fff88f16fda188a9776eb4b81c7e4c9c0a7d7fc"},
{file = "lxml-4.6.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2"},
{file = "lxml-4.6.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f"},
{file = "lxml-4.6.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d"},
{file = "lxml-4.6.2-cp27-cp27m-win32.whl", hash = "sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106"},
{file = "lxml-4.6.2-cp27-cp27m-win_amd64.whl", hash = "sha256:8157dadbb09a34a6bd95a50690595e1fa0af1a99445e2744110e3dca7831c4ee"},
{file = "lxml-4.6.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7728e05c35412ba36d3e9795ae8995e3c86958179c9770e65558ec3fdfd3724f"},
{file = "lxml-4.6.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4"},
{file = "lxml-4.6.2-cp35-cp35m-win32.whl", hash = "sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2"},
{file = "lxml-4.6.2-cp35-cp35m-win_amd64.whl", hash = "sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4"},
{file = "lxml-4.6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4"},
{file = "lxml-4.6.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3"},
{file = "lxml-4.6.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d"},
{file = "lxml-4.6.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:820628b7b3135403540202e60551e741f9b6d3304371712521be939470b454ec"},
{file = "lxml-4.6.2-cp36-cp36m-win32.whl", hash = "sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04"},
{file = "lxml-4.6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:92e821e43ad382332eade6812e298dc9701c75fe289f2a2d39c7960b43d1e92a"},
{file = "lxml-4.6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654"},
{file = "lxml-4.6.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0"},
{file = "lxml-4.6.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3"},
{file = "lxml-4.6.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2"},
{file = "lxml-4.6.2-cp37-cp37m-win32.whl", hash = "sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade"},
{file = "lxml-4.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b"},
{file = "lxml-4.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa"},
{file = "lxml-4.6.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a"},
{file = "lxml-4.6.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927"},
{file = "lxml-4.6.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791"},
{file = "lxml-4.6.2-cp38-cp38-win32.whl", hash = "sha256:89b8b22a5ff72d89d48d0e62abb14340d9e99fd637d046c27b8b257a01ffbe28"},
{file = "lxml-4.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7"},
{file = "lxml-4.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0"},
{file = "lxml-4.6.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1"},
{file = "lxml-4.6.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f8380c03e45cf09f8557bdaa41e1fa7c81f3ae22828e1db470ab2a6c96d8bc23"},
{file = "lxml-4.6.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:884ab9b29feaca361f7f88d811b1eea9bfca36cf3da27768d28ad45c3ee6f969"},
{file = "lxml-4.6.2-cp39-cp39-win32.whl", hash = "sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f"},
{file = "lxml-4.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83"},
]
markdown = [
{file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"},

View file

@ -34,7 +34,7 @@ def api_client():
yield TestClient(app)
# SQLITE_FILE.unlink()
SQLITE_FILE.unlink()
@fixture(scope="session")

View file

@ -1,45 +0,0 @@
from pathlib import Path
import pytest
from mealie.core.config import TEMP_DIR
from mealie.schema.recipe import Recipe
from mealie.services.image_services import IMG_DIR
from mealie.services.migrations.nextcloud import (
cleanup,
import_recipes,
prep,
process_selection,
)
from tests.test_config import TEST_NEXTCLOUD_DIR
CWD = Path(__file__).parent
TEST_NEXTCLOUD_DIR
TEMP_NEXTCLOUD = TEMP_DIR.joinpath("nextcloud")
@pytest.mark.parametrize(
"file_name,final_path",
[("nextcloud.zip", TEMP_NEXTCLOUD)],
)
def test_zip_extraction(file_name: str, final_path: Path):
prep()
zip = TEST_NEXTCLOUD_DIR.joinpath(file_name)
dir = process_selection(zip)
assert dir == final_path
cleanup()
assert dir.exists() == False
@pytest.mark.parametrize(
"recipe_dir",
[
TEST_NEXTCLOUD_DIR.joinpath("Air Fryer Shrimp"),
TEST_NEXTCLOUD_DIR.joinpath("Chicken Parmigiana"),
TEST_NEXTCLOUD_DIR.joinpath("Skillet Shepherd's Pie"),
],
)
def test_nextcloud_migration(recipe_dir: Path):
recipe = import_recipes(recipe_dir)
assert isinstance(recipe, Recipe)
IMG_DIR.joinpath(recipe.image).unlink(missing_ok=True)

View file

@ -2,13 +2,7 @@ import json
import pytest
from tests.test_routes.utils.routes_data import recipe_test_data
from tests.utils.routes import (
MEALPLAN_ALL,
MEALPLAN_CREATE,
MEALPLAN_PREFIX,
RECIPES_CREATE_URL,
RECIPES_PREFIX,
)
from tests.utils.routes import MEALPLAN_ALL, MEALPLAN_CREATE, MEALPLAN_PREFIX, RECIPES_CREATE_URL, RECIPES_PREFIX
def get_meal_plan_template(first=None, second=None):
@ -90,9 +84,7 @@ def test_update_mealplan(api_client, slug_1, slug_2, token):
existing_mealplan["meals"][0]["slug"] = slug_2
existing_mealplan["meals"][1]["slug"] = slug_1
response = api_client.put(
f"{MEALPLAN_PREFIX}/{plan_uid}", json=existing_mealplan, headers=token
)
response = api_client.put(f"{MEALPLAN_PREFIX}/{plan_uid}", json=existing_mealplan, headers=token)
assert response.status_code == 200

View file

@ -2,11 +2,8 @@ import json
import pytest
from slugify import slugify
from tests.test_routes.utils.routes_data import (RecipeTestData, raw_recipe,
raw_recipe_no_image,
recipe_test_data)
from tests.utils.routes import (RECIPES_ALL, RECIPES_CREATE,
RECIPES_CREATE_URL, RECIPES_PREFIX)
from tests.test_routes.utils.routes_data import RecipeTestData, raw_recipe, raw_recipe_no_image, recipe_test_data
from tests.utils.routes import RECIPES_ALL, RECIPES_CREATE, RECIPES_CREATE_URL, RECIPES_PREFIX
@pytest.mark.parametrize("recipe_data", recipe_test_data)
@ -43,9 +40,7 @@ def test_create_no_image(api_client):
def test_read_all_post(api_client):
response = api_client.post(
RECIPES_ALL, json={"properties": ["slug", "description", "rating"]}
)
response = api_client.post(RECIPES_ALL, json={"properties": ["slug", "description", "rating"]})
assert response.status_code == 200
@ -65,9 +60,7 @@ def test_read_update(api_client, recipe_data):
test_categories = ["one", "two", "three"]
recipe["recipeCategory"] = test_categories
response = api_client.put(
f"{RECIPES_PREFIX}/{recipe_data.expected_slug}", json=recipe
)
response = api_client.put(f"{RECIPES_PREFIX}/{recipe_data.expected_slug}", json=recipe)
assert response.status_code == 200
assert json.loads(response.text) == recipe_data.expected_slug
@ -90,9 +83,7 @@ def test_rename(api_client, recipe_data):
new_slug = slugify(new_name)
recipe["name"] = new_name
response = api_client.put(
f"{RECIPES_PREFIX}/{recipe_data.expected_slug}", json=recipe
)
response = api_client.put(f"{RECIPES_PREFIX}/{recipe_data.expected_slug}", json=recipe)
assert response.status_code == 200
assert json.loads(response.text) == new_slug

View file

@ -7,29 +7,14 @@ BASE = "/api/users"
TOKEN_URL = "/api/auth/token"
@fixture(scope="session")
def default_user():
return {
"id": 1,
"fullName": "Change Me",
"email": "changeme@email.com",
"group": "Home",
"admin": True
}
return {"id": 1, "fullName": "Change Me", "email": "changeme@email.com", "group": "Home", "admin": True}
@fixture(scope="session")
def new_user():
return {
"id": 2,
"fullName": "My New User",
"email": "newuser@email.com",
"group": "Home",
"admin": False
}
return {"id": 2, "fullName": "My New User", "email": "newuser@email.com", "group": "Home", "admin": False}
def test_superuser_login(api_client: requests):
@ -55,7 +40,7 @@ def test_create_user(api_client: requests, token, new_user):
"email": "newuser@email.com",
"password": "MyStrongPassword",
"group": "Home",
"admin": False
"admin": False,
}
response = api_client.post(f"{BASE}", json=create_data, headers=token)
@ -74,13 +59,7 @@ def test_get_all_users(api_client: requests, token, new_user, default_user):
def test_update_user(api_client: requests, token):
update_data = {
"id": 1,
"fullName": "Updated Name",
"email": "updated@email.com",
"group": "Home",
"admin": True
}
update_data = {"id": 1, "fullName": "Updated Name", "email": "updated@email.com", "group": "Home", "admin": True}
response = api_client.put(f"{BASE}/1", headers=token, json=update_data)
assert response.status_code == 200