mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 14:33:33 -07:00
add server side events
This commit is contained in:
parent
7f63f35ec5
commit
832e95f27e
26 changed files with 393 additions and 34 deletions
59
frontend/src/api/about.js
Normal file
59
frontend/src/api/about.js
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import { baseURL } from "./api-utils";
|
||||||
|
import { apiReq } from "./api-utils";
|
||||||
|
|
||||||
|
const prefix = baseURL + "about";
|
||||||
|
|
||||||
|
const aboutURLs = {
|
||||||
|
version: `${prefix}/version`,
|
||||||
|
debug: `${prefix}`,
|
||||||
|
lastRecipe: `${prefix}/last-recipe-json`,
|
||||||
|
demo: `${prefix}/is-demo`,
|
||||||
|
log: num => `${prefix}/log/${num}`,
|
||||||
|
statistics: `${prefix}/statistics`,
|
||||||
|
events: `${prefix}/events`,
|
||||||
|
event: id => `${prefix}/events/${id}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const aboutAPI = {
|
||||||
|
async getEvents() {
|
||||||
|
const resposne = await apiReq.get(aboutURLs.events);
|
||||||
|
return resposne.data;
|
||||||
|
},
|
||||||
|
async deleteEvent(id) {
|
||||||
|
const resposne = await apiReq.delete(aboutURLs.event(id));
|
||||||
|
return resposne.data;
|
||||||
|
},
|
||||||
|
async deleteAllEvents() {
|
||||||
|
const resposne = await apiReq.delete(aboutURLs.events);
|
||||||
|
return resposne.data;
|
||||||
|
},
|
||||||
|
// async getAppInfo() {
|
||||||
|
// const response = await apiReq.get(aboutURLs.version);
|
||||||
|
// return response.data;
|
||||||
|
// },
|
||||||
|
|
||||||
|
// async getDebugInfo() {
|
||||||
|
// const response = await apiReq.get(aboutURLs.debug);
|
||||||
|
// return response.data;
|
||||||
|
// },
|
||||||
|
|
||||||
|
// async getLogText(num) {
|
||||||
|
// const response = await apiReq.get(aboutURLs.log(num));
|
||||||
|
// return response.data;
|
||||||
|
// },
|
||||||
|
|
||||||
|
// async getLastJson() {
|
||||||
|
// const response = await apiReq.get(aboutURLs.lastRecipe);
|
||||||
|
// return response.data;
|
||||||
|
// },
|
||||||
|
|
||||||
|
// async getIsDemo() {
|
||||||
|
// const response = await apiReq.get(aboutURLs.demo);
|
||||||
|
// return response.data;
|
||||||
|
// },
|
||||||
|
|
||||||
|
// async getStatistics() {
|
||||||
|
// const response = await apiReq.get(aboutURLs.statistics);
|
||||||
|
// return response.data;
|
||||||
|
// },
|
||||||
|
};
|
|
@ -11,6 +11,7 @@ import { userAPI } from "./users";
|
||||||
import { signupAPI } from "./signUps";
|
import { signupAPI } from "./signUps";
|
||||||
import { groupAPI } from "./groups";
|
import { groupAPI } from "./groups";
|
||||||
import { siteSettingsAPI } from "./siteSettings";
|
import { siteSettingsAPI } from "./siteSettings";
|
||||||
|
import { aboutAPI } from "./about";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main object namespace for interacting with the backend database
|
* The main object namespace for interacting with the backend database
|
||||||
|
@ -30,4 +31,5 @@ export const api = {
|
||||||
users: userAPI,
|
users: userAPI,
|
||||||
signUps: signupAPI,
|
signUps: signupAPI,
|
||||||
groups: groupAPI,
|
groups: groupAPI,
|
||||||
|
about: aboutAPI,
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,5 +9,14 @@
|
||||||
"day": "numeric",
|
"day": "numeric",
|
||||||
"weekday": "long",
|
"weekday": "long",
|
||||||
"year": "numeric"
|
"year": "numeric"
|
||||||
|
},
|
||||||
|
"long": {
|
||||||
|
"year": "numeric",
|
||||||
|
"month": "long",
|
||||||
|
"day": "numeric",
|
||||||
|
"weekday": "long",
|
||||||
|
"hour": "numeric",
|
||||||
|
"minute": "numeric",
|
||||||
|
"hour12": true
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -9,5 +9,14 @@
|
||||||
"day": "numeric",
|
"day": "numeric",
|
||||||
"weekday": "long",
|
"weekday": "long",
|
||||||
"year": "numeric"
|
"year": "numeric"
|
||||||
|
},
|
||||||
|
"long": {
|
||||||
|
"year": "numeric",
|
||||||
|
"month": "long",
|
||||||
|
"day": "numeric",
|
||||||
|
"weekday": "long",
|
||||||
|
"hour": "numeric",
|
||||||
|
"minute": "numeric",
|
||||||
|
"hour12": true
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -19,7 +19,7 @@
|
||||||
<div class="text-truncate">
|
<div class="text-truncate">
|
||||||
<strong>{{ backup.name }}</strong>
|
<strong>{{ backup.name }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-truncate">{{ $d(new Date(backup.date), "medium") }}</div>
|
<div class="text-truncate">{{ $d(Date.parse(backup.date), "medium") }}</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
</v-toolbar-items>
|
</v-toolbar-items>
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
<v-card-title> {{ name }} </v-card-title>
|
<v-card-title> {{ name }} </v-card-title>
|
||||||
<v-card-subtitle class="mb-n3"> {{ $d(new Date(date), "medium") }} </v-card-subtitle>
|
<v-card-subtitle class="mb-n3" v-if="date"> {{ $d(new Date(date), "medium") }} </v-card-subtitle>
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
|
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
|
|
107
frontend/src/pages/Admin/Dashboard/EventViewer.vue
Normal file
107
frontend/src/pages/Admin/Dashboard/EventViewer.vue
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<StatCard icon="mdi-bell-ring">
|
||||||
|
<template v-slot:after-heading>
|
||||||
|
<div class="ml-auto text-right">
|
||||||
|
<div class="body-3 grey--text font-weight-light" v-text="'Events'" />
|
||||||
|
|
||||||
|
<h3 class="display-2 font-weight-light text--primary">
|
||||||
|
<small> {{ total }}</small>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="d-flex row py-3 justify-end">
|
||||||
|
<v-btn class="mx-2" small color="primary" @click="deleteAll">
|
||||||
|
<v-icon left> mdi-notification-clear-all </v-icon> Clear
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
<template v-slot:bottom>
|
||||||
|
<v-list subheader two-line>
|
||||||
|
<v-list-item v-for="(event, index) in events" :key="index">
|
||||||
|
<v-list-item-avatar>
|
||||||
|
<v-icon large dark :color="icons[event.category].color">
|
||||||
|
{{ icons[event.category].icon }}
|
||||||
|
</v-icon>
|
||||||
|
</v-list-item-avatar>
|
||||||
|
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title v-text="event.title"></v-list-item-title>
|
||||||
|
|
||||||
|
<v-list-item-subtitle v-text="event.text"></v-list-item-subtitle>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
{{ $d(Date.parse(event.timeStamp), "long") }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
|
||||||
|
<v-list-item-action class="ml-auto">
|
||||||
|
<v-btn large icon @click="deleteEvent(event.id)">
|
||||||
|
<v-icon color="error">mdi-delete</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-list-item-action>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</template>
|
||||||
|
</StatCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { api } from "@/api";
|
||||||
|
import StatCard from "./StatCard";
|
||||||
|
export default {
|
||||||
|
components: { StatCard },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
total: 0,
|
||||||
|
events: [],
|
||||||
|
icons: {
|
||||||
|
general: {
|
||||||
|
icon: "mdi-information",
|
||||||
|
color: "info",
|
||||||
|
},
|
||||||
|
recipe: {
|
||||||
|
icon: "mdi-silverware-fork-knife",
|
||||||
|
color: "primary",
|
||||||
|
},
|
||||||
|
backup: {
|
||||||
|
icon: "mdi-backup-restore",
|
||||||
|
color: "primary",
|
||||||
|
},
|
||||||
|
schedule: {
|
||||||
|
icon: "mdi-calendar-clock",
|
||||||
|
color: "primary",
|
||||||
|
},
|
||||||
|
migration: {
|
||||||
|
icon: "mdi-database-import",
|
||||||
|
color: "primary",
|
||||||
|
},
|
||||||
|
signup: {
|
||||||
|
icon: "mdi-account",
|
||||||
|
color: "primary",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.getEvents();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async getEvents() {
|
||||||
|
const events = await api.about.getEvents();
|
||||||
|
this.events = events.events;
|
||||||
|
this.total = events.total;
|
||||||
|
},
|
||||||
|
async deleteEvent(id) {
|
||||||
|
await api.about.deleteEvent(id);
|
||||||
|
this.getEvents();
|
||||||
|
},
|
||||||
|
async deleteAll() {
|
||||||
|
await api.about.deleteAllEvents();
|
||||||
|
this.getEvents();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
</style>
|
|
@ -2,14 +2,11 @@ w<template>
|
||||||
<v-card v-bind="$attrs" :class="classes" class="v-card--material pa-3">
|
<v-card v-bind="$attrs" :class="classes" class="v-card--material pa-3">
|
||||||
<div class="d-flex grow flex-wrap">
|
<div class="d-flex grow flex-wrap">
|
||||||
<v-sheet
|
<v-sheet
|
||||||
:class="{
|
|
||||||
'pa-7': !$slots.image,
|
|
||||||
}"
|
|
||||||
:color="color"
|
:color="color"
|
||||||
:max-height="icon ? 90 : undefined"
|
:max-height="icon ? 90 : undefined"
|
||||||
:width="icon ? 'auto' : '100%'"
|
:width="icon ? 'auto' : '100%'"
|
||||||
elevation="6"
|
elevation="6"
|
||||||
class="text-start v-card--material__heading mb-n6"
|
class="text-start v-card--material__heading mb-n6 mt-n10 pa-7"
|
||||||
dark
|
dark
|
||||||
>
|
>
|
||||||
<v-icon v-if="icon" size="32" v-text="icon" />
|
<v-icon v-if="icon" size="32" v-text="icon" />
|
||||||
|
@ -30,6 +27,14 @@ w<template>
|
||||||
<slot name="actions" />
|
<slot name="actions" />
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-if="$slots.bottom">
|
||||||
|
<v-divider class="mt-2" />
|
||||||
|
|
||||||
|
<div class="pb-0">
|
||||||
|
<slot name="bottom" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -71,10 +76,10 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
hasHeading() {
|
hasHeading() {
|
||||||
return Boolean(this.$slots.heading || this.title || this.icon);
|
return false;
|
||||||
},
|
},
|
||||||
hasAltHeading() {
|
hasAltHeading() {
|
||||||
return Boolean(this.$slots.heading || (this.title && this.icon));
|
return false;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -82,14 +87,14 @@ export default {
|
||||||
|
|
||||||
<style lang="sass">
|
<style lang="sass">
|
||||||
.v-card--material
|
.v-card--material
|
||||||
&__avatar
|
&__avatar
|
||||||
position: relative
|
position: relative
|
||||||
top: -64px
|
top: -64px
|
||||||
margin-bottom: -32px
|
margin-bottom: -32px
|
||||||
|
|
||||||
&__heading
|
&__heading
|
||||||
position: relative
|
position: relative
|
||||||
top: -40px
|
top: -40px
|
||||||
transition: .3s ease
|
transition: .3s ease
|
||||||
z-index: 1
|
z-index: 1
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mt-15">
|
<div class="mt-10">
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col>
|
<v-col cols="12" sm="12" md="4">
|
||||||
<StatCard icon="mdi-silverware-fork-knife">
|
<StatCard icon="mdi-silverware-fork-knife">
|
||||||
<template v-slot:after-heading>
|
<template v-slot:after-heading>
|
||||||
<div class="ml-auto text-right">
|
<div class="ml-auto text-right">
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
</template>
|
</template>
|
||||||
</StatCard>
|
</StatCard>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col>
|
<v-col cols="12" sm="12" md="4">
|
||||||
<StatCard icon="mdi-account">
|
<StatCard icon="mdi-account">
|
||||||
<template v-slot:after-heading>
|
<template v-slot:after-heading>
|
||||||
<div class="ml-auto text-right">
|
<div class="ml-auto text-right">
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
</template>
|
</template>
|
||||||
</StatCard>
|
</StatCard>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col>
|
<v-col cols="12" sm="12" md="4">
|
||||||
<StatCard icon="mdi-account-group">
|
<StatCard icon="mdi-account-group">
|
||||||
<template v-slot:after-heading>
|
<template v-slot:after-heading>
|
||||||
<div class="ml-auto text-right">
|
<div class="ml-auto text-right">
|
||||||
|
@ -67,14 +67,21 @@
|
||||||
</StatCard>
|
</StatCard>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
<v-row class="mt-10">
|
||||||
|
<v-col cols="12" sm="12" lg="6">
|
||||||
|
<EventViewer />
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="12" lg="6"> </v-col>
|
||||||
|
</v-row>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
import StatCard from "./StatCard";
|
import StatCard from "./StatCard";
|
||||||
|
import EventViewer from "./EventViewer";
|
||||||
export default {
|
export default {
|
||||||
components: { StatCard },
|
components: { StatCard, EventViewer },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
statistics: {
|
statistics: {
|
||||||
|
@ -92,7 +99,6 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
async getStatistics() {
|
async getStatistics() {
|
||||||
this.statistics = await api.meta.getStatistics();
|
this.statistics = await api.meta.getStatistics();
|
||||||
console.log(this.statistics);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -160,10 +160,10 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
updateClipboard(newClip) {
|
updateClipboard(newClip) {
|
||||||
navigator.clipboard.writeText(newClip).then(
|
navigator.clipboard.writeText(newClip).then(
|
||||||
function() {
|
() => {
|
||||||
console.log("Copied", newClip);
|
console.log("Copied", newClip);
|
||||||
},
|
},
|
||||||
function() {
|
() => {
|
||||||
console.log("Copy Failed", newClip);
|
console.log("Copy Failed", newClip);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -37,7 +37,6 @@ export default {
|
||||||
return this.$store.getters.getSiteSettings;
|
return this.$store.getters.getSiteSettings;
|
||||||
},
|
},
|
||||||
recentRecipes() {
|
recentRecipes() {
|
||||||
console.log("Recent Recipes");
|
|
||||||
return this.$store.getters.getRecentRecipes;
|
return this.$store.getters.getRecentRecipes;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,11 +4,13 @@ from fastapi import FastAPI
|
||||||
from mealie.core import root_logger
|
from mealie.core import root_logger
|
||||||
from mealie.core.config import APP_VERSION, settings
|
from mealie.core.config import APP_VERSION, settings
|
||||||
from mealie.routes import backup_routes, debug_routes, migration_routes, theme_routes, utility_routes
|
from mealie.routes import backup_routes, debug_routes, migration_routes, theme_routes, utility_routes
|
||||||
|
from mealie.routes.about import about_router
|
||||||
from mealie.routes.groups import groups
|
from mealie.routes.groups import groups
|
||||||
from mealie.routes.mealplans import mealplans
|
from mealie.routes.mealplans import mealplans
|
||||||
from mealie.routes.recipe import router as recipe_router
|
from mealie.routes.recipe import router as recipe_router
|
||||||
from mealie.routes.site_settings import all_settings
|
from mealie.routes.site_settings import all_settings
|
||||||
from mealie.routes.users import users
|
from mealie.routes.users import users
|
||||||
|
from mealie.services.events import create_general_event
|
||||||
|
|
||||||
logger = root_logger.get_logger()
|
logger = root_logger.get_logger()
|
||||||
|
|
||||||
|
@ -31,6 +33,7 @@ def api_routers():
|
||||||
app.include_router(groups.router)
|
app.include_router(groups.router)
|
||||||
# Recipes
|
# Recipes
|
||||||
app.include_router(recipe_router)
|
app.include_router(recipe_router)
|
||||||
|
app.include_router(about_router)
|
||||||
# Meal Routes
|
# Meal Routes
|
||||||
app.include_router(mealplans.router)
|
app.include_router(mealplans.router)
|
||||||
# Settings Routes
|
# Settings Routes
|
||||||
|
@ -53,6 +56,7 @@ def system_startup():
|
||||||
logger.info("-----SYSTEM STARTUP----- \n")
|
logger.info("-----SYSTEM STARTUP----- \n")
|
||||||
logger.info("------APP SETTINGS------")
|
logger.info("------APP SETTINGS------")
|
||||||
logger.info(settings.json(indent=4, exclude={"SECRET", "DEFAULT_PASSWORD", "SFTP_PASSWORD", "SFTP_USERNAME"}))
|
logger.info(settings.json(indent=4, exclude={"SECRET", "DEFAULT_PASSWORD", "SFTP_PASSWORD", "SFTP_USERNAME"}))
|
||||||
|
create_general_event("Application Startup", f"Mealie API started on port {settings.API_PORT}")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from mealie.db.db_base import BaseDocument
|
from mealie.db.db_base import BaseDocument
|
||||||
|
from mealie.db.models.event import Event
|
||||||
from mealie.db.models.group import Group
|
from mealie.db.models.group import Group
|
||||||
from mealie.db.models.mealplan import MealPlanModel
|
from mealie.db.models.mealplan import MealPlanModel
|
||||||
from mealie.db.models.recipe.recipe import Category, RecipeModel, Tag
|
from mealie.db.models.recipe.recipe import Category, RecipeModel, Tag
|
||||||
|
@ -9,6 +10,7 @@ from mealie.db.models.sign_up import SignUp
|
||||||
from mealie.db.models.theme import SiteThemeModel
|
from mealie.db.models.theme import SiteThemeModel
|
||||||
from mealie.db.models.users import User
|
from mealie.db.models.users import User
|
||||||
from mealie.schema.category import RecipeCategoryResponse, RecipeTagResponse
|
from mealie.schema.category import RecipeCategoryResponse, RecipeTagResponse
|
||||||
|
from mealie.schema.events import Event as EventSchema
|
||||||
from mealie.schema.meal import MealPlanInDB
|
from mealie.schema.meal import MealPlanInDB
|
||||||
from mealie.schema.recipe import Recipe
|
from mealie.schema.recipe import Recipe
|
||||||
from mealie.schema.settings import CustomPageOut
|
from mealie.schema.settings import CustomPageOut
|
||||||
|
@ -35,10 +37,10 @@ class _Recipes(BaseDocument):
|
||||||
return f"{slug}.{extension}"
|
return f"{slug}.{extension}"
|
||||||
|
|
||||||
def count_uncategorized(self, session: Session) -> int:
|
def count_uncategorized(self, session: Session) -> int:
|
||||||
return session.query(self.sql_model).filter(RecipeModel.recipe_category == None).count()
|
return session.query(self.sql_model).filter(RecipeModel.recipe_category == None).count() # noqa: 711
|
||||||
|
|
||||||
def count_untagged(self, session: Session) -> int:
|
def count_untagged(self, session: Session) -> int:
|
||||||
return session.query(self.sql_model).filter(RecipeModel.tags == None).count()
|
return session.query(self.sql_model).filter(RecipeModel.tags == None).count() # noqa: 711
|
||||||
|
|
||||||
|
|
||||||
class _Categories(BaseDocument):
|
class _Categories(BaseDocument):
|
||||||
|
@ -115,8 +117,6 @@ class _Groups(BaseDocument):
|
||||||
"""
|
"""
|
||||||
group: GroupInDB = session.query(self.sql_model).filter_by(**{match_key: match_value}).one_or_none()
|
group: GroupInDB = session.query(self.sql_model).filter_by(**{match_key: match_value}).one_or_none()
|
||||||
|
|
||||||
# Potentially not needed? column is sorted by SqlAlchemy based on startDate
|
|
||||||
# return sorted(group.mealplans, key=lambda mealplan: mealplan.startDate)
|
|
||||||
return group.mealplans
|
return group.mealplans
|
||||||
|
|
||||||
|
|
||||||
|
@ -134,6 +134,13 @@ class _CustomPages(BaseDocument):
|
||||||
self.schema = CustomPageOut
|
self.schema = CustomPageOut
|
||||||
|
|
||||||
|
|
||||||
|
class _Events(BaseDocument):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.primary_key = "id"
|
||||||
|
self.sql_model = Event
|
||||||
|
self.schema = EventSchema
|
||||||
|
|
||||||
|
|
||||||
class Database:
|
class Database:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.recipes = _Recipes()
|
self.recipes = _Recipes()
|
||||||
|
@ -146,6 +153,7 @@ class Database:
|
||||||
self.sign_ups = _SignUps()
|
self.sign_ups = _SignUps()
|
||||||
self.groups = _Groups()
|
self.groups = _Groups()
|
||||||
self.custom_pages = _CustomPages()
|
self.custom_pages = _CustomPages()
|
||||||
|
self.events = _Events()
|
||||||
|
|
||||||
|
|
||||||
db = Database()
|
db = Database()
|
||||||
|
|
|
@ -23,6 +23,14 @@ class BaseDocument:
|
||||||
) -> List[dict]:
|
) -> List[dict]:
|
||||||
eff_schema = override_schema or self.schema
|
eff_schema = override_schema or self.schema
|
||||||
|
|
||||||
|
if order_by:
|
||||||
|
order_attr = getattr(self.sql_model, str(order_by))
|
||||||
|
|
||||||
|
return [
|
||||||
|
eff_schema.from_orm(x)
|
||||||
|
for x in session.query(self.sql_model).order_by(order_attr.desc()).offset(start).limit(limit).all()
|
||||||
|
]
|
||||||
|
|
||||||
return [eff_schema.from_orm(x) for x in session.query(self.sql_model).offset(start).limit(limit).all()]
|
return [eff_schema.from_orm(x) for x in session.query(self.sql_model).offset(start).limit(limit).all()]
|
||||||
|
|
||||||
def get_all_limit_columns(self, session: Session, fields: List[str], limit: int = None) -> List[SqlAlchemyBase]:
|
def get_all_limit_columns(self, session: Session, fields: List[str], limit: int = None) -> List[SqlAlchemyBase]:
|
||||||
|
@ -155,6 +163,10 @@ class BaseDocument:
|
||||||
|
|
||||||
return results_as_model
|
return results_as_model
|
||||||
|
|
||||||
|
def delete_all(self, session: Session) -> None:
|
||||||
|
session.query(self.sql_model).delete()
|
||||||
|
session.commit()
|
||||||
|
|
||||||
def count_all(self, session: Session, match_key=None, match_value=None) -> int:
|
def count_all(self, session: Session, match_key=None, match_value=None) -> int:
|
||||||
|
|
||||||
if None in [match_key, match_value]:
|
if None in [match_key, match_value]:
|
||||||
|
|
|
@ -5,6 +5,7 @@ from mealie.db.database import db
|
||||||
from mealie.db.db_setup import create_session
|
from mealie.db.db_setup import create_session
|
||||||
from mealie.schema.settings import SiteSettings
|
from mealie.schema.settings import SiteSettings
|
||||||
from mealie.schema.theme import SiteTheme
|
from mealie.schema.theme import SiteTheme
|
||||||
|
from mealie.services.events import create_general_event
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
logger = root_logger.get_logger("init_db")
|
logger = root_logger.get_logger("init_db")
|
||||||
|
@ -58,6 +59,7 @@ def main():
|
||||||
else:
|
else:
|
||||||
print("Database Doesn't Exists, Initializing...")
|
print("Database Doesn't Exists, Initializing...")
|
||||||
init_db()
|
init_db()
|
||||||
|
create_general_event("Initialize Database", "Initialize database with default values", session)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
from mealie.db.models.event import *
|
||||||
|
from mealie.db.models.group import *
|
||||||
from mealie.db.models.mealplan import *
|
from mealie.db.models.mealplan import *
|
||||||
from mealie.db.models.recipe.recipe import *
|
from mealie.db.models.recipe.recipe import *
|
||||||
from mealie.db.models.settings import *
|
from mealie.db.models.settings import *
|
||||||
|
from mealie.db.models.sign_up import *
|
||||||
from mealie.db.models.theme import *
|
from mealie.db.models.theme import *
|
||||||
from mealie.db.models.users import *
|
from mealie.db.models.users import *
|
||||||
from mealie.db.models.sign_up import *
|
|
||||||
from mealie.db.models.group import *
|
|
||||||
|
|
17
mealie/db/models/event.py
Normal file
17
mealie/db/models/event.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||||
|
|
||||||
|
|
||||||
|
class Event(SqlAlchemyBase, BaseMixins):
|
||||||
|
__tablename__ = "events"
|
||||||
|
id = sa.Column(sa.Integer, primary_key=True)
|
||||||
|
title = sa.Column(sa.String)
|
||||||
|
text = sa.Column(sa.String)
|
||||||
|
time_stamp = sa.Column(sa.DateTime)
|
||||||
|
category = sa.Column(sa.String)
|
||||||
|
|
||||||
|
def __init__(self, title, text, time_stamp, category, *args, **kwargs) -> None:
|
||||||
|
self.title = title
|
||||||
|
self.text = text
|
||||||
|
self.time_stamp = time_stamp
|
||||||
|
self.category = category
|
|
@ -4,5 +4,5 @@ SqlAlchemyBase = dec.declarative_base()
|
||||||
|
|
||||||
|
|
||||||
class BaseMixins:
|
class BaseMixins:
|
||||||
def _pass_on_me():
|
def update(self, *args, **kwarg):
|
||||||
pass
|
self.__init__(*args, **kwarg)
|
||||||
|
|
7
mealie/routes/about/__init__.py
Normal file
7
mealie/routes/about/__init__.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from .events import router as events_router
|
||||||
|
|
||||||
|
about_router = APIRouter(prefix="/api/about")
|
||||||
|
|
||||||
|
about_router.include_router(events_router)
|
29
mealie/routes/about/events.py
Normal file
29
mealie/routes/about/events.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from mealie.db.database import db
|
||||||
|
from mealie.db.db_setup import generate_session
|
||||||
|
from mealie.db.models.event import Event
|
||||||
|
from mealie.routes.deps import get_current_user
|
||||||
|
from mealie.schema.events import EventsOut
|
||||||
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/events", tags=["App Events"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=EventsOut)
|
||||||
|
async def get_events(session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
|
||||||
|
""" Get event from the Database """
|
||||||
|
# Get Item
|
||||||
|
return EventsOut(total=db.events.count_all(session), events=db.events.get_all(session, order_by="time_stamp"))
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("")
|
||||||
|
async def get_events(session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
|
||||||
|
""" Get event from the Database """
|
||||||
|
# Get Item
|
||||||
|
return db.events.delete_all(session)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{id}")
|
||||||
|
async def delete_event(id: int, session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
|
||||||
|
""" Delete event from the Database """
|
||||||
|
return db.events.delete(session, id)
|
|
@ -1,5 +1,6 @@
|
||||||
import operator
|
import operator
|
||||||
import shutil
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
||||||
from mealie.core.config import app_dirs
|
from mealie.core.config import app_dirs
|
||||||
|
@ -10,6 +11,7 @@ from mealie.routes.deps import get_current_user
|
||||||
from mealie.schema.backup import BackupJob, ImportJob, Imports, LocalBackup
|
from mealie.schema.backup import BackupJob, ImportJob, Imports, LocalBackup
|
||||||
from mealie.services.backups import imports
|
from mealie.services.backups import imports
|
||||||
from mealie.services.backups.exports import backup_all
|
from mealie.services.backups.exports import backup_all
|
||||||
|
from mealie.services.events import create_backup_event
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/backups", tags=["Backups"], dependencies=[Depends(get_current_user)])
|
router = APIRouter(prefix="/api/backups", tags=["Backups"], dependencies=[Depends(get_current_user)])
|
||||||
|
@ -45,6 +47,7 @@ def export_database(data: BackupJob, session: Session = Depends(generate_session
|
||||||
export_users=data.options.users,
|
export_users=data.options.users,
|
||||||
export_groups=data.options.groups,
|
export_groups=data.options.groups,
|
||||||
)
|
)
|
||||||
|
create_backup_event("Manual Backup", f"Manual Backup Created '{Path(export_path).name}'", session)
|
||||||
return {"export_path": export_path}
|
return {"export_path": export_path}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
|
@ -75,7 +78,7 @@ async def download_backup_file(file_name: str):
|
||||||
def import_database(file_name: str, import_data: ImportJob, session: Session = Depends(generate_session)):
|
def import_database(file_name: str, import_data: ImportJob, session: Session = Depends(generate_session)):
|
||||||
""" Import a database backup file generated from Mealie. """
|
""" Import a database backup file generated from Mealie. """
|
||||||
|
|
||||||
return imports.import_database(
|
db_import = imports.import_database(
|
||||||
session=session,
|
session=session,
|
||||||
archive=import_data.name,
|
archive=import_data.name,
|
||||||
import_recipes=import_data.recipes,
|
import_recipes=import_data.recipes,
|
||||||
|
@ -87,6 +90,8 @@ 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)
|
||||||
|
return db_import
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{file_name}/delete", status_code=status.HTTP_200_OK)
|
@router.delete("/{file_name}/delete", status_code=status.HTTP_200_OK)
|
||||||
|
|
|
@ -2,6 +2,7 @@ from pathlib import Path
|
||||||
|
|
||||||
from fastapi_camelcase import CamelModel
|
from fastapi_camelcase import CamelModel
|
||||||
|
|
||||||
|
|
||||||
class AppStatistics(CamelModel):
|
class AppStatistics(CamelModel):
|
||||||
total_recipes: int
|
total_recipes: int
|
||||||
total_users: int
|
total_users: int
|
||||||
|
|
31
mealie/schema/events.py
Normal file
31
mealie/schema/events.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi_camelcase import CamelModel
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
|
||||||
|
class EventCategory(str, Enum):
|
||||||
|
general = "general"
|
||||||
|
recipe = "recipe"
|
||||||
|
backup = "backup"
|
||||||
|
scheduled = "scheduled"
|
||||||
|
migration = "migration"
|
||||||
|
sign_up = "signup"
|
||||||
|
|
||||||
|
|
||||||
|
class Event(CamelModel):
|
||||||
|
id: Optional[int]
|
||||||
|
title: str
|
||||||
|
text: str
|
||||||
|
time_stamp: datetime = Field(default_factory=datetime.now)
|
||||||
|
category: EventCategory = EventCategory.general
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class EventsOut(CamelModel):
|
||||||
|
total: int
|
||||||
|
events: list[Event]
|
|
@ -9,6 +9,7 @@ from mealie.core import root_logger
|
||||||
from mealie.core.config import app_dirs
|
from mealie.core.config import app_dirs
|
||||||
from mealie.db.database import db
|
from mealie.db.database import db
|
||||||
from mealie.db.db_setup import create_session
|
from mealie.db.db_setup import create_session
|
||||||
|
from mealie.services.events import create_backup_event
|
||||||
from pathvalidate import sanitize_filename
|
from pathvalidate import sanitize_filename
|
||||||
from pydantic.main import BaseModel
|
from pydantic.main import BaseModel
|
||||||
|
|
||||||
|
@ -149,3 +150,5 @@ def auto_backup_job():
|
||||||
session = create_session()
|
session = create_session()
|
||||||
backup_all(session=session, tag="Auto", templates=templates)
|
backup_all(session=session, tag="Auto", templates=templates)
|
||||||
logger.info("Auto Backup Called")
|
logger.info("Auto Backup Called")
|
||||||
|
create_backup_event("Automated Backup", "Automated backup created", session)
|
||||||
|
session.close()
|
||||||
|
|
40
mealie/services/events.py
Normal file
40
mealie/services/events.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
from mealie.db.database import db
|
||||||
|
from mealie.db.db_setup import create_session
|
||||||
|
from mealie.schema.events import Event, EventCategory
|
||||||
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
|
|
||||||
|
def save_event(title, text, category, session: Session):
|
||||||
|
event = Event(title=title, text=text, category=category)
|
||||||
|
session = session or create_session()
|
||||||
|
db.events.create(session, event.dict())
|
||||||
|
|
||||||
|
|
||||||
|
def create_general_event(title, text, session=None):
|
||||||
|
category = EventCategory.general
|
||||||
|
save_event(title=title, text=text, category=category, session=session)
|
||||||
|
|
||||||
|
|
||||||
|
def create_recipe_event(title, text, session=None):
|
||||||
|
category = EventCategory.recipe
|
||||||
|
save_event(title=title, text=text, category=category, session=session)
|
||||||
|
|
||||||
|
|
||||||
|
def create_backup_event(title, text, session=None):
|
||||||
|
category = EventCategory.backup
|
||||||
|
save_event(title=title, text=text, category=category, session=session)
|
||||||
|
|
||||||
|
|
||||||
|
def create_scheduled_event(title, text, session=None):
|
||||||
|
category = EventCategory.scheduled
|
||||||
|
save_event(title=title, text=text, category=category, session=session)
|
||||||
|
|
||||||
|
|
||||||
|
def create_migration_event(title, text, session=None):
|
||||||
|
category = EventCategory.migration
|
||||||
|
save_event(title=title, text=text, category=category, session=session)
|
||||||
|
|
||||||
|
|
||||||
|
def create_sign_up_event(title, text, session=None):
|
||||||
|
category = EventCategory.sign_up
|
||||||
|
save_event(title=title, text=text, category=category, session=session)
|
|
@ -2,6 +2,7 @@ import requests
|
||||||
from mealie.db.database import db
|
from mealie.db.database import db
|
||||||
from mealie.db.db_setup import create_session
|
from mealie.db.db_setup import create_session
|
||||||
from mealie.schema.user import GroupInDB
|
from mealie.schema.user import GroupInDB
|
||||||
|
from mealie.services.events import create_scheduled_event
|
||||||
from mealie.services.meal_services import get_todays_meal
|
from mealie.services.meal_services import get_todays_meal
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
|
@ -21,4 +22,6 @@ def post_webhooks(group: int, session: Session = None):
|
||||||
for url in group_settings.webhook_urls:
|
for url in group_settings.webhook_urls:
|
||||||
requests.post(url, json=todays_recipe.json())
|
requests.post(url, json=todays_recipe.json())
|
||||||
|
|
||||||
|
create_scheduled_event("Meal Plan Webhook", f"Meal plan webhook executed for group '{group}'")
|
||||||
|
|
||||||
session.close()
|
session.close()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue