mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-05-24 01:12:54 -04:00
Refactor/group page (#666)
* refactor(backend): ♻️ Refactor base class to be abstract and create a router factory method * feat(frontend): ✨ add group edit * refactor(backend): ✨ add group edit support Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
parent
9b1bf56a5d
commit
990244e37e
25
frontend/api/class-interfaces/group-webhooks.ts
Normal file
25
frontend/api/class-interfaces/group-webhooks.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { BaseCRUDAPI } from "./_base";
|
||||
|
||||
const prefix = "/api";
|
||||
|
||||
const routes = {
|
||||
webhooks: `${prefix}/groups/webhooks`,
|
||||
webhooksId: (id: string | number) => `${prefix}/groups/webhooks/${id}`,
|
||||
};
|
||||
|
||||
export interface CreateGroupWebhook {
|
||||
enabled: boolean;
|
||||
name: string;
|
||||
url: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
export interface GroupWebhook extends CreateGroupWebhook {
|
||||
id: string;
|
||||
groupId: string;
|
||||
}
|
||||
|
||||
export class WebhooksAPI extends BaseCRUDAPI<GroupWebhook, CreateGroupWebhook> {
|
||||
baseRoute = routes.webhooks;
|
||||
itemRoute = routes.webhooksId;
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import { requests } from "../requests";
|
||||
import { BaseCRUDAPI } from "./_base";
|
||||
import { GroupInDB } from "~/types/api-types/user";
|
||||
|
||||
@ -7,10 +6,17 @@ const prefix = "/api";
|
||||
const routes = {
|
||||
groups: `${prefix}/groups`,
|
||||
groupsSelf: `${prefix}/groups/self`,
|
||||
categories: `${prefix}/groups/categories`,
|
||||
|
||||
groupsId: (id: string | number) => `${prefix}/groups/${id}`,
|
||||
};
|
||||
|
||||
interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface CreateGroup {
|
||||
name: string;
|
||||
}
|
||||
@ -21,6 +27,14 @@ export class GroupAPI extends BaseCRUDAPI<GroupInDB, CreateGroup> {
|
||||
/** Returns the Group Data for the Current User
|
||||
*/
|
||||
async getCurrentUserGroup() {
|
||||
return await requests.get(routes.groupsSelf);
|
||||
return await this.requests.get(routes.groupsSelf);
|
||||
}
|
||||
|
||||
async getCategories() {
|
||||
return await this.requests.get<Category[]>(routes.categories);
|
||||
}
|
||||
|
||||
async setCategories(payload: Category[]) {
|
||||
return await this.requests.put<Category[]>(routes.categories, payload);
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import { NotificationsAPI } from "./class-interfaces/event-notifications";
|
||||
import { FoodAPI } from "./class-interfaces/recipe-foods";
|
||||
import { UnitAPI } from "./class-interfaces/recipe-units";
|
||||
import { CookbookAPI } from "./class-interfaces/cookbooks";
|
||||
import { WebhooksAPI } from "./class-interfaces/group-webhooks";
|
||||
import { ApiRequestInstance } from "~/types/api";
|
||||
|
||||
class Api {
|
||||
@ -29,6 +30,7 @@ class Api {
|
||||
public foods: FoodAPI;
|
||||
public units: UnitAPI;
|
||||
public cookbooks: CookbookAPI;
|
||||
public groupWebhooks: WebhooksAPI;
|
||||
|
||||
// Utils
|
||||
public upload: UploadFile;
|
||||
@ -49,6 +51,7 @@ class Api {
|
||||
this.users = new UserApi(requests);
|
||||
this.groups = new GroupAPI(requests);
|
||||
this.cookbooks = new CookbookAPI(requests);
|
||||
this.groupWebhooks = new WebhooksAPI(requests);
|
||||
|
||||
// Admin
|
||||
this.debug = new DebugAPI(requests);
|
||||
|
75
frontend/composables/use-group-webhooks.ts
Normal file
75
frontend/composables/use-group-webhooks.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { useAsync, ref } from "@nuxtjs/composition-api";
|
||||
import { useAsyncKey } from "./use-utils";
|
||||
import { useApiSingleton } from "~/composables/use-api";
|
||||
import { GroupWebhook } from "~/api/class-interfaces/group-webhooks";
|
||||
|
||||
export const useGroupWebhooks = function () {
|
||||
const api = useApiSingleton();
|
||||
const loading = ref(false);
|
||||
const validForm = ref(true);
|
||||
|
||||
const actions = {
|
||||
getAll() {
|
||||
loading.value = true;
|
||||
const units = useAsync(async () => {
|
||||
const { data } = await api.groupWebhooks.getAll();
|
||||
|
||||
return data;
|
||||
}, useAsyncKey());
|
||||
|
||||
loading.value = false;
|
||||
return units;
|
||||
},
|
||||
async refreshAll() {
|
||||
loading.value = true;
|
||||
const { data } = await api.groupWebhooks.getAll();
|
||||
|
||||
if (data) {
|
||||
webhooks.value = data;
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
},
|
||||
async createOne() {
|
||||
loading.value = true;
|
||||
|
||||
const payload = {
|
||||
enabled: false,
|
||||
name: "New Webhook",
|
||||
url: "",
|
||||
time: "00:00",
|
||||
};
|
||||
|
||||
const { data } = await api.groupWebhooks.createOne(payload);
|
||||
if (data) {
|
||||
this.refreshAll();
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
},
|
||||
async updateOne(updateData: GroupWebhook) {
|
||||
if (!updateData.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const { data } = await api.groupWebhooks.updateOne(updateData.id, updateData);
|
||||
if (data) {
|
||||
this.refreshAll();
|
||||
}
|
||||
loading.value = false;
|
||||
},
|
||||
|
||||
async deleteOne(id: string | number) {
|
||||
loading.value = true;
|
||||
const { data } = await api.groupWebhooks.deleteOne(id);
|
||||
if (data) {
|
||||
this.refreshAll();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const webhooks = actions.getAll();
|
||||
|
||||
return { webhooks, actions, validForm };
|
||||
};
|
@ -1,8 +1,33 @@
|
||||
import { useAsync, ref } from "@nuxtjs/composition-api";
|
||||
import { useAsyncKey } from "./use-utils";
|
||||
import { useApiSingleton } from "~/composables/use-api";
|
||||
import { CreateGroup } from "~/api/class-interfaces/groups";
|
||||
|
||||
export const useGroup = function () {
|
||||
const api = useApiSingleton();
|
||||
|
||||
const actions = {
|
||||
getAll() {
|
||||
const units = useAsync(async () => {
|
||||
const { data } = await api.groups.getCategories();
|
||||
return data;
|
||||
}, useAsyncKey());
|
||||
|
||||
return units;
|
||||
},
|
||||
async updateAll() {
|
||||
if (!categories.value) {
|
||||
return;
|
||||
}
|
||||
const { data } = await api.groups.setCategories(categories.value);
|
||||
categories.value = data;
|
||||
},
|
||||
};
|
||||
|
||||
const categories = actions.getAll();
|
||||
|
||||
return { actions, categories };
|
||||
};
|
||||
|
||||
export const useGroups = function () {
|
||||
const api = useApiSingleton();
|
||||
|
@ -61,6 +61,11 @@ export default defineComponent({
|
||||
to: "/user/group/cookbooks",
|
||||
title: this.$t("sidebar.cookbooks"),
|
||||
},
|
||||
{
|
||||
icon: this.$globals.icons.webhook,
|
||||
to: "/user/group/webhooks",
|
||||
title: "Webhooks",
|
||||
},
|
||||
],
|
||||
adminLinks: [
|
||||
{
|
||||
|
@ -31,7 +31,7 @@ import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||
import AppHeader from "@/components/Layout/AppHeader.vue";
|
||||
import AppSidebar from "@/components/Layout/AppSidebar.vue";
|
||||
import AppFloatingButton from "@/components/Layout/AppFloatingButton.vue";
|
||||
import { useCookbooks } from "~/composables/use-cookbooks";
|
||||
import { useCookbooks } from "~/composables/use-group-cookbooks";
|
||||
|
||||
export default defineComponent({
|
||||
components: { AppHeader, AppSidebar, AppFloatingButton },
|
||||
|
@ -25,7 +25,7 @@
|
||||
<script lang="ts">
|
||||
import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue";
|
||||
import { defineComponent, useRoute, ref } from "@nuxtjs/composition-api";
|
||||
import { useCookbook } from "~/composables/use-cookbooks";
|
||||
import { useCookbook } from "~/composables/use-group-cookbooks";
|
||||
export default defineComponent({
|
||||
components: { RecipeCardSection },
|
||||
setup() {
|
||||
|
@ -43,27 +43,24 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import { useCookbooks } from "@/composables/use-cookbooks";
|
||||
import { useCookbooks } from "@/composables/use-group-cookbooks";
|
||||
import draggable from "vuedraggable";
|
||||
|
||||
export default defineComponent({
|
||||
components: { draggable },
|
||||
layout: "admin",
|
||||
setup() {
|
||||
const { cookbooks, actions, workingCookbookData, deleteTargetId, validForm } = useCookbooks();
|
||||
const { cookbooks, actions } = useCookbooks();
|
||||
|
||||
return {
|
||||
cookbooks,
|
||||
actions,
|
||||
workingCookbookData,
|
||||
deleteTargetId,
|
||||
validForm,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style>
|
||||
.my-border {
|
||||
border-left: 5px solid var(--v-primary-base);
|
||||
}
|
||||
|
@ -1,21 +1,33 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<BaseCardSectionTitle title="Group Settings">
|
||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Alias error provident, eveniet, laboriosam assumenda
|
||||
earum amet quaerat vel consequatur molestias sed enim. Adipisci a consequuntur dolor culpa expedita voluptatem
|
||||
praesentium optio iste atque, ea reiciendis iure non aut suscipit modi ducimus ratione, quam numquam quaerat
|
||||
distinctio illum nemo. Dicta, doloremque!
|
||||
</BaseCardSectionTitle>
|
||||
<section>
|
||||
<BaseCardSectionTitle title="Group Settings">
|
||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Alias error provident, eveniet, laboriosam assumenda
|
||||
earum amet quaerat vel consequatur molestias sed enim. Adipisci a consequuntur dolor culpa expedita voluptatem
|
||||
praesentium optio iste atque, ea reiciendis iure non aut suscipit modi ducimus ratione, quam numquam quaerat
|
||||
distinctio illum nemo. Dicta, doloremque!
|
||||
</BaseCardSectionTitle>
|
||||
<div v-if="categories" class="d-flex">
|
||||
<DomainRecipeCategoryTagSelector v-model="categories" class="mt-5 mr-5" />
|
||||
<BaseButton save class="mt-auto mb-3" @click="actions.updateAll()" />
|
||||
</div>
|
||||
</section>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import { useGroup } from "~/composables/use-groups";
|
||||
|
||||
export default defineComponent({
|
||||
layout: "admin",
|
||||
setup() {
|
||||
return {};
|
||||
const { categories, actions } = useGroup();
|
||||
|
||||
return {
|
||||
categories,
|
||||
actions,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
68
frontend/pages/user/group/webhooks.vue
Normal file
68
frontend/pages/user/group/webhooks.vue
Normal file
@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<BaseCardSectionTitle title="MealPlan Webhooks">
|
||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Alias error provident, eveniet, laboriosam assumenda
|
||||
earum amet quaerat vel consequatur molestias sed enim. Adipisci a consequuntur dolor culpa expedita voluptatem
|
||||
praesentium optio iste atque, ea reiciendis iure non aut suscipit modi ducimus ratione, quam numquam quaerat
|
||||
distinctio illum nemo. Dicta, doloremque!
|
||||
</BaseCardSectionTitle>
|
||||
<BaseButton create @click="actions.createOne()" />
|
||||
<v-expansion-panels class="mt-2">
|
||||
<v-expansion-panel v-for="(webhook, index) in webhooks" :key="index" class="my-2 my-border rounded">
|
||||
<v-expansion-panel-header disable-icon-rotate class="headline">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon large left :color="webhook.enabled ? 'info' : null">
|
||||
{{ $globals.icons.webhook }}
|
||||
</v-icon>
|
||||
{{ webhook.name }} - {{ webhook.time }}
|
||||
</div>
|
||||
<template #actions>
|
||||
<v-btn color="info" fab small class="ml-2">
|
||||
<v-icon color="white">
|
||||
{{ $globals.icons.edit }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<v-card-text>
|
||||
<v-switch v-model="webhook.enabled" label="Enabled"></v-switch>
|
||||
<v-text-field v-model="webhook.name" label="Webhook Name"></v-text-field>
|
||||
<v-text-field v-model="webhook.url" label="Webhook Url"></v-text-field>
|
||||
<v-time-picker v-model="webhook.time" class="elevation-2" ampm-in-title format="ampm"></v-time-picker>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<BaseButton secondary color="info">
|
||||
<template #icon>
|
||||
{{ $globals.icons.testTube }}
|
||||
</template>
|
||||
Test
|
||||
</BaseButton>
|
||||
<v-spacer></v-spacer>
|
||||
<BaseButton delete @click="actions.deleteOne(webhook.id)" />
|
||||
<BaseButton save @click="actions.updateOne(webhook)" />
|
||||
</v-card-actions>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import { useGroupWebhooks } from "~/composables/use-group-webhooks";
|
||||
|
||||
export default defineComponent({
|
||||
layout: "admin",
|
||||
setup() {
|
||||
const { actions, webhooks } = useGroupWebhooks();
|
||||
return {
|
||||
actions,
|
||||
webhooks,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
@ -26,6 +26,7 @@ app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||
|
||||
|
||||
def start_scheduler():
|
||||
return # TODO: Disable Scheduler for now
|
||||
import mealie.services.scheduler.scheduled_jobs # noqa: F401
|
||||
|
||||
|
||||
|
@ -6,6 +6,7 @@ from mealie.db.data_access_layer.group_access_model import GroupDataAccessModel
|
||||
from mealie.db.models.cookbook import CookBook
|
||||
from mealie.db.models.event import Event, EventNotification
|
||||
from mealie.db.models.group import Group
|
||||
from mealie.db.models.group.webhooks import GroupWebhooksModel
|
||||
from mealie.db.models.mealplan import MealPlan
|
||||
from mealie.db.models.recipe.category import Category
|
||||
from mealie.db.models.recipe.comment import RecipeComment
|
||||
@ -19,6 +20,7 @@ from mealie.schema.admin import SiteSettings as SiteSettingsSchema
|
||||
from mealie.schema.cookbook import ReadCookBook
|
||||
from mealie.schema.events import Event as EventSchema
|
||||
from mealie.schema.events import EventNotificationIn
|
||||
from mealie.schema.group.webhook import ReadWebhook
|
||||
from mealie.schema.meal_plan import MealPlanOut, ShoppingListOut
|
||||
from mealie.schema.recipe import (
|
||||
CommentOut,
|
||||
@ -42,7 +44,6 @@ DEFAULT_PK = "id"
|
||||
|
||||
class CategoryDataAccessModel(BaseAccessModel):
|
||||
def get_empty(self, session: Session):
|
||||
self.schema
|
||||
return session.query(Category).filter(~Category.recipes.any()).all()
|
||||
|
||||
|
||||
@ -77,10 +78,13 @@ class DatabaseAccessLayer:
|
||||
self.event_notifications = BaseAccessModel(DEFAULT_PK, EventNotification, EventNotificationIn)
|
||||
self.events = BaseAccessModel(DEFAULT_PK, Event, EventSchema)
|
||||
|
||||
# Users / Groups
|
||||
# Users
|
||||
self.users = UserDataAccessModel(DEFAULT_PK, User, PrivateUser)
|
||||
self.api_tokens = BaseAccessModel(DEFAULT_PK, LongLiveToken, LongLiveTokenInDB)
|
||||
|
||||
# Group Data
|
||||
self.groups = GroupDataAccessModel(DEFAULT_PK, Group, GroupInDB)
|
||||
self.meals = BaseAccessModel(DEFAULT_PK, MealPlan, MealPlanOut)
|
||||
self.webhooks = BaseAccessModel(DEFAULT_PK, GroupWebhooksModel, ReadWebhook)
|
||||
self.shopping_lists = BaseAccessModel(DEFAULT_PK, ShoppingList, ShoppingListOut)
|
||||
self.cookbooks = BaseAccessModel(DEFAULT_PK, CookBook, ReadCookBook)
|
||||
|
@ -9,7 +9,7 @@ def handle_one_to_many_list(get_attr, relation_cls, all_elements: list[dict]):
|
||||
updated_elems = []
|
||||
|
||||
for elem in all_elements:
|
||||
elem_id = elem.get("id", None)
|
||||
elem_id = elem.get(get_attr, None)
|
||||
|
||||
existing_elem = relation_cls.get_ref(match_value=elem_id)
|
||||
|
||||
|
1
mealie/db/models/group/__init__.py
Normal file
1
mealie/db/models/group/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .group import *
|
@ -5,14 +5,10 @@ from sqlalchemy.orm.session import Session
|
||||
from mealie.core.config import settings
|
||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||
from mealie.db.models.cookbook import CookBook
|
||||
from mealie.db.models.group.webhooks import GroupWebhooksModel
|
||||
from mealie.db.models.recipe.category import Category, group2categories
|
||||
|
||||
|
||||
class WebhookURLModel(SqlAlchemyBase):
|
||||
__tablename__ = "webhook_urls"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
url = sa.Column(sa.String)
|
||||
parent_id = sa.Column(sa.Integer, sa.ForeignKey("groups.id"))
|
||||
from .._model_utils import auto_init
|
||||
|
||||
|
||||
class Group(SqlAlchemyBase, BaseMixins):
|
||||
@ -20,25 +16,17 @@ class Group(SqlAlchemyBase, BaseMixins):
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
name = sa.Column(sa.String, index=True, nullable=False, unique=True)
|
||||
users = orm.relationship("User", back_populates="group")
|
||||
categories = orm.relationship(Category, secondary=group2categories, single_parent=True)
|
||||
|
||||
# CRUD From Others
|
||||
mealplans = orm.relationship("MealPlan", back_populates="group", single_parent=True, order_by="MealPlan.start_date")
|
||||
shopping_lists = orm.relationship("ShoppingList", back_populates="group", single_parent=True)
|
||||
webhooks = orm.relationship(GroupWebhooksModel, uselist=True, cascade="all, delete-orphan")
|
||||
cookbooks = orm.relationship(CookBook, back_populates="group", single_parent=True)
|
||||
categories = orm.relationship("Category", secondary=group2categories, single_parent=True)
|
||||
shopping_lists = orm.relationship("ShoppingList", back_populates="group", single_parent=True)
|
||||
|
||||
# Webhook Settings
|
||||
webhook_enable = sa.Column(sa.Boolean, default=False)
|
||||
webhook_time = sa.Column(sa.String, default="00:00")
|
||||
webhook_urls = orm.relationship("WebhookURLModel", uselist=True, cascade="all, delete-orphan")
|
||||
|
||||
def __init__(
|
||||
self, name, categories=[], session=None, webhook_enable=False, webhook_time="00:00", webhook_urls=[], **_
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.categories = [Category.get_ref(session=session, slug=cat.get("slug")) for cat in categories]
|
||||
|
||||
self.webhook_enable = webhook_enable
|
||||
self.webhook_time = webhook_time
|
||||
self.webhook_urls = [WebhookURLModel(url=x) for x in webhook_urls]
|
||||
@auto_init({"users", "webhooks", "shopping_lists", "cookbooks"})
|
||||
def __init__(self, **_) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_ref(session: Session, name: str):
|
20
mealie/db/models/group/webhooks.py
Normal file
20
mealie/db/models/group/webhooks.py
Normal file
@ -0,0 +1,20 @@
|
||||
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
|
||||
|
||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||
|
||||
from .._model_utils import auto_init
|
||||
|
||||
|
||||
class GroupWebhooksModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "webhook_urls"
|
||||
id = Column(Integer, primary_key=True)
|
||||
group_id = Column(Integer, ForeignKey("groups.id"), index=True)
|
||||
|
||||
enabled = Column(Boolean, default=False)
|
||||
name = Column(String)
|
||||
url = Column(String)
|
||||
time = Column(String, default="00:00")
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
pass
|
@ -61,7 +61,6 @@ class Category(SqlAlchemyBase, BaseMixins):
|
||||
if not session or not match_value:
|
||||
return None
|
||||
|
||||
print(match_value)
|
||||
slug = slugify(match_value)
|
||||
|
||||
result = session.query(Category).filter(Category.slug == slug).one_or_none()
|
||||
|
@ -1,9 +1,18 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from . import cookbooks, crud
|
||||
from mealie.services.base_http_service import RouterFactory
|
||||
from mealie.services.cookbook.cookbook_service import CookbookService
|
||||
from mealie.services.group.webhook_service import WebhookService
|
||||
|
||||
from . import categories, crud, self_service
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(cookbooks.user_router)
|
||||
webhook_router = RouterFactory(service=WebhookService, prefix="/groups/webhooks", tags=["Groups: Webhooks"])
|
||||
cookbook_router = RouterFactory(service=CookbookService, prefix="/groups/cookbooks", tags=["Groups: Cookbooks"])
|
||||
router.include_router(self_service.user_router)
|
||||
router.include_router(cookbook_router)
|
||||
router.include_router(categories.user_router)
|
||||
router.include_router(webhook_router)
|
||||
router.include_router(crud.user_router)
|
||||
router.include_router(crud.admin_router)
|
||||
|
22
mealie/routes/groups/categories.py
Normal file
22
mealie/routes/groups/categories.py
Normal file
@ -0,0 +1,22 @@
|
||||
from fastapi import Depends
|
||||
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.schema.recipe.recipe_category import CategoryBase
|
||||
from mealie.services.group.group_service import GroupSelfService
|
||||
|
||||
user_router = UserAPIRouter(prefix="/groups/categories", tags=["Groups: Mealplan Categories"])
|
||||
|
||||
|
||||
@user_router.get("", response_model=list[CategoryBase])
|
||||
def get_mealplan_categories(group_service: GroupSelfService = Depends(GroupSelfService.read_existing)):
|
||||
return group_service.item.categories
|
||||
|
||||
|
||||
@user_router.put("", response_model=list[CategoryBase])
|
||||
def update_mealplan_categories(
|
||||
new_categories: list[CategoryBase], group_service: GroupSelfService = Depends(GroupSelfService.write_existing)
|
||||
):
|
||||
|
||||
items = group_service.update_categories(new_categories)
|
||||
|
||||
return items.categories
|
@ -1,49 +0,0 @@
|
||||
from fastapi import Depends
|
||||
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.schema.cookbook.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook
|
||||
from mealie.services.cookbook import CookbookService
|
||||
|
||||
user_router = UserAPIRouter(prefix="/groups/cookbooks", tags=["Groups: Cookbooks"])
|
||||
|
||||
|
||||
@user_router.get("", response_model=list[ReadCookBook])
|
||||
def get_all_cookbook(cb_service: CookbookService = Depends(CookbookService.private)):
|
||||
""" Get cookbook from the Database """
|
||||
# Get Item
|
||||
return cb_service.get_all()
|
||||
|
||||
|
||||
@user_router.post("", response_model=ReadCookBook)
|
||||
def create_cookbook(data: CreateCookBook, cb_service: CookbookService = Depends(CookbookService.private)):
|
||||
""" Create cookbook in the Database """
|
||||
# Create Item
|
||||
return cb_service.create_one(data)
|
||||
|
||||
|
||||
@user_router.put("", response_model=list[ReadCookBook])
|
||||
def update_many(data: list[ReadCookBook], cb_service: CookbookService = Depends(CookbookService.private)):
|
||||
""" Create cookbook in the Database """
|
||||
# Create Item
|
||||
return cb_service.update_many(data)
|
||||
|
||||
|
||||
@user_router.get("/{id}", response_model=RecipeCookBook)
|
||||
def get_cookbook(cb_service: CookbookService = Depends(CookbookService.write_existing)):
|
||||
""" Get cookbook from the Database """
|
||||
# Get Item
|
||||
return cb_service.cookbook
|
||||
|
||||
|
||||
@user_router.put("/{id}")
|
||||
def update_cookbook(data: CreateCookBook, cb_service: CookbookService = Depends(CookbookService.write_existing)):
|
||||
""" Update cookbook in the Database """
|
||||
# Update Item
|
||||
return cb_service.update_one(data)
|
||||
|
||||
|
||||
@user_router.delete("/{id}")
|
||||
def delete_cookbook(cd_service: CookbookService = Depends(CookbookService.write_existing)):
|
||||
""" Delete cookbook from the Database """
|
||||
# Delete Item
|
||||
return cd_service.delete_one()
|
@ -12,17 +12,6 @@ admin_router = AdminAPIRouter(prefix="/groups", tags=["Groups: CRUD"])
|
||||
user_router = UserAPIRouter(prefix="/groups", tags=["Groups: CRUD"])
|
||||
|
||||
|
||||
@user_router.get("/self", response_model=GroupInDB)
|
||||
async def get_current_user_group(
|
||||
current_user: PrivateUser = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Returns the Group Data for the Current User """
|
||||
current_user: PrivateUser
|
||||
|
||||
return db.groups.get(session, current_user.group, "name")
|
||||
|
||||
|
||||
@admin_router.get("", response_model=list[GroupInDB])
|
||||
async def get_all_groups(
|
||||
session: Session = Depends(generate_session),
|
||||
|
14
mealie/routes/groups/self_service.py
Normal file
14
mealie/routes/groups/self_service.py
Normal file
@ -0,0 +1,14 @@
|
||||
from fastapi import Depends
|
||||
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.schema.user.user import GroupInDB
|
||||
from mealie.services.group.group_service import GroupSelfService
|
||||
|
||||
user_router = UserAPIRouter(prefix="/groups/self", tags=["Groups: Self Service"])
|
||||
|
||||
|
||||
@user_router.get("", response_model=GroupInDB)
|
||||
async def get_logged_in_user_group(g_self_service: GroupSelfService = Depends(GroupSelfService.write_existing)):
|
||||
""" Returns the Group Data for the Current User """
|
||||
|
||||
return g_self_service.item
|
@ -27,7 +27,7 @@ logger = get_logger()
|
||||
@public_router.get("/{slug}", response_model=Recipe)
|
||||
def get_recipe(recipe_service: RecipeService = Depends(RecipeService.read_existing)):
|
||||
""" Takes in a recipe slug, returns all data for a recipe """
|
||||
return recipe_service.recipe
|
||||
return recipe_service.item
|
||||
|
||||
|
||||
@user_router.post("", status_code=201, response_model=str)
|
||||
|
1
mealie/schema/group/__init__.py
Normal file
1
mealie/schema/group/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .webhook import *
|
19
mealie/schema/group/webhook.py
Normal file
19
mealie/schema/group/webhook.py
Normal file
@ -0,0 +1,19 @@
|
||||
from fastapi_camelcase import CamelModel
|
||||
|
||||
|
||||
class CreateWebhook(CamelModel):
|
||||
enabled: bool = True
|
||||
name: str = ""
|
||||
url: str = ""
|
||||
time: str = "00:00"
|
||||
|
||||
|
||||
class SaveWebhook(CreateWebhook):
|
||||
group_id: int
|
||||
|
||||
|
||||
class ReadWebhook(SaveWebhook):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
@ -1,4 +1,4 @@
|
||||
from typing import Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
from fastapi_camelcase import CamelModel
|
||||
from pydantic.types import constr
|
||||
@ -119,9 +119,7 @@ class UpdateGroup(GroupBase):
|
||||
name: str
|
||||
categories: Optional[list[CategoryBase]] = []
|
||||
|
||||
webhook_urls: list[str] = []
|
||||
webhook_time: str = "00:00"
|
||||
webhook_enable: bool
|
||||
webhooks: list[Any] = []
|
||||
|
||||
|
||||
class GroupInDB(UpdateGroup):
|
||||
@ -136,7 +134,6 @@ class GroupInDB(UpdateGroup):
|
||||
def getter_dict(_cls, orm_model: Group):
|
||||
return {
|
||||
**GetterDict(orm_model),
|
||||
"webhook_urls": [x.url for x in orm_model.webhook_urls if x],
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,2 +1,3 @@
|
||||
from .base_http_service import *
|
||||
from .base_service import *
|
||||
from .router_factory import *
|
||||
|
@ -1,6 +1,7 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Callable, Generic, TypeVar
|
||||
|
||||
from fastapi import BackgroundTasks, Depends
|
||||
from fastapi import BackgroundTasks, Depends, HTTPException, status
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.core.config import get_app_dirs, get_settings
|
||||
@ -16,13 +17,13 @@ T = TypeVar("T")
|
||||
D = TypeVar("D")
|
||||
|
||||
|
||||
class BaseHttpService(Generic[T, D]):
|
||||
class BaseHttpService(Generic[T, D], ABC):
|
||||
"""The BaseHttpService class is a generic class that can be used to create
|
||||
http services that are injected via `Depends` into a route function. To use,
|
||||
you must define the Generic type arguments:
|
||||
|
||||
`T`: The type passed into the *_existing functions (e.g. id) which is then passed into assert_existing
|
||||
`D`: Not yet implemented
|
||||
`D`: Item returned from database layer
|
||||
|
||||
Child Requirements:
|
||||
Define the following functions:
|
||||
@ -32,8 +33,29 @@ class BaseHttpService(Generic[T, D]):
|
||||
`event_func`: A function that is called when an event is created.
|
||||
"""
|
||||
|
||||
item: D = None
|
||||
|
||||
# Function that Generate Corrsesponding Routes through RouterFactor
|
||||
get_all: Callable = None
|
||||
create_one: Callable = None
|
||||
update_one: Callable = None
|
||||
update_many: Callable = None
|
||||
populate_item: Callable = None
|
||||
delete_one: Callable = None
|
||||
delete_all: Callable = None
|
||||
|
||||
# Type Definitions
|
||||
_schema = None
|
||||
_create_schema = None
|
||||
_update_schema = None
|
||||
|
||||
# Function called to create a server side event
|
||||
event_func: Callable = None
|
||||
|
||||
# Config
|
||||
_restrict_by_group = False
|
||||
_group_id_cache = None
|
||||
|
||||
def __init__(self, session: Session, user: PrivateUser, background_tasks: BackgroundTasks = None) -> None:
|
||||
self.session = session or SessionLocal()
|
||||
self.user = user
|
||||
@ -45,33 +67,32 @@ class BaseHttpService(Generic[T, D]):
|
||||
self.app_dirs = get_app_dirs()
|
||||
self.settings = get_settings()
|
||||
|
||||
def assert_existing(self, data: T) -> None:
|
||||
raise NotImplementedError("`assert_existing` must by implemented by child class")
|
||||
|
||||
def _create_event(self, title: str, message: str) -> None:
|
||||
if not self.__class__.event_func:
|
||||
raise NotImplementedError("`event_func` must be set by child class")
|
||||
|
||||
self.background_tasks.add_task(self.__class__.event_func, title, message, self.session)
|
||||
@property
|
||||
def group_id(self):
|
||||
# TODO: Populate Group in Private User Call WARNING: May require significant refactoring
|
||||
if not self._group_id_cache:
|
||||
group = self.db.groups.get(self.session, self.user.group, "name")
|
||||
self._group_id_cache = group.id
|
||||
return self._group_id_cache
|
||||
|
||||
@classmethod
|
||||
def read_existing(cls, id: T, deps: ReadDeps = Depends()):
|
||||
def read_existing(cls, item_id: T, deps: ReadDeps = Depends()):
|
||||
"""
|
||||
Used for dependency injection for routes that require an existing recipe. If the recipe doesn't exist
|
||||
or the user doens't not have the required permissions, the proper HTTP Status code will be raised.
|
||||
"""
|
||||
new_class = cls(deps.session, deps.user, deps.bg_task)
|
||||
new_class.assert_existing(id)
|
||||
new_class.assert_existing(item_id)
|
||||
return new_class
|
||||
|
||||
@classmethod
|
||||
def write_existing(cls, id: T, deps: WriteDeps = Depends()):
|
||||
def write_existing(cls, item_id: T, deps: WriteDeps = Depends()):
|
||||
"""
|
||||
Used for dependency injection for routes that require an existing recipe. The only difference between
|
||||
read_existing and write_existing is that the user is required to be logged in on write_existing method.
|
||||
"""
|
||||
new_class = cls(deps.session, deps.user, deps.bg_task)
|
||||
new_class.assert_existing(id)
|
||||
new_class.assert_existing(item_id)
|
||||
return new_class
|
||||
|
||||
@classmethod
|
||||
@ -87,3 +108,27 @@ class BaseHttpService(Generic[T, D]):
|
||||
A Base instance to be used as a router dependency
|
||||
"""
|
||||
return cls(deps.session, deps.user, deps.bg_task)
|
||||
|
||||
@abstractmethod
|
||||
def populate_item(self) -> None:
|
||||
...
|
||||
|
||||
def assert_existing(self, id: T) -> None:
|
||||
self.populate_item(id)
|
||||
self._check_item()
|
||||
|
||||
def _check_item(self) -> None:
|
||||
if not self.item:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if self.__class__._restrict_by_group:
|
||||
group_id = getattr(self.item, "group_id", False)
|
||||
|
||||
if not group_id or group_id != self.group_id:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def _create_event(self, title: str, message: str) -> None:
|
||||
if not self.__class__.event_func:
|
||||
raise NotImplementedError("`event_func` must be set by child class")
|
||||
|
||||
self.background_tasks.add_task(self.__class__.event_func, title, message, self.session)
|
||||
|
190
mealie/services/base_http_service/router_factory.py
Normal file
190
mealie/services/base_http_service/router_factory.py
Normal file
@ -0,0 +1,190 @@
|
||||
from typing import Any, Callable, Optional, Sequence, Type, TypeVar
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.params import Depends
|
||||
from fastapi.types import DecoratedCallable
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .base_http_service import BaseHttpService
|
||||
|
||||
""""
|
||||
This code is largely based off of the FastAPI Crud Router
|
||||
https://github.com/awtkns/fastapi-crudrouter/blob/master/fastapi_crudrouter/core/_base.py
|
||||
"""
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
S = TypeVar("S", bound=BaseHttpService)
|
||||
DEPENDENCIES = Optional[Sequence[Depends]]
|
||||
|
||||
|
||||
class RouterFactory(APIRouter):
|
||||
schema: Type[T]
|
||||
create_schema: Type[T]
|
||||
update_schema: Type[T]
|
||||
_base_path: str = "/"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
service: Type[S],
|
||||
prefix: Optional[str] = None,
|
||||
tags: Optional[list[str]] = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
self.service: Type[S] = service
|
||||
self.schema: Type[T] = service._schema
|
||||
|
||||
# HACK: Special Case for Coobooks, not sure this is a good way to handle the abstraction :/
|
||||
if hasattr(self.service, "_get_one_schema"):
|
||||
self.get_one_schema = self.service._get_one_schema
|
||||
else:
|
||||
self.get_one_schema = self.schema
|
||||
|
||||
self.update_schema: Type[T] = service._update_schema
|
||||
self.create_schema: Type[T] = service._create_schema
|
||||
|
||||
prefix = str(prefix or self.schema.__name__).lower()
|
||||
prefix = self._base_path + prefix.strip("/")
|
||||
tags = tags or [prefix.strip("/").capitalize()]
|
||||
|
||||
super().__init__(prefix=prefix, tags=tags, **kwargs)
|
||||
|
||||
if self.service.get_all:
|
||||
self._add_api_route(
|
||||
"",
|
||||
self._get_all(),
|
||||
methods=["GET"],
|
||||
response_model=Optional[list[self.schema]], # type: ignore
|
||||
summary="Get All",
|
||||
)
|
||||
|
||||
if self.service.create_one:
|
||||
self._add_api_route(
|
||||
"",
|
||||
self._create(),
|
||||
methods=["POST"],
|
||||
response_model=self.schema,
|
||||
summary="Create One",
|
||||
)
|
||||
|
||||
if self.service.update_many:
|
||||
self._add_api_route(
|
||||
"",
|
||||
self._update_many(),
|
||||
methods=["PUT"],
|
||||
response_model=Optional[list[self.schema]], # type: ignore
|
||||
summary="Update Many",
|
||||
)
|
||||
|
||||
if self.service.delete_all:
|
||||
self._add_api_route(
|
||||
"",
|
||||
self._delete_all(),
|
||||
methods=["DELETE"],
|
||||
response_model=Optional[list[self.schema]], # type: ignore
|
||||
summary="Delete All",
|
||||
)
|
||||
|
||||
if self.service.populate_item:
|
||||
self._add_api_route(
|
||||
"/{item_id}",
|
||||
self._get_one(),
|
||||
methods=["GET"],
|
||||
response_model=self.get_one_schema,
|
||||
summary="Get One",
|
||||
)
|
||||
|
||||
if self.service.update_one:
|
||||
self._add_api_route(
|
||||
"/{item_id}",
|
||||
self._update(),
|
||||
methods=["PUT"],
|
||||
response_model=self.schema,
|
||||
summary="Update One",
|
||||
)
|
||||
|
||||
if self.service.delete_one:
|
||||
self._add_api_route(
|
||||
"/{item_id}",
|
||||
self._delete_one(),
|
||||
methods=["DELETE"],
|
||||
response_model=self.schema,
|
||||
summary="Delete One",
|
||||
)
|
||||
|
||||
def _add_api_route(self, path: str, endpoint: Callable[..., Any], **kwargs: Any) -> None:
|
||||
dependencies = []
|
||||
super().add_api_route(path, endpoint, dependencies=dependencies, **kwargs)
|
||||
|
||||
def api_route(self, path: str, *args: Any, **kwargs: Any) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
"""Overrides and exiting route if it exists"""
|
||||
methods = kwargs["methods"] if "methods" in kwargs else ["GET"]
|
||||
self.remove_api_route(path, methods)
|
||||
return super().api_route(path, *args, **kwargs)
|
||||
|
||||
def get(self, path: str, *args: Any, **kwargs: Any) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
self.remove_api_route(path, ["Get"])
|
||||
return super().get(path, *args, **kwargs)
|
||||
|
||||
def post(self, path: str, *args: Any, **kwargs: Any) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
self.remove_api_route(path, ["POST"])
|
||||
return super().post(path, *args, **kwargs)
|
||||
|
||||
def put(self, path: str, *args: Any, **kwargs: Any) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
self.remove_api_route(path, ["PUT"])
|
||||
return super().put(path, *args, **kwargs)
|
||||
|
||||
def delete(self, path: str, *args: Any, **kwargs: Any) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
self.remove_api_route(path, ["DELETE"])
|
||||
return super().delete(path, *args, **kwargs)
|
||||
|
||||
def remove_api_route(self, path: str, methods: list[str]) -> None:
|
||||
methods_ = set(methods)
|
||||
|
||||
for route in self.routes:
|
||||
if route.path == f"{self.prefix}{path}" and route.methods == methods_:
|
||||
self.routes.remove(route)
|
||||
|
||||
def _get_all(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
||||
def route(service: S = Depends(self.service.private)) -> T: # type: ignore
|
||||
return service.get_all()
|
||||
|
||||
return route
|
||||
|
||||
def _get_one(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
||||
def route(service: S = Depends(self.service.write_existing)) -> T: # type: ignore
|
||||
return service.item
|
||||
|
||||
return route
|
||||
|
||||
def _create(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
||||
def route(data: self.create_schema, service: S = Depends(self.service.private)) -> T: # type: ignore
|
||||
return service.create_one(data)
|
||||
|
||||
return route
|
||||
|
||||
def _update(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
||||
def route(data: self.update_schema, service: S = Depends(self.service.write_existing)) -> T: # type: ignore
|
||||
return service.update_one(data)
|
||||
|
||||
return route
|
||||
|
||||
def _update_many(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
||||
def route(data: list[self.update_schema], service: S = Depends(self.service.write_existing)) -> T: # type: ignore
|
||||
return service.update_many(data)
|
||||
|
||||
return route
|
||||
|
||||
def _delete_one(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
||||
def route(service: S = Depends(self.service.write_existing)) -> T: # type: ignore
|
||||
return service.delete_one()
|
||||
|
||||
return route
|
||||
|
||||
def _delete_all(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def get_routes() -> list[str]:
|
||||
return ["get_all", "create", "delete_all", "get_one", "update", "delete_one"]
|
@ -3,55 +3,33 @@ from __future__ import annotations
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.schema.cookbook.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook
|
||||
from mealie.schema.cookbook.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook
|
||||
from mealie.services.base_http_service.base_http_service import BaseHttpService
|
||||
from mealie.services.events import create_group_event
|
||||
|
||||
logger = get_logger(module=__name__)
|
||||
|
||||
|
||||
class CookbookService(BaseHttpService[int, str]):
|
||||
"""
|
||||
Class Methods:
|
||||
`read_existing`: Reads an existing recipe from the database.
|
||||
`write_existing`: Updates an existing recipe in the database.
|
||||
`base`: Requires write permissions, but doesn't perform recipe checks
|
||||
"""
|
||||
|
||||
class CookbookService(BaseHttpService[int, ReadCookBook]):
|
||||
event_func = create_group_event
|
||||
cookbook: ReadCookBook # Required for proper type hints
|
||||
_restrict_by_group = True
|
||||
|
||||
_group_id_cache = None
|
||||
_schema = ReadCookBook
|
||||
_create_schema = CreateCookBook
|
||||
_update_schema = UpdateCookBook
|
||||
_get_one_schema = RecipeCookBook
|
||||
|
||||
@property
|
||||
def group_id(self):
|
||||
# TODO: Populate Group in Private User Call WARNING: May require significant refactoring
|
||||
if not self._group_id_cache:
|
||||
group = self.db.groups.get(self.session, self.user.group, "name")
|
||||
print(group)
|
||||
self._group_id_cache = group.id
|
||||
return self._group_id_cache
|
||||
|
||||
def assert_existing(self, id: str):
|
||||
self.populate_cookbook(id)
|
||||
|
||||
if not self.cookbook:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if self.cookbook.group_id != self.group_id:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def populate_cookbook(self, id: int | str):
|
||||
def populate_item(self, id: int | str):
|
||||
try:
|
||||
id = int(id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if isinstance(id, int):
|
||||
self.cookbook = self.db.cookbooks.get_one(self.session, id, override_schema=RecipeCookBook)
|
||||
self.item = self.db.cookbooks.get_one(self.session, id, override_schema=RecipeCookBook)
|
||||
|
||||
else:
|
||||
self.cookbook = self.db.cookbooks.get_one(self.session, id, key="slug", override_schema=RecipeCookBook)
|
||||
self.item = self.db.cookbooks.get_one(self.session, id, key="slug", override_schema=RecipeCookBook)
|
||||
|
||||
def get_all(self) -> list[ReadCookBook]:
|
||||
items = self.db.cookbooks.get(self.session, self.group_id, "group_id", limit=999)
|
||||
@ -60,22 +38,22 @@ class CookbookService(BaseHttpService[int, str]):
|
||||
|
||||
def create_one(self, data: CreateCookBook) -> ReadCookBook:
|
||||
try:
|
||||
self.cookbook = self.db.cookbooks.create(self.session, SaveCookBook(group_id=self.group_id, **data.dict()))
|
||||
self.item = self.db.cookbooks.create(self.session, SaveCookBook(group_id=self.group_id, **data.dict()))
|
||||
except Exception as ex:
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST, detail={"message": "PAGE_CREATION_ERROR", "exception": str(ex)}
|
||||
)
|
||||
|
||||
return self.cookbook
|
||||
return self.item
|
||||
|
||||
def update_one(self, data: CreateCookBook, id: int = None) -> ReadCookBook:
|
||||
if not self.cookbook:
|
||||
if not self.item:
|
||||
return
|
||||
|
||||
target_id = id or self.cookbook.id
|
||||
self.cookbook = self.db.cookbooks.update(self.session, target_id, data)
|
||||
target_id = id or self.item.id
|
||||
self.item = self.db.cookbooks.update(self.session, target_id, data)
|
||||
|
||||
return self.cookbook
|
||||
return self.item
|
||||
|
||||
def update_many(self, data: list[ReadCookBook]) -> list[ReadCookBook]:
|
||||
updated = []
|
||||
@ -87,10 +65,10 @@ class CookbookService(BaseHttpService[int, str]):
|
||||
return updated
|
||||
|
||||
def delete_one(self, id: int = None) -> ReadCookBook:
|
||||
if not self.cookbook:
|
||||
if not self.item:
|
||||
return
|
||||
|
||||
target_id = id or self.cookbook.id
|
||||
self.cookbook = self.db.cookbooks.delete(self.session, target_id)
|
||||
target_id = id or self.item.id
|
||||
self.item = self.db.cookbooks.delete(self.session, target_id)
|
||||
|
||||
return self.cookbook
|
||||
return self.item
|
||||
|
2
mealie/services/group/__init__.py
Normal file
2
mealie/services/group/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .group_service import *
|
||||
from .webhook_service import *
|
47
mealie/services/group/group_service.py
Normal file
47
mealie/services/group/group_service.py
Normal file
@ -0,0 +1,47 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
|
||||
from mealie.core.dependencies.grouped import WriteDeps
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.schema.recipe.recipe_category import CategoryBase
|
||||
from mealie.schema.user.user import GroupInDB
|
||||
from mealie.services.base_http_service.base_http_service import BaseHttpService
|
||||
from mealie.services.events import create_group_event
|
||||
|
||||
logger = get_logger(module=__name__)
|
||||
|
||||
|
||||
class GroupSelfService(BaseHttpService[int, str]):
|
||||
_restrict_by_group = True
|
||||
event_func = create_group_event
|
||||
item: GroupInDB
|
||||
|
||||
@classmethod
|
||||
def read_existing(cls, deps: WriteDeps = Depends()):
|
||||
"""Override parent method to remove `item_id` from arguments"""
|
||||
return super().read_existing(item_id=0, deps=deps)
|
||||
|
||||
@classmethod
|
||||
def write_existing(cls, deps: WriteDeps = Depends()):
|
||||
"""Override parent method to remove `item_id` from arguments"""
|
||||
return super().write_existing(item_id=0, deps=deps)
|
||||
|
||||
def assert_existing(self, _: str = None):
|
||||
self.populate_item()
|
||||
|
||||
if not self.item:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if self.item.id != self.group_id:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def populate_item(self, _: str = None):
|
||||
self.item = self.db.groups.get(self.session, self.group_id)
|
||||
|
||||
def update_categories(self, new_categories: list[CategoryBase]):
|
||||
if not self.item:
|
||||
return
|
||||
self.item.categories = new_categories
|
||||
|
||||
return self.db.groups.update(self.session, self.group_id, self.item)
|
54
mealie/services/group/webhook_service.py
Normal file
54
mealie/services/group/webhook_service.py
Normal file
@ -0,0 +1,54 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.schema.group import ReadWebhook
|
||||
from mealie.schema.group.webhook import CreateWebhook, SaveWebhook
|
||||
from mealie.services.base_http_service.base_http_service import BaseHttpService
|
||||
from mealie.services.events import create_group_event
|
||||
|
||||
logger = get_logger(module=__name__)
|
||||
|
||||
|
||||
class WebhookService(BaseHttpService[int, ReadWebhook]):
|
||||
event_func = create_group_event
|
||||
_restrict_by_group = True
|
||||
|
||||
_schema = ReadWebhook
|
||||
_create_schema = CreateWebhook
|
||||
_update_schema = CreateWebhook
|
||||
|
||||
def populate_item(self, id: int | str):
|
||||
self.item = self.db.webhooks.get_one(self.session, id)
|
||||
|
||||
def get_all(self) -> list[ReadWebhook]:
|
||||
return self.db.webhooks.get(self.session, self.group_id, match_key="group_id", limit=9999)
|
||||
|
||||
def create_one(self, data: CreateWebhook) -> ReadWebhook:
|
||||
try:
|
||||
self.item = self.db.webhooks.create(self.session, SaveWebhook(group_id=self.group_id, **data.dict()))
|
||||
except Exception as ex:
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST, detail={"message": "WEBHOOK_CREATION_ERROR", "exception": str(ex)}
|
||||
)
|
||||
|
||||
return self.item
|
||||
|
||||
def update_one(self, data: CreateWebhook, id: int = None) -> ReadWebhook:
|
||||
if not self.item:
|
||||
return
|
||||
|
||||
target_id = id or self.item.id
|
||||
self.item = self.db.webhooks.update(self.session, target_id, data)
|
||||
|
||||
return self.item
|
||||
|
||||
def delete_one(self, id: int = None) -> ReadWebhook:
|
||||
if not self.item:
|
||||
return
|
||||
|
||||
target_id = id or self.item.id
|
||||
self.db.webhooks.delete(self.session, target_id)
|
||||
|
||||
return self.item
|
@ -14,7 +14,7 @@ from mealie.services.events import create_recipe_event
|
||||
logger = get_logger(module=__name__)
|
||||
|
||||
|
||||
class RecipeService(BaseHttpService[str, str]):
|
||||
class RecipeService(BaseHttpService[str, Recipe]):
|
||||
"""
|
||||
Class Methods:
|
||||
`read_existing`: Reads an existing recipe from the database.
|
||||
@ -23,7 +23,6 @@ class RecipeService(BaseHttpService[str, str]):
|
||||
"""
|
||||
|
||||
event_func = create_recipe_event
|
||||
recipe: Recipe # Required for proper type hints
|
||||
|
||||
@classmethod
|
||||
def write_existing(cls, slug: str, deps: WriteDeps = Depends()):
|
||||
@ -34,17 +33,17 @@ class RecipeService(BaseHttpService[str, str]):
|
||||
return super().write_existing(slug, deps)
|
||||
|
||||
def assert_existing(self, slug: str):
|
||||
self.pupulate_recipe(slug)
|
||||
self.populate_item(slug)
|
||||
|
||||
if not self.recipe:
|
||||
if not self.item:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if not self.recipe.settings.public and not self.user:
|
||||
if not self.item.settings.public and not self.user:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def pupulate_recipe(self, slug: str) -> Recipe:
|
||||
self.recipe = self.db.recipes.get(self.session, slug)
|
||||
return self.recipe
|
||||
def populate_item(self, slug: str) -> Recipe:
|
||||
self.item = self.db.recipes.get(self.session, slug)
|
||||
return self.item
|
||||
|
||||
# CRUD METHODS
|
||||
def create_recipe(self, create_data: Union[Recipe, CreateRecipe]) -> Recipe:
|
||||
@ -52,34 +51,34 @@ class RecipeService(BaseHttpService[str, str]):
|
||||
create_data = Recipe(name=create_data.name)
|
||||
|
||||
try:
|
||||
self.recipe = self.db.recipes.create(self.session, create_data)
|
||||
self.item = self.db.recipes.create(self.session, create_data)
|
||||
except IntegrityError:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": "RECIPE_ALREADY_EXISTS"})
|
||||
|
||||
self._create_event(
|
||||
"Recipe Created (URL)",
|
||||
f"'{self.recipe.name}' by {self.user.username} \n {self.settings.BASE_URL}/recipe/{self.recipe.slug}",
|
||||
f"'{self.item.name}' by {self.user.username} \n {self.settings.BASE_URL}/recipe/{self.item.slug}",
|
||||
)
|
||||
|
||||
return self.recipe
|
||||
return self.item
|
||||
|
||||
def update_recipe(self, update_data: Recipe) -> Recipe:
|
||||
original_slug = self.recipe.slug
|
||||
original_slug = self.item.slug
|
||||
|
||||
try:
|
||||
self.recipe = self.db.recipes.update(self.session, original_slug, update_data)
|
||||
self.item = self.db.recipes.update(self.session, original_slug, update_data)
|
||||
except IntegrityError:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": "RECIPE_ALREADY_EXISTS"})
|
||||
|
||||
self._check_assets(original_slug)
|
||||
|
||||
return self.recipe
|
||||
return self.item
|
||||
|
||||
def patch_recipe(self, patch_data: Recipe) -> Recipe:
|
||||
original_slug = self.recipe.slug
|
||||
original_slug = self.item.slug
|
||||
|
||||
try:
|
||||
self.recipe = self.db.recipes.patch(
|
||||
self.item = self.db.recipes.patch(
|
||||
self.session, original_slug, patch_data.dict(exclude_unset=True, exclude_defaults=True)
|
||||
)
|
||||
except IntegrityError:
|
||||
@ -87,7 +86,7 @@ class RecipeService(BaseHttpService[str, str]):
|
||||
|
||||
self._check_assets(original_slug)
|
||||
|
||||
return self.recipe
|
||||
return self.item
|
||||
|
||||
def delete_recipe(self) -> Recipe:
|
||||
"""removes a recipe from the database and purges the existing files from the filesystem.
|
||||
@ -100,7 +99,7 @@ class RecipeService(BaseHttpService[str, str]):
|
||||
"""
|
||||
|
||||
try:
|
||||
recipe: Recipe = self.db.recipes.delete(self.session, self.recipe.slug)
|
||||
recipe: Recipe = self.db.recipes.delete(self.session, self.item.slug)
|
||||
self._delete_assets()
|
||||
except Exception:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
@ -110,18 +109,18 @@ class RecipeService(BaseHttpService[str, str]):
|
||||
|
||||
def _check_assets(self, original_slug: str) -> None:
|
||||
"""Checks if the recipe slug has changed, and if so moves the assets to a new file with the new slug."""
|
||||
if original_slug != self.recipe.slug:
|
||||
if original_slug != self.item.slug:
|
||||
current_dir = self.app_dirs.RECIPE_DATA_DIR.joinpath(original_slug)
|
||||
|
||||
try:
|
||||
copytree(current_dir, self.recipe.directory, dirs_exist_ok=True)
|
||||
logger.info(f"Renaming Recipe Directory: {original_slug} -> {self.recipe.slug}")
|
||||
copytree(current_dir, self.item.directory, dirs_exist_ok=True)
|
||||
logger.info(f"Renaming Recipe Directory: {original_slug} -> {self.item.slug}")
|
||||
except FileNotFoundError:
|
||||
logger.error(f"Recipe Directory not Found: {original_slug}")
|
||||
|
||||
all_asset_files = [x.file_name for x in self.recipe.assets]
|
||||
all_asset_files = [x.file_name for x in self.item.assets]
|
||||
|
||||
for file in self.recipe.asset_dir.iterdir():
|
||||
for file in self.item.asset_dir.iterdir():
|
||||
file: Path
|
||||
if file.is_dir():
|
||||
continue
|
||||
@ -129,6 +128,6 @@ class RecipeService(BaseHttpService[str, str]):
|
||||
file.unlink()
|
||||
|
||||
def _delete_assets(self) -> None:
|
||||
recipe_dir = self.recipe.directory
|
||||
recipe_dir = self.item.directory
|
||||
rmtree(recipe_dir, ignore_errors=True)
|
||||
logger.info(f"Recipe Directory Removed: {self.recipe.slug}")
|
||||
logger.info(f"Recipe Directory Removed: {self.item.slug}")
|
||||
|
@ -29,9 +29,7 @@ def test_update_group(api_client: TestClient, api_routes: AppRoutes, admin_token
|
||||
"name": "New Group Name",
|
||||
"id": 2,
|
||||
"categories": [],
|
||||
"webhookUrls": [],
|
||||
"webhookTime": "00:00",
|
||||
"webhookEnable": False,
|
||||
"webhooks": [],
|
||||
"users": [],
|
||||
"mealplans": [],
|
||||
"shoppingLists": [],
|
||||
|
Loading…
x
Reference in New Issue
Block a user