backend-events + code-cleanup (#395)

* additional server events

* sort 'recent recipes' by updated

* remove duplicate code

* fixes #396

* set color

* consolidate tag/category pages

* set colors

* list unorganized recipes

* cleanup old code

* remove flash message, switch to global snackbar

* cancel to close

* cleanup

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden 2021-05-07 14:33:20 -08:00 committed by GitHub
parent 96919319b1
commit 466997febc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1604 additions and 686 deletions

View file

@ -59,7 +59,7 @@
- All images are now converted to .webp for better compression - All images are now converted to .webp for better compression
### General ### General
- New 'Dark' Theme Packages with Mealie - New 'Dark' Color Theme Packaged with Mealie
- Updated Recipe Card Sections Toolbar - Updated Recipe Card Sections Toolbar
- New Sort Options (They work this time!) - New Sort Options (They work this time!)
- Alphabetical - Alphabetical
@ -82,6 +82,7 @@
- Improved styling for search bar in desktop - Improved styling for search bar in desktop
- Improved search layout on mobile - Improved search layout on mobile
- Profile image now shown on all sidebars - Profile image now shown on all sidebars
- Switched from Flash Messages to Snackbar (Removed dependency
### Behind the Scenes ### Behind the Scenes
- Black and Flake8 now run as CI/CD checks - Black and Flake8 now run as CI/CD checks

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,6 @@
}, },
"dependencies": { "dependencies": {
"@adapttive/vue-markdown": "^4.0.1", "@adapttive/vue-markdown": "^4.0.1",
"@smartweb/vue-flash-message": "^0.6.10",
"axios": "^0.21.1", "axios": "^0.21.1",
"core-js": "^3.9.1", "core-js": "^3.9.1",
"fast-levenshtein": "^3.0.0", "fast-levenshtein": "^3.0.0",
@ -31,7 +30,7 @@
"@mdi/font": "^5.9.55", "@mdi/font": "^5.9.55",
"@vue/cli-plugin-babel": "^4.5.11", "@vue/cli-plugin-babel": "^4.5.11",
"@vue/cli-plugin-eslint": "^4.5.11", "@vue/cli-plugin-eslint": "^4.5.11",
"@vue/cli-service": "^4.1.1", "@vue/cli-service": "^4.5.12",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"eslint": "^6.7.2", "eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2", "eslint-plugin-vue": "^6.2.2",

View file

@ -6,14 +6,15 @@
<v-banner v-if="demo" sticky <v-banner v-if="demo" sticky
><div class="text-center"><b> This is a Demo</b> | Username: changeme@email.com | Password: demo</div></v-banner ><div class="text-center"><b> This is a Demo</b> | Username: changeme@email.com | Password: demo</div></v-banner
> >
<GlobalSnackbar />
<router-view></router-view> <router-view></router-view>
</v-main> </v-main>
<FlashMessage :position="'right bottom'"></FlashMessage>
</v-app> </v-app>
</template> </template>
<script> <script>
import TheAppBar from "@/components/UI/TheAppBar"; import TheAppBar from "@/components/UI/TheAppBar";
import GlobalSnackbar from "@/components/UI/GlobalSnackbar";
import Vuetify from "./plugins/vuetify"; import Vuetify from "./plugins/vuetify";
import { user } from "@/mixins/user"; import { user } from "@/mixins/user";
@ -22,6 +23,7 @@ export default {
components: { components: {
TheAppBar, TheAppBar,
GlobalSnackbar,
}, },
mixins: [user], mixins: [user],
@ -71,38 +73,6 @@ export default {
</script> </script>
<style> <style>
.notify-info-color {
border: 1px, solid, var(--v-info-base) !important;
border-left: 3px, solid, var(--v-info-base) !important;
background-color: var(--v-info-base) !important;
}
.notify-warning-color {
border: 1px, solid, var(--v-warning-base) !important;
border-left: 3px, solid, var(--v-warning-base) !important;
background-color: var(--v-warning-base) !important;
}
.notify-error-color {
border: 1px, solid, var(--v-error-base) !important;
border-left: 3px, solid, var(--v-error-base) !important;
background-color: var(--v-error-base) !important;
}
.notify-success-color {
border: 1px, solid, var(--v-success-base) !important;
border-left: 3px, solid, var(--v-success-base) !important;
background-color: var(--v-success-base) !important;
}
.notify-base {
color: white !important;
/* min-height: 50px; */
margin-right: 60px;
margin-bottom: -5px;
opacity: 0.9 !important;
}
*::-webkit-scrollbar { *::-webkit-scrollbar {
width: 0.25rem; width: 0.25rem;
} }

View file

@ -17,6 +17,8 @@ const recipeURLs = {
createAsset: slug => `${prefix}${slug}/assets`, createAsset: slug => `${prefix}${slug}/assets`,
recipeImage: slug => `${prefix}${slug}/image`, recipeImage: slug => `${prefix}${slug}/image`,
updateImage: slug => `${prefix}${slug}/image`, updateImage: slug => `${prefix}${slug}/image`,
untagged: prefix + "summary/untagged",
uncategorized: prefix + "summary/uncategorized ",
}; };
export const recipeAPI = { export const recipeAPI = {
@ -134,6 +136,16 @@ export const recipeAPI = {
return response.data; return response.data;
}, },
async allUntagged() {
const response = await apiReq.get(recipeURLs.untagged);
return response.data;
},
async allUnategorized() {
const response = await apiReq.get(recipeURLs.uncategorized);
return response.data;
},
recipeImage(recipeSlug) { recipeImage(recipeSlug) {
return `/api/media/recipes/${recipeSlug}/images/original.webp`; return `/api/media/recipes/${recipeSlug}/images/original.webp`;
}, },

View file

@ -0,0 +1,30 @@
<template>
<div class="text-center ma-2">
<v-snackbar v-model="snackbar.open" top :color="snackbar.color" timeout="3500">
{{ snackbar.title }}
{{ snackbar.text }}
<template v-slot:action="{ attrs }">
<v-btn text v-bind="attrs" @click="snackbar.open = false">
{{ $t("general.close") }}
</v-btn>
</template>
</v-snackbar>
</div>
</template>
<script>
export default {
data: () => ({}),
computed: {
snackbar: {
set(val) {
this.$store.commit("setSnackbar", val);
},
get() {
return this.$store.getters.getSnackbar;
},
},
},
};
</script>

View file

@ -106,7 +106,7 @@ export default {
this.processing = false; this.processing = false;
}, },
isValidWebUrl(url) { isValidWebUrl(url) {
let regEx = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/gm; let regEx = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,256}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/gm;
return regEx.test(url) ? true : "Must be a Valid URL"; return regEx.test(url) ? true : "Must be a Valid URL";
}, },
}, },

View file

@ -5,11 +5,9 @@ import store from "./store";
import VueRouter from "vue-router"; import VueRouter from "vue-router";
import { router } from "./routes"; import { router } from "./routes";
import i18n from "./i18n"; import i18n from "./i18n";
import FlashMessage from "@smartweb/vue-flash-message";
import "@mdi/font/css/materialdesignicons.css"; import "@mdi/font/css/materialdesignicons.css";
import "typeface-roboto/index.css"; import "typeface-roboto/index.css";
Vue.use(FlashMessage);
Vue.config.productionTip = false; Vue.config.productionTip = false;
Vue.use(VueRouter); Vue.use(VueRouter);

View file

@ -12,7 +12,7 @@
<template v-slot:after-heading> <template v-slot:after-heading>
<div class="ml-auto text-right"> <div class="ml-auto text-right">
<h2 class="body-3 grey--text font-weight-light"> <h2 class="body-3 grey--text font-weight-light">
{{$t('settings.backup-and-exports')}} {{ $t("settings.backup-and-exports") }}
</h2> </h2>
<h3 class="display-2 font-weight-light text--primary"> <h3 class="display-2 font-weight-light text--primary">
@ -23,15 +23,15 @@
<div class="d-flex row py-3 justify-end"> <div class="d-flex row py-3 justify-end">
<TheUploadBtn url="/api/backups/upload" @uploaded="getAvailableBackups"> <TheUploadBtn url="/api/backups/upload" @uploaded="getAvailableBackups">
<template v-slot="{ isSelecting, onButtonClick }"> <template v-slot="{ isSelecting, onButtonClick }">
<v-btn :loading="isSelecting" class="mx-2" small :color="color" @click="onButtonClick"> <v-btn :loading="isSelecting" class="mx-2" small color="info" @click="onButtonClick">
<v-icon left> mdi-cloud-upload </v-icon> {{$t('general.upload')}} <v-icon left> mdi-cloud-upload </v-icon> {{ $t("general.upload") }}
</v-btn> </v-btn>
</template> </template>
</TheUploadBtn> </TheUploadBtn>
<BackupDialog :color="color" /> <BackupDialog :color="color" />
<v-btn :loading="loading" class="mx-2" small :color="color" @click="createBackup"> <v-btn :loading="loading" class="mx-2" small color="success" @click="createBackup">
<v-icon left> mdi-plus </v-icon> {{$t('general.create')}} <v-icon left> mdi-plus </v-icon> {{ $t("general.create") }}
</v-btn> </v-btn>
</div> </div>
<template v-slot:bottom> <template v-slot:bottom>

View file

@ -4,7 +4,7 @@
<template v-slot:after-heading> <template v-slot:after-heading>
<div class="ml-auto text-right"> <div class="ml-auto text-right">
<h2 class="body-3 grey--text font-weight-light"> <h2 class="body-3 grey--text font-weight-light">
{{$t('settings.events')}} {{ $t("settings.events") }}
</h2> </h2>
<h3 class="display-2 font-weight-light text--primary"> <h3 class="display-2 font-weight-light text--primary">
@ -13,8 +13,8 @@
</div> </div>
</template> </template>
<div class="d-flex row py-3 justify-end"> <div class="d-flex row py-3 justify-end">
<v-btn class="mx-2" small :color="color" @click="deleteAll"> <v-btn class="mx-2" small color="error lighten-1" @click="deleteAll">
<v-icon left> mdi-notification-clear-all </v-icon> {{$t('general.clear')}} <v-icon left> mdi-notification-clear-all </v-icon> {{ $t("general.clear") }}
</v-btn> </v-btn>
</div> </div>
<template v-slot:bottom> <template v-slot:bottom>
@ -69,7 +69,7 @@ export default {
color: "primary", color: "primary",
}, },
backup: { backup: {
icon: "mdi-backup-restore", icon: "mdi-database",
color: "primary", color: "primary",
}, },
schedule: { schedule: {
@ -80,9 +80,13 @@ export default {
icon: "mdi-database-import", icon: "mdi-database-import",
color: "primary", color: "primary",
}, },
signup: { user: {
icon: "mdi-account", icon: "mdi-account",
color: "primary", color: "accent",
},
group: {
icon: "mdi-account-group-outline",
color: "accent",
}, },
}, },
}; };

View file

@ -74,7 +74,7 @@
<v-divider></v-divider> <v-divider></v-divider>
<v-card-actions> <v-card-actions>
<v-spacer class="mx-2"></v-spacer> <v-spacer class="mx-2"></v-spacer>
<v-btn class="my-1 mb-n1" :color="color" @click="createTheme"> <v-btn class="my-1 mb-n1" color="success" @click="createTheme">
<v-icon left> mdi-plus </v-icon> {{ $t("general.create") }} <v-icon left> mdi-plus </v-icon> {{ $t("general.create") }}
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>

View file

@ -27,7 +27,7 @@
:top="true" :top="true"
> >
<template v-slot:open="{ open }"> <template v-slot:open="{ open }">
<v-btn color="primary" class="mr-1" small @click="open"> <v-btn color="info" class="mr-1" small @click="open">
<v-icon left>mdi-lock</v-icon> <v-icon left>mdi-lock</v-icon>
Change Password Change Password
</v-btn> </v-btn>
@ -99,7 +99,6 @@
</template> </template>
<script> <script>
import BaseDialog from "@/components/UI/Dialogs/BaseDialog"; import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
import StatCard from "@/components/UI/StatCard"; import StatCard from "@/components/UI/StatCard";
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn"; import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";

View file

@ -1,9 +1,10 @@
<template> <template>
<v-card outlined class="mt-n1"> <v-card outlined class="mt-n1">
<base-dialog <BaseDialog
ref="renameDialog" ref="renameDialog"
title-icon="mdi-tag" title-icon="mdi-tag"
:title="renameTarget.title" :title="renameTarget.title"
:submit-text="$t('general.update')"
modal-width="800" modal-width="800"
@submit="renameFromDialog(renameTarget.slug, renameTarget.newName)" @submit="renameFromDialog(renameTarget.slug, renameTarget.newName)"
> >
@ -32,7 +33,7 @@
:tags="recipe.tags" :tags="recipe.tags"
/> />
</template> </template>
</base-dialog> </BaseDialog>
<div class="d-flex justify-center align-center pa-2 flex-wrap"> <div class="d-flex justify-center align-center pa-2 flex-wrap">
<new-category-tag-dialog ref="newDialog" :tag-dialog="isTags"> <new-category-tag-dialog ref="newDialog" :tag-dialog="isTags">

View file

@ -13,38 +13,45 @@
</v-btn> </v-btn>
</v-btn-toggle> </v-btn-toggle>
<v-spacer v-if="!isMobile"> </v-spacer> <v-spacer v-if="!isMobile"> </v-spacer>
<FuseSearchBar :raw-data="allItems" @results="filterItems" :search="searchString">
<v-text-field
v-model="searchString"
clearable
solo
dense
class="mx-2"
hide-details
single-line
:placeholder="$t('search.search')"
prepend-inner-icon="mdi-magnify"
>
</v-text-field>
</FuseSearchBar>
</div> </div>
<v-card-text>
<CardSection :sortable="true" title="Unorganized" :recipes="shownRecipes" @sort="assignSorted" />
</v-card-text>
</v-card> </v-card>
</template> </template>
<script> <script>
import FuseSearchBar from "@/components/UI/Search/FuseSearchBar"; import { api } from "@/api";
import CardSection from "@/components/UI/CardSection";
export default { export default {
components: { FuseSearchBar }, components: {
// FuseSearchBar,
CardSection,
},
data() { data() {
return { return {
buttonToggle: 0, buttonToggle: 0,
allItems: [], tagRecipes: [],
categoryRecipes: [],
searchString: "", searchString: "",
searchResults: [], sortedResults: [],
}; };
}, },
computed: { computed: {
shownRecipes() {
if (this.sortedResults.length > 0) {
return this.sortedResults;
} else {
switch (this.filter) {
case "category":
return this.categoryRecipes;
case "tag":
return this.tagRecipes;
default:
return [];
}
}
},
isMobile() { isMobile() {
return this.$vuetify.breakpoint.name === "xs"; return this.$vuetify.breakpoint.name === "xs";
}, },
@ -60,10 +67,22 @@ export default {
}, },
}, },
}, },
mounted() {
this.refreshUnorganized();
},
methods: { methods: {
filterItems(val) { filterItems(val) {
this.searchResults = val.map(x => x.item); this.searchResults = val.map(x => x.item);
}, },
async refreshUnorganized() {
this.loading = true;
this.tagRecipes = await api.recipes.allUntagged();
this.categoryRecipes = await api.recipes.allUnategorized();
this.loading = false;
},
assignSorted(val) {
this.sortedResults = val;
},
}, },
}; };
</script> </script>

View file

@ -22,6 +22,12 @@ export default {
currentCategory() { currentCategory() {
return this.$route.params.category; return this.$route.params.category;
}, },
currentTag() {
return this.$route.params.tag;
},
TagOrCategory() {
return this.currentCategory || this.currentTag;
},
shownRecipes() { shownRecipes() {
if (this.sortedResults.length > 0) { if (this.sortedResults.length > 0) {
return this.sortedResults; return this.sortedResults;
@ -31,7 +37,7 @@ export default {
}, },
}, },
watch: { watch: {
async currentCategory() { async TagOrCategory() {
this.sortedResults = []; this.sortedResults = [];
this.getRecipes(); this.getRecipes();
}, },
@ -42,7 +48,14 @@ export default {
}, },
methods: { methods: {
async getRecipes() { async getRecipes() {
let data = await api.categories.getRecipesInCategory(this.currentCategory); if (!this.TagOrCategory === null) return;
let data = {};
if (this.currentCategory) {
data = await api.categories.getRecipesInCategory(this.TagOrCategory);
} else {
data = await api.tags.getRecipesInTag(this.TagOrCategory);
}
this.title = data.name; this.title = data.name;
this.recipes = data.recipes; this.recipes = data.recipes;
}, },

View file

@ -69,10 +69,7 @@ export default {
methods: { methods: {
async buildPage() { async buildPage() {
this.page = await api.siteSettings.getPage(this.pageSlug); this.page = await api.siteSettings.getPage(this.pageSlug);
}, this.tab = this.page.categories[0];
filterRecipe(slug) {
const storeCategory = this.recipeStore.find(element => element.slug === slug);
return storeCategory ? storeCategory.recipes : [];
}, },
sortRecipes(sortedRecipes, destKey) { sortRecipes(sortedRecipes, destKey) {
this.page.categories[destKey].recipes = sortedRecipes; this.page.categories[destKey].recipes = sortedRecipes;

View file

@ -1,57 +0,0 @@
<template>
<v-container>
<CardSection :sortable="true" :title="title" :recipes="shownRecipes" @sort="assignSorted" />
</v-container>
</template>
<script>
import { api } from "@/api";
import CardSection from "@/components/UI/CardSection";
export default {
components: {
CardSection,
},
data() {
return {
title: "",
recipes: [],
sortedResults: [],
};
},
computed: {
currentTag() {
return this.$route.params.tag;
},
shownRecipes() {
if (this.sortedResults.length > 0) {
return this.sortedResults;
} else {
return this.recipes;
}
},
},
watch: {
async currentTag() {
this.getRecipes();
this.sortedResults = [];
},
},
mounted() {
this.getRecipes();
this.sortedResults = [];
},
methods: {
async getRecipes() {
let data = await api.tags.getRecipesInTag(this.currentTag);
this.title = data.name;
this.recipes = data.recipes;
},
assignSorted(val) {
console.log(val);
this.sortedResults = val.slice();
},
},
};
</script>
<style></style>

View file

@ -2,15 +2,14 @@ import ViewRecipe from "@/pages/Recipe/ViewRecipe";
import NewRecipe from "@/pages/Recipe/NewRecipe"; import NewRecipe from "@/pages/Recipe/NewRecipe";
import CustomPage from "@/pages/Recipes/CustomPage"; import CustomPage from "@/pages/Recipes/CustomPage";
import AllRecipes from "@/pages/Recipes/AllRecipes"; import AllRecipes from "@/pages/Recipes/AllRecipes";
import CategoryPage from "@/pages/Recipes/CategoryPage"; import CategoryTagPage from "@/pages/Recipes/CategoryTagPage";
import TagPage from "@/pages/Recipes/TagPage";
import { api } from "@/api"; import { api } from "@/api";
export const recipeRoutes = [ export const recipeRoutes = [
// Recipes // Recipes
{ path: "/recipes/all", component: AllRecipes }, { path: "/recipes/all", component: AllRecipes },
{ path: "/recipes/tag/:tag", component: TagPage }, { path: "/recipes/tag/:tag", component: CategoryTagPage },
{ path: "/recipes/category/:category", component: CategoryPage }, { path: "/recipes/category/:category", component: CategoryTagPage },
// Misc // Misc
{ path: "/new/", component: NewRecipe }, { path: "/new/", component: NewRecipe },
{ path: "/pages/:customPage", component: CustomPage }, { path: "/pages/:customPage", component: CustomPage },

View file

@ -7,6 +7,7 @@ import language from "./modules/language";
import siteSettings from "./modules/siteSettings"; import siteSettings from "./modules/siteSettings";
import recipes from "./modules/recipes"; import recipes from "./modules/recipes";
import groups from "./modules/groups"; import groups from "./modules/groups";
import snackbar from "./modules/snackbar";
Vue.use(Vuex); Vue.use(Vuex);
@ -22,6 +23,7 @@ const store = new Vuex.Store({
siteSettings, siteSettings,
groups, groups,
recipes, recipes,
snackbar,
}, },
state: { state: {
// All Recipe Data Store // All Recipe Data Store

View file

@ -1,5 +1,6 @@
import { api } from "@/api"; import { api } from "@/api";
import Vue from "vue"; import Vue from "vue";
import { recipe } from "@/utils/recipe";
const state = { const state = {
recentRecipes: [], recentRecipes: [],
@ -36,7 +37,6 @@ const mutations = {
const actions = { const actions = {
async requestRecentRecipes() { async requestRecentRecipes() {
const payload = await api.recipes.allSummary(0, 30); const payload = await api.recipes.allSummary(0, 30);
payload.sort((a, b) => (a.dateAdded > b.dateAdded ? -1 : 1));
const hash = Object.fromEntries(payload.map(e => [e.id, e])); const hash = Object.fromEntries(payload.map(e => [e.id, e]));
this.commit("setRecentRecipes", hash); this.commit("setRecentRecipes", hash);
}, },
@ -60,7 +60,11 @@ const actions = {
const getters = { const getters = {
getAllRecipes: state => Object.values(state.allRecipes), getAllRecipes: state => Object.values(state.allRecipes),
getAllRecipesHash: state => state.allRecipes, getAllRecipesHash: state => state.allRecipes,
getRecentRecipes: state => Object.values(state.recentRecipes), getRecentRecipes: state => {
let list = Object.values(state.recentRecipes);
recipe.sortByUpdated(list);
return list;
},
getRecentRecipesHash: state => state.recentRecipes, getRecentRecipesHash: state => state.recentRecipes,
}; };

View file

@ -0,0 +1,23 @@
const state = {
snackbar: {
open: false,
text: "Hello From The Store",
color: "info",
},
};
const mutations = {
setSnackbar(state, payload) {
state.snackbar = payload;
},
};
const getters = {
getSnackbar: state => state.snackbar,
};
export default {
state,
mutations,
getters,
};

View file

@ -1,14 +1,7 @@
import { vueApp } from "../main";
import { recipe } from "@/utils/recipe"; import { recipe } from "@/utils/recipe";
import { store } from "@/store";
// TODO: Migrate to Mixins // TODO: Migrate to Mixins
const notifyHelpers = {
baseCSS: "notify-base",
error: "notify-error-color",
warning: "notify-warning-color",
success: "notify-success-color",
info: "notify-info-color",
};
export const utils = { export const utils = {
recipe: recipe, recipe: recipe,
@ -27,27 +20,37 @@ export const utils = {
return `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
}, },
notify: { notify: {
show: function(text, type = "info", title = null) { info: function(text, title = null) {
vueApp.flashMessage.show({ store.commit("setSnackbar", {
status: type, open: true,
title: title, title: title,
message: text, text: text,
time: 3000, color: "info",
blockClass: `${notifyHelpers.baseCSS} ${notifyHelpers[type]}`,
contentClass: `${notifyHelpers.baseCSS} ${notifyHelpers[type]}`,
}); });
}, },
info: function(text, title = null) {
this.show(text, "info", title);
},
success: function(text, title = null) { success: function(text, title = null) {
this.show(text, "success", title); store.commit("setSnackbar", {
open: true,
title: title,
text: text,
color: "success",
});
}, },
error: function(text, title = null) { error: function(text, title = null) {
this.show(text, "error", title); store.commit("setSnackbar", {
open: true,
title: title,
text: text,
color: "error",
});
}, },
warning: function(text, title = null) { warning: function(text, title = null) {
this.show(text, "warning", title); store.commit("setSnackbar", {
open: true,
title: title,
text: text,
color: "warning",
});
}, },
}, },
}; };

View file

@ -90,7 +90,7 @@ def import_database(file_name: str, import_data: ImportJob, session: Session = D
force_import=import_data.force, force_import=import_data.force,
rebase=import_data.rebase, rebase=import_data.rebase,
) )
create_backup_event("Database Restore", f"Restored Database File {file_name}", session) create_backup_event("Database Restore", f"Restore File: {file_name}", session)
return db_import return db_import

View file

@ -1,8 +1,9 @@
from fastapi import APIRouter, Depends, status, HTTPException from fastapi import APIRouter, Depends, HTTPException, status
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.schema.user import GroupBase, GroupInDB, UpdateGroup, UserInDB from mealie.schema.user import GroupBase, GroupInDB, UpdateGroup, UserInDB
from mealie.services.events import create_group_event
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/groups", tags=["Groups"]) router = APIRouter(prefix="/api/groups", tags=["Groups"])
@ -39,6 +40,7 @@ async def create_group(
try: try:
db.groups.create(session, group_data.dict()) db.groups.create(session, group_data.dict())
create_group_event("Group Created", f"'{group_data.name}' created")
except Exception: except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST) raise HTTPException(status.HTTP_400_BAD_REQUEST)
@ -68,7 +70,8 @@ async def delete_user_group(
if not group: if not group:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="GROUP_NOT_FOUND") raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="GROUP_NOT_FOUND")
if not group.users == []: if group.users != []:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="GROUP_WITH_USERS") raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="GROUP_WITH_USERS")
create_group_event("Group Deleted", f"'{group.name}' Deleted")
db.groups.delete(session, id) db.groups.delete(session, id)

View file

@ -4,6 +4,7 @@ from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.schema.meal import MealPlanIn, MealPlanInDB from mealie.schema.meal import MealPlanIn, MealPlanInDB
from mealie.schema.user import GroupInDB, UserInDB from mealie.schema.user import GroupInDB, UserInDB
from mealie.services.events import create_group_event
from mealie.services.image import image from mealie.services.image import image
from mealie.services.meal_services import get_todays_meal, process_meals from mealie.services.meal_services import get_todays_meal, process_meals
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
@ -24,10 +25,11 @@ def get_all_meals(
@router.post("/create", status_code=status.HTTP_201_CREATED) @router.post("/create", status_code=status.HTTP_201_CREATED)
def create_meal_plan( def create_meal_plan(
data: MealPlanIn, session: Session = Depends(generate_session), current_user=Depends(get_current_user) data: MealPlanIn, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
): ):
""" Creates a meal plan database entry """ """ Creates a meal plan database entry """
processed_plan = process_meals(session, data) processed_plan = process_meals(session, data)
create_group_event("Meal Plan Created", f"Mealplan Created for '{current_user.group}'")
return db.meals.create(session, processed_plan.dict()) return db.meals.create(session, processed_plan.dict())
@ -36,23 +38,29 @@ def update_meal_plan(
plan_id: str, plan_id: str,
meal_plan: MealPlanIn, meal_plan: MealPlanIn,
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
current_user=Depends(get_current_user), current_user: UserInDB = Depends(get_current_user),
): ):
""" Updates a meal plan based off ID """ """ Updates a meal plan based off ID """
processed_plan = process_meals(session, meal_plan) processed_plan = process_meals(session, meal_plan)
processed_plan = MealPlanInDB(uid=plan_id, **processed_plan.dict()) processed_plan = MealPlanInDB(uid=plan_id, **processed_plan.dict())
try: try:
db.meals.update(session, plan_id, processed_plan.dict()) db.meals.update(session, plan_id, processed_plan.dict())
create_group_event("Meal Plan Updated", f"Mealplan Updated for '{current_user.group}'")
except Exception: except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST) raise HTTPException(status.HTTP_400_BAD_REQUEST)
@router.delete("/{plan_id}") @router.delete("/{plan_id}")
def delete_meal_plan(plan_id, session: Session = Depends(generate_session), current_user=Depends(get_current_user)): def delete_meal_plan(
plan_id,
session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
):
""" Removes a meal plan from the database """ """ Removes a meal plan from the database """
try: try:
db.meals.delete(session, plan_id) db.meals.delete(session, plan_id)
create_group_event("Meal Plan Deleted", f"Mealplan Deleted for '{current_user.group}'")
except Exception: except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST) raise HTTPException(status.HTTP_400_BAD_REQUEST)

View file

@ -26,17 +26,17 @@ async def get_recipe_summary(
""" """
return db.recipes.get_all(session, limit=limit, start=start, override_schema=RecipeSummary) return db.recipes.get_all(session, limit=limit, start=start, order_by="date_updated", override_schema=RecipeSummary)
@router.get("/api/recipes/summary/untagged", response_model=list[RecipeSummary]) @router.get("/api/recipes/summary/untagged", response_model=list[RecipeSummary])
async def get_untagged_recipes(session: Session = Depends(generate_session)): async def get_untagged_recipes(count: bool = False, session: Session = Depends(generate_session)):
return db.recipes.count_untagged(session, False, override_schema=RecipeSummary) return db.recipes.count_untagged(session, count=count, override_schema=RecipeSummary)
@router.get("/api/recipes/summary/uncategorized", response_model=list[RecipeSummary]) @router.get("/api/recipes/summary/uncategorized", response_model=list[RecipeSummary])
async def get_uncategorized_recipes(session: Session = Depends(generate_session)): async def get_uncategorized_recipes(count: bool = False, session: Session = Depends(generate_session)):
return db.recipes.count_uncategorized(session, False, override_schema=RecipeSummary) return db.recipes.count_uncategorized(session, count=count, override_schema=RecipeSummary)
@router.post("/api/recipes/category") @router.post("/api/recipes/category")

View file

@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, status from fastapi import APIRouter, Depends, Request, status
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from mealie.core import security from mealie.core import security
@ -6,6 +6,7 @@ from mealie.core.security import authenticate_user
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.schema.user import UserInDB from mealie.schema.user import UserInDB
from mealie.services.events import create_user_event
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/auth", tags=["Authentication"]) router = APIRouter(prefix="/api/auth", tags=["Authentication"])
@ -14,6 +15,7 @@ router = APIRouter(prefix="/api/auth", tags=["Authentication"])
@router.post("/token/long") @router.post("/token/long")
@router.post("/token") @router.post("/token")
def get_token( def get_token(
request: Request,
data: OAuth2PasswordRequestForm = Depends(), data: OAuth2PasswordRequestForm = Depends(),
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
@ -23,6 +25,7 @@ def get_token(
user = authenticate_user(session, email, password) user = authenticate_user(session, email, password)
if not user: if not user:
create_user_event("Failed Login", f"Username: {email}, Source IP: '{request.client.host}'")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},

View file

@ -9,7 +9,7 @@ from mealie.db.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.schema.user import ChangePassword, UserBase, UserIn, UserInDB, UserOut from mealie.schema.user import ChangePassword, UserBase, UserIn, UserInDB, UserOut
from mealie.services.events import create_sign_up_event from mealie.services.events import create_user_event
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/users", tags=["Users"]) router = APIRouter(prefix="/api/users", tags=["Users"])
@ -23,7 +23,7 @@ async def create_user(
): ):
new_user.password = get_password_hash(new_user.password) new_user.password = get_password_hash(new_user.password)
create_sign_up_event("User Created", f"Created by {current_user.full_name}", session=session) create_user_event("User Created", f"Created by {current_user.full_name}", session=session)
return db.users.create(session, new_user.dict()) return db.users.create(session, new_user.dict())
@ -150,5 +150,6 @@ async def delete_user(
if current_user.id == id or current_user.admin: if current_user.id == id or current_user.admin:
try: try:
db.users.delete(session, id) db.users.delete(session, id)
create_user_event("User Deleted", f"User ID: {id}", session=session)
except Exception: except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST) raise HTTPException(status.HTTP_400_BAD_REQUEST)

View file

@ -7,7 +7,7 @@ from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.schema.sign_up import SignUpIn, SignUpOut, SignUpToken from mealie.schema.sign_up import SignUpIn, SignUpOut, SignUpToken
from mealie.schema.user import UserIn, UserInDB from mealie.schema.user import UserIn, UserInDB
from mealie.services.events import create_sign_up_event from mealie.services.events import create_user_event
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/users/sign-ups", tags=["User Signup"]) router = APIRouter(prefix="/api/users/sign-ups", tags=["User Signup"])
@ -39,7 +39,7 @@ async def create_user_sign_up_key(
"name": key_data.name, "name": key_data.name,
"admin": key_data.admin, "admin": key_data.admin,
} }
create_sign_up_event("Sign-up Token Created", f"Created by {current_user.full_name}", session=session) create_user_event("Sign-up Token Created", f"Created by {current_user.full_name}", session=session)
return db.sign_ups.create(session, sign_up) return db.sign_ups.create(session, sign_up)
@ -62,7 +62,7 @@ async def create_user_with_token(
db.users.create(session, new_user.dict()) db.users.create(session, new_user.dict())
# DeleteToken # DeleteToken
create_sign_up_event("Sign-up Token Used", f"New User {new_user.full_name}", session=session) create_user_event("Sign-up Token Used", f"New User {new_user.full_name}", session=session)
db.sign_ups.delete(session, token) db.sign_ups.delete(session, token)

View file

@ -12,7 +12,8 @@ class EventCategory(str, Enum):
backup = "backup" backup = "backup"
scheduled = "scheduled" scheduled = "scheduled"
migration = "migration" migration = "migration"
sign_up = "signup" group = "group"
user = "user"
class Event(CamelModel): class Event(CamelModel):

View file

@ -35,6 +35,11 @@ def create_migration_event(title, text, session=None):
save_event(title=title, text=text, category=category, session=session) save_event(title=title, text=text, category=category, session=session)
def create_sign_up_event(title, text, session=None): def create_group_event(title, text, session=None):
category = EventCategory.sign_up category = EventCategory.group
save_event(title=title, text=text, category=category, session=session)
def create_user_event(title, text, session=None):
category = EventCategory.user
save_event(title=title, text=text, category=category, session=session) save_event(title=title, text=text, category=category, session=session)