mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 14:33:33 -07:00
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:
parent
d621e4e580
commit
6bb1e42026
37 changed files with 574 additions and 508 deletions
22
.gitignore
vendored
22
.gitignore
vendored
|
@ -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
4
.vscode/tasks.json
vendored
|
@ -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",
|
||||
|
|
|
@ -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/" ]
|
||||
|
|
|
@ -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);
|
||||
|
|
62
frontend/src/components/Admin/Backup/ImportOptions.vue
Normal file
62
frontend/src/components/Admin/Backup/ImportOptions.vue
Normal 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>
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -17,7 +17,7 @@ function inDarkMode(payload) {
|
|||
|
||||
const state = {
|
||||
activeTheme: {},
|
||||
darkMode: "system",
|
||||
darkMode: "light",
|
||||
isDark: false,
|
||||
isLoggedIn: false,
|
||||
token: "",
|
||||
|
|
15
makefile
15
makefile
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
7
mealie/run.sh
Normal file → Executable 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
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
76
poetry.lock
generated
|
@ -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"},
|
||||
|
|
|
@ -34,7 +34,7 @@ def api_client():
|
|||
|
||||
yield TestClient(app)
|
||||
|
||||
# SQLITE_FILE.unlink()
|
||||
SQLITE_FILE.unlink()
|
||||
|
||||
|
||||
@fixture(scope="session")
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue