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/
|
.temp/
|
||||||
|
|
||||||
|
|
||||||
app_data/backups/*
|
dev/data/backups/*
|
||||||
app_data/debug/*
|
dev/data/debug/*
|
||||||
app_data/img/*
|
dev/data/img/*
|
||||||
app_data/migration/*
|
dev/data/migration/*
|
||||||
app_data/users/*
|
dev/data/users/*
|
||||||
|
|
||||||
#Exception to keep folders
|
#Exception to keep folders
|
||||||
!mealie/dist/.gitkeep
|
!mealie/dist/.gitkeep
|
||||||
!app_data/backups/.gitkeep
|
!dev/data/backups/.gitkeep
|
||||||
!app_data/backups/dev_sample_data*
|
!dev/data/backups/dev_sample_data*
|
||||||
!app_data/debug/.gitkeep
|
!dev/data/debug/.gitkeep
|
||||||
!app_data/migration/.gitkeep
|
!dev/data/migration/.gitkeep
|
||||||
!app_data/img/.gitkeep
|
!dev/data/img/.gitkeep
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
|
@ -153,5 +153,5 @@ ENV/
|
||||||
node_modules/
|
node_modules/
|
||||||
mealie/data/debug/last_recipe.json
|
mealie/data/debug/last_recipe.json
|
||||||
*.sqlite
|
*.sqlite
|
||||||
app_data/db/test.db
|
dev/data/db/test.db
|
||||||
scratch.py
|
scratch.py
|
||||||
|
|
4
.vscode/tasks.json
vendored
4
.vscode/tasks.json
vendored
|
@ -35,7 +35,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Dev: Start Frontend",
|
"label": "Dev: Start Frontend",
|
||||||
"command": "make vue",
|
"command": "make frontend",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"reveal": "always",
|
"reveal": "always",
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Dev: Start Docs Server",
|
"label": "Dev: Start Docs Server",
|
||||||
"command": "make mdocs",
|
"command": "make docs",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"reveal": "always",
|
"reveal": "always",
|
||||||
|
|
|
@ -31,8 +31,9 @@ RUN apk add --update --no-cache --virtual .build-deps \
|
||||||
|
|
||||||
|
|
||||||
COPY ./mealie /app/mealie
|
COPY ./mealie /app/mealie
|
||||||
|
RUN poetry install --no-dev
|
||||||
COPY ./Caddyfile /app
|
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
|
COPY --from=build-stage /app/dist /app/dist
|
||||||
|
|
||||||
VOLUME [ "/app/data/" ]
|
VOLUME [ "/app/data/" ]
|
||||||
|
|
|
@ -23,59 +23,15 @@
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
|
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-row>
|
<ImportOptions @update-options="updateOptions" class="mt-5 mb-2" />
|
||||||
<v-col>
|
|
||||||
<v-checkbox
|
<v-divider></v-divider>
|
||||||
class="mb-n4 mt-1"
|
|
||||||
dense
|
<v-checkbox
|
||||||
:label="$t('settings.backup.import-recipes')"
|
dense
|
||||||
v-model="importRecipes"
|
label="Remove existing entries matching imported entries"
|
||||||
></v-checkbox>
|
v-model="forceImport"
|
||||||
<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>
|
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
|
@ -104,7 +60,9 @@
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import ImportOptions from "@/components/Admin/Backup/ImportOptions";
|
||||||
export default {
|
export default {
|
||||||
|
components: { ImportOptions },
|
||||||
props: {
|
props: {
|
||||||
name: {
|
name: {
|
||||||
default: "Backup Name",
|
default: "Backup Name",
|
||||||
|
@ -115,15 +73,22 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
options: {
|
||||||
|
recipes: true,
|
||||||
|
settings: true,
|
||||||
|
themes: true,
|
||||||
|
users: true,
|
||||||
|
groups: true,
|
||||||
|
},
|
||||||
dialog: false,
|
dialog: false,
|
||||||
importRecipes: true,
|
|
||||||
forceImport: false,
|
forceImport: false,
|
||||||
rebaseImport: false,
|
rebaseImport: false,
|
||||||
importThemes: false,
|
|
||||||
importSettings: false,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
updateOptions(options) {
|
||||||
|
this.options = options;
|
||||||
|
},
|
||||||
open() {
|
open() {
|
||||||
this.dialog = true;
|
this.dialog = true;
|
||||||
},
|
},
|
||||||
|
@ -133,11 +98,13 @@ export default {
|
||||||
raiseEvent(event) {
|
raiseEvent(event) {
|
||||||
let eventData = {
|
let eventData = {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
recipes: this.importRecipes,
|
|
||||||
force: this.forceImport,
|
force: this.forceImport,
|
||||||
rebase: this.rebaseImport,
|
rebase: this.rebaseImport,
|
||||||
themes: this.importThemes,
|
recipes: this.options.recipes,
|
||||||
settings: this.importSettings,
|
settings: this.options.settings,
|
||||||
|
themes: this.options.themes,
|
||||||
|
users: this.options.users,
|
||||||
|
groups: this.options.groups,
|
||||||
};
|
};
|
||||||
this.close();
|
this.close();
|
||||||
this.$emit(event, eventData);
|
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-app-bar>
|
||||||
<v-card-text class="mb-n4">
|
<v-card-text class="mb-n4">
|
||||||
<v-row>
|
<v-row>
|
||||||
<div>
|
<div v-for="values in allNumbers" :key="values.title">
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<div>
|
<div>
|
||||||
<h3>Recipes</h3>
|
<h3>{{ values.title }}</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 }}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="success--text">Success: {{ values.success }}</div>
|
||||||
|
<div class="error--text">Failed: {{ values.failure }}</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</div>
|
</div>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-tabs v-model="tab">
|
<v-tabs v-model="tab">
|
||||||
<v-tab>Recipes</v-tab>
|
<v-tab>{{ $t("general.recipes") }}</v-tab>
|
||||||
<v-tab>Themes</v-tab>
|
<v-tab>{{ $t("general.themes") }}</v-tab>
|
||||||
<v-tab>Settings</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>
|
||||||
<v-tabs-items v-model="tab">
|
<v-tabs-items v-model="tab">
|
||||||
<v-tab-item>
|
<v-tab-item>
|
||||||
<v-card flat>
|
<v-card flat>
|
||||||
<DataTable :data-headers="recipeHeaders" :data-set="recipeData" />
|
<DataTable :data-headers="importHeaders" :data-set="recipeData" />
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-tab-item>
|
</v-tab-item>
|
||||||
<v-tab-item>
|
<v-tab-item>
|
||||||
<v-card>
|
<v-card>
|
||||||
<DataTable
|
<DataTable
|
||||||
:data-headers="recipeHeaders"
|
:data-headers="importHeaders"
|
||||||
:data-set="themeData"
|
:data-set="themeData"
|
||||||
/> </v-card
|
/> </v-card
|
||||||
></v-tab-item>
|
></v-tab-item>
|
||||||
<v-tab-item>
|
<v-tab-item>
|
||||||
<v-card
|
<v-card
|
||||||
><DataTable
|
><DataTable
|
||||||
:data-headers="recipeHeaders"
|
:data-headers="importHeaders"
|
||||||
:data-set="settingsData"
|
:data-set="settingsData"
|
||||||
/>
|
/>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-tab-item>
|
</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-tabs-items>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
@ -98,7 +80,9 @@ export default {
|
||||||
recipeData: [],
|
recipeData: [],
|
||||||
themeData: [],
|
themeData: [],
|
||||||
settingsData: [],
|
settingsData: [],
|
||||||
recipeHeaders: [
|
userData: [],
|
||||||
|
groupData: [],
|
||||||
|
importHeaders: [
|
||||||
{
|
{
|
||||||
text: "Status",
|
text: "Status",
|
||||||
value: "status",
|
value: "status",
|
||||||
|
@ -117,39 +101,52 @@ export default {
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
recipeNumbers() {
|
recipeNumbers() {
|
||||||
let numbers = { success: 0, failure: 0 };
|
return this.calculateNumbers(this.$t("general.recipes"), this.recipeData);
|
||||||
this.recipeData.forEach(element => {
|
|
||||||
if (element.status) {
|
|
||||||
numbers.success++;
|
|
||||||
} else numbers.failure++;
|
|
||||||
});
|
|
||||||
return numbers;
|
|
||||||
},
|
},
|
||||||
settingsNumbers() {
|
settingsNumbers() {
|
||||||
let numbers = { success: 0, failure: 0 };
|
return this.calculateNumbers(
|
||||||
this.settingsData.forEach(element => {
|
this.$t("general.settings"),
|
||||||
if (element.status) {
|
this.settingsData
|
||||||
numbers.success++;
|
);
|
||||||
} else numbers.failure++;
|
|
||||||
});
|
|
||||||
return numbers;
|
|
||||||
},
|
},
|
||||||
themeNumbers() {
|
themeNumbers() {
|
||||||
let numbers = { success: 0, failure: 0 };
|
return this.calculateNumbers(this.$t("general.themes"), this.themeData);
|
||||||
this.themeData.forEach(element => {
|
},
|
||||||
if (element.status) {
|
userNumbers() {
|
||||||
numbers.success++;
|
return this.calculateNumbers(this.$t("general.users"), this.userData);
|
||||||
} else numbers.failure++;
|
},
|
||||||
});
|
groupNumbers() {
|
||||||
return numbers;
|
return this.calculateNumbers(this.$t("general.groups"), this.groupData);
|
||||||
|
},
|
||||||
|
allNumbers() {
|
||||||
|
return [
|
||||||
|
this.recipeNumbers,
|
||||||
|
this.settingsNumbers,
|
||||||
|
this.themeNumbers,
|
||||||
|
this.userNumbers,
|
||||||
|
this.groupNumbers,
|
||||||
|
];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
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) {
|
open(importData) {
|
||||||
|
console.log(importData);
|
||||||
this.recipeData = importData.recipeImports;
|
this.recipeData = importData.recipeImports;
|
||||||
this.themeData = importData.themeReport;
|
this.themeData = importData.themeImports;
|
||||||
this.settingsData = importData.settingsReport;
|
this.settingsData = importData.settingsImports;
|
||||||
|
this.userData = importData.userImports;
|
||||||
|
this.groupData = importData.groupImports;
|
||||||
this.dialog = true;
|
this.dialog = true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,57 +15,48 @@
|
||||||
{{ $t("general.create") }}
|
{{ $t("general.create") }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
|
<v-expand-transition>
|
||||||
<v-card-text v-if="!fullBackup" class="mt-n6">
|
<div v-if="!fullBackup">
|
||||||
<v-row>
|
<v-card-text class="mt-n4">
|
||||||
<v-col sm="4">
|
<v-row>
|
||||||
<p>{{ $t("general.options") }}:</p>
|
<v-col sm="4">
|
||||||
<v-checkbox
|
<p>{{ $t("general.options") }}:</p>
|
||||||
v-for="option in options"
|
<ImportOptions @update-options="updateOptions" class="mt-5" />
|
||||||
:key="option.text"
|
</v-col>
|
||||||
class="mb-n4 mt-n3"
|
<v-col>
|
||||||
dense
|
<p>{{ $t("general.templates") }}:</p>
|
||||||
:label="option.text"
|
<v-checkbox
|
||||||
v-model="option.value"
|
v-for="template in availableTemplates"
|
||||||
></v-checkbox>
|
:key="template"
|
||||||
</v-col>
|
class="mb-n4 mt-n3"
|
||||||
<v-col>
|
dense
|
||||||
<p>{{ $t("general.templates") }}:</p>
|
:label="template"
|
||||||
<v-checkbox
|
@click="appendTemplate(template)"
|
||||||
v-for="template in availableTemplates"
|
></v-checkbox>
|
||||||
:key="template"
|
</v-col>
|
||||||
class="mb-n4 mt-n3"
|
</v-row>
|
||||||
dense
|
</v-card-text>
|
||||||
:label="template"
|
</div>
|
||||||
@click="appendTemplate(template)"
|
</v-expand-transition>
|
||||||
></v-checkbox>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import ImportOptions from "@/components/Admin/Backup/ImportOptions";
|
||||||
import api from "@/api";
|
import api from "@/api";
|
||||||
export default {
|
export default {
|
||||||
|
components: { ImportOptions },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
tag: null,
|
tag: null,
|
||||||
fullBackup: true,
|
fullBackup: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
options: {
|
options: {
|
||||||
recipes: {
|
recipes: true,
|
||||||
value: true,
|
settings: true,
|
||||||
text: this.$t("general.recipes"),
|
themes: true,
|
||||||
},
|
users: true,
|
||||||
settings: {
|
groups: true,
|
||||||
value: true,
|
|
||||||
text: this.$t("general.settings"),
|
|
||||||
},
|
|
||||||
themes: {
|
|
||||||
value: true,
|
|
||||||
text: this.$t("general.themes"),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
availableTemplates: [],
|
availableTemplates: [],
|
||||||
selectedTemplates: [],
|
selectedTemplates: [],
|
||||||
|
@ -82,6 +73,9 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
updateOptions(options) {
|
||||||
|
this.options = options;
|
||||||
|
},
|
||||||
async getAvailableBackups() {
|
async getAvailableBackups() {
|
||||||
let response = await api.backups.requestAvailable();
|
let response = await api.backups.requestAvailable();
|
||||||
response.templates.forEach(element => {
|
response.templates.forEach(element => {
|
||||||
|
@ -94,9 +88,11 @@ export default {
|
||||||
let data = {
|
let data = {
|
||||||
tag: this.tag,
|
tag: this.tag,
|
||||||
options: {
|
options: {
|
||||||
recipes: this.options.recipes.value,
|
recipes: this.options.recipes,
|
||||||
settings: this.options.settings.value,
|
settings: this.options.settings,
|
||||||
themes: this.options.themes.value,
|
themes: this.options.themes,
|
||||||
|
users: this.options.users,
|
||||||
|
groups: this.options.groups,
|
||||||
},
|
},
|
||||||
templates: this.selectedTemplates,
|
templates: this.selectedTemplates,
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,13 +9,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const UPLOAD_EVENT = "uploaded";
|
||||||
import api from "@/api";
|
import api from "@/api";
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
url: String,
|
url: String,
|
||||||
text: { default: "Upload" },
|
text: { default: "Upload" },
|
||||||
icon: { default: "mdi-cloud-upload" },
|
icon: { default: "mdi-cloud-upload" },
|
||||||
fileName: { defaul: "archive" },
|
fileName: { default: "archive" },
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
file: null,
|
file: null,
|
||||||
|
@ -38,7 +39,7 @@ export default {
|
||||||
await api.utils.uploadFile(this.url, formData);
|
await api.utils.uploadFile(this.url, formData);
|
||||||
|
|
||||||
this.isSelecting = false;
|
this.isSelecting = false;
|
||||||
this.$emit("uploaded");
|
this.$emit(UPLOAD_EVENT);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onButtonClick() {
|
onButtonClick() {
|
||||||
|
|
|
@ -46,7 +46,9 @@
|
||||||
"token": "Token",
|
"token": "Token",
|
||||||
"field-required": "Field Required",
|
"field-required": "Field Required",
|
||||||
"apply": "Apply",
|
"apply": "Apply",
|
||||||
"current-parenthesis": "(Current)"
|
"current-parenthesis": "(Current)",
|
||||||
|
"users": "Users",
|
||||||
|
"groups": "Groups"
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"home-page": "Home Page",
|
"home-page": "Home Page",
|
||||||
|
|
|
@ -17,7 +17,7 @@ function inDarkMode(payload) {
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
activeTheme: {},
|
activeTheme: {},
|
||||||
darkMode: "system",
|
darkMode: "light",
|
||||||
isDark: false,
|
isDark: false,
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
token: "",
|
token: "",
|
||||||
|
|
15
makefile
15
makefile
|
@ -4,16 +4,17 @@ setup:
|
||||||
npm install && \
|
npm install && \
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
source ./.venv/bin/activate && python mealie/app.py
|
poetry run python mealie/db/init_db.py && \
|
||||||
|
poetry run python mealie/app.py
|
||||||
|
|
||||||
vue:
|
.PHONY: frontend
|
||||||
|
frontend:
|
||||||
cd frontend && npm run serve
|
cd frontend && npm run serve
|
||||||
|
|
||||||
mdocs:
|
.PHONY: docs
|
||||||
source ./.venv/bin/activate && \
|
docs:
|
||||||
cd docs && \
|
cd docs && poetry run python -m mkdocs serve
|
||||||
mkdocs serve
|
|
||||||
|
|
||||||
docker-dev:
|
docker-dev:
|
||||||
docker-compose -f docker-compose.dev.yml -p dev-mealie up --build
|
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
|
# import utils.startup as startup
|
||||||
from mealie.core.config import APP_VERSION, PORT, docs_url, redoc_url
|
from mealie.core.config import APP_VERSION, PORT, docs_url, redoc_url
|
||||||
from mealie.db.db_setup import sql_exists
|
from mealie.routes import backup_routes, debug_routes, migration_routes, setting_routes, theme_routes
|
||||||
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.groups import groups
|
from mealie.routes.groups import groups
|
||||||
from mealie.routes.mealplans import mealplans
|
from mealie.routes.mealplans import mealplans
|
||||||
from mealie.routes.recipe import (
|
from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_crud_routes, tag_routes
|
||||||
all_recipe_routes,
|
|
||||||
category_routes,
|
|
||||||
recipe_crud_routes,
|
|
||||||
tag_routes,
|
|
||||||
)
|
|
||||||
from mealie.routes.users import users
|
from mealie.routes.users import users
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
|
@ -32,10 +19,6 @@ app = FastAPI(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def data_base_first_run():
|
|
||||||
init_db()
|
|
||||||
|
|
||||||
|
|
||||||
def start_scheduler():
|
def start_scheduler():
|
||||||
import mealie.services.scheduler.scheduled_jobs
|
import mealie.services.scheduler.scheduled_jobs
|
||||||
|
|
||||||
|
@ -62,9 +45,6 @@ def api_routers():
|
||||||
app.include_router(debug_routes.router)
|
app.include_router(debug_routes.router)
|
||||||
|
|
||||||
|
|
||||||
if not sql_exists:
|
|
||||||
data_base_first_run()
|
|
||||||
|
|
||||||
api_routers()
|
api_routers()
|
||||||
start_scheduler()
|
start_scheduler()
|
||||||
|
|
||||||
|
@ -76,6 +56,7 @@ def main():
|
||||||
host="0.0.0.0",
|
host="0.0.0.0",
|
||||||
port=PORT,
|
port=PORT,
|
||||||
reload=True,
|
reload=True,
|
||||||
|
reload_dirs=["mealie"],
|
||||||
debug=True,
|
debug=True,
|
||||||
log_level="info",
|
log_level="info",
|
||||||
workers=1,
|
workers=1,
|
||||||
|
|
|
@ -34,7 +34,7 @@ else:
|
||||||
redoc_url = None
|
redoc_url = None
|
||||||
|
|
||||||
# Helpful Globals
|
# Helpful Globals
|
||||||
DATA_DIR = CWD.parent.parent.joinpath("app_data")
|
DATA_DIR = CWD.parent.parent.joinpath("dev", "data")
|
||||||
if PRODUCTION:
|
if PRODUCTION:
|
||||||
DATA_DIR = Path("/app/data")
|
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.db_base import BaseDocument
|
||||||
from mealie.db.models.group import Group
|
from mealie.db.models.group import Group
|
||||||
from mealie.db.models.mealplan import MealPlanModel
|
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.sign_up import SignUp
|
||||||
from mealie.db.models.theme import SiteThemeModel
|
from mealie.db.models.theme import SiteThemeModel
|
||||||
from mealie.db.models.users import User
|
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):
|
class _Recipes(BaseDocument):
|
||||||
|
|
|
@ -76,7 +76,7 @@ class BaseDocument:
|
||||||
|
|
||||||
return result
|
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
|
"""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.
|
key is provided the class objects primary key will be used to match against.
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@ class BaseDocument:
|
||||||
return None
|
return None
|
||||||
return [self.schema.from_orm(x) for x in result]
|
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.
|
"""Creates a new database entry for the given SQL Alchemy Model.
|
||||||
|
|
||||||
Args: \n
|
Args: \n
|
||||||
|
@ -121,7 +121,7 @@ class BaseDocument:
|
||||||
return_data = new_document.dict()
|
return_data = new_document.dict()
|
||||||
return return_data
|
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.
|
"""Update a database entry.
|
||||||
|
|
||||||
Args: \n
|
Args: \n
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
|
from fastapi.logger import logger
|
||||||
from mealie.core.config import DEFAULT_GROUP
|
from mealie.core.config import DEFAULT_GROUP
|
||||||
from mealie.core.security import get_password_hash
|
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.settings import SiteSettings
|
||||||
from mealie.schema.theme import SiteTheme
|
from mealie.schema.theme import SiteTheme
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy.orm.session 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:
|
def init_db(db: Session = None) -> None:
|
||||||
if not db:
|
if not db:
|
||||||
|
@ -55,3 +54,12 @@ def default_user_init(session: Session):
|
||||||
|
|
||||||
logger.info("Generating Default User")
|
logger.info("Generating Default User")
|
||||||
db.users.create(session, 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 as sa
|
||||||
import sqlalchemy.orm as orm
|
import sqlalchemy.orm as orm
|
||||||
|
from fastapi.logger import logger
|
||||||
from mealie.core.config import DEFAULT_GROUP
|
from mealie.core.config import DEFAULT_GROUP
|
||||||
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
|
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||||
from mealie.db.models.recipe.category import Category, group2categories
|
from mealie.db.models.recipe.category import Category, group2categories
|
||||||
from fastapi.logger import logger
|
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ class Group(SqlAlchemyBase, BaseMixins):
|
||||||
# Webhook Settings
|
# Webhook Settings
|
||||||
webhook_enable = sa.Column(sa.Boolean, default=False)
|
webhook_enable = sa.Column(sa.Boolean, default=False)
|
||||||
webhook_time = sa.Column(sa.String, default="00:00")
|
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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -52,13 +52,17 @@ class Group(SqlAlchemyBase, BaseMixins):
|
||||||
self.webhook_urls = [WebhookURLModel(url=x) for x in webhook_urls]
|
self.webhook_urls = [WebhookURLModel(url=x) for x in webhook_urls]
|
||||||
|
|
||||||
def update(self, session: Session, *args, **kwargs):
|
def update(self, session: Session, *args, **kwargs):
|
||||||
self._sql_remove_list(session, [WebhookURLModel], self.id)
|
|
||||||
|
|
||||||
self.__init__(session=session, *args, **kwargs)
|
self.__init__(session=session, *args, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_ref(session: Session, name: str):
|
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
|
@staticmethod
|
||||||
def create_if_not_exist(session, name: str = DEFAULT_GROUP):
|
def create_if_not_exist(session, name: str = DEFAULT_GROUP):
|
||||||
|
|
|
@ -32,19 +32,19 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||||
cookTime = sa.Column(sa.String)
|
cookTime = sa.Column(sa.String)
|
||||||
recipeYield = sa.Column(sa.String)
|
recipeYield = sa.Column(sa.String)
|
||||||
recipeCuisine = sa.Column(sa.String)
|
recipeCuisine = sa.Column(sa.String)
|
||||||
tool: List[Tool] = orm.relationship("Tool", cascade="all, delete")
|
tool: List[Tool] = orm.relationship("Tool", cascade="all, delete-orphan")
|
||||||
nutrition: Nutrition = orm.relationship("Nutrition", uselist=False, cascade="all, delete")
|
nutrition: Nutrition = orm.relationship("Nutrition", uselist=False, cascade="all, delete-orphan")
|
||||||
recipeCategory: List = orm.relationship("Category", secondary=recipes2categories, back_populates="recipes")
|
recipeCategory: List = orm.relationship("Category", secondary=recipes2categories, back_populates="recipes")
|
||||||
|
|
||||||
recipeIngredient: List[RecipeIngredient] = orm.relationship(
|
recipeIngredient: List[RecipeIngredient] = orm.relationship(
|
||||||
"RecipeIngredient",
|
"RecipeIngredient",
|
||||||
cascade="all, delete",
|
cascade="all, delete-orphan",
|
||||||
order_by="RecipeIngredient.position",
|
order_by="RecipeIngredient.position",
|
||||||
collection_class=ordering_list("position"),
|
collection_class=ordering_list("position"),
|
||||||
)
|
)
|
||||||
recipeInstructions: List[RecipeInstruction] = orm.relationship(
|
recipeInstructions: List[RecipeInstruction] = orm.relationship(
|
||||||
"RecipeInstruction",
|
"RecipeInstruction",
|
||||||
cascade="all, delete",
|
cascade="all, delete-orphan",
|
||||||
order_by="RecipeInstruction.position",
|
order_by="RecipeInstruction.position",
|
||||||
collection_class=ordering_list("position"),
|
collection_class=ordering_list("position"),
|
||||||
)
|
)
|
||||||
|
@ -53,10 +53,10 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||||
slug = sa.Column(sa.String, index=True, unique=True)
|
slug = sa.Column(sa.String, index=True, unique=True)
|
||||||
tags: List[Tag] = orm.relationship("Tag", secondary=recipes2tags, back_populates="recipes")
|
tags: List[Tag] = orm.relationship("Tag", secondary=recipes2tags, back_populates="recipes")
|
||||||
dateAdded = sa.Column(sa.Date, default=date.today)
|
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)
|
rating = sa.Column(sa.Integer)
|
||||||
orgURL = sa.Column(sa.String)
|
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")
|
@validates("name")
|
||||||
def validate_name(self, key, name):
|
def validate_name(self, key, name):
|
||||||
|
@ -145,8 +145,6 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||||
extras: dict = None,
|
extras: dict = None,
|
||||||
):
|
):
|
||||||
"""Updated a database entry by removing nested rows and rebuilds the row through the __init__ functions"""
|
"""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__(
|
self.__init__(
|
||||||
session=session,
|
session=session,
|
||||||
|
|
|
@ -28,20 +28,24 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||||
password,
|
password,
|
||||||
group: str = DEFAULT_GROUP,
|
group: str = DEFAULT_GROUP,
|
||||||
admin=False,
|
admin=False,
|
||||||
|
id=None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
group = group if group else DEFAULT_GROUP
|
group = group if group else DEFAULT_GROUP
|
||||||
self.full_name = full_name
|
self.full_name = full_name
|
||||||
self.email = email
|
self.email = email
|
||||||
self.group = Group.create_if_not_exist(session, group)
|
self.group = Group.get_ref(session, group)
|
||||||
self.admin = admin
|
self.admin = admin
|
||||||
self.password = password
|
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.full_name = full_name
|
||||||
self.email = email
|
self.email = email
|
||||||
self.group = Group.create_if_not_exist(session, group)
|
self.group = Group.get_ref(session, group)
|
||||||
self.admin = admin
|
self.admin = admin
|
||||||
|
|
||||||
|
if password:
|
||||||
|
self.password = password
|
||||||
|
|
||||||
def update_password(self, password):
|
def update_password(self, password):
|
||||||
self.password = password
|
self.password = password
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import operator
|
import operator
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||||
from mealie.core.config import BACKUP_DIR, TEMPLATE_DIR
|
from mealie.core.config import BACKUP_DIR, TEMPLATE_DIR
|
||||||
from mealie.db.db_setup import generate_session
|
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.backup import BackupJob, ImportJob, Imports, LocalBackup
|
||||||
from mealie.schema.snackbar import SnackResponse
|
from mealie.schema.snackbar import SnackResponse
|
||||||
|
from mealie.services.backups import imports
|
||||||
from mealie.services.backups.exports import backup_all
|
from mealie.services.backups.exports import backup_all
|
||||||
from mealie.services.backups.imports import ImportDatabase
|
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
from starlette.responses import FileResponse
|
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_recipes=data.options.recipes,
|
||||||
export_settings=data.options.settings,
|
export_settings=data.options.settings,
|
||||||
export_themes=data.options.themes,
|
export_themes=data.options.themes,
|
||||||
|
export_users=data.options.users,
|
||||||
|
export_groups=data.options.groups,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
return SnackResponse.success("Backup Created at " + export_path)
|
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)):
|
def import_database(file_name: str, import_data: ImportJob, session: Session = Depends(generate_session)):
|
||||||
""" Import a database backup file generated from Mealie. """
|
""" Import a database backup file generated from Mealie. """
|
||||||
|
|
||||||
import_db = ImportDatabase(
|
imported = imports.import_database(
|
||||||
session=session,
|
session=session,
|
||||||
zip_archive=import_data.name,
|
archive=import_data.name,
|
||||||
import_recipes=import_data.recipes,
|
import_recipes=import_data.recipes,
|
||||||
force_import=import_data.force,
|
|
||||||
rebase=import_data.rebase,
|
|
||||||
import_settings=import_data.settings,
|
import_settings=import_data.settings,
|
||||||
import_themes=import_data.themes,
|
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
|
return imported
|
||||||
|
|
||||||
|
|
||||||
|
|
7
mealie/run.sh
Normal file → Executable file
7
mealie/run.sh
Normal file → Executable file
|
@ -1,12 +1,13 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Initialize Database Prerun
|
||||||
|
python mealie/db/init_db.py
|
||||||
|
|
||||||
## Migrations
|
## Migrations
|
||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
# Database Init
|
|
||||||
|
|
||||||
## Web Server
|
## Web Server
|
||||||
caddy start --config ./Caddyfile
|
caddy start --config ./Caddyfile
|
||||||
|
|
||||||
## Start API
|
# Start API
|
||||||
uvicorn mealie.app:app --host 0.0.0.0 --port 9000
|
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):
|
class BackupJob(BaseModel):
|
||||||
tag: Optional[str]
|
tag: Optional[str]
|
||||||
options: BackupOptions
|
options: BackupOptions
|
||||||
|
@ -59,24 +77,3 @@ class Imports(BaseModel):
|
||||||
"templates": ["recipes.md", "custom_template.md"],
|
"templates": ["recipes.md", "custom_template.md"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ImportJob(BaseModel):
|
|
||||||
name: str
|
|
||||||
recipes: bool
|
|
||||||
force: bool = False
|
|
||||||
rebase: bool = False
|
|
||||||
themes: bool = False
|
|
||||||
settings: bool = False
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
schema_extra = {
|
|
||||||
"example": {
|
|
||||||
"name": "my_local_backup.zip",
|
|
||||||
"recipes": True,
|
|
||||||
"force": False,
|
|
||||||
"rebase": False,
|
|
||||||
"themes": False,
|
|
||||||
"settings": False,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,20 +3,27 @@ from typing import Optional
|
||||||
from pydantic.main import BaseModel
|
from pydantic.main import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class RecipeImport(BaseModel):
|
class ImportBase(BaseModel):
|
||||||
name: Optional[str]
|
|
||||||
slug: str
|
|
||||||
status: bool
|
|
||||||
exception: Optional[str]
|
|
||||||
|
|
||||||
|
|
||||||
class ThemeImport(BaseModel):
|
|
||||||
name: str
|
name: str
|
||||||
status: bool
|
status: bool
|
||||||
exception: Optional[str]
|
exception: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
class SettingsImport(BaseModel):
|
class RecipeImport(ImportBase):
|
||||||
name: str
|
slug: Optional[str]
|
||||||
status: bool
|
|
||||||
exception: 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 shutil
|
||||||
import zipfile
|
import zipfile
|
||||||
from pathlib import Path
|
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.core.config import BACKUP_DIR, IMG_DIR, TEMP_DIR
|
||||||
from mealie.db.database import db
|
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.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.theme import SiteTheme
|
||||||
|
from mealie.schema.user import UpdateGroup, UserInDB
|
||||||
|
from pydantic.main import BaseModel
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,32 +20,21 @@ class ImportDatabase:
|
||||||
self,
|
self,
|
||||||
session: Session,
|
session: Session,
|
||||||
zip_archive: str,
|
zip_archive: str,
|
||||||
import_recipes: bool = True,
|
|
||||||
import_settings: bool = True,
|
|
||||||
import_themes: bool = True,
|
|
||||||
force_import: bool = False,
|
force_import: bool = False,
|
||||||
rebase: bool = False,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Import a database.zip file exported from mealie.
|
"""Import a database.zip file exported from mealie.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
session (Session): SqlAlchemy Session
|
||||||
zip_archive (str): The filename contained in the backups directory
|
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.
|
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:
|
Raises:
|
||||||
Exception: If the zip file does not exists an exception raise.
|
Exception: If the zip file does not exists an exception raise.
|
||||||
"""
|
"""
|
||||||
self.session = session
|
self.session = session
|
||||||
self.archive = BACKUP_DIR.joinpath(zip_archive)
|
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_imports = force_import
|
||||||
self.force_rebase = rebase
|
|
||||||
|
|
||||||
if self.archive.is_file():
|
if self.archive.is_file():
|
||||||
self.import_dir = TEMP_DIR.joinpath("active_import")
|
self.import_dir = TEMP_DIR.joinpath("active_import")
|
||||||
|
@ -56,58 +46,31 @@ class ImportDatabase:
|
||||||
else:
|
else:
|
||||||
raise Exception("Import file does not exist")
|
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):
|
def import_recipes(self):
|
||||||
session = create_session()
|
|
||||||
recipe_dir: Path = self.import_dir.joinpath("recipes")
|
recipe_dir: Path = self.import_dir.joinpath("recipes")
|
||||||
|
|
||||||
imports = []
|
imports = []
|
||||||
successful_imports = []
|
successful_imports = []
|
||||||
|
|
||||||
for recipe in recipe_dir.glob("*.json"):
|
recipes = ImportDatabase.read_models_file(
|
||||||
with open(recipe, "r") as f:
|
file_path=recipe_dir, model=Recipe, single_file=False, migrate=ImportDatabase._recipe_migration
|
||||||
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"]
|
|
||||||
|
|
||||||
recipe_obj = Recipe(**recipe_dict)
|
for recipe in recipes:
|
||||||
db.recipes.create(session, recipe_obj.dict())
|
recipe: Recipe
|
||||||
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}")
|
|
||||||
|
|
||||||
except Exception as inst:
|
import_status = self.import_model(
|
||||||
logger.error(inst)
|
db_table=db.recipes,
|
||||||
logger.info(f"Failed Import: {recipe.stem}")
|
model=recipe,
|
||||||
import_status = RecipeImport(
|
return_model=RecipeImport,
|
||||||
name=recipe.stem,
|
name_attr="name",
|
||||||
slug=recipe.stem,
|
search_key="slug",
|
||||||
status=False,
|
slug=recipe.slug,
|
||||||
exception=str(inst),
|
)
|
||||||
)
|
|
||||||
imports.append(import_status)
|
if import_status.status:
|
||||||
|
successful_imports.append(recipe.slug)
|
||||||
|
|
||||||
|
imports.append(import_status)
|
||||||
|
|
||||||
self._import_images(successful_imports)
|
self._import_images(successful_imports)
|
||||||
|
|
||||||
|
@ -115,6 +78,9 @@ class ImportDatabase:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _recipe_migration(recipe_dict: dict) -> dict:
|
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:
|
try:
|
||||||
del recipe_dict["_id"]
|
del recipe_dict["_id"]
|
||||||
del recipe_dict["dateAdded"]
|
del recipe_dict["dateAdded"]
|
||||||
|
@ -146,42 +112,201 @@ class ImportDatabase:
|
||||||
|
|
||||||
def import_themes(self):
|
def import_themes(self):
|
||||||
themes_file = self.import_dir.joinpath("themes", "themes.json")
|
themes_file = self.import_dir.joinpath("themes", "themes.json")
|
||||||
|
themes = ImportDatabase.read_models_file(themes_file, SiteTheme)
|
||||||
theme_imports = []
|
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())
|
for theme in themes:
|
||||||
theme_imports.append(ThemeImport(name=new_theme.name, status=True))
|
if theme.name == "default":
|
||||||
except Exception as inst:
|
continue
|
||||||
logger.info(f"Unable Import Theme {new_theme.name}")
|
|
||||||
theme_imports.append(ThemeImport(name=new_theme.name, status=False, exception=str(inst)))
|
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
|
return theme_imports
|
||||||
|
|
||||||
def import_settings(self):
|
def import_settings(self): #! Broken
|
||||||
settings_file = self.import_dir.joinpath("settings", "settings.json")
|
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:
|
try:
|
||||||
settings: dict = json.loads(f.read())
|
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:
|
return [import_status]
|
||||||
db.settings.update(self.session, name, settings)
|
|
||||||
import_status = SettingsImport(name=name, status=True)
|
|
||||||
|
|
||||||
except Exception as inst:
|
def import_groups(self):
|
||||||
import_status = SettingsImport(name=name, status=False, exception=str(inst))
|
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):
|
def clean_up(self):
|
||||||
shutil.rmtree(TEMP_DIR)
|
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]]
|
[[package]]
|
||||||
name = "isort"
|
name = "isort"
|
||||||
version = "5.7.0"
|
version = "5.8.0"
|
||||||
description = "A Python utility / library to sort Python imports."
|
description = "A Python utility / library to sort Python imports."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
|
@ -1366,8 +1366,8 @@ isodate = [
|
||||||
{file = "isodate-0.6.0.tar.gz", hash = "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8"},
|
{file = "isodate-0.6.0.tar.gz", hash = "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8"},
|
||||||
]
|
]
|
||||||
isort = [
|
isort = [
|
||||||
{file = "isort-5.7.0-py3-none-any.whl", hash = "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc"},
|
{file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"},
|
||||||
{file = "isort-5.7.0.tar.gz", hash = "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e"},
|
{file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"},
|
||||||
]
|
]
|
||||||
jinja2 = [
|
jinja2 = [
|
||||||
{file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"},
|
{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"},
|
{file = "lunr-0.5.8.tar.gz", hash = "sha256:c4fb063b98eff775dd638b3df380008ae85e6cb1d1a24d1cd81a10ef6391c26e"},
|
||||||
]
|
]
|
||||||
lxml = [
|
lxml = [
|
||||||
{file = "lxml-4.6.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a9d6bc8642e2c67db33f1247a77c53476f3a166e09067c0474facb045756087f"},
|
{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:791394449e98243839fa822a637177dd42a95f4883ad3dec2a0ce6ac99fb0a9d"},
|
{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:68a5d77e440df94011214b7db907ec8f19e439507a70c958f750c18d88f995d2"},
|
{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:fc37870d6716b137e80d19241d0e2cff7a7643b925dfa49b4c8ebd1295eb506e"},
|
{file = "lxml-4.6.2-cp27-cp27m-win32.whl", hash = "sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106"},
|
||||||
{file = "lxml-4.6.2-cp27-cp27m-win_amd64.whl", hash = "sha256:69a63f83e88138ab7642d8f61418cf3180a4d8cd13995df87725cb8b893e950e"},
|
{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:42ebca24ba2a21065fb546f3e6bd0c58c3fe9ac298f3a320147029a4850f51a2"},
|
{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:f83d281bb2a6217cd806f4cf0ddded436790e66f393e124dfe9731f6b3fb9afe"},
|
{file = "lxml-4.6.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4"},
|
||||||
{file = "lxml-4.6.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:535f067002b0fd1a4e5296a8f1bf88193080ff992a195e66964ef2a6cfec5388"},
|
{file = "lxml-4.6.2-cp35-cp35m-win32.whl", hash = "sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2"},
|
||||||
{file = "lxml-4.6.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:366cb750140f221523fa062d641393092813b81e15d0e25d9f7c6025f910ee80"},
|
{file = "lxml-4.6.2-cp35-cp35m-win_amd64.whl", hash = "sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4"},
|
||||||
{file = "lxml-4.6.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:97db258793d193c7b62d4e2586c6ed98d51086e93f9a3af2b2034af01450a74b"},
|
{file = "lxml-4.6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4"},
|
||||||
{file = "lxml-4.6.2-cp35-cp35m-win32.whl", hash = "sha256:648914abafe67f11be7d93c1a546068f8eff3c5fa938e1f94509e4a5d682b2d8"},
|
{file = "lxml-4.6.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3"},
|
||||||
{file = "lxml-4.6.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4e751e77006da34643ab782e4a5cc21ea7b755551db202bc4d3a423b307db780"},
|
{file = "lxml-4.6.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d"},
|
||||||
{file = "lxml-4.6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:681d75e1a38a69f1e64ab82fe4b1ed3fd758717bed735fb9aeaa124143f051af"},
|
{file = "lxml-4.6.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:820628b7b3135403540202e60551e741f9b6d3304371712521be939470b454ec"},
|
||||||
{file = "lxml-4.6.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:127f76864468d6630e1b453d3ffbbd04b024c674f55cf0a30dc2595137892d37"},
|
{file = "lxml-4.6.2-cp36-cp36m-win32.whl", hash = "sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04"},
|
||||||
{file = "lxml-4.6.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4fb85c447e288df535b17ebdebf0ec1cf3a3f1a8eba7e79169f4f37af43c6b98"},
|
{file = "lxml-4.6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:92e821e43ad382332eade6812e298dc9701c75fe289f2a2d39c7960b43d1e92a"},
|
||||||
{file = "lxml-4.6.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5be4a2e212bb6aa045e37f7d48e3e1e4b6fd259882ed5a00786f82e8c37ce77d"},
|
{file = "lxml-4.6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654"},
|
||||||
{file = "lxml-4.6.2-cp36-cp36m-win32.whl", hash = "sha256:8c88b599e226994ad4db29d93bc149aa1aff3dc3a4355dd5757569ba78632bdf"},
|
{file = "lxml-4.6.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0"},
|
||||||
{file = "lxml-4.6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:6e4183800f16f3679076dfa8abf2db3083919d7e30764a069fb66b2b9eff9939"},
|
{file = "lxml-4.6.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3"},
|
||||||
{file = "lxml-4.6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d8d3d4713f0c28bdc6c806a278d998546e8efc3498949e3ace6e117462ac0a5e"},
|
{file = "lxml-4.6.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2"},
|
||||||
{file = "lxml-4.6.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:8246f30ca34dc712ab07e51dc34fea883c00b7ccb0e614651e49da2c49a30711"},
|
{file = "lxml-4.6.2-cp37-cp37m-win32.whl", hash = "sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade"},
|
||||||
{file = "lxml-4.6.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:923963e989ffbceaa210ac37afc9b906acebe945d2723e9679b643513837b089"},
|
{file = "lxml-4.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b"},
|
||||||
{file = "lxml-4.6.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:1471cee35eba321827d7d53d104e7b8c593ea3ad376aa2df89533ce8e1b24a01"},
|
{file = "lxml-4.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa"},
|
||||||
{file = "lxml-4.6.2-cp37-cp37m-win32.whl", hash = "sha256:2363c35637d2d9d6f26f60a208819e7eafc4305ce39dc1d5005eccc4593331c2"},
|
{file = "lxml-4.6.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a"},
|
||||||
{file = "lxml-4.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:f4822c0660c3754f1a41a655e37cb4dbbc9be3d35b125a37fab6f82d47674ebc"},
|
{file = "lxml-4.6.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927"},
|
||||||
{file = "lxml-4.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0448576c148c129594d890265b1a83b9cd76fd1f0a6a04620753d9a6bcfd0a4d"},
|
{file = "lxml-4.6.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791"},
|
||||||
{file = "lxml-4.6.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:60a20bfc3bd234d54d49c388950195d23a5583d4108e1a1d47c9eef8d8c042b3"},
|
{file = "lxml-4.6.2-cp38-cp38-win32.whl", hash = "sha256:89b8b22a5ff72d89d48d0e62abb14340d9e99fd637d046c27b8b257a01ffbe28"},
|
||||||
{file = "lxml-4.6.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2e5cc908fe43fe1aa299e58046ad66981131a66aea3129aac7770c37f590a644"},
|
{file = "lxml-4.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7"},
|
||||||
{file = "lxml-4.6.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:50c348995b47b5a4e330362cf39fc503b4a43b14a91c34c83b955e1805c8e308"},
|
{file = "lxml-4.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0"},
|
||||||
{file = "lxml-4.6.2-cp38-cp38-win32.whl", hash = "sha256:94d55bd03d8671686e3f012577d9caa5421a07286dd351dfef64791cf7c6c505"},
|
{file = "lxml-4.6.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1"},
|
||||||
{file = "lxml-4.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:7a7669ff50f41225ca5d6ee0a1ec8413f3a0d8aa2b109f86d540887b7ec0d72a"},
|
{file = "lxml-4.6.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f8380c03e45cf09f8557bdaa41e1fa7c81f3ae22828e1db470ab2a6c96d8bc23"},
|
||||||
{file = "lxml-4.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e0bfe9bb028974a481410432dbe1b182e8191d5d40382e5b8ff39cdd2e5c5931"},
|
{file = "lxml-4.6.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:884ab9b29feaca361f7f88d811b1eea9bfca36cf3da27768d28ad45c3ee6f969"},
|
||||||
{file = "lxml-4.6.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:6fd8d5903c2e53f49e99359b063df27fdf7acb89a52b6a12494208bf61345a03"},
|
{file = "lxml-4.6.2-cp39-cp39-win32.whl", hash = "sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f"},
|
||||||
{file = "lxml-4.6.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7e9eac1e526386df7c70ef253b792a0a12dd86d833b1d329e038c7a235dfceb5"},
|
{file = "lxml-4.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83"},
|
||||||
{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"},
|
|
||||||
]
|
]
|
||||||
markdown = [
|
markdown = [
|
||||||
{file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"},
|
{file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"},
|
||||||
|
|
|
@ -34,7 +34,7 @@ def api_client():
|
||||||
|
|
||||||
yield TestClient(app)
|
yield TestClient(app)
|
||||||
|
|
||||||
# SQLITE_FILE.unlink()
|
SQLITE_FILE.unlink()
|
||||||
|
|
||||||
|
|
||||||
@fixture(scope="session")
|
@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
|
import pytest
|
||||||
from tests.test_routes.utils.routes_data import recipe_test_data
|
from tests.test_routes.utils.routes_data import recipe_test_data
|
||||||
from tests.utils.routes import (
|
from tests.utils.routes import MEALPLAN_ALL, MEALPLAN_CREATE, MEALPLAN_PREFIX, RECIPES_CREATE_URL, RECIPES_PREFIX
|
||||||
MEALPLAN_ALL,
|
|
||||||
MEALPLAN_CREATE,
|
|
||||||
MEALPLAN_PREFIX,
|
|
||||||
RECIPES_CREATE_URL,
|
|
||||||
RECIPES_PREFIX,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_meal_plan_template(first=None, second=None):
|
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"][0]["slug"] = slug_2
|
||||||
existing_mealplan["meals"][1]["slug"] = slug_1
|
existing_mealplan["meals"][1]["slug"] = slug_1
|
||||||
|
|
||||||
response = api_client.put(
|
response = api_client.put(f"{MEALPLAN_PREFIX}/{plan_uid}", json=existing_mealplan, headers=token)
|
||||||
f"{MEALPLAN_PREFIX}/{plan_uid}", json=existing_mealplan, headers=token
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,8 @@ import json
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
from tests.test_routes.utils.routes_data import (RecipeTestData, raw_recipe,
|
from tests.test_routes.utils.routes_data import RecipeTestData, raw_recipe, raw_recipe_no_image, recipe_test_data
|
||||||
raw_recipe_no_image,
|
from tests.utils.routes import RECIPES_ALL, RECIPES_CREATE, RECIPES_CREATE_URL, RECIPES_PREFIX
|
||||||
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)
|
@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):
|
def test_read_all_post(api_client):
|
||||||
response = api_client.post(
|
response = api_client.post(RECIPES_ALL, json={"properties": ["slug", "description", "rating"]})
|
||||||
RECIPES_ALL, json={"properties": ["slug", "description", "rating"]}
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
@ -65,9 +60,7 @@ def test_read_update(api_client, recipe_data):
|
||||||
test_categories = ["one", "two", "three"]
|
test_categories = ["one", "two", "three"]
|
||||||
recipe["recipeCategory"] = test_categories
|
recipe["recipeCategory"] = test_categories
|
||||||
|
|
||||||
response = api_client.put(
|
response = api_client.put(f"{RECIPES_PREFIX}/{recipe_data.expected_slug}", json=recipe)
|
||||||
f"{RECIPES_PREFIX}/{recipe_data.expected_slug}", json=recipe
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert json.loads(response.text) == recipe_data.expected_slug
|
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)
|
new_slug = slugify(new_name)
|
||||||
recipe["name"] = new_name
|
recipe["name"] = new_name
|
||||||
|
|
||||||
response = api_client.put(
|
response = api_client.put(f"{RECIPES_PREFIX}/{recipe_data.expected_slug}", json=recipe)
|
||||||
f"{RECIPES_PREFIX}/{recipe_data.expected_slug}", json=recipe
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert json.loads(response.text) == new_slug
|
assert json.loads(response.text) == new_slug
|
||||||
|
|
|
@ -7,29 +7,14 @@ BASE = "/api/users"
|
||||||
TOKEN_URL = "/api/auth/token"
|
TOKEN_URL = "/api/auth/token"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@fixture(scope="session")
|
@fixture(scope="session")
|
||||||
def default_user():
|
def default_user():
|
||||||
return {
|
return {"id": 1, "fullName": "Change Me", "email": "changeme@email.com", "group": "Home", "admin": True}
|
||||||
"id": 1,
|
|
||||||
"fullName": "Change Me",
|
|
||||||
"email": "changeme@email.com",
|
|
||||||
"group": "Home",
|
|
||||||
"admin": True
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@fixture(scope="session")
|
@fixture(scope="session")
|
||||||
def new_user():
|
def new_user():
|
||||||
return {
|
return {"id": 2, "fullName": "My New User", "email": "newuser@email.com", "group": "Home", "admin": False}
|
||||||
"id": 2,
|
|
||||||
"fullName": "My New User",
|
|
||||||
"email": "newuser@email.com",
|
|
||||||
"group": "Home",
|
|
||||||
"admin": False
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_superuser_login(api_client: requests):
|
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",
|
"email": "newuser@email.com",
|
||||||
"password": "MyStrongPassword",
|
"password": "MyStrongPassword",
|
||||||
"group": "Home",
|
"group": "Home",
|
||||||
"admin": False
|
"admin": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
response = api_client.post(f"{BASE}", json=create_data, headers=token)
|
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):
|
def test_update_user(api_client: requests, token):
|
||||||
update_data = {
|
update_data = {"id": 1, "fullName": "Updated Name", "email": "updated@email.com", "group": "Home", "admin": True}
|
||||||
"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)
|
response = api_client.put(f"{BASE}/1", headers=token, json=update_data)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue