Merge branch 'mealie-next' into mealie-next

This commit is contained in:
Julian van der Horst 2024-11-06 11:40:37 +01:00 committed by GitHub
commit 782cdcfc77
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 471 additions and 147 deletions

View file

@ -12,7 +12,7 @@ repos:
exclude: ^tests/data/ exclude: ^tests/data/
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.7.1 rev: v0.7.2
hooks: hooks:
- id: ruff - id: ruff
- id: ruff-format - id: ruff-format

View file

@ -61,6 +61,15 @@ Changing the webworker settings may cause unforeseen memory leak issues with Mea
| --------------- | :-----: | ----------------------------------------------------------------------------- | | --------------- | :-----: | ----------------------------------------------------------------------------- |
| UVICORN_WORKERS | 1 | Sets the number of workers for the web server. [More info here][unicorn_workers] | | UVICORN_WORKERS | 1 | Sets the number of workers for the web server. [More info here][unicorn_workers] |
### TLS
Use this only when mealie is run without a webserver or reverse proxy.
| Variables | Default | Description |
| -------------------- | :-----: | ------------------------ |
| TLS_CERTIFICATE_PATH | None | File path to Certificate |
| TLS_PRIVATE_KEY_PATH | None | File path to private key |
### LDAP ### LDAP
| Variables | Default | Description | | Variables | Default | Description |

View file

@ -7,7 +7,7 @@
width="100%" width="100%"
max-width="1100px" max-width="1100px"
:icon="$globals.icons.pages" :icon="$globals.icons.pages"
:title="$t('general.edit')" :title="$tc('general.edit')"
:submit-icon="$globals.icons.save" :submit-icon="$globals.icons.save"
:submit-text="$tc('general.save')" :submit-text="$tc('general.save')"
:submit-disabled="!editTarget.queryFilterString" :submit-disabled="!editTarget.queryFilterString"
@ -25,7 +25,7 @@
<v-toolbar-title class="headline"> {{ book.name }} </v-toolbar-title> <v-toolbar-title class="headline"> {{ book.name }} </v-toolbar-title>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<BaseButton <BaseButton
v-if="isOwnGroup" v-if="canEdit"
class="mx-1" class="mx-1"
:edit="true" :edit="true"
@click="handleEditCookbook" @click="handleEditCookbook"
@ -79,6 +79,15 @@
const tab = ref(null); const tab = ref(null);
const book = getOne(slug); const book = getOne(slug);
const isOwnHousehold = computed(() => {
if (!($auth.user && book.value?.householdId)) {
return false;
}
return $auth.user.householdId === book.value.householdId;
})
const canEdit = computed(() => isOwnGroup.value && isOwnHousehold.value);
const dialogStates = reactive({ const dialogStates = reactive({
edit: false, edit: false,
}); });
@ -118,7 +127,7 @@
recipes, recipes,
removeRecipe, removeRecipe,
replaceRecipes, replaceRecipes,
isOwnGroup, canEdit,
dialogStates, dialogStates,
editTarget, editTarget,
handleEditCookbook, handleEditCookbook,

View file

@ -82,12 +82,17 @@ import { computed, defineComponent, onMounted, ref, useContext, useRoute } from
import { useLoggedInState } from "~/composables/use-logged-in-state"; import { useLoggedInState } from "~/composables/use-logged-in-state";
import AppHeader from "@/components/Layout/LayoutParts/AppHeader.vue"; import AppHeader from "@/components/Layout/LayoutParts/AppHeader.vue";
import AppSidebar from "@/components/Layout/LayoutParts/AppSidebar.vue"; import AppSidebar from "@/components/Layout/LayoutParts/AppSidebar.vue";
import { SidebarLinks } from "~/types/application-types"; import { SideBarLink } from "~/types/application-types";
import LanguageDialog from "~/components/global/LanguageDialog.vue"; import LanguageDialog from "~/components/global/LanguageDialog.vue";
import TheSnackbar from "@/components/Layout/LayoutParts/TheSnackbar.vue"; import TheSnackbar from "@/components/Layout/LayoutParts/TheSnackbar.vue";
import { useAppInfo } from "~/composables/api"; import { useAppInfo } from "~/composables/api";
import { useCookbooks, usePublicCookbooks } from "~/composables/use-group-cookbooks"; import { useCookbooks, usePublicCookbooks } from "~/composables/use-group-cookbooks";
import { useCookbookPreferences } from "~/composables/use-users/preferences";
import { useHouseholdStore, usePublicHouseholdStore } from "~/composables/store/use-household-store";
import { useToggleDarkMode } from "~/composables/use-utils"; import { useToggleDarkMode } from "~/composables/use-utils";
import { ReadCookBook } from "~/lib/api/types/cookbook";
import { HouseholdSummary } from "~/lib/api/types/household";
export default defineComponent({ export default defineComponent({
components: { AppHeader, AppSidebar, LanguageDialog, TheSnackbar }, components: { AppHeader, AppSidebar, LanguageDialog, TheSnackbar },
@ -99,6 +104,15 @@ export default defineComponent({
const route = useRoute(); const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || ""); const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const { cookbooks } = isOwnGroup.value ? useCookbooks() : usePublicCookbooks(groupSlug.value || ""); const { cookbooks } = isOwnGroup.value ? useCookbooks() : usePublicCookbooks(groupSlug.value || "");
const cookbookPreferences = useCookbookPreferences();
const { store: households } = isOwnGroup.value ? useHouseholdStore() : usePublicHouseholdStore(groupSlug.value || "");
const householdsById = computed(() => {
return households.value.reduce((acc, household) => {
acc[household.id] = household;
return acc;
}, {} as { [key: string]: HouseholdSummary });
});
const appInfo = useAppInfo(); const appInfo = useAppInfo();
const showImageImport = computed(() => appInfo.value?.enableOpenaiImageServices); const showImageImport = computed(() => appInfo.value?.enableOpenaiImageServices);
@ -113,29 +127,57 @@ export default defineComponent({
sidebar.value = !$vuetify.breakpoint.md; sidebar.value = !$vuetify.breakpoint.md;
}); });
const cookbookLinks = computed(() => { function cookbookAsLink(cookbook: ReadCookBook): SideBarLink {
if (!cookbooks.value) return []; return {
return cookbooks.value.map((cookbook) => { key: cookbook.slug || "",
return { icon: $globals.icons.pages,
key: cookbook.slug, title: cookbook.name,
icon: $globals.icons.pages, to: `/g/${groupSlug.value}/cookbooks/${cookbook.slug || ""}`,
title: cookbook.name, restricted: false,
to: `/g/${groupSlug.value}/cookbooks/${cookbook.slug as string}`, };
};
});
});
interface Link {
insertDivider: boolean;
icon: string;
title: string;
subtitle: string | null;
to: string;
restricted: boolean;
hide: boolean;
} }
const createLinks = computed<Link[]>(() => [ const currentUserHouseholdId = computed(() => $auth.user?.householdId);
const cookbookLinks = computed<SideBarLink[]>(() => {
if (!cookbooks.value) {
return [];
}
cookbooks.value.sort((a, b) => (a.position || 0) - (b.position || 0));
const ownLinks: SideBarLink[] = [];
const links: SideBarLink[] = [];
const cookbooksByHousehold = cookbooks.value.reduce((acc, cookbook) => {
const householdName = householdsById.value[cookbook.householdId]?.name || "";
if (!acc[householdName]) {
acc[householdName] = [];
}
acc[householdName].push(cookbook);
return acc;
}, {} as Record<string, ReadCookBook[]>);
Object.entries(cookbooksByHousehold).forEach(([householdName, cookbooks]) => {
if (cookbooks[0].householdId === currentUserHouseholdId.value) {
ownLinks.push(...cookbooks.map(cookbookAsLink));
} else {
links.push({
key: householdName,
icon: $globals.icons.book,
title: householdName,
children: cookbooks.map(cookbookAsLink),
restricted: false,
});
}
});
links.sort((a, b) => a.title.localeCompare(b.title));
if ($auth.user && cookbookPreferences.value.hideOtherHouseholds) {
return ownLinks;
} else {
return [...ownLinks, ...links];
}
});
const createLinks = computed<SideBarLink[]>(() => [
{ {
insertDivider: false, insertDivider: false,
icon: $globals.icons.link, icon: $globals.icons.link,
@ -165,7 +207,7 @@ export default defineComponent({
}, },
]); ]);
const bottomLinks = computed<SidebarLinks>(() => [ const bottomLinks = computed<SideBarLink[]>(() => [
{ {
icon: $globals.icons.cog, icon: $globals.icons.cog,
title: i18n.tc("general.settings"), title: i18n.tc("general.settings"),
@ -174,7 +216,7 @@ export default defineComponent({
}, },
]); ]);
const topLinks = computed<SidebarLinks>(() => [ const topLinks = computed<SideBarLink[]>(() => [
{ {
icon: $globals.icons.silverwareForkKnife, icon: $globals.icons.silverwareForkKnife,
to: `/g/${groupSlug.value}`, to: `/g/${groupSlug.value}`,

View file

@ -135,7 +135,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, reactive, toRefs, useContext } from "@nuxtjs/composition-api"; import { computed, defineComponent, reactive, toRefs, useContext, watch } from "@nuxtjs/composition-api";
import { useLoggedInState } from "~/composables/use-logged-in-state"; import { useLoggedInState } from "~/composables/use-logged-in-state";
import { SidebarLinks } from "~/types/application-types"; import { SidebarLinks } from "~/types/application-types";
import UserAvatar from "~/components/Domain/User/UserAvatar.vue"; import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
@ -192,13 +192,29 @@ export default defineComponent({
const userProfileLink = computed(() => $auth.user ? "/user/profile" : undefined); const userProfileLink = computed(() => $auth.user ? "/user/profile" : undefined);
const state = reactive({ const state = reactive({
dropDowns: {}, dropDowns: {} as Record<string, boolean>,
topSelected: null as string[] | null, topSelected: null as string[] | null,
secondarySelected: null as string[] | null, secondarySelected: null as string[] | null,
bottomSelected: null as string[] | null, bottomSelected: null as string[] | null,
hasOpenedBefore: false as boolean, hasOpenedBefore: false as boolean,
}); });
const allLinks = computed(() => [...props.topLink, ...(props.secondaryLinks || []), ...(props.bottomLinks || [])]);
function initDropdowns() {
allLinks.value.forEach((link) => {
state.dropDowns[link.title] = link.childrenStartExpanded || false;
})
}
watch(
() => allLinks,
() => {
initDropdowns();
},
{
deep: true,
}
);
return { return {
...toRefs(state), ...toRefs(state),
userFavoritesLink, userFavoritesLink,

View file

@ -99,10 +99,10 @@ export const useCookbooks = function () {
loading.value = false; loading.value = false;
}, },
async createOne() { async createOne(name: string | null = null) {
loading.value = true; loading.value = true;
const { data } = await api.cookbooks.createOne({ const { data } = await api.cookbooks.createOne({
name: i18n.t("cookbook.household-cookbook-name", [household.value?.name || "", String((cookbookStore?.value?.length ?? 0) + 1)]) as string, name: name || i18n.t("cookbook.household-cookbook-name", [household.value?.name || "", String((cookbookStore?.value?.length ?? 0) + 1)]) as string,
position: (cookbookStore?.value?.length ?? 0) + 1, position: (cookbookStore?.value?.length ?? 0) + 1,
queryFilterString: "", queryFilterString: "",
}); });
@ -129,18 +129,18 @@ export const useCookbooks = function () {
return data; return data;
}, },
async updateOrder() { async updateOrder(cookbooks: ReadCookBook[]) {
if (!cookbookStore?.value) { if (!cookbooks?.length) {
return; return;
} }
loading.value = true; loading.value = true;
cookbookStore.value.forEach((element, index) => { cookbooks.forEach((element, index) => {
element.position = index + 1; element.position = index + 1;
}); });
const { data } = await api.cookbooks.updateAll(cookbookStore.value); const { data } = await api.cookbooks.updateAll(cookbooks);
if (data && cookbookStore?.value) { if (data && cookbookStore?.value) {
this.refreshAll(); this.refreshAll();

View file

@ -45,6 +45,10 @@ export interface UserParsingPreferences {
parser: RegisteredParser; parser: RegisteredParser;
} }
export interface UserCookbooksPreferences {
hideOtherHouseholds: boolean;
}
export function useUserMealPlanPreferences(): Ref<UserMealPlanPreferences> { export function useUserMealPlanPreferences(): Ref<UserMealPlanPreferences> {
const fromStorage = useLocalStorage( const fromStorage = useLocalStorage(
"meal-planner-preferences", "meal-planner-preferences",
@ -153,3 +157,17 @@ export function useParsingPreferences(): Ref<UserParsingPreferences> {
return fromStorage; return fromStorage;
} }
export function useCookbookPreferences(): Ref<UserCookbooksPreferences> {
const fromStorage = useLocalStorage(
"cookbook-preferences",
{
hideOtherHouseholds: false,
},
{ mergeDefaults: true }
// we cast to a Ref because by default it will return an optional type ref
// but since we pass defaults we know all properties are set.
) as unknown as Ref<UserCookbooksPreferences>;
return fromStorage;
}

View file

@ -1327,6 +1327,8 @@
"cookbook": { "cookbook": {
"cookbooks": "Cookbooks", "cookbooks": "Cookbooks",
"description": "Cookbooks are another way to organize recipes by creating cross sections of recipes, organizers, and other filters. Creating a cookbook will add an entry to the side-bar and all the recipes with the filters chosen will be displayed in the cookbook.", "description": "Cookbooks are another way to organize recipes by creating cross sections of recipes, organizers, and other filters. Creating a cookbook will add an entry to the side-bar and all the recipes with the filters chosen will be displayed in the cookbook.",
"hide-cookbooks-from-other-households": "Hide Cookbooks from Other Households",
"hide-cookbooks-from-other-households-description": "When enabled, only cookbooks from your household will appear on the sidebar",
"public-cookbook": "Public Cookbook", "public-cookbook": "Public Cookbook",
"public-cookbook-description": "Public Cookbooks can be shared with non-mealie users and will be displayed on your groups page.", "public-cookbook-description": "Public Cookbooks can be shared with non-mealie users and will be displayed on your groups page.",
"filter-options": "Filter Options", "filter-options": "Filter Options",

View file

@ -48,20 +48,33 @@
{{ $t('cookbook.description') }} {{ $t('cookbook.description') }}
</BasePageTitle> </BasePageTitle>
<div class="my-6">
<v-checkbox
v-model="cookbookPreferences.hideOtherHouseholds"
:label="$tc('cookbook.hide-cookbooks-from-other-households')"
hide-details
/>
<div class="ml-8">
<p class="text-subtitle-2 my-0 py-0">
{{ $tc("cookbook.hide-cookbooks-from-other-households-description") }}
</p>
</div>
</div>
<!-- Create New --> <!-- Create New -->
<BaseButton create @click="createCookbook" /> <BaseButton create @click="createCookbook" />
<!-- Cookbook List --> <!-- Cookbook List -->
<v-expansion-panels class="mt-2"> <v-expansion-panels class="mt-2">
<draggable <draggable
v-model="cookbooks" v-model="myCookbooks"
handle=".handle" handle=".handle"
delay="250" delay="250"
:delay-on-touch-only="true" :delay-on-touch-only="true"
style="width: 100%" style="width: 100%"
@change="actions.updateOrder()" @change="actions.updateOrder(myCookbooks)"
> >
<v-expansion-panel v-for="cookbook in cookbooks" :key="cookbook.id" class="my-2 left-border rounded"> <v-expansion-panel v-for="cookbook in myCookbooks" :key="cookbook.id" class="my-2 left-border rounded">
<v-expansion-panel-header disable-icon-rotate class="headline"> <v-expansion-panel-header disable-icon-rotate class="headline">
<div class="d-flex align-center"> <div class="d-flex align-center">
<v-icon large left> <v-icon large left>
@ -110,11 +123,13 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, onBeforeUnmount, onMounted, reactive, ref } from "@nuxtjs/composition-api"; import { computed, defineComponent, onBeforeUnmount, onMounted, reactive, ref, useContext } from "@nuxtjs/composition-api";
import draggable from "vuedraggable"; import draggable from "vuedraggable";
import { useCookbooks } from "@/composables/use-group-cookbooks"; import { useCookbooks } from "@/composables/use-group-cookbooks";
import { useHouseholdSelf } from "@/composables/use-households";
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue"; import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
import { ReadCookBook } from "~/lib/api/types/cookbook"; import { ReadCookBook } from "~/lib/api/types/cookbook";
import { useCookbookPreferences } from "~/composables/use-users/preferences";
export default defineComponent({ export default defineComponent({
components: { CookbookEditor, draggable }, components: { CookbookEditor, draggable },
@ -124,13 +139,28 @@ export default defineComponent({
create: false, create: false,
delete: false, delete: false,
}); });
const { cookbooks, actions } = useCookbooks();
const { $auth, i18n } = useContext();
const { cookbooks: allCookbooks, actions } = useCookbooks();
const myCookbooks = computed<ReadCookBook[]>({
get: () => {
return allCookbooks.value?.filter((cookbook) => {
return cookbook.householdId === $auth.user?.householdId;
}) || [];
},
set: (value: ReadCookBook[]) => {
actions.updateOrder(value);
},
});
const { household } = useHouseholdSelf();
const cookbookPreferences = useCookbookPreferences()
// create // create
const createTargetKey = ref(0); const createTargetKey = ref(0);
const createTarget = ref<ReadCookBook | null>(null); const createTarget = ref<ReadCookBook | null>(null);
async function createCookbook() { async function createCookbook() {
await actions.createOne().then((cookbook) => { const name = i18n.t("cookbook.household-cookbook-name", [household.value?.name || "", String((myCookbooks.value?.length ?? 0) + 1)]) as string
await actions.createOne(name).then((cookbook) => {
createTarget.value = cookbook as ReadCookBook; createTarget.value = cookbook as ReadCookBook;
createTargetKey.value++; createTargetKey.value++;
}); });
@ -177,7 +207,8 @@ export default defineComponent({
}); });
return { return {
cookbooks, myCookbooks,
cookbookPreferences,
actions, actions,
dialogStates, dialogStates,
// create // create

View file

@ -56,7 +56,7 @@
<!-- View By Label --> <!-- View By Label -->
<div v-else> <div v-else>
<div v-for="(value, key) in itemsByLabel" :key="key" class="mb-6"> <div v-for="(value, key) in itemsByLabel" :key="key" class="pb-4">
<v-btn <v-btn
:color="getLabelColor(value[0]) ? getLabelColor(value[0]) : '#959595'" :color="getLabelColor(value[0]) ? getLabelColor(value[0]) : '#959595'"
:style="{ :style="{
@ -73,20 +73,20 @@
<v-divider/> <v-divider/>
<v-expand-transition group> <v-expand-transition group>
<div v-show="labelOpenState[key]"> <div v-show="labelOpenState[key]">
<draggable :value="value" handle=".handle" delay="250" :delay-on-touch-only="true" @start="loadingCounter += 1" @end="loadingCounter -= 1" @input="updateIndexUncheckedByLabel(key, $event)"> <draggable :value="value" handle=".handle" delay="250" :delay-on-touch-only="true" @start="loadingCounter += 1" @end="loadingCounter -= 1" @input="updateIndexUncheckedByLabel(key, $event)">
<v-lazy v-for="(item, index) in value" :key="item.id" class="ml-2 my-2"> <v-lazy v-for="(item, index) in value" :key="item.id" class="ml-2 my-2">
<ShoppingListItem <ShoppingListItem
v-model="value[index]" v-model="value[index]"
:show-label=false :show-label=false
:labels="allLabels || []" :labels="allLabels || []"
:units="allUnits || []" :units="allUnits || []"
:foods="allFoods || []" :foods="allFoods || []"
:recipes="recipeMap" :recipes="recipeMap"
@checked="saveListItem" @checked="saveListItem"
@save="saveListItem" @save="saveListItem"
@delete="deleteListItem(item)" @delete="deleteListItem(item)"
/> />
</v-lazy> </v-lazy>
</draggable> </draggable>
</div> </div>
</v-expand-transition> </v-expand-transition>
@ -470,7 +470,7 @@ export default defineComponent({
}); });
// ===================================== // =====================================
// Collapsables // Collapsable Labels
const labelOpenState = ref<{ [key: string]: boolean }>({}); const labelOpenState = ref<{ [key: string]: boolean }>({});
const initializeLabelOpenStates = () => { const initializeLabelOpenStates = () => {
@ -480,8 +480,8 @@ export default defineComponent({
let hasChanges = false; let hasChanges = false;
for (const item of shoppingList.value.listItems) { for (const item of shoppingList.value.listItems) {
const labelName = item.label?.name; const labelName = item.label?.name || i18n.tc("shopping-list.no-label");
if (labelName && !existingLabels.has(labelName) && !(labelName in labelOpenState.value)) { if (!existingLabels.has(labelName) && !(labelName in labelOpenState.value)) {
labelOpenState.value[labelName] = true; labelOpenState.value[labelName] = true;
hasChanges = true; hasChanges = true;
} }
@ -492,9 +492,13 @@ export default defineComponent({
} }
}; };
const labelNames = computed(() => const labelNames = computed(() => {
new Set(shoppingList.value?.listItems?.map(item => item.label?.name).filter(Boolean) ?? []) return new Set(
); shoppingList.value?.listItems
?.map(item => item.label?.name || i18n.tc("shopping-list.no-label"))
.filter(Boolean) ?? []
);
});
watch(labelNames, initializeLabelOpenStates, { immediate: true }); watch(labelNames, initializeLabelOpenStates, { immediate: true });

View file

@ -5,6 +5,7 @@ export interface SideBarLink {
href?: string; href?: string;
title: string; title: string;
children?: SideBarLink[]; children?: SideBarLink[];
childrenStartExpanded?: boolean;
restricted: boolean; restricted: boolean;
} }

View file

@ -353,6 +353,15 @@ class AppSettings(AppLoggingSettings):
model_config = SettingsConfigDict(arbitrary_types_allowed=True, extra="allow") model_config = SettingsConfigDict(arbitrary_types_allowed=True, extra="allow")
# ===============================================
# TLS
TLS_CERTIFICATE_PATH: str | os.PathLike[str] | None = None
"""Path where the certificate resides."""
TLS_PRIVATE_KEY_PATH: str | os.PathLike[str] | None = None
"""Path where the private key resides."""
def app_settings_constructor(data_dir: Path, production: bool, env_file: Path, env_encoding="utf-8") -> AppSettings: def app_settings_constructor(data_dir: Path, production: bool, env_file: Path, env_encoding="utf-8") -> AppSettings:
""" """

View file

@ -151,6 +151,14 @@ class User(SqlAlchemyBase, BaseMixins):
else: else:
self.household = None self.household = None
if self.group is None:
raise ValueError(f"Group {group} does not exist; cannot create user")
if self.household is None:
raise ValueError(
f'Household "{household}" does not exist on group '
f'"{self.group.name}" ({self.group.id}); cannot create user'
)
self.rated_recipes = [] self.rated_recipes = []
self.password = password self.password = password

View file

@ -13,6 +13,8 @@ def main():
log_config=log_config(), log_config=log_config(),
workers=settings.WORKERS, workers=settings.WORKERS,
forwarded_allow_ips=settings.HOST_IP, forwarded_allow_ips=settings.HOST_IP,
ssl_keyfile=settings.TLS_PRIVATE_KEY_PATH,
ssl_certfile=settings.TLS_CERTIFICATE_PATH,
) )

View file

@ -2,6 +2,7 @@ from collections.abc import Callable
from logging import Logger from logging import Logger
from typing import Generic, TypeVar from typing import Generic, TypeVar
import sqlalchemy.exc
from fastapi import HTTPException, status from fastapi import HTTPException, status
from pydantic import UUID4, BaseModel from pydantic import UUID4, BaseModel
@ -57,10 +58,16 @@ class HttpRepo(Generic[C, R, U]):
# Respond # Respond
msg = self.get_exception_message(ex) msg = self.get_exception_message(ex)
raise HTTPException( if isinstance(ex, sqlalchemy.exc.NoResultFound):
status.HTTP_400_BAD_REQUEST, raise HTTPException(
detail=ErrorResponse.respond(message=msg, exception=str(ex)), status.HTTP_404_NOT_FOUND,
) detail=ErrorResponse.respond(message=msg, exception=str(ex)),
)
else:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=ErrorResponse.respond(message=msg, exception=str(ex)),
)
def create_one(self, data: C) -> R | None: def create_one(self, data: C) -> R | None:
item: R | None = None item: R | None = None

View file

@ -6,6 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException
from pydantic import UUID4 from pydantic import UUID4
from mealie.core.exceptions import mealie_registered_exceptions from mealie.core.exceptions import mealie_registered_exceptions
from mealie.repos.all_repositories import get_repositories
from mealie.routes._base import BaseCrudController, controller from mealie.routes._base import BaseCrudController, controller
from mealie.routes._base.mixins import HttpRepo from mealie.routes._base.mixins import HttpRepo
from mealie.routes._base.routers import MealieCrudRoute from mealie.routes._base.routers import MealieCrudRoute
@ -26,9 +27,13 @@ router = APIRouter(prefix="/households/cookbooks", tags=["Households: Cookbooks"
@controller(router) @controller(router)
class GroupCookbookController(BaseCrudController): class GroupCookbookController(BaseCrudController):
@cached_property @cached_property
def repo(self): def cookbooks(self):
return self.repos.cookbooks return self.repos.cookbooks
@cached_property
def group_cookbooks(self):
return get_repositories(self.session, group_id=self.group_id, household_id=None).cookbooks
def registered_exceptions(self, ex: type[Exception]) -> str: def registered_exceptions(self, ex: type[Exception]) -> str:
registered = { registered = {
**mealie_registered_exceptions(self.translator), **mealie_registered_exceptions(self.translator),
@ -38,14 +43,15 @@ class GroupCookbookController(BaseCrudController):
@cached_property @cached_property
def mixins(self): def mixins(self):
return HttpRepo[CreateCookBook, ReadCookBook, UpdateCookBook]( return HttpRepo[CreateCookBook, ReadCookBook, UpdateCookBook](
self.repo, self.cookbooks,
self.logger, self.logger,
self.registered_exceptions, self.registered_exceptions,
) )
@router.get("", response_model=CookBookPagination) @router.get("", response_model=CookBookPagination)
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)): def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
response = self.repo.page_all( # Fetch all cookbooks for the group, rather than the household
response = self.group_cookbooks.page_all(
pagination=q, pagination=q,
override=ReadCookBook, override=ReadCookBook,
) )
@ -106,7 +112,8 @@ class GroupCookbookController(BaseCrudController):
except ValueError: except ValueError:
match_attr = "slug" match_attr = "slug"
cookbook = self.repo.get_one(item_id, match_attr) # Allow fetching other households' cookbooks
cookbook = self.group_cookbooks.get_one(item_id, match_attr)
if cookbook is None: if cookbook is None:
raise HTTPException(status_code=404) raise HTTPException(status_code=404)

View file

@ -105,8 +105,8 @@ class BaseRecipeController(BaseCrudController):
return get_repositories(self.session, group_id=self.group_id, household_id=None).recipes return get_repositories(self.session, group_id=self.group_id, household_id=None).recipes
@cached_property @cached_property
def cookbooks_repo(self) -> RepositoryGeneric[ReadCookBook, CookBook]: def group_cookbooks(self) -> RepositoryGeneric[ReadCookBook, CookBook]:
return self.repos.cookbooks return get_repositories(self.session, group_id=self.group_id, household_id=None).cookbooks
@cached_property @cached_property
def service(self) -> RecipeService: def service(self) -> RecipeService:
@ -354,7 +354,7 @@ class RecipeController(BaseRecipeController):
cb_match_attr = "id" cb_match_attr = "id"
except ValueError: except ValueError:
cb_match_attr = "slug" cb_match_attr = "slug"
cookbook_data = self.cookbooks_repo.get_one(search_query.cookbook, cb_match_attr) cookbook_data = self.group_cookbooks.get_one(search_query.cookbook, cb_match_attr)
if cookbook_data is None: if cookbook_data is None:
raise HTTPException(status_code=404, detail="cookbook not found") raise HTTPException(status_code=404, detail="cookbook not found")

138
poetry.lock generated
View file

@ -1464,13 +1464,13 @@ pyyaml = ">=5.1"
[[package]] [[package]]
name = "mkdocs-material" name = "mkdocs-material"
version = "9.5.43" version = "9.5.44"
description = "Documentation that simply works" description = "Documentation that simply works"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "mkdocs_material-9.5.43-py3-none-any.whl", hash = "sha256:4aae0664c456fd12837a3192e0225c17960ba8bf55d7f0a7daef7e4b0b914a34"}, {file = "mkdocs_material-9.5.44-py3-none-any.whl", hash = "sha256:47015f9c167d58a5ff5e682da37441fc4d66a1c79334bfc08d774763cacf69ca"},
{file = "mkdocs_material-9.5.43.tar.gz", hash = "sha256:83be7ff30b65a1e4930dfa4ab911e75780a3afc9583d162692e434581cb46979"}, {file = "mkdocs_material-9.5.44.tar.gz", hash = "sha256:f3a6c968e524166b3f3ed1fb97d3ed3e0091183b0545cedf7156a2a6804c56c0"},
] ]
[package.dependencies] [package.dependencies]
@ -1598,13 +1598,13 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
[[package]] [[package]]
name = "openai" name = "openai"
version = "1.53.0" version = "1.54.1"
description = "The official Python library for the openai API" description = "The official Python library for the openai API"
optional = false optional = false
python-versions = ">=3.7.1" python-versions = ">=3.8"
files = [ files = [
{file = "openai-1.53.0-py3-none-any.whl", hash = "sha256:20f408c32fc5cb66e60c6882c994cdca580a5648e10045cd840734194f033418"}, {file = "openai-1.54.1-py3-none-any.whl", hash = "sha256:3cb49ccb6bfdc724ad01cc397d323ef8314fc7d45e19e9de2afdd6484a533324"},
{file = "openai-1.53.0.tar.gz", hash = "sha256:be2c4e77721b166cce8130e544178b7d579f751b4b074ffbaade3854b6f85ec5"}, {file = "openai-1.54.1.tar.gz", hash = "sha256:5b832bf82002ba8c4f6e5e25c1c0f5d468c22f043711544c716eaffdb30dd6f1"},
] ]
[package.dependencies] [package.dependencies]
@ -1622,69 +1622,69 @@ datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"]
[[package]] [[package]]
name = "orjson" name = "orjson"
version = "3.10.10" version = "3.10.11"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "orjson-3.10.10-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b788a579b113acf1c57e0a68e558be71d5d09aa67f62ca1f68e01117e550a998"}, {file = "orjson-3.10.11-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6dade64687f2bd7c090281652fe18f1151292d567a9302b34c2dbb92a3872f1f"},
{file = "orjson-3.10.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:804b18e2b88022c8905bb79bd2cbe59c0cd014b9328f43da8d3b28441995cda4"}, {file = "orjson-3.10.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82f07c550a6ccd2b9290849b22316a609023ed851a87ea888c0456485a7d196a"},
{file = "orjson-3.10.10-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9972572a1d042ec9ee421b6da69f7cc823da5962237563fa548ab17f152f0b9b"}, {file = "orjson-3.10.11-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd9a187742d3ead9df2e49240234d728c67c356516cf4db018833a86f20ec18c"},
{file = "orjson-3.10.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc6993ab1c2ae7dd0711161e303f1db69062955ac2668181bfdf2dd410e65258"}, {file = "orjson-3.10.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77b0fed6f209d76c1c39f032a70df2d7acf24b1812ca3e6078fd04e8972685a3"},
{file = "orjson-3.10.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d78e4cacced5781b01d9bc0f0cd8b70b906a0e109825cb41c1b03f9c41e4ce86"}, {file = "orjson-3.10.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63fc9d5fe1d4e8868f6aae547a7b8ba0a2e592929245fff61d633f4caccdcdd6"},
{file = "orjson-3.10.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6eb2598df518281ba0cbc30d24c5b06124ccf7e19169e883c14e0831217a0bc"}, {file = "orjson-3.10.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65cd3e3bb4fbb4eddc3c1e8dce10dc0b73e808fcb875f9fab40c81903dd9323e"},
{file = "orjson-3.10.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23776265c5215ec532de6238a52707048401a568f0fa0d938008e92a147fe2c7"}, {file = "orjson-3.10.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f67c570602300c4befbda12d153113b8974a3340fdcf3d6de095ede86c06d92"},
{file = "orjson-3.10.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8cc2a654c08755cef90b468ff17c102e2def0edd62898b2486767204a7f5cc9c"}, {file = "orjson-3.10.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1f39728c7f7d766f1f5a769ce4d54b5aaa4c3f92d5b84817053cc9995b977acc"},
{file = "orjson-3.10.10-cp310-none-win32.whl", hash = "sha256:081b3fc6a86d72efeb67c13d0ea7c030017bd95f9868b1e329a376edc456153b"}, {file = "orjson-3.10.11-cp310-none-win32.whl", hash = "sha256:1789d9db7968d805f3d94aae2c25d04014aae3a2fa65b1443117cd462c6da647"},
{file = "orjson-3.10.10-cp310-none-win_amd64.whl", hash = "sha256:ff38c5fb749347768a603be1fb8a31856458af839f31f064c5aa74aca5be9efe"}, {file = "orjson-3.10.11-cp310-none-win_amd64.whl", hash = "sha256:5576b1e5a53a5ba8f8df81872bb0878a112b3ebb1d392155f00f54dd86c83ff6"},
{file = "orjson-3.10.10-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:879e99486c0fbb256266c7c6a67ff84f46035e4f8749ac6317cc83dacd7f993a"}, {file = "orjson-3.10.11-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1444f9cb7c14055d595de1036f74ecd6ce15f04a715e73f33bb6326c9cef01b6"},
{file = "orjson-3.10.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:019481fa9ea5ff13b5d5d95e6fd5ab25ded0810c80b150c2c7b1cc8660b662a7"}, {file = "orjson-3.10.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdec57fe3b4bdebcc08a946db3365630332dbe575125ff3d80a3272ebd0ddafe"},
{file = "orjson-3.10.10-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0dd57eff09894938b4c86d4b871a479260f9e156fa7f12f8cad4b39ea8028bb5"}, {file = "orjson-3.10.11-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4eed32f33a0ea6ef36ccc1d37f8d17f28a1d6e8eefae5928f76aff8f1df85e67"},
{file = "orjson-3.10.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dbde6d70cd95ab4d11ea8ac5e738e30764e510fc54d777336eec09bb93b8576c"}, {file = "orjson-3.10.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80df27dd8697242b904f4ea54820e2d98d3f51f91e97e358fc13359721233e4b"},
{file = "orjson-3.10.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2625cb37b8fb42e2147404e5ff7ef08712099197a9cd38895006d7053e69d6"}, {file = "orjson-3.10.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:705f03cee0cb797256d54de6695ef219e5bc8c8120b6654dd460848d57a9af3d"},
{file = "orjson-3.10.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbf3c20c6a7db69df58672a0d5815647ecf78c8e62a4d9bd284e8621c1fe5ccb"}, {file = "orjson-3.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03246774131701de8e7059b2e382597da43144a9a7400f178b2a32feafc54bd5"},
{file = "orjson-3.10.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:75c38f5647e02d423807d252ce4528bf6a95bd776af999cb1fb48867ed01d1f6"}, {file = "orjson-3.10.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8b5759063a6c940a69c728ea70d7c33583991c6982915a839c8da5f957e0103a"},
{file = "orjson-3.10.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23458d31fa50ec18e0ec4b0b4343730928296b11111df5f547c75913714116b2"}, {file = "orjson-3.10.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:677f23e32491520eebb19c99bb34675daf5410c449c13416f7f0d93e2cf5f981"},
{file = "orjson-3.10.10-cp311-none-win32.whl", hash = "sha256:2787cd9dedc591c989f3facd7e3e86508eafdc9536a26ec277699c0aa63c685b"}, {file = "orjson-3.10.11-cp311-none-win32.whl", hash = "sha256:a11225d7b30468dcb099498296ffac36b4673a8398ca30fdaec1e6c20df6aa55"},
{file = "orjson-3.10.10-cp311-none-win_amd64.whl", hash = "sha256:6514449d2c202a75183f807bc755167713297c69f1db57a89a1ef4a0170ee269"}, {file = "orjson-3.10.11-cp311-none-win_amd64.whl", hash = "sha256:df8c677df2f9f385fcc85ab859704045fa88d4668bc9991a527c86e710392bec"},
{file = "orjson-3.10.10-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:8564f48f3620861f5ef1e080ce7cd122ee89d7d6dacf25fcae675ff63b4d6e05"}, {file = "orjson-3.10.11-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:360a4e2c0943da7c21505e47cf6bd725588962ff1d739b99b14e2f7f3545ba51"},
{file = "orjson-3.10.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5bf161a32b479034098c5b81f2608f09167ad2fa1c06abd4e527ea6bf4837a9"}, {file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:496e2cb45de21c369079ef2d662670a4892c81573bcc143c4205cae98282ba97"},
{file = "orjson-3.10.10-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:68b65c93617bcafa7f04b74ae8bc2cc214bd5cb45168a953256ff83015c6747d"}, {file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7dfa8db55c9792d53c5952900c6a919cfa377b4f4534c7a786484a6a4a350c19"},
{file = "orjson-3.10.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8e28406f97fc2ea0c6150f4c1b6e8261453318930b334abc419214c82314f85"}, {file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51f3382415747e0dbda9dade6f1e1a01a9d37f630d8c9049a8ed0e385b7a90c0"},
{file = "orjson-3.10.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4d0d9fe174cc7a5bdce2e6c378bcdb4c49b2bf522a8f996aa586020e1b96cee"}, {file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f35a1b9f50a219f470e0e497ca30b285c9f34948d3c8160d5ad3a755d9299433"},
{file = "orjson-3.10.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3be81c42f1242cbed03cbb3973501fcaa2675a0af638f8be494eaf37143d999"}, {file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f3b7c5803138e67028dde33450e054c87e0703afbe730c105f1fcd873496d5"},
{file = "orjson-3.10.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:65f9886d3bae65be026219c0a5f32dbbe91a9e6272f56d092ab22561ad0ea33b"}, {file = "orjson-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f91d9eb554310472bd09f5347950b24442600594c2edc1421403d7610a0998fd"},
{file = "orjson-3.10.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:730ed5350147db7beb23ddaf072f490329e90a1d059711d364b49fe352ec987b"}, {file = "orjson-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dfbb2d460a855c9744bbc8e36f9c3a997c4b27d842f3d5559ed54326e6911f9b"},
{file = "orjson-3.10.10-cp312-none-win32.whl", hash = "sha256:a8f4bf5f1c85bea2170800020d53a8877812892697f9c2de73d576c9307a8a5f"}, {file = "orjson-3.10.11-cp312-none-win32.whl", hash = "sha256:d4a62c49c506d4d73f59514986cadebb7e8d186ad510c518f439176cf8d5359d"},
{file = "orjson-3.10.10-cp312-none-win_amd64.whl", hash = "sha256:384cd13579a1b4cd689d218e329f459eb9ddc504fa48c5a83ef4889db7fd7a4f"}, {file = "orjson-3.10.11-cp312-none-win_amd64.whl", hash = "sha256:f1eec3421a558ff7a9b010a6c7effcfa0ade65327a71bb9b02a1c3b77a247284"},
{file = "orjson-3.10.10-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44bffae68c291f94ff5a9b4149fe9d1bdd4cd0ff0fb575bcea8351d48db629a1"}, {file = "orjson-3.10.11-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c46294faa4e4d0eb73ab68f1a794d2cbf7bab33b1dda2ac2959ffb7c61591899"},
{file = "orjson-3.10.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e27b4c6437315df3024f0835887127dac2a0a3ff643500ec27088d2588fa5ae1"}, {file = "orjson-3.10.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52e5834d7d6e58a36846e059d00559cb9ed20410664f3ad156cd2cc239a11230"},
{file = "orjson-3.10.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca84df16d6b49325a4084fd8b2fe2229cb415e15c46c529f868c3387bb1339d"}, {file = "orjson-3.10.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2fc947e5350fdce548bfc94f434e8760d5cafa97fb9c495d2fef6757aa02ec0"},
{file = "orjson-3.10.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c14ce70e8f39bd71f9f80423801b5d10bf93d1dceffdecd04df0f64d2c69bc01"}, {file = "orjson-3.10.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0efabbf839388a1dab5b72b5d3baedbd6039ac83f3b55736eb9934ea5494d258"},
{file = "orjson-3.10.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:24ac62336da9bda1bd93c0491eff0613003b48d3cb5d01470842e7b52a40d5b4"}, {file = "orjson-3.10.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3f29634260708c200c4fe148e42b4aae97d7b9fee417fbdd74f8cfc265f15b0"},
{file = "orjson-3.10.10-cp313-none-win32.whl", hash = "sha256:eb0a42831372ec2b05acc9ee45af77bcaccbd91257345f93780a8e654efc75db"}, {file = "orjson-3.10.11-cp313-none-win32.whl", hash = "sha256:1a1222ffcee8a09476bbdd5d4f6f33d06d0d6642df2a3d78b7a195ca880d669b"},
{file = "orjson-3.10.10-cp313-none-win_amd64.whl", hash = "sha256:f0c4f37f8bf3f1075c6cc8dd8a9f843689a4b618628f8812d0a71e6968b95ffd"}, {file = "orjson-3.10.11-cp313-none-win_amd64.whl", hash = "sha256:bc274ac261cc69260913b2d1610760e55d3c0801bb3457ba7b9004420b6b4270"},
{file = "orjson-3.10.10-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:829700cc18503efc0cf502d630f612884258020d98a317679cd2054af0259568"}, {file = "orjson-3.10.11-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:19b3763e8bbf8ad797df6b6b5e0fc7c843ec2e2fc0621398534e0c6400098f87"},
{file = "orjson-3.10.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0ceb5e0e8c4f010ac787d29ae6299846935044686509e2f0f06ed441c1ca949"}, {file = "orjson-3.10.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be83a13312e5e58d633580c5eb8d0495ae61f180da2722f20562974188af205"},
{file = "orjson-3.10.10-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c25908eb86968613216f3db4d3003f1c45d78eb9046b71056ca327ff92bdbd4"}, {file = "orjson-3.10.11-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:afacfd1ab81f46dedd7f6001b6d4e8de23396e4884cd3c3436bd05defb1a6446"},
{file = "orjson-3.10.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:218cb0bc03340144b6328a9ff78f0932e642199ac184dd74b01ad691f42f93ff"}, {file = "orjson-3.10.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb4d0bea56bba596723d73f074c420aec3b2e5d7d30698bc56e6048066bd560c"},
{file = "orjson-3.10.10-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2277ec2cea3775640dc81ab5195bb5b2ada2fe0ea6eee4677474edc75ea6785"}, {file = "orjson-3.10.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96ed1de70fcb15d5fed529a656df29f768187628727ee2788344e8a51e1c1350"},
{file = "orjson-3.10.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:848ea3b55ab5ccc9d7bbd420d69432628b691fba3ca8ae3148c35156cbd282aa"}, {file = "orjson-3.10.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bfb30c891b530f3f80e801e3ad82ef150b964e5c38e1fb8482441c69c35c61c"},
{file = "orjson-3.10.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:e3e67b537ac0c835b25b5f7d40d83816abd2d3f4c0b0866ee981a045287a54f3"}, {file = "orjson-3.10.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d496c74fc2b61341e3cefda7eec21b7854c5f672ee350bc55d9a4997a8a95204"},
{file = "orjson-3.10.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:7948cfb909353fce2135dcdbe4521a5e7e1159484e0bb024c1722f272488f2b8"}, {file = "orjson-3.10.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:655a493bac606655db9a47fe94d3d84fc7f3ad766d894197c94ccf0c5408e7d3"},
{file = "orjson-3.10.10-cp38-none-win32.whl", hash = "sha256:78bee66a988f1a333dc0b6257503d63553b1957889c17b2c4ed72385cd1b96ae"}, {file = "orjson-3.10.11-cp38-none-win32.whl", hash = "sha256:b9546b278c9fb5d45380f4809e11b4dd9844ca7aaf1134024503e134ed226161"},
{file = "orjson-3.10.10-cp38-none-win_amd64.whl", hash = "sha256:f1d647ca8d62afeb774340a343c7fc023efacfd3a39f70c798991063f0c681dd"}, {file = "orjson-3.10.11-cp38-none-win_amd64.whl", hash = "sha256:b592597fe551d518f42c5a2eb07422eb475aa8cfdc8c51e6da7054b836b26782"},
{file = "orjson-3.10.10-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5a059afddbaa6dd733b5a2d76a90dbc8af790b993b1b5cb97a1176ca713b5df8"}, {file = "orjson-3.10.11-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c95f2ecafe709b4e5c733b5e2768ac569bed308623c85806c395d9cca00e08af"},
{file = "orjson-3.10.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f9b5c59f7e2a1a410f971c5ebc68f1995822837cd10905ee255f96074537ee6"}, {file = "orjson-3.10.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80c00d4acded0c51c98754fe8218cb49cb854f0f7eb39ea4641b7f71732d2cb7"},
{file = "orjson-3.10.10-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d5ef198bafdef4aa9d49a4165ba53ffdc0a9e1c7b6f76178572ab33118afea25"}, {file = "orjson-3.10.11-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:461311b693d3d0a060439aa669c74f3603264d4e7a08faa68c47ae5a863f352d"},
{file = "orjson-3.10.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf29ce0bb5d3320824ec3d1508652421000ba466abd63bdd52c64bcce9eb1fa"}, {file = "orjson-3.10.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52ca832f17d86a78cbab86cdc25f8c13756ebe182b6fc1a97d534051c18a08de"},
{file = "orjson-3.10.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dddd5516bcc93e723d029c1633ae79c4417477b4f57dad9bfeeb6bc0315e654a"}, {file = "orjson-3.10.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c57ea78a753812f528178aa2f1c57da633754c91d2124cb28991dab4c79a54"},
{file = "orjson-3.10.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12f2003695b10817f0fa8b8fca982ed7f5761dcb0d93cff4f2f9f6709903fd7"}, {file = "orjson-3.10.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7fcfc6f7ca046383fb954ba528587e0f9336828b568282b27579c49f8e16aad"},
{file = "orjson-3.10.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:672f9874a8a8fb9bb1b771331d31ba27f57702c8106cdbadad8bda5d10bc1019"}, {file = "orjson-3.10.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:86b9dd983857970c29e4c71bb3e95ff085c07d3e83e7c46ebe959bac07ebd80b"},
{file = "orjson-3.10.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1dcbb0ca5fafb2b378b2c74419480ab2486326974826bbf6588f4dc62137570a"}, {file = "orjson-3.10.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4d83f87582d223e54efb2242a79547611ba4ebae3af8bae1e80fa9a0af83bb7f"},
{file = "orjson-3.10.10-cp39-none-win32.whl", hash = "sha256:d9bbd3a4b92256875cb058c3381b782649b9a3c68a4aa9a2fff020c2f9cfc1be"}, {file = "orjson-3.10.11-cp39-none-win32.whl", hash = "sha256:9fd0ad1c129bc9beb1154c2655f177620b5beaf9a11e0d10bac63ef3fce96950"},
{file = "orjson-3.10.10-cp39-none-win_amd64.whl", hash = "sha256:766f21487a53aee8524b97ca9582d5c6541b03ab6210fbaf10142ae2f3ced2aa"}, {file = "orjson-3.10.11-cp39-none-win_amd64.whl", hash = "sha256:10f416b2a017c8bd17f325fb9dee1fb5cdd7a54e814284896b7c3f2763faa017"},
{file = "orjson-3.10.10.tar.gz", hash = "sha256:37949383c4df7b4337ce82ee35b6d7471e55195efa7dcb45ab8226ceadb0fe3b"}, {file = "orjson-3.10.11.tar.gz", hash = "sha256:e35b6d730de6384d5b2dab5fd23f0d76fae8bbc8c353c2f78210aa5fa4beb3ef"},
] ]
[[package]] [[package]]
@ -2796,13 +2796,13 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
[[package]] [[package]]
name = "rich" name = "rich"
version = "13.9.3" version = "13.9.4"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false optional = false
python-versions = ">=3.8.0" python-versions = ">=3.8.0"
files = [ files = [
{file = "rich-13.9.3-py3-none-any.whl", hash = "sha256:9836f5096eb2172c9e77df411c1b009bace4193d6a481d534fea75ebba758283"}, {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"},
{file = "rich-13.9.3.tar.gz", hash = "sha256:bc1e01b899537598cf02579d2b9f4a415104d3fc439313a7a2c165d76557a08e"}, {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"},
] ]
[package.dependencies] [package.dependencies]

View file

@ -60,6 +60,8 @@ def test_create_cookbook(api_client: TestClient, unique_user: TestUser):
page_data = get_page_data(unique_user.group_id, unique_user.household_id) page_data = get_page_data(unique_user.group_id, unique_user.household_id)
response = api_client.post(api_routes.households_cookbooks, json=page_data, headers=unique_user.token) response = api_client.post(api_routes.households_cookbooks, json=page_data, headers=unique_user.token)
assert response.status_code == 201 assert response.status_code == 201
assert response.json()["groupId"] == unique_user.group_id
assert response.json()["householdId"] == unique_user.household_id
@pytest.mark.parametrize("name_input", ["", " ", "@"]) @pytest.mark.parametrize("name_input", ["", " ", "@"])
@ -78,9 +80,22 @@ def test_create_cookbook_bad_name(api_client: TestClient, unique_user: TestUser,
assert response.status_code == 422 assert response.status_code == 422
def test_read_cookbook(api_client: TestClient, unique_user: TestUser, cookbooks: list[TestCookbook]): @pytest.mark.parametrize("use_other_household", [True, False])
def test_read_cookbook(
api_client: TestClient,
unique_user: TestUser,
h2_user: TestUser,
cookbooks: list[TestCookbook],
use_other_household: bool,
):
sample = random.choice(cookbooks) sample = random.choice(cookbooks)
response = api_client.get(api_routes.households_cookbooks_item_id(sample.id), headers=unique_user.token) if use_other_household:
headers = h2_user.token
else:
headers = unique_user.token
# all households should be able to fetch all cookbooks
response = api_client.get(api_routes.households_cookbooks_item_id(sample.id), headers=headers)
assert response.status_code == 200 assert response.status_code == 200
page_data = response.json() page_data = response.json()
@ -111,6 +126,28 @@ def test_update_cookbook(api_client: TestClient, unique_user: TestUser, cookbook
assert page_data["slug"] == update_data["name"] assert page_data["slug"] == update_data["name"]
def test_update_cookbook_other_household(
api_client: TestClient, unique_user: TestUser, h2_user: TestUser, cookbooks: list[TestCookbook]
):
cookbook = random.choice(cookbooks)
update_data = get_page_data(unique_user.group_id, unique_user.household_id)
update_data["name"] = random_string(10)
response = api_client.put(
api_routes.households_cookbooks_item_id(cookbook.id), json=update_data, headers=h2_user.token
)
assert response.status_code == 404
response = api_client.get(api_routes.households_cookbooks_item_id(cookbook.id), headers=unique_user.token)
assert response.status_code == 200
page_data = response.json()
assert page_data["name"] != update_data["name"]
assert page_data["slug"] != update_data["name"]
def test_update_cookbooks_many(api_client: TestClient, unique_user: TestUser, cookbooks: list[TestCookbook]): def test_update_cookbooks_many(api_client: TestClient, unique_user: TestUser, cookbooks: list[TestCookbook]):
pages = [x.data for x in cookbooks] pages = [x.data for x in cookbooks]
@ -135,6 +172,20 @@ def test_update_cookbooks_many(api_client: TestClient, unique_user: TestUser, co
assert str(know) in server_ids assert str(know) in server_ids
def test_update_cookbooks_many_other_household(
api_client: TestClient, unique_user: TestUser, h2_user: TestUser, cookbooks: list[TestCookbook]
):
pages = [x.data for x in cookbooks]
reverse_order = sorted(pages, key=lambda x: x["position"], reverse=True)
for x, page in enumerate(reverse_order):
page["position"] = x
page["group_id"] = str(unique_user.group_id)
response = api_client.put(api_routes.households_cookbooks, json=utils.jsonify(reverse_order), headers=h2_user.token)
assert response.status_code == 404
def test_delete_cookbook(api_client: TestClient, unique_user: TestUser, cookbooks: list[TestCookbook]): def test_delete_cookbook(api_client: TestClient, unique_user: TestUser, cookbooks: list[TestCookbook]):
sample = random.choice(cookbooks) sample = random.choice(cookbooks)
response = api_client.delete(api_routes.households_cookbooks_item_id(sample.id), headers=unique_user.token) response = api_client.delete(api_routes.households_cookbooks_item_id(sample.id), headers=unique_user.token)
@ -145,6 +196,18 @@ def test_delete_cookbook(api_client: TestClient, unique_user: TestUser, cookbook
assert response.status_code == 404 assert response.status_code == 404
def test_delete_cookbook_other_household(
api_client: TestClient, unique_user: TestUser, h2_user: TestUser, cookbooks: list[TestCookbook]
):
sample = random.choice(cookbooks)
response = api_client.delete(api_routes.households_cookbooks_item_id(sample.id), headers=h2_user.token)
assert response.status_code == 404
response = api_client.get(api_routes.households_cookbooks_item_id(sample.slug), headers=unique_user.token)
assert response.status_code == 200
@pytest.mark.parametrize( @pytest.mark.parametrize(
"qf_string, expected_code", "qf_string, expected_code",
[ [

View file

@ -299,3 +299,16 @@ def test_cookbook_recipes_includes_all_households(api_client: TestClient, unique
assert recipe.id in fetched_recipe_ids assert recipe.id in fetched_recipe_ids
for recipe in other_recipes: for recipe in other_recipes:
assert recipe.id in fetched_recipe_ids assert recipe.id in fetched_recipe_ids
def test_cookbooks_from_other_households(api_client: TestClient, unique_user: TestUser, h2_user: TestUser):
h2_cookbook = h2_user.repos.cookbooks.create(
SaveCookBook(
name=random_string(),
group_id=h2_user.group_id,
household_id=h2_user.household_id,
)
)
response = api_client.get(api_routes.recipes, params={"cookbook": h2_cookbook.slug}, headers=unique_user.token)
assert response.status_code == 200

View file

@ -1,8 +1,10 @@
import pytest
from pytest import MonkeyPatch, Session from pytest import MonkeyPatch, Session
from mealie.core.config import get_app_settings from mealie.core.config import get_app_settings
from mealie.core.security.providers.openid_provider import OpenIDProvider from mealie.core.security.providers.openid_provider import OpenIDProvider
from mealie.repos.all_repositories import get_repositories from mealie.repos.all_repositories import get_repositories
from tests.utils.factories import random_email, random_string
from tests.utils.fixture_schemas import TestUser from tests.utils.fixture_schemas import TestUser
@ -125,3 +127,38 @@ def test_has_admin_group_new_user(monkeypatch: MonkeyPatch, session: Session):
user = db.users.get_one("dude2", "username") user = db.users.get_one("dude2", "username")
assert user is not None assert user is not None
assert user.admin assert user.admin
@pytest.mark.parametrize("valid_group", [True, False])
@pytest.mark.parametrize("valid_household", [True, False])
def test_ldap_user_creation_invalid_group_or_household(
monkeypatch: MonkeyPatch, session: Session, valid_group: bool, valid_household: bool
):
monkeypatch.setenv("OIDC_USER_GROUP", "mealie_user")
monkeypatch.setenv("OIDC_ADMIN_GROUP", "mealie_admin")
if not valid_group:
monkeypatch.setenv("DEFAULT_GROUP", random_string())
if not valid_household:
monkeypatch.setenv("DEFAULT_HOUSEHOLD", random_string())
get_app_settings.cache_clear()
data = {
"preferred_username": random_string(),
"email": random_email(),
"name": random_string(),
"groups": ["mealie_user"],
}
auth_provider = OpenIDProvider(session, data)
if valid_group and valid_household:
assert auth_provider.authenticate() is not None
else:
assert auth_provider.authenticate() is None
db = get_repositories(session, group_id=None, household_id=None)
user = db.users.get_one(data["preferred_username"], "username")
if valid_group and valid_household:
assert user is not None
else:
assert user is None

View file

@ -1,6 +1,7 @@
from pathlib import Path from pathlib import Path
import ldap import ldap
import pytest
from pytest import MonkeyPatch from pytest import MonkeyPatch
from mealie.core import security from mealie.core import security
@ -13,6 +14,7 @@ from mealie.core.security.providers.credentials_provider import (
from mealie.core.security.providers.ldap_provider import LDAPProvider from mealie.core.security.providers.ldap_provider import LDAPProvider
from mealie.db.db_setup import session_context from mealie.db.db_setup import session_context
from mealie.db.models.users.users import AuthMethod from mealie.db.models.users.users import AuthMethod
from mealie.repos.repository_factory import AllRepositories
from mealie.schema.user.auth import CredentialsRequestForm from mealie.schema.user.auth import CredentialsRequestForm
from mealie.schema.user.user import PrivateUser from mealie.schema.user.user import PrivateUser
from tests.utils import random_string from tests.utils import random_string
@ -92,7 +94,7 @@ class LdapConnMock:
pass pass
def setup_env(monkeypatch: MonkeyPatch): def setup_env(monkeypatch: MonkeyPatch, **kwargs):
user = random_string(10) user = random_string(10)
mail = random_string(10) mail = random_string(10)
name = random_string(10) name = random_string(10)
@ -140,11 +142,55 @@ def test_ldap_user_creation(monkeypatch: MonkeyPatch):
provider = get_provider(session, user, password) provider = get_provider(session, user, password)
result = provider.get_user() result = provider.get_user()
app_settings = get_app_settings()
assert result assert result
assert result.username == user assert result.username == user
assert result.email == mail assert result.email == mail
assert result.full_name == name assert result.full_name == name
assert result.admin is False assert result.admin is False
assert result.group == app_settings.DEFAULT_GROUP
assert result.household == app_settings.DEFAULT_HOUSEHOLD
assert result.auth_method == AuthMethod.LDAP
@pytest.mark.parametrize("valid_group", [True, False])
@pytest.mark.parametrize("valid_household", [True, False])
def test_ldap_user_creation_invalid_group_or_household(
unfiltered_database: AllRepositories, monkeypatch: MonkeyPatch, valid_group: bool, valid_household: bool
):
user, mail, name, password, query_bind, query_password = setup_env(monkeypatch)
if not valid_group:
monkeypatch.setenv("DEFAULT_GROUP", random_string())
if not valid_household:
monkeypatch.setenv("DEFAULT_HOUSEHOLD", random_string())
def ldap_initialize_mock(url):
assert url == ""
return LdapConnMock(user, password, False, query_bind, query_password, mail, name)
monkeypatch.setattr(ldap, "initialize", ldap_initialize_mock)
get_app_settings.cache_clear()
with session_context() as session:
provider = get_provider(session, user, password)
try:
result = provider.get_user()
except ValueError:
result = None
if valid_group and valid_household:
assert result
else:
assert not result
# check if the user exists in the db
user = unfiltered_database.users.get_by_username(user)
if valid_group and valid_household:
assert user
else:
assert not user
def test_ldap_user_creation_fail(monkeypatch: MonkeyPatch): def test_ldap_user_creation_fail(monkeypatch: MonkeyPatch):