feat: additional events dispatch (#1999)

* formatting

* add new user type

* add registration event

* publish events on new mealplan

* update UI for supported types + type updates
This commit is contained in:
Hayden 2023-01-07 11:18:08 -08:00 committed by GitHub
parent c33b14e3d2
commit fc92c39b7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 177 additions and 124 deletions

View File

@ -3,7 +3,7 @@
<BaseDialog <BaseDialog
v-model="deleteDialog" v-model="deleteDialog"
color="error" color="error"
:title="$t('general.confirm')" :title="$tc('general.confirm')"
:icon="$globals.icons.alertCircle" :icon="$globals.icons.alertCircle"
@confirm="deleteNotifier(deleteTargetId)" @confirm="deleteNotifier(deleteTargetId)"
> >
@ -35,8 +35,6 @@
</div> </div>
</BasePageTitle> </BasePageTitle>
<BannerExperimental issue="https://github.com/hay-kot/mealie/issues/833" />
<BaseButton create @click="createDialog = true" /> <BaseButton create @click="createDialog = true" />
<v-expansion-panels v-if="notifiers" class="mt-2"> <v-expansion-panels v-if="notifiers" class="mt-2">
<v-expansion-panel v-for="(notifier, index) in notifiers" :key="index" class="my-2 left-border rounded"> <v-expansion-panel v-for="(notifier, index) in notifiers" :key="index" class="my-2 left-border rounded">
@ -59,36 +57,38 @@
<v-divider></v-divider> <v-divider></v-divider>
<p class="pt-4">What events should this notifier subscribe to?</p> <p class="pt-4">What events should this notifier subscribe to?</p>
<template v-for="(opt, idx) in optionsKeys"> <div class="notifier-options">
<v-checkbox <section v-for="sec in optionsSections" :key="sec.id">
v-if="!opt.divider" <h4>
:key="'option-' + idx" {{ sec.text }}
v-model="notifiers[index].options[opt.key]" </h4>
hide-details <v-checkbox
dense v-for="opt in sec.options"
:label="opt.text" :key="opt.key"
></v-checkbox> v-model="notifiers[index].options[opt.key]"
<div v-else :key="'divider-' + idx" class="mt-4"> hide-details
{{ opt.text }} dense
</div> :label="opt.text"
</template> />
</section>
</div>
<v-card-actions class="py-0"> <v-card-actions class="py-0">
<v-spacer></v-spacer> <v-spacer></v-spacer>
<BaseButtonGroup <BaseButtonGroup
:buttons="[ :buttons="[
{ {
icon: $globals.icons.delete, icon: $globals.icons.delete,
text: $t('general.delete'), text: $tc('general.delete'),
event: 'delete', event: 'delete',
}, },
{ {
icon: $globals.icons.testTube, icon: $globals.icons.testTube,
text: $t('general.test'), text: $tc('general.test'),
event: 'test', event: 'test',
}, },
{ {
icon: $globals.icons.save, icon: $globals.icons.save,
text: $t('general.save'), text: $tc('general.save'),
event: 'save', event: 'save',
}, },
]" ]"
@ -110,12 +110,14 @@ import { GroupEventNotifierCreate, GroupEventNotifierOut } from "~/lib/api/types
interface OptionKey { interface OptionKey {
text: string; text: string;
key: string; key: keyof GroupEventNotifierOut["options"];
} }
interface OptionDivider {
divider: true; interface OptionSection {
id: number;
text: string; text: string;
options: OptionKey[];
} }
export default defineComponent({ export default defineComponent({
@ -173,127 +175,125 @@ export default defineComponent({
// Options Definitions // Options Definitions
const { i18n } = useContext(); const { i18n } = useContext();
const optionsKeys: (OptionKey | OptionDivider)[] = [ const optionsSections: OptionSection[] = [
{ {
divider: true, id: 1,
text: "Recipe Events", text: "Recipe Events",
options: [
{
text: i18n.t("general.create") as string,
key: "recipeCreated",
},
{
text: i18n.t("general.update") as string,
key: "recipeUpdated",
},
{
text: i18n.t("general.delete") as string,
key: "recipeDeleted",
},
],
}, },
{ {
text: i18n.t("general.create") as string, id: 2,
key: "recipeCreated",
},
{
text: i18n.t("general.update") as string,
key: "recipeUpdated",
},
{
text: i18n.t("general.delete") as string,
key: "recipeDeleted",
},
{
divider: true,
text: "User Events", text: "User Events",
options: [
{
text: "When a new user joins your group",
key: "userSignup",
},
],
}, },
{ {
text: "When a new user joins your group", id: 3,
key: "userSignup",
},
{
divider: true,
text: "Data Events",
},
{
text: "When a new data migration is completed",
key: "dataMigrations",
},
{
text: "When a data export is completed",
key: "dataExport",
},
{
text: "When a data import is completed",
key: "dataImport",
},
{
divider: true,
text: "Mealplan Events", text: "Mealplan Events",
options: [
{
text: "When a user in your group creates a new mealplan",
key: "mealplanEntryCreated",
},
],
}, },
{ {
text: "When a user in your group creates a new mealplan", id: 4,
key: "mealplanEntryCreated",
},
{
divider: true,
text: "Shopping List Events", text: "Shopping List Events",
options: [
{
text: i18n.t("general.create") as string,
key: "shoppingListCreated",
},
{
text: i18n.t("general.update") as string,
key: "shoppingListUpdated",
},
{
text: i18n.t("general.delete") as string,
key: "shoppingListDeleted",
},
],
}, },
{ {
text: i18n.t("general.create") as string, id: 5,
key: "shoppingListCreated",
},
{
text: i18n.t("general.update") as string,
key: "shoppingListUpdated",
},
{
text: i18n.t("general.delete") as string,
key: "shoppingListDeleted",
},
{
divider: true,
text: "Cookbook Events", text: "Cookbook Events",
options: [
{
text: i18n.t("general.create") as string,
key: "cookbookCreated",
},
{
text: i18n.t("general.update") as string,
key: "cookbookUpdated",
},
{
text: i18n.t("general.delete") as string,
key: "cookbookDeleted",
},
],
}, },
{ {
text: i18n.t("general.create") as string, id: 6,
key: "cookbookCreated",
},
{
text: i18n.t("general.update") as string,
key: "cookbookUpdated",
},
{
text: i18n.t("general.delete") as string,
key: "cookbookDeleted",
},
{
divider: true,
text: "Tag Events", text: "Tag Events",
options: [
{
text: i18n.t("general.create") as string,
key: "tagCreated",
},
{
text: i18n.t("general.update") as string,
key: "tagUpdated",
},
{
text: i18n.t("general.delete") as string,
key: "tagDeleted",
},
],
}, },
{ {
text: i18n.t("general.create") as string, id: 7,
key: "tagCreated",
},
{
text: i18n.t("general.update") as string,
key: "tagUpdated",
},
{
text: i18n.t("general.delete") as string,
key: "tagDeleted",
},
{
divider: true,
text: "Category Events", text: "Category Events",
}, options: [
{ {
text: i18n.t("general.create") as string, text: i18n.t("general.create") as string,
key: "categoryCreated", key: "categoryCreated",
}, },
{ {
text: i18n.t("general.update") as string, text: i18n.t("general.update") as string,
key: "categoryUpdated", key: "categoryUpdated",
}, },
{ {
text: i18n.t("general.delete") as string, text: i18n.t("general.delete") as string,
key: "categoryDeleted", key: "categoryDeleted",
},
],
}, },
]; ];
return { return {
...toRefs(state), ...toRefs(state),
openDelete, openDelete,
optionsKeys,
notifiers, notifiers,
createNotifierData, createNotifierData,
optionsSections,
deleteNotifier, deleteNotifier,
testNotifier, testNotifier,
saveNotifier, saveNotifier,
@ -305,3 +305,11 @@ export default defineComponent({
}, },
}); });
</script> </script>
<style>
.notifier-options {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
</style>

View File

@ -5,7 +5,8 @@ from fastapi import APIRouter, Depends, HTTPException
from mealie.core.exceptions import mealie_registered_exceptions from mealie.core.exceptions import mealie_registered_exceptions
from mealie.repos.repository_meals import RepositoryMeals from mealie.repos.repository_meals import RepositoryMeals
from mealie.routes._base import BaseUserController, controller from mealie.routes._base import controller
from mealie.routes._base.base_controllers import BaseCrudController
from mealie.routes._base.mixins import HttpRepo from mealie.routes._base.mixins import HttpRepo
from mealie.schema import mapper from mealie.schema import mapper
from mealie.schema.meal_plan import CreatePlanEntry, ReadPlanEntry, SavePlanEntry, UpdatePlanEntry from mealie.schema.meal_plan import CreatePlanEntry, ReadPlanEntry, SavePlanEntry, UpdatePlanEntry
@ -14,12 +15,13 @@ from mealie.schema.meal_plan.plan_rules import PlanRulesDay
from mealie.schema.recipe.recipe import Recipe from mealie.schema.recipe.recipe import Recipe
from mealie.schema.response.pagination import PaginationQuery from mealie.schema.response.pagination import PaginationQuery
from mealie.schema.response.responses import ErrorResponse from mealie.schema.response.responses import ErrorResponse
from mealie.services.event_bus_service.event_types import EventMealplanCreatedData, EventTypes
router = APIRouter(prefix="/groups/mealplans", tags=["Groups: Mealplans"]) router = APIRouter(prefix="/groups/mealplans", tags=["Groups: Mealplans"])
@controller(router) @controller(router)
class GroupMealplanController(BaseUserController): class GroupMealplanController(BaseCrudController):
@cached_property @cached_property
def repo(self) -> RepositoryMeals: def repo(self) -> RepositoryMeals:
return self.repos.meals.by_group(self.group_id) return self.repos.meals.by_group(self.group_id)
@ -117,7 +119,21 @@ class GroupMealplanController(BaseUserController):
@router.post("", response_model=ReadPlanEntry, status_code=201) @router.post("", response_model=ReadPlanEntry, status_code=201)
def create_one(self, data: CreatePlanEntry): def create_one(self, data: CreatePlanEntry):
data = mapper.cast(data, SavePlanEntry, group_id=self.group.id) data = mapper.cast(data, SavePlanEntry, group_id=self.group.id)
return self.mixins.create_one(data) result = self.mixins.create_one(data)
self.publish_event(
event_type=EventTypes.mealplan_entry_created,
document_data=EventMealplanCreatedData(
mealplan_id=result.id,
recipe_id=data.recipe_id,
recipe_name=result.recipe.name if result.recipe else None,
recipe_slug=result.recipe.slug if result.recipe else None,
date=data.date,
),
message=f"Mealplan entry created for {data.date} for {data.entry_type}",
)
return result
@router.get("/{item_id}", response_model=ReadPlanEntry) @router.get("/{item_id}", response_model=ReadPlanEntry)
def get_one(self, item_id: int): def get_one(self, item_id: int):

View File

@ -1,4 +1,4 @@
from fastapi import APIRouter, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from mealie.core.config import get_app_settings from mealie.core.config import get_app_settings
from mealie.repos.all_repositories import get_repositories from mealie.repos.all_repositories import get_repositories
@ -6,6 +6,8 @@ from mealie.routes._base import BasePublicController, controller
from mealie.schema.response import ErrorResponse from mealie.schema.response import ErrorResponse
from mealie.schema.user.registration import CreateUserRegistration from mealie.schema.user.registration import CreateUserRegistration
from mealie.schema.user.user import UserOut from mealie.schema.user.user import UserOut
from mealie.services.event_bus_service.event_bus_service import EventBusService
from mealie.services.event_bus_service.event_types import EventTypes, EventUserSignupData
from mealie.services.user_services.registration_service import RegistrationService from mealie.services.user_services.registration_service import RegistrationService
router = APIRouter(prefix="/register") router = APIRouter(prefix="/register")
@ -13,6 +15,8 @@ router = APIRouter(prefix="/register")
@controller(router) @controller(router)
class RegistrationController(BasePublicController): class RegistrationController(BasePublicController):
event_bus: EventBusService = Depends(EventBusService.create)
@router.post("", response_model=UserOut, status_code=status.HTTP_201_CREATED) @router.post("", response_model=UserOut, status_code=status.HTTP_201_CREATED)
def register_new_user(self, data: CreateUserRegistration): def register_new_user(self, data: CreateUserRegistration):
settings = get_app_settings() settings = get_app_settings()
@ -29,4 +33,13 @@ class RegistrationController(BasePublicController):
self.translator, self.translator,
) )
return registration_service.register_user(data) result = registration_service.register_user(data)
self.event_bus.dispatch(
integration_id="registration",
group_id=result.group_id,
event_type=EventTypes.user_signup,
document_data=EventUserSignupData(username=result.username, email=result.email),
)
return result

View File

@ -1,5 +1,5 @@
import uuid import uuid
from datetime import datetime from datetime import date, datetime
from enum import Enum, auto from enum import Enum, auto
from typing import Any from typing import Any
@ -57,6 +57,8 @@ class EventTypes(Enum):
class EventDocumentType(Enum): class EventDocumentType(Enum):
generic = "generic" generic = "generic"
user = "user"
category = "category" category = "category"
cookbook = "cookbook" cookbook = "cookbook"
mealplan = "mealplan" mealplan = "mealplan"
@ -82,6 +84,23 @@ class EventDocumentDataBase(MealieModel):
... ...
class EventMealplanCreatedData(EventDocumentDataBase):
document_type = EventDocumentType.mealplan
operation = EventOperation.create
mealplan_id: int
date: date
recipe_id: UUID4 | None
recipe_name: str | None
recipe_slug: str | None
class EventUserSignupData(EventDocumentDataBase):
document_type = EventDocumentType.user
operation = EventOperation.create
username: str
email: str
class EventCategoryData(EventDocumentDataBase): class EventCategoryData(EventDocumentDataBase):
document_type = EventDocumentType.category document_type = EventDocumentType.category
category_id: UUID4 category_id: UUID4

View File

@ -70,7 +70,6 @@ class RegistrationService:
if registration.group: if registration.group:
new_group = True new_group = True
group = self._register_new_group() group = self._register_new_group()
elif registration.group_token and registration.group_token != "": elif registration.group_token and registration.group_token != "":
token_entry = self.repos.group_invite_tokens.get_one(registration.group_token) token_entry = self.repos.group_invite_tokens.get_one(registration.group_token)
if not token_entry: if not token_entry:
@ -88,9 +87,7 @@ class RegistrationService:
user = self._create_new_user(group, new_group) user = self._create_new_user(group, new_group)
if new_group and registration.seed_data: if new_group and registration.seed_data:
seeder_service = SeederService(self.repos, user, group) seeder_service = SeederService(self.repos, user, group)
seeder_service.seed_foods(registration.locale) seeder_service.seed_foods(registration.locale)
seeder_service.seed_labels(registration.locale) seeder_service.seed_labels(registration.locale)
seeder_service.seed_units(registration.locale) seeder_service.seed_units(registration.locale)