mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 14:33:33 -07:00
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:
parent
894d6b9c9b
commit
9e23587c0d
52 changed files with 807 additions and 196 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -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/*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
8
docs/docs/site-administration/building-pages.md
Normal file
8
docs/docs/site-administration/building-pages.md
Normal 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.
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
|
|
|
@ -10,7 +10,7 @@ const categoryURLs = {
|
|||
};
|
||||
|
||||
export default {
|
||||
async get_all() {
|
||||
async getAll() {
|
||||
let response = await apiReq.get(categoryURLs.get_all);
|
||||
return response.data;
|
||||
},
|
||||
|
|
|
@ -16,7 +16,6 @@ const mealPlanURLs = {
|
|||
|
||||
export default {
|
||||
async create(postBody) {
|
||||
console.log(postBody);
|
||||
let response = await apiReq.post(mealPlanURLs.create, postBody);
|
||||
return response;
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
|
|
98
frontend/src/components/Admin/General/CreatePageDialog.vue
Normal file
98
frontend/src/components/Admin/General/CreatePageDialog.vue
Normal 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>
|
129
frontend/src/components/Admin/General/CustomPageCreator.vue
Normal file
129
frontend/src/components/Admin/General/CustomPageCreator.vue
Normal 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>
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
50
frontend/src/components/FormHelpers/CategorySelector.vue
Normal file
50
frontend/src/components/FormHelpers/CategorySelector.vue
Normal 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>
|
|
@ -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">
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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",
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
|
|
95
frontend/src/pages/Recipes/CustomPage.vue
Normal file
95
frontend/src/pages/Recipes/CustomPage.vue
Normal 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>
|
|
@ -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 },
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ const state = {
|
|||
|
||||
const mutations = {
|
||||
setSettings(state, payload) {
|
||||
state.settings = payload;
|
||||
state.siteSettings = payload;
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
7
mealie/routes/site_settings/all_settings.py
Normal file
7
mealie/routes/site_settings/all_settings.py
Normal 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)
|
75
mealie/routes/site_settings/custom_pages.py
Normal file
75
mealie/routes/site_settings/custom_pages.py
Normal 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
|
|
@ -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("")
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -27,3 +27,7 @@ class GroupImport(ImportBase):
|
|||
|
||||
class UserImport(ImportBase):
|
||||
pass
|
||||
|
||||
|
||||
class CustomPageImport(ImportBase):
|
||||
pass
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue