Feature/site settings (#225)

* general cleanup

* submit on enter

* fix signup form

* fix duplicate slugs when testing

* custom pages starter

* api start

* functional

* docs

* fix page reload on submit

* set initial selection for category button

* Site Pages Import/Export

* documentation updates

* generate .secret file for jwt hashing

* properly align default passwords

* set default password globally

* set group for signups

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden 2021-03-28 11:52:34 -08:00 committed by GitHub
commit 9e23587c0d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 807 additions and 196 deletions

3
.gitignore vendored
View file

@ -1,6 +1,6 @@
# Byte-compiled / optimized / DLL files
.env
__pycache__/
*__pycache__/
*.py[cod]
*$py.class
# frontend/.env.development
@ -8,6 +8,7 @@ docs/site/
mealie/temp/*
mealie/temp/api.html
.temp/
.secret
dev/data/backups/*

View file

@ -12,7 +12,7 @@
A new database will be created. You must export your data and then import it after upgrading.
#### Site Settings
With the addition of group settings and a re-write of the database layer of the application backend, there is no migration path for your current site settings. Webhooks Settings, Meal Plan Categories are now managed by groups. Site settings, mainly homepage settings, are not site specific and managed by administrators.
With the addition of group settings and a re-write of the database layer of the application backend, there is no migration path for your current site settings. Webhooks Settings, Meal Plan Categories are now managed by groups. Site settings, mainly homepage settings, are now site specific and managed by administrators.
#### ENV Variables
Names have been changed to be more consistent with industry standards. See the [Installation Page](/getting-started/install/) for new parameters.
@ -49,6 +49,11 @@
- Group Management
- Create/Edit/Delete Restrictions
### Custom Pages
- You can now create custom pages that are displayed on the homepage sidebar to organize categories of recipes into pages. For example, if you have several categories that encompass "Entrée" you can group all those categories together under the "Entrée" page. See [Building Pages](/site-administration/building-pages/) for more information.
!!! tip
Note that this replaces the behavior of automatically adding categories to the sidebar.
### UI Improvements
- Completed Redesign of the Admin Panel
- Profile Pages

View file

@ -45,13 +45,14 @@ services:
## Env Variables
| Variables | Default | Description |
| ---------------- | -------- | ----------------------------------------------------------------------------------- |
| DB_TYPE | sqlite | The database type to be used. Current Options 'sqlite' |
| API_PORT | 9000 | The port exposed by backend API. **do not change this if you're running in docker** |
| API_DOCS | True | Turns on/off access to the API documentation locally. |
| DEFAULT_PASSWORD | ChangeMe | The default password for all users created in Mealie |
| TZ | UTC | Must be set to get correct date/time on the server |
| Variables | Default | Description |
| ---------------- | ---------- | ----------------------------------------------------------------------------------- |
| DB_TYPE | sqlite | The database type to be used. Current Options 'sqlite' |
| DEFAULT_GROUP | Home | The default group for users |
| DEFAULT_PASSWORD | MyPassword | The default password for all users created in Mealie |
| API_PORT | 9000 | The port exposed by backend API. **do not change this if you're running in docker** |
| API_DOCS | True | Turns on/off access to the API documentation locally. |
| TZ | UTC | Must be set to get correct date/time on the server |
## Deployed as a Python Application

View file

@ -9,6 +9,7 @@ All recipe data can be imported and exported as necessary from the UI. Under the
- [x] Recipe Data
- [ ] Meal Plan
- [x] Site Settings
- [x] Custom Pages
- [x] User Data
- [x] Group Data

View file

@ -0,0 +1,8 @@
# Building Pages
Custom pages can be created to organize multiple categories into a single page. Links to your custom pages are displayed on the home page sidebar and accessible by all users, however only Administrators can create or update pages.
To create a new page. Navigate to the settings page at `/admin/settings` and scroll down to the custom pages section. Here you can create, view, and edit your custom pages. To reorder how they are displayed on the sidebar you can drag and drop the pages into the preferred order.
!!! tip
To save the order of pages you must click the save button displayed on the bottom of the Custom Page section. This is not necessary for updating individual page data.

View file

@ -45,6 +45,7 @@ nav:
- Site Administration:
- User Settings: "site-administration/user-settings.md"
- Site Settings: "site-administration/site-settings.md"
- Building Pages: "site-administration/building-pages.md"
- User Management: "site-administration/user-management.md"
- Backups and Restore: "site-administration/backups-and-exports.md"
- Recipe Migration: "site-administration/migration-imports.md"

View file

@ -76,10 +76,9 @@ export default {
mounted() {
this.$store.dispatch("initTheme");
this.$store.dispatch("requestRecentRecipes");
this.$store.dispatch("requestHomePageSettings");
this.$store.dispatch("requestSiteSettings");
this.$store.dispatch("refreshToken");
this.$store.dispatch("requestCurrentGroup");
this.$store.dispatch("requestCategories");
this.darkModeSystemCheck();
this.darkModeAddEventListener();
},

View file

@ -10,7 +10,7 @@ const categoryURLs = {
};
export default {
async get_all() {
async getAll() {
let response = await apiReq.get(categoryURLs.get_all);
return response.data;
},

View file

@ -16,7 +16,6 @@ const mealPlanURLs = {
export default {
async create(postBody) {
console.log(postBody);
let response = await apiReq.post(mealPlanURLs.create, postBody);
return response;
},

View file

@ -7,6 +7,8 @@ const settingsURLs = {
siteSettings: `${settingsBase}`,
updateSiteSettings: `${settingsBase}`,
testWebhooks: `${settingsBase}/webhooks/test`,
customPages: `${settingsBase}/custom-pages`,
customPage: id => `${settingsBase}/custom-pages/${id}`,
};
export default {
@ -19,4 +21,34 @@ export default {
let response = await apiReq.put(settingsURLs.updateSiteSettings, body);
return response.data;
},
async getPages() {
let response = await apiReq.get(settingsURLs.customPages);
return response.data;
},
async getPage(id) {
let response = await apiReq.get(settingsURLs.customPage(id));
return response.data;
},
async createPage(body) {
let response = await apiReq.post(settingsURLs.customPages, body);
return response.data;
},
async deletePage(id) {
let response = await apiReq.delete(settingsURLs.customPage(id));
return response.data;
},
async updatePage(body) {
let response = await apiReq.put(settingsURLs.customPage(body.id), body);
return response.data;
},
async updateAllPages(allPages) {
let response = await apiReq.put(settingsURLs.customPages, allPages);
return response;
},
};

View file

@ -26,6 +26,10 @@ export default {
value: true,
text: this.$t("general.settings"),
},
pages: {
value: true,
text: "Pages",
},
themes: {
value: true,
text: this.$t("general.themes"),
@ -50,6 +54,7 @@ export default {
recipes: this.options.recipes.value,
settings: this.options.settings.value,
themes: this.options.themes.value,
pages: this.options.pages.value,
users: this.options.users.value,
groups: this.options.groups.value,
});

View file

@ -28,38 +28,14 @@
<v-tab>{{ $t("general.recipes") }}</v-tab>
<v-tab>{{ $t("general.themes") }}</v-tab>
<v-tab>{{ $t("general.settings") }}</v-tab>
<v-tab> Pages </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-tab-item v-for="(table, index) in allTables" :key="index">
<v-card flat>
<DataTable :data-headers="importHeaders" :data-set="recipeData" />
</v-card>
</v-tab-item>
<v-tab-item>
<v-card>
<DataTable
:data-headers="importHeaders"
:data-set="themeData"
/> </v-card
></v-tab-item>
<v-tab-item>
<v-card
><DataTable
: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" />
<DataTable :data-headers="importHeaders" :data-set="table" />
</v-card>
</v-tab-item>
</v-tabs-items>
@ -82,6 +58,7 @@ export default {
settingsData: [],
userData: [],
groupData: [],
pageData: [],
importHeaders: [
{
text: "Status",
@ -118,15 +95,29 @@ export default {
groupNumbers() {
return this.calculateNumbers(this.$t("general.groups"), this.groupData);
},
pageNumbers() {
return this.calculateNumbers("Pages", this.pageData);
},
allNumbers() {
return [
this.recipeNumbers,
this.settingsNumbers,
this.themeNumbers,
this.settingsNumbers,
this.pageNumbers,
this.userNumbers,
this.groupNumbers,
];
},
allTables() {
return [
this.recipeData,
this.themeData,
this.settingsData,
this.pageData,
this.userData,
this.groupData,
];
},
},
methods: {
@ -141,12 +132,12 @@ export default {
return numbers;
},
open(importData) {
console.log(importData);
this.recipeData = importData.recipeImports;
this.themeData = importData.themeImports;
this.settingsData = importData.settingsImports;
this.userData = importData.userImports;
this.groupData = importData.groupImports;
this.pageData = importData.pageImports;
this.dialog = true;
},
},

View file

@ -0,0 +1,98 @@
<template>
<v-dialog v-model="pageDialog" max-width="500">
<v-card>
<v-app-bar dark dense color="primary">
<v-icon left>
mdi-page-layout-body
</v-icon>
<v-toolbar-title class="headline">
{{ title }}
</v-toolbar-title>
<v-spacer></v-spacer>
</v-app-bar>
<v-form ref="newGroup" @submit.prevent="submitForm">
<v-card-text>
<v-text-field
autofocus
v-model="page.name"
label="Page Name"
></v-text-field>
<CategorySelector
v-model="page.categories"
ref="categoryFormSelector"
@mounted="catMounted = true"
/>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" text @click="pageDialog = false">
{{ $t("general.cancel") }}
</v-btn>
<v-btn color="primary" type="submit">
{{ buttonText }}
</v-btn>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
</template>
<script>
const NEW_PAGE_EVENT = "refresh-page";
import api from "@/api";
import CategorySelector from "@/components/FormHelpers/CategorySelector";
export default {
components: {
CategorySelector,
},
data() {
return {
catMounted: false,
title: "",
buttonText: "",
create: false,
pageDialog: false,
page: {
name: "",
position: 0,
categories: [],
},
};
},
watch: {
catMounted(val) {
if (val) this.pushSelected();
},
},
methods: {
open(parameters) {
this.page = parameters.data;
this.create = parameters.create;
this.buttonText = parameters.buttonText;
this.title = parameters.title;
this.pageDialog = true;
if (this.catMounted) this.pushSelected();
},
pushSelected() {
this.$refs.categoryFormSelector.setInit(this.page.categories);
},
async submitForm() {
if (this.create) {
await api.siteSettings.createPage(this.page);
} else {
await api.siteSettings.updatePage(this.page);
}
this.pageDialog = false;
this.page.categories = [];
this.$emit(NEW_PAGE_EVENT);
},
},
};
</script>
<style lang="scss" scoped>
</style>

View file

@ -0,0 +1,129 @@
<template>
<v-card flat>
<CreatePageDialog ref="createDialog" @refresh-page="getPages" />
<v-card-text>
<h2 class="mt-1 mb-1 ">
Custom Pages
<span>
<v-btn color="success" @click="newPage" small class="ml-3">
Create
</v-btn>
</span>
</h2>
<draggable class="row mt-1" v-model="customPages">
<v-col
:sm="6"
:md="6"
:lg="4"
:xl="3"
v-for="(item, index) in customPages"
:key="item + item.id"
>
<v-card>
<v-card-text class="mb-0 pb-0">
<h3>{{ item.name }}</h3>
<v-divider></v-divider>
</v-card-text>
<v-card-text class="mt-0">
<div>
<v-chip
v-for="cat in item.categories"
:key="cat.slug + cat.id"
class="my-2 mr-2"
label
small
color="accent lighten-1"
>
{{ cat.name }}
</v-chip>
</div>
</v-card-text>
<v-card-actions>
<v-btn text small color="error" @click="deletePage(item.id)">
Delete
</v-btn>
<v-spacer> </v-spacer>
<v-btn small text color="success" @click="editPage(index)">
Edit
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</draggable>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="success" @click="savePages">
Save
</v-btn>
</v-card-actions>
</v-card>
</template>
<script>
import draggable from "vuedraggable";
import CreatePageDialog from "@/components/Admin/General/CreatePageDialog";
import api from "@/api";
export default {
components: {
draggable,
CreatePageDialog,
},
data() {
return {
pageDialog: false,
customPages: [],
newPageData: {
create: true,
title: "New Page",
buttonText: "Create",
data: {
name: "",
categories: [],
position: 0,
},
},
editPageData: {
create: false,
title: "Edit Page",
buttonText: "Update",
data: {},
},
};
},
async mounted() {
this.getPages();
},
methods: {
async getPages() {
this.customPages = await api.siteSettings.getPages();
this.customPages.sort((a, b) => a.position - b.position);
},
async deletePage(id) {
await api.siteSettings.deletePage(id);
this.getPages();
},
async savePages() {
this.customPages.forEach((element, index) => {
element.position = index;
});
await api.siteSettings.updateAllPages(this.customPages);
this.getPages();
},
editPage(index) {
this.editPageData.data = this.customPages[index];
this.$refs.createDialog.open(this.editPageData);
},
newPage() {
this.newPageData.position = this.customPages.length;
this.$refs.createDialog.open(this.newPageData);
},
},
};
</script>
<style lang="scss" scoped>
</style>

View file

@ -38,7 +38,7 @@
</v-icon>
<v-toolbar-title class="headline">
{{$t('settings.homepage.home-page-sections')}}
{{ $t("settings.homepage.home-page-sections") }}
</v-toolbar-title>
<v-spacer></v-spacer>
@ -80,7 +80,7 @@
</v-icon>
<v-toolbar-title class="headline">
{{$t('settings.homepage.all-categories')}}
{{ $t("settings.homepage.all-categories") }}
</v-toolbar-title>
<v-spacer></v-spacer>
@ -153,13 +153,12 @@ export default {
},
computed: {
allCategories() {
return this.$store.getters.getCategories;
return this.$store.getters.getAllCategories;
},
},
methods: {
writeLang(val) {
console.log(val);
this.settings.language = val;
},
deleteCategoryfromDatabase(category) {

View file

@ -39,7 +39,7 @@
<v-spacer></v-spacer>
</v-app-bar>
<v-form ref="newGroup" @submit="createGroup">
<v-form ref="newGroup" @submit.prevent="createGroup">
<v-card-text>
<v-text-field
v-model="newGroupName"
@ -53,7 +53,7 @@
<v-btn color="grey" text @click="groupDialog = false">
{{ $t("general.cancel") }}
</v-btn>
<v-btn color="primary" type="submit" @click.prevent="createGroup">
<v-btn color="primary" type="submit">
{{ $t("general.create") }}
</v-btn>
</v-card-actions>

View file

@ -40,7 +40,7 @@
<v-spacer></v-spacer>
</v-app-bar>
<v-form ref="newUser" @submit="save">
<v-form ref="newUser" @submit.prevent="save">
<v-card-text>
<v-row class="justify-center mt-3">
<v-text-field
@ -62,7 +62,7 @@
<v-btn color="grey" text @click="close">
{{ $t("general.cancel") }}
</v-btn>
<v-btn color="primary" type="submit" @click.prevent="save">
<v-btn color="primary" type="submit">
{{ $t("general.save") }}
</v-btn>
</v-card-actions>

View file

@ -48,7 +48,7 @@
{{ $t("user.user-id-with-value", { id: editedItem.id }) }}
</v-toolbar-title>
</v-app-bar>
<v-form ref="newUser" @submit="save">
<v-form ref="newUser" @submit.prevent="save">
<v-card-text>
<v-row>
<v-col cols="12" sm="12" md="6">
@ -97,7 +97,7 @@
<v-btn color="grey" text @click="close">
{{ $t("general.cancel") }}
</v-btn>
<v-btn color="primary" type="submit" @click.prevent="save">
<v-btn color="primary" type="submit">
{{ $t("general.save") }}
</v-btn>
</v-card-actions>

View file

@ -17,7 +17,7 @@
<v-spacer></v-spacer>
</v-app-bar>
<v-card-title> </v-card-title>
<v-form @submit="select">
<v-form @submit.prevent="select">
<v-card-text>
<v-text-field
:label="$t('settings.theme.theme-name')"
@ -30,7 +30,7 @@
<v-btn color="grey" text @click="dialog = false">
{{ $t("general.cancel") }}
</v-btn>
<v-btn color="success" text type="submit" @click.prevent="select" :disabled="!themeName">
<v-btn color="success" text type="submit" :disabled="!themeName">
{{ $t("general.create") }}
</v-btn>
</v-card-actions>

View file

@ -0,0 +1,50 @@
<template>
<div>
<v-select
:items="allCategories"
v-model="selected"
label="Categories"
chips
deletable-chips
dense
item-text="name"
multiple
return-object
@input="emitChange"
></v-select>
</div>
</template>
<script>
const MOUNTED_EVENT = "mounted";
export default {
props: {
value: Array,
},
data() {
return {
selected: [],
};
},
mounted() {
this.$emit(MOUNTED_EVENT);
},
computed: {
allCategories() {
return this.$store.getters.getAllCategories;
},
},
methods: {
emitChange() {
this.$emit("input", this.selected);
},
setInit(val) {
this.selected = val;
},
},
};
</script>
<style lang="scss" scoped>
</style>

View file

@ -17,7 +17,7 @@
<v-spacer></v-spacer>
</v-app-bar>
<v-form @submit="login">
<v-form @submit.prevent="login">
<v-card-text>
<v-text-field
v-if="!options.isLoggingIn"
@ -47,14 +47,12 @@
<v-card-actions>
<v-btn
v-if="options.isLoggingIn"
@click.prevent="login"
dark
color="primary"
block="block"
type="submit"
>{{ $t("user.sign-in") }}
</v-btn
>
</v-btn>
</v-card-actions>
<v-alert v-if="error" outlined class="mt-3 mb-0" type="error">

View file

@ -21,7 +21,7 @@
have a valid invitation link. If you haven't recieved an invitation you
are unable to sign-up. To recieve a link, contact the sites administrator.
<v-divider class="mt-3"></v-divider>
<v-form ref="signUpForm" @submit="signUp">
<v-form ref="signUpForm" @submit.prevent="signUp">
<v-text-field
v-model="user.name"
light="light"
@ -66,7 +66,6 @@
<v-card-actions>
<v-btn
v-if="options.isLoggingIn"
@click.prevent="signUp"
dark
color="primary"
block="block"

View file

@ -21,7 +21,7 @@
<v-spacer></v-spacer>
</v-app-bar>
<v-form ref="urlForm" @submit="createRecipe">
<v-form ref="urlForm" @submit.prevent="createRecipe">
<v-card-text>
<v-text-field
v-model="recipeURL"
@ -47,12 +47,7 @@
<v-btn color="grey" text @click="reset">
{{ $t("general.close") }}
</v-btn>
<v-btn
color="success"
text
@click.prevent="createRecipe"
:loading="processing"
>
<v-btn color="success" text type="submit" :loading="processing">
{{ $t("general.submit") }}
</v-btn>
</v-card-actions>

View file

@ -1,8 +1,8 @@
<template>
<div class="mt-n5">
<div class="mt-n5" v-if="recipes">
<v-card flat class="transparent" height="60px">
<v-card-text>
<v-row>
<v-row v-if="title != null">
<v-col>
<v-btn-toggle group>
<v-btn text :to="`/recipes/${title.toLowerCase()}`">
@ -15,15 +15,21 @@
<v-menu offset-y v-if="sortable">
<template v-slot:activator="{ on, attrs }">
<v-btn-toggle group>
<v-btn text v-bind="attrs" v-on="on">{{$t('general.sort')}}</v-btn>
<v-btn text v-bind="attrs" v-on="on">{{
$t("general.sort")
}}</v-btn>
</v-btn-toggle>
</template>
<v-list>
<v-list-item @click="$emit('sort-recent')">
<v-list-item-title>{{$t('general.recent')}}</v-list-item-title>
<v-list-item-title>{{
$t("general.recent")
}}</v-list-item-title>
</v-list-item>
<v-list-item @click="$emit('sort')">
<v-list-item-title>{{$t('general.sort-alphabetically')}}</v-list-item-title>
<v-list-item-title>{{
$t("general.sort-alphabetically")
}}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
@ -31,44 +37,45 @@
</v-row>
</v-card-text>
</v-card>
<v-row v-if="!viewScale">
<v-col
:sm="6"
:md="6"
:lg="4"
:xl="3"
v-for="recipe in recipes.slice(0, cardLimit)"
:key="recipe.name"
>
<RecipeCard
:name="recipe.name"
:description="recipe.description"
:slug="recipe.slug"
:rating="recipe.rating"
:image="recipe.image"
/>
</v-col>
</v-row>
<v-row v-else dense>
<v-col
cols="12"
sm="12"
md="6"
lg="4"
xl="3"
v-for="recipe in recipes.slice(0, cardLimit)"
:key="recipe.name"
>
<MobileRecipeCard
:name="recipe.name"
:description="recipe.description"
:slug="recipe.slug"
:rating="recipe.rating"
:image="recipe.image"
/>
</v-col>
</v-row>
<div v-if="recipes">
<v-row v-if="!viewScale">
<v-col
:sm="6"
:md="6"
:lg="4"
:xl="3"
v-for="recipe in recipes.slice(0, cardLimit)"
:key="recipe.name"
>
<RecipeCard
:name="recipe.name"
:description="recipe.description"
:slug="recipe.slug"
:rating="recipe.rating"
:image="recipe.image"
/>
</v-col>
</v-row>
<v-row v-else dense>
<v-col
cols="12"
sm="12"
md="6"
lg="4"
xl="3"
v-for="recipe in recipes.slice(0, cardLimit)"
:key="recipe.name"
>
<MobileRecipeCard
:name="recipe.name"
:description="recipe.description"
:slug="recipe.slug"
:rating="recipe.rating"
:image="recipe.image"
/>
</v-col>
</v-row>
</div>
</div>
</template>
@ -84,10 +91,12 @@ export default {
sortable: {
default: false,
},
title: String,
title: {
default: null,
},
recipes: Array,
cardLimit: {
default: 6,
default: 999,
},
},
computed: {

View file

@ -33,6 +33,7 @@
</template>
<script>
import api from "@/api";
export default {
data() {
return {
@ -62,8 +63,7 @@ export default {
allCategories() {
this.buildSidebar();
},
showSidebar() {
},
showSidebar() {},
},
mounted() {
this.buildSidebar();
@ -75,10 +75,12 @@ export default {
async buildSidebar() {
this.links = [];
this.links.push(...this.baseLinks);
this.allCategories.forEach(async element => {
const pages = await api.siteSettings.getPages();
pages.sort((a, b) => a.position - b.position);
pages.forEach(async element => {
this.links.push({
title: element.name,
to: `/recipes/${element.slug}`,
to: `/pages/${element.slug}`,
icon: "mdi-tag",
});
});

View file

@ -14,15 +14,19 @@
<v-divider></v-divider>
<HomePageSettings />
<v-divider></v-divider>
<CustomPageCreator />
<v-divider></v-divider>
</v-card>
</template>
<script>
import HomePageSettings from "@/components/Admin/General/HomePageSettings";
import CustomPageCreator from "@/components/Admin/General/CustomPageCreator";
export default {
components: {
HomePageSettings,
CustomPageCreator,
},
data() {
return {
@ -46,6 +50,7 @@ export default {
removeCategory(index) {
this.value.categories.splice(index, 1);
},
},
};
</script>

View file

@ -2,10 +2,10 @@
<v-container>
<CategorySidebar />
<CardSection
v-if="showRecent"
v-if="siteSettings.showRecent"
:title="$t('page.recent')"
:recipes="recentRecipes"
:card-limit="showLimit"
:card-limit="siteSettings.cardsPerSection"
/>
<CardSection
:sortable="true"
@ -13,7 +13,7 @@
:key="section.name + section.position"
:title="section.name"
:recipes="section.recipes"
:card-limit="showLimit"
:card-limit="siteSettings.cardsPerSection"
@sort="sortAZ(index)"
@sort-recent="sortRecent(index)"
/>
@ -35,14 +35,9 @@ export default {
};
},
computed: {
showRecent() {
return this.$store.getters.getShowRecent;
},
showLimit() {
return this.$store.getters.getShowLimit;
},
homeCategories() {
return this.$store.getters.getHomeCategories;
siteSettings() {
console.log(this.$store.getters.getSiteSettings);
return this.$store.getters.getSiteSettings;
},
recentRecipes() {
let recipes = this.$store.getters.getRecentRecipes;
@ -55,9 +50,11 @@ export default {
},
methods: {
async buildPage() {
this.homeCategories.forEach(async element => {
await this.$store.dispatch("requestSiteSettings");
this.siteSettings.categories.forEach(async element => {
let recipes = await this.getRecipeByCategory(element.slug);
recipes.position = element.position;
if (recipes.recipes.length < 0 ) recipes.recipes = []
console.log(recipes)
this.recipeByCategory.push(recipes);
});
},

View file

@ -0,0 +1,95 @@
<template>
<v-container>
<CategorySidebar />
<v-card flat height="100%">
<v-app-bar flat>
<v-spacer></v-spacer>
<v-card-title class="text-center justify-center py-3 ">
{{ title.toUpperCase() }}
</v-card-title>
<v-spacer></v-spacer>
</v-app-bar>
<div v-if="render">
<v-tabs v-model="tab" background-color="transparent" grow>
<v-tab v-for="item in categories" :key="item.slug">
{{ item.name }}
</v-tab>
</v-tabs>
<v-tabs-items v-model="tab">
<v-tab-item
v-for="(item, index) in categories"
:key="item.slug + index"
>
<CardSection class="mb-5 mx-1" :recipes="filterRecipe(item.slug)" />
</v-tab-item>
</v-tabs-items>
</div>
</v-card>
</v-container>
</template>
<script>
import CardSection from "@/components/UI/CardSection";
import CategorySidebar from "@/components/UI/CategorySidebar";
import api from "@/api";
export default {
components: {
CardSection,
CategorySidebar,
},
data() {
return {
title: "",
tab: null,
render: false,
recipeStore: [],
categories: [],
};
},
computed: {
pageSlug() {
return this.$route.params.customPage;
},
},
watch: {
pageSlug() {
this.buildPage();
},
},
async mounted() {
await this.buildPage();
this.render = true;
},
methods: {
async buildPage() {
const page = await api.siteSettings.getPage(this.pageSlug);
this.title = page.name;
this.categories = page.categories;
page.categories.forEach(async element => {
let categoryRecipes = await this.getRecipeByCategory(element.slug);
this.recipeStore.push(categoryRecipes);
});
},
async getRecipeByCategory(category) {
return await api.categories.get_recipes_in_category(category);
},
filterRecipe(slug) {
const storeCategory = this.recipeStore.find(
element => element.slug === slug
);
return storeCategory ? storeCategory.recipes : [];
},
},
};
</script>
<style>
.header-background {
background-color: #121619;
}
</style>

View file

@ -3,6 +3,7 @@ import Page404 from "@/pages/404Page";
import SearchPage from "@/pages/SearchPage";
import ViewRecipe from "@/pages/Recipe/ViewRecipe";
import NewRecipe from "@/pages/Recipe/NewRecipe";
import CustomPage from "@/pages/Recipes/CustomPage";
import AllRecipes from "@/pages/Recipes/AllRecipes";
import CategoryPage from "@/pages/Recipes/CategoryPage";
import Planner from "@/pages/MealPlan/Planner";
@ -31,6 +32,7 @@ export const routes = [
{ path: "/debug", component: Debug },
{ path: "/search", component: SearchPage },
{ path: "/recipes/all", component: AllRecipes },
{ path: "/pages/:customPage", component: CustomPage },
{ path: "/recipes/:category", component: CategoryPage },
{ path: "/recipe/:recipe", component: ViewRecipe },
{ path: "/new/", component: NewRecipe },

View file

@ -4,7 +4,6 @@ import api from "@/api";
import createPersistedState from "vuex-persistedstate";
import userSettings from "./modules/userSettings";
import language from "./modules/language";
import homePage from "./modules/homePage";
import siteSettings from "./modules/siteSettings";
import groups from "./modules/groups";
@ -13,13 +12,12 @@ Vue.use(Vuex);
const store = new Vuex.Store({
plugins: [
createPersistedState({
paths: ["userSettings", "language", "homePage", "SideSettings"],
paths: ["userSettings", "language", "SideSettings"],
}),
],
modules: {
userSettings,
language,
homePage,
siteSettings,
groups,
},
@ -28,6 +26,7 @@ const store = new Vuex.Store({
recentRecipes: [],
allRecipes: [],
mealPlanCategories: [],
allCategories: [],
},
mutations: {
@ -38,6 +37,9 @@ const store = new Vuex.Store({
setMealPlanCategories(state, payload) {
state.mealPlanCategories = payload;
},
setAllCategories(state, payload) {
state.allCategories = payload;
},
},
actions: {
@ -54,11 +56,16 @@ const store = new Vuex.Store({
this.commit("setRecentRecipes", payload);
},
async requestCategories({ commit }) {
const categories = await api.categories.getAll();
commit("setAllCategories", categories);
},
},
getters: {
getRecentRecipes: state => state.recentRecipes,
getMealPlanCategories: state => state.mealPlanCategories,
getAllCategories: state => state.allCategories,
},
});

View file

@ -11,7 +11,7 @@ const state = {
const mutations = {
setSettings(state, payload) {
state.settings = payload;
state.siteSettings = payload;
},
};

View file

@ -4,7 +4,8 @@ from fastapi.logger import logger
# import utils.startup as startup
from mealie.core.config import APP_VERSION, PORT, docs_url, redoc_url
from mealie.routes import backup_routes, debug_routes, migration_routes, setting_routes, theme_routes
from mealie.routes import backup_routes, debug_routes, migration_routes, theme_routes
from mealie.routes.site_settings import all_settings
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
@ -36,7 +37,7 @@ def api_routers():
# Meal Routes
app.include_router(mealplans.router)
# Settings Routes
app.include_router(setting_routes.router)
app.include_router(all_settings.router)
app.include_router(theme_routes.router)
# Backups/Imports Routes
app.include_router(backup_routes.router)

View file

@ -1,4 +1,5 @@
import os
import secrets
from pathlib import Path
import dotenv
@ -17,12 +18,10 @@ def ensure_dirs():
# Register ENV
ENV = CWD.joinpath(".env") #! I'm Broken Fix Me!
dotenv.load_dotenv(ENV)
PRODUCTION = os.environ.get("ENV")
SECRET = "test-secret-shhh"
# General
PRODUCTION = os.environ.get("ENV")
PORT = int(os.getenv("mealie_port", 9000))
API = os.getenv("api_docs", True)
@ -72,7 +71,7 @@ LOGGER_FILE = DATA_DIR.joinpath("mealie.log")
# DATABASE ENV
SQLITE_FILE = None
DATABASE_TYPE = os.getenv("db_type", "sqlite")
DATABASE_TYPE = os.getenv("DB_TYPE", "sqlite")
if DATABASE_TYPE == "sqlite":
USE_SQL = True
SQLITE_FILE = SQLITE_DIR.joinpath(f"mealie_{DB_VERSION}.sqlite")
@ -80,9 +79,28 @@ if DATABASE_TYPE == "sqlite":
else:
raise Exception("Unable to determine database type. Acceptible options are 'sqlite' ")
def determine_secrets() -> str:
if not PRODUCTION:
return "shh-secret-test-key"
secrets_file = DATA_DIR.joinpath(".secret")
if secrets_file.is_file():
with open(secrets_file, "r") as f:
return f.read()
else:
with open(secrets_file, "w") as f:
f.write(secrets.token_hex(32))
SECRET = "determine_secrets()"
# Mongo Database
DEFAULT_GROUP = os.getenv("DEFAULT_GROUP", "Home")
DEFAULT_PASSWORD = os.getenv("DEFAULT_PASSWORD", "MyPassword")
# Database
MEALIE_DB_NAME = os.getenv("mealie_db_name", "mealie")
DEFAULT_GROUP = os.getenv("default_group", "Home")
DB_USERNAME = os.getenv("db_username", "root")
DB_PASSWORD = os.getenv("db_password", "example")
DB_HOST = os.getenv("db_host", "mongo")

View file

@ -17,8 +17,7 @@ def create_access_token(data: dict(), expires_delta: timedelta = None) -> str:
else:
expire = datetime.utcnow() + timedelta(minutes=120)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET, algorithm=ALGORITHM)
return encoded_jwt
return jwt.encode(to_encode, SECRET, algorithm=ALGORITHM)
def authenticate_user(session, email: str, password: str) -> UserInDB:

View file

@ -2,14 +2,14 @@ from mealie.db.db_base import BaseDocument
from mealie.db.models.group import Group
from mealie.db.models.mealplan import MealPlanModel
from mealie.db.models.recipe.recipe import Category, RecipeModel, Tag
from mealie.db.models.settings import SiteSettings
from mealie.db.models.settings import CustomPage, 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.settings import CustomPageOut, SiteSettings as SiteSettingsSchema
from mealie.schema.sign_up import SignUpOut
from mealie.schema.theme import SiteTheme
from mealie.schema.user import GroupInDB, UserInDB
@ -118,6 +118,13 @@ class _SignUps(BaseDocument):
self.orm_mode = True
self.schema = SignUpOut
class _CustomPages(BaseDocument):
def __init__(self) -> None:
self.primary_key = "id"
self.sql_model = CustomPage
self.orm_mode = True
self.schema = CustomPageOut
class Database:
def __init__(self) -> None:
@ -130,6 +137,7 @@ class Database:
self.users = _Users()
self.sign_ups = _SignUps()
self.groups = _Groups()
self.custom_pages = _CustomPages()
db = Database()

View file

@ -40,9 +40,12 @@ class BaseDocument:
Returns:
list[SqlAlchemyBase]: Returns a list of ORM objects
"""
results = session.query(self.sql_model).options(load_only(*fields)).limit(limit).all()
return results
return (
session.query(self.sql_model)
.options(load_only(*fields))
.limit(limit)
.all()
)
def get_all_primary_keys(self, session: Session) -> List[str]:
"""Queries the database of the selected model and returns a list
@ -69,12 +72,14 @@ class BaseDocument:
Returns:
Union[Session, SqlAlchemyBase]: Will return both the session and found model
"""
if match_key == None:
if match_key is None:
match_key = self.primary_key
result = session.query(self.sql_model).filter_by(**{match_key: match_value}).one()
return result
return (
session.query(self.sql_model)
.filter_by(**{match_key: match_value})
.one()
)
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
@ -89,7 +94,7 @@ class BaseDocument:
Returns:
dict or list[dict]:
"""
if match_key == None:
if match_key is None:
match_key = self.primary_key
result = session.query(self.sql_model).filter_by(**{match_key: match_value}).limit(limit).all()
@ -118,8 +123,7 @@ class BaseDocument:
if self.orm_mode:
return self.schema.from_orm(new_document)
return_data = new_document.dict()
return return_data
return new_document.dict()
def update(self, session: Session, match_value: str, new_data: str) -> BaseModel:
"""Update a database entry.

View file

@ -1,5 +1,5 @@
from fastapi.logger import logger
from mealie.core.config import DEFAULT_GROUP
from mealie.core.config import DEFAULT_GROUP, DEFAULT_PASSWORD
from mealie.core.security import get_password_hash
from mealie.db.database import db
from mealie.db.db_setup import create_session, sql_exists
@ -40,14 +40,13 @@ def default_group_init(session: Session):
default_group = {"name": DEFAULT_GROUP}
logger.info("Generating Default Group")
db.groups.create(session, default_group)
pass
def default_user_init(session: Session):
default_user = {
"full_name": "Change Me",
"email": "changeme@email.com",
"password": get_password_hash("MyPassword"),
"password": get_password_hash(DEFAULT_PASSWORD),
"group": DEFAULT_GROUP,
"admin": True,
}
@ -62,4 +61,4 @@ if __name__ == "__main__":
exit()
else:
print("Database Doesn't Exists, Initializing...")
init_db()
init_db()

View file

@ -57,12 +57,10 @@ class Group(SqlAlchemyBase, BaseMixins):
@staticmethod
def get_ref(session: Session, name: str):
item = session.query(Group).filter(Group.name == name).one()
if item:
return item
else:
return session.query(Group).filter(Group.id == 1).one()
item = session.query(Group).filter(Group.name == name).one_or_none()
if item is None:
item = session.query(Group).filter(Group.id == 1).one()
return item
@staticmethod
def create_if_not_exist(session, name: str = DEFAULT_GROUP):

View file

@ -26,6 +26,13 @@ recipes2categories = sa.Table(
sa.Column("category_slug", sa.String, sa.ForeignKey("categories.slug")),
)
custom_pages2categories = sa.Table(
"custom_pages2categories",
SqlAlchemyBase.metadata,
sa.Column("custom_page_id", sa.Integer, sa.ForeignKey("custom_pages.id")),
sa.Column("category_slug", sa.String, sa.ForeignKey("categories.slug")),
)
class Category(SqlAlchemyBase):
__tablename__ = "categories"
@ -36,7 +43,7 @@ class Category(SqlAlchemyBase):
@validates("name")
def validate_name(self, key, name):
assert not name == ""
assert name != ""
return name
def __init__(self, name) -> None:

View file

@ -1,7 +1,7 @@
import sqlalchemy as sa
import sqlalchemy.orm as orm
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
from mealie.db.models.recipe.category import Category, site_settings2categories
from mealie.db.models.recipe.category import Category, custom_pages2categories, site_settings2categories
from sqlalchemy.orm import Session
@ -29,7 +29,29 @@ class SiteSettings(SqlAlchemyBase, BaseMixins):
self.language = language
self.cards_per_section = cards_per_section
self.show_recent = show_recent
self.categories = [Category.get_ref(session=session, name=cat.get("slug")) for cat in categories]
self.categories = [Category.get_ref(session=session, slug=cat.get("slug")) for cat in categories]
def update(self, *args, **kwarg):
self.__init__(*args, **kwarg)
class CustomPage(SqlAlchemyBase, BaseMixins):
__tablename__ = "custom_pages"
id = sa.Column(sa.Integer, primary_key=True)
position = sa.Column(sa.Integer, nullable=False)
name = sa.Column(sa.String, nullable=False)
slug = sa.Column(sa.String, nullable=False)
categories = orm.relationship(
"Category",
secondary=custom_pages2categories,
single_parent=True,
)
def __init__(self, session=None, name=None, slug=None, position=0, categories=[], *args, **kwargs) -> None:
self.name = name
self.slug = slug
self.position = position
self.categories = [Category.get_ref(session=session, slug=cat.get("slug")) for cat in categories]
def update(self, *args, **kwarg):
self.__init__(*args, **kwarg)

View file

@ -18,14 +18,11 @@ router = APIRouter(prefix="/api/backups", tags=["Backups"])
def available_imports():
"""Returns a list of avaiable .zip files for import into Mealie."""
imports = []
templates = []
for archive in BACKUP_DIR.glob("*.zip"):
backup = LocalBackup(name=archive.name, date=archive.stat().st_ctime)
imports.append(backup)
for template in TEMPLATE_DIR.glob("*.*"):
templates.append(template.name)
templates = [template.name for template in TEMPLATE_DIR.glob("*.*")]
imports.sort(key=operator.attrgetter("date"), reverse=True)
return Imports(imports=imports, templates=templates)
@ -40,6 +37,7 @@ def export_database(data: BackupJob, session: Session = Depends(generate_session
templates=data.templates,
export_recipes=data.options.recipes,
export_settings=data.options.settings,
export_pages=data.options.pages,
export_themes=data.options.themes,
export_users=data.options.users,
export_groups=data.options.groups,
@ -82,11 +80,12 @@ 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. """
imported = imports.import_database(
return imports.import_database(
session=session,
archive=import_data.name,
import_recipes=import_data.recipes,
import_settings=import_data.settings,
import_pages=import_data.pages,
import_themes=import_data.themes,
import_users=import_data.users,
import_groups=import_data.groups,
@ -94,8 +93,6 @@ def import_database(file_name: str, import_data: ImportJob, session: Session = D
rebase=import_data.rebase,
)
return imported
@router.delete("/{file_name}/delete", status_code=200)
def delete_backup(file_name: str):

View file

@ -0,0 +1,7 @@
from fastapi import APIRouter
from mealie.routes.site_settings import custom_pages, site_settings
router = APIRouter()
router.include_router(custom_pages.router)
router.include_router(site_settings.router)

View file

@ -0,0 +1,75 @@
from typing import Union
from fastapi import APIRouter, Depends
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.schema.settings import CustomPageBase, CustomPageOut
from mealie.schema.snackbar import SnackResponse
from mealie.schema.user import UserInDB
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/site-settings/custom-pages", tags=["Settings"])
@router.get("")
def get_custom_pages(session: Session = Depends(generate_session)):
""" Returns the sites custom pages """
return db.custom_pages.get_all(session)
@router.post("")
async def create_new_page(
new_page: CustomPageBase,
session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
):
""" Creates a new Custom Page """
db.custom_pages.create(session, new_page.dict())
return SnackResponse.success("New Page Created")
@router.put("")
async def update_multiple_pages(
pages: list[CustomPageOut],
session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
):
""" Update multiple custom pages """
for page in pages:
db.custom_pages.update(session, page.id, page.dict())
return SnackResponse.success("Pages Updated")
@router.get("/{id}")
async def get_single_page(
id: Union[int, str],
session: Session = Depends(generate_session),
):
""" Removes a custom page from the database """
if isinstance(id, int):
return db.custom_pages.get(session, id)
elif isinstance(id, str):
return db.custom_pages.get(session, id, "slug")
@router.put("/{id}")
async def update_single_age(data: CustomPageOut, id: int, session: Session = Depends(generate_session)):
""" Removes a custom page from the database """
return db.custom_pages.update(session, id, data.dict())
@router.delete("/{id}")
async def delete_custom_page(
id: int,
session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
):
""" Removes a custom page from the database """
db.custom_pages.delete(session, id)
return

View file

@ -16,9 +16,7 @@ router = APIRouter(prefix="/api/site-settings", tags=["Settings"])
def get_main_settings(session: Session = Depends(generate_session)):
""" Returns basic site settings """
data = db.settings.get(session, 1)
return data
return db.settings.get(session, 1)
@router.put("")

View file

@ -7,6 +7,7 @@ from pydantic import BaseModel
class BackupOptions(BaseModel):
recipes: bool = True
settings: bool = True
pages: bool = True
themes: bool = True
groups: bool = True
users: bool = True

View file

@ -101,11 +101,10 @@ class Recipe(BaseModel):
name: str = values["name"]
calc_slug: str = slugify(name)
if slug == calc_slug:
return slug
else:
if slug != calc_slug:
slug = calc_slug
return slug
return slug
class AllRecipeRequest(BaseModel):

View file

@ -27,3 +27,7 @@ class GroupImport(ImportBase):
class UserImport(ImportBase):
pass
class CustomPageImport(ImportBase):
pass

View file

@ -1,8 +1,9 @@
from typing import Optional
from fastapi_camelcase import CamelModel
from mealie.schema.category import CategoryBase
from pydantic import validator
from slugify import slugify
class SiteSettings(CamelModel):
@ -25,3 +26,30 @@ class SiteSettings(CamelModel):
],
}
}
class CustomPageBase(CamelModel):
name: str
slug: Optional[str]
position: int
categories: list[CategoryBase] = []
class Config:
orm_mode = True
@validator("slug", always=True, pre=True)
def validate_slug(slug: str, values):
name: str = values["name"]
calc_slug: str = slugify(name)
if slug != calc_slug:
slug = calc_slug
return slug
class CustomPageOut(CustomPageBase):
id: int
class Config:
orm_mode = True

View file

@ -4,11 +4,11 @@ from datetime import datetime
from pathlib import Path
from typing import Union
from fastapi.logger import logger
from jinja2 import Template
from mealie.core.config import BACKUP_DIR, IMG_DIR, TEMP_DIR, TEMPLATE_DIR
from mealie.db.database import db
from mealie.db.db_setup import create_session
from fastapi.logger import logger
from jinja2 import Template
from pydantic.main import BaseModel
@ -101,6 +101,7 @@ def backup_all(
templates=None,
export_recipes=True,
export_settings=True,
export_pages=True,
export_themes=True,
export_users=True,
export_groups=True,
@ -125,6 +126,10 @@ def backup_all(
all_settings = db.settings.get_all(session)
db_export.export_items(all_settings, "settings")
if export_pages:
all_pages = db.custom_pages.get_all(session)
db_export.export_items(all_pages, "pages")
if export_themes:
all_themes = db.themes.get_all(session)
db_export.export_items(all_themes, "themes")
@ -136,10 +141,7 @@ def auto_backup_job():
for backup in BACKUP_DIR.glob("Auto*.zip"):
backup.unlink()
templates = []
for template in TEMPLATE_DIR.iterdir():
templates.append(template)
templates = [template for template in TEMPLATE_DIR.iterdir()]
session = create_session()
backup_all(session=session, tag="Auto", templates=templates)
logger.info("Auto Backup Called")

View file

@ -7,8 +7,8 @@ from typing import Callable, List
from mealie.core.config import BACKUP_DIR, IMG_DIR, TEMP_DIR
from mealie.db.database import db
from mealie.schema.recipe import Recipe
from mealie.schema.restore import GroupImport, RecipeImport, SettingsImport, ThemeImport, UserImport
from mealie.schema.settings import SiteSettings
from mealie.schema.restore import CustomPageImport, GroupImport, RecipeImport, SettingsImport, ThemeImport, UserImport
from mealie.schema.settings import CustomPageOut, SiteSettings
from mealie.schema.theme import SiteTheme
from mealie.schema.user import UpdateGroup, UserInDB
from pydantic.main import BaseModel
@ -42,7 +42,6 @@ class ImportDatabase:
with zipfile.ZipFile(self.archive, "r") as zip_ref:
zip_ref.extractall(self.import_dir)
pass
else:
raise Exception("Import file does not exist")
@ -95,9 +94,7 @@ class ImportDatabase:
try:
if "" in recipe_dict["categories"]:
recipe_dict["categories"] = [
cat for cat in recipe_dict["categories"] if cat != ""
]
recipe_dict["categories"] = [cat for cat in recipe_dict["categories"] if cat != ""]
except:
pass
@ -149,6 +146,19 @@ class ImportDatabase:
return [import_status]
def import_pages(self):
pages_file = self.import_dir.joinpath("pages", "pages.json")
pages = ImportDatabase.read_models_file(pages_file, CustomPageOut)
page_imports = []
for page in pages:
import_stats = self.import_model(
db_table=db.custom_pages, model=page, return_model=CustomPageImport, name_attr="name", search_key="slug"
)
page_imports.append(import_stats)
return page_imports
def import_groups(self):
groups_file = self.import_dir.joinpath("groups", "groups.json")
groups = ImportDatabase.read_models_file(groups_file, UpdateGroup)
@ -273,6 +283,7 @@ def import_database(
archive,
import_recipes=True,
import_settings=True,
import_pages=True,
import_themes=True,
import_users=True,
import_groups=True,
@ -293,6 +304,10 @@ def import_database(
if import_themes:
theme_report = import_session.import_themes()
if import_pages:
print("IMport Pages")
page_report = import_session.import_pages()
group_report = []
if import_groups:
group_report = import_session.import_groups()
@ -307,6 +322,7 @@ def import_database(
"recipeImports": recipe_report,
"settingsImports": settings_report,
"themeImports": theme_report,
"pageImports": page_report,
"groupImports": group_report,
"userImports": user_report,
}

View file

@ -3,7 +3,7 @@ import json
import requests
from fastapi.testclient import TestClient
from mealie.app import app
from mealie.core.config import SQLITE_DIR
from mealie.core.config import DEFAULT_PASSWORD, SQLITE_DIR
from mealie.db.db_setup import generate_session, sql_global_init
from mealie.db.init_db import init_db
from pytest import fixture
@ -44,7 +44,7 @@ def test_image():
@fixture(scope="session")
def token(api_client: requests):
form_data = {"username": "changeme@email.com", "password": "MyPassword"}
form_data = {"username": "changeme@email.com", "password": DEFAULT_PASSWORD}
response = api_client.post(TOKEN_URL, form_data)
token = json.loads(response.text).get("access_token")