Merge pull request #3213 from michael-genson/feat/filter-shopping-lists

feat: Filter Out Shopping Lists That Aren't Yours
This commit is contained in:
boc-the-git 2024-03-07 09:27:04 +11:00 committed by GitHub
commit e84e5e2910
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 253 additions and 27 deletions

View File

@ -9,7 +9,7 @@ Create Date: 2023-21-02 22:03:19.837244
from uuid import uuid4 from uuid import uuid4
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy.orm.session import Session from sqlalchemy import orm
import mealie.db.migration_types import mealie.db.migration_types
from alembic import op from alembic import op
@ -23,8 +23,10 @@ branch_labels = None
depends_on = None depends_on = None
def populate_shopping_lists_multi_purpose_labels(shopping_lists_multi_purpose_labels_table: sa.Table, session: Session): def populate_shopping_lists_multi_purpose_labels(
shopping_lists = session.query(ShoppingList).all() shopping_lists_multi_purpose_labels_table: sa.Table, session: orm.Session
):
shopping_lists = session.query(ShoppingList).options(orm.load_only(ShoppingList.id, ShoppingList.group_id)).all()
shopping_lists_labels_data: list[dict] = [] shopping_lists_labels_data: list[dict] = []
for shopping_list in shopping_lists: for shopping_list in shopping_lists:
@ -60,7 +62,7 @@ def upgrade():
) )
# ### end Alembic commands ### # ### end Alembic commands ###
session = Session(bind=op.get_bind()) session = orm.Session(bind=op.get_bind())
populate_shopping_lists_multi_purpose_labels(shopping_lists_multi_purpose_labels_table, session) populate_shopping_lists_multi_purpose_labels(shopping_lists_multi_purpose_labels_table, session)

View File

@ -0,0 +1,85 @@
"""added user to shopping list
Revision ID: 2298bb460ffd
Revises: ba1e4a6cfe99
Create Date: 2024-02-23 16:15:07.115641
"""
from uuid import UUID
import sqlalchemy as sa
from sqlalchemy import orm
import mealie.db.migration_types
from alembic import op
# revision identifiers, used by Alembic.
revision = "2298bb460ffd"
down_revision = "ba1e4a6cfe99"
branch_labels = None
depends_on = None
def is_postgres():
return op.get_context().dialect.name == "postgresql"
def find_user_id_for_group(group_id: UUID):
bind = op.get_bind()
session = orm.Session(bind=bind)
if is_postgres():
stmt = "SELECT id FROM users WHERE group_id=:group_id AND admin = TRUE LIMIT 1"
else:
stmt = "SELECT id FROM users WHERE group_id=:group_id AND admin = 1 LIMIT 1"
with session:
try:
# try to find an admin user
user_id = session.execute(sa.text(stmt).bindparams(group_id=group_id)).scalar_one()
except orm.exc.NoResultFound:
# fallback to any user
user_id = session.execute(
sa.text("SELECT id FROM users WHERE group_id=:group_id LIMIT 1").bindparams(group_id=group_id)
).scalar_one()
return user_id
def populate_shopping_list_users():
bind = op.get_bind()
session = orm.Session(bind=bind)
with session:
list_ids_and_group_ids = session.execute(sa.text("SELECT id, group_id FROM shopping_lists")).all()
for list_id, group_id in list_ids_and_group_ids:
user_id = find_user_id_for_group(group_id)
session.execute(
sa.text(f"UPDATE shopping_lists SET user_id=:user_id WHERE id=:id").bindparams(
user_id=user_id, id=list_id
)
)
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("shopping_lists") as batch_op:
# allow nulls during migration
batch_op.add_column(sa.Column("user_id", mealie.db.migration_types.GUID(), nullable=True))
batch_op.create_index(op.f("ix_shopping_lists_user_id"), ["user_id"], unique=False)
batch_op.create_foreign_key("fk_user_shopping_lists", "users", ["user_id"], ["id"])
# ### end Alembic commands ###
populate_shopping_list_users()
# forbid nulls after migration
with op.batch_alter_table("shopping_lists") as batch_op:
batch_op.alter_column("user_id", nullable=False)
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, "shopping_lists", type_="foreignkey")
op.drop_index(op.f("ix_shopping_lists_user_id"), table_name="shopping_lists")
op.drop_column("shopping_lists", "user_id")
# ### end Alembic commands ###

View File

@ -107,7 +107,7 @@ export default defineComponent({
}) })
async function getShoppingLists() { async function getShoppingLists() {
const { data } = await api.shopping.lists.getAll(); const { data } = await api.shopping.lists.getAll(1, -1, { orderBy: "name", orderDirection: "asc" });
if (data) { if (data) {
shoppingLists.value = data.items ?? []; shoppingLists.value = data.items ?? [];
} }

View File

@ -321,7 +321,7 @@ export default defineComponent({
const recipeRefWithScale = computed(() => recipeRef.value ? { scale: props.recipeScale, ...recipeRef.value } : undefined); const recipeRefWithScale = computed(() => recipeRef.value ? { scale: props.recipeScale, ...recipeRef.value } : undefined);
async function getShoppingLists() { async function getShoppingLists() {
const { data } = await api.shopping.lists.getAll(); const { data } = await api.shopping.lists.getAll(1, -1, { orderBy: "name", orderDirection: "asc" });
if (data) { if (data) {
shoppingLists.value = data.items ?? []; shoppingLists.value = data.items ?? [];
} }

View File

@ -3,7 +3,7 @@
<BaseDialog v-if="shoppingListDialog" v-model="dialog" :title="$t('recipe.add-to-list')" :icon="$globals.icons.cartCheck"> <BaseDialog v-if="shoppingListDialog" v-model="dialog" :title="$t('recipe.add-to-list')" :icon="$globals.icons.cartCheck">
<v-card-text> <v-card-text>
<v-card <v-card
v-for="list in shoppingLists" v-for="list in shoppingListChoices"
:key="list.id" :key="list.id"
hover hover
class="my-2 left-border" class="my-2 left-border"
@ -14,6 +14,18 @@
</v-card-title> </v-card-title>
</v-card> </v-card>
</v-card-text> </v-card-text>
<template #card-actions>
<v-btn
text
color="grey"
@click="dialog = false"
>
{{ $t("general.cancel") }}
</v-btn>
<div class="d-flex justify-end" style="width: 100%;">
<v-checkbox v-model="preferences.viewAllLists" hide-details :label="$tc('general.show-all')" class="my-auto mr-4" />
</div>
</template>
</BaseDialog> </BaseDialog>
<BaseDialog <BaseDialog
v-if="shoppingListIngredientDialog" v-if="shoppingListIngredientDialog"
@ -120,6 +132,7 @@ import { toRefs } from "@vueuse/core";
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue"; import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { alert } from "~/composables/use-toast"; import { alert } from "~/composables/use-toast";
import { useShoppingListPreferences } from "~/composables/use-users/preferences";
import { ShoppingListSummary } from "~/lib/api/types/group"; import { ShoppingListSummary } from "~/lib/api/types/group";
import { Recipe, RecipeIngredient } from "~/lib/api/types/recipe"; import { Recipe, RecipeIngredient } from "~/lib/api/types/recipe";
@ -164,8 +177,9 @@ export default defineComponent({
}, },
}, },
setup(props, context) { setup(props, context) {
const { i18n } = useContext(); const { $auth, i18n } = useContext();
const api = useUserApi(); const api = useUserApi();
const preferences = useShoppingListPreferences();
// v-model support // v-model support
const dialog = computed({ const dialog = computed({
@ -183,6 +197,10 @@ export default defineComponent({
shoppingListIngredientDialog: false, shoppingListIngredientDialog: false,
}); });
const shoppingListChoices = computed(() => {
return props.shoppingLists.filter((list) => preferences.value.viewAllLists || list.userId === $auth.user?.id);
});
const recipeIngredientSections = ref<ShoppingListRecipeIngredientSection[]>([]); const recipeIngredientSections = ref<ShoppingListRecipeIngredientSection[]>([]);
const selectedShoppingList = ref<ShoppingListSummary | null>(null); const selectedShoppingList = ref<ShoppingListSummary | null>(null);
@ -334,6 +352,8 @@ export default defineComponent({
return { return {
dialog, dialog,
preferences,
shoppingListChoices,
...toRefs(state), ...toRefs(state),
addRecipesToList, addRecipesToList,
bulkCheckIngredients, bulkCheckIngredients,

View File

@ -22,6 +22,7 @@ export interface UserRecipePreferences {
} }
export interface UserShoppingListPreferences { export interface UserShoppingListPreferences {
viewAllLists: boolean;
viewByLabel: boolean; viewByLabel: boolean;
} }
@ -70,6 +71,7 @@ export function useShoppingListPreferences(): Ref<UserShoppingListPreferences> {
const fromStorage = useLocalStorage( const fromStorage = useLocalStorage(
"shopping-list-preferences", "shopping-list-preferences",
{ {
viewAllLists: false,
viewByLabel: false, viewByLabel: false,
}, },
{ mergeDefaults: true } { mergeDefaults: true }

View File

@ -142,6 +142,7 @@
"save": "Save", "save": "Save",
"settings": "Settings", "settings": "Settings",
"share": "Share", "share": "Share",
"show-all": "Show All",
"shuffle": "Shuffle", "shuffle": "Shuffle",
"sort": "Sort", "sort": "Sort",
"sort-alphabetically": "Alphabetical", "sort-alphabetically": "Alphabetical",

View File

@ -505,6 +505,7 @@ export interface ShoppingListOut {
createdAt?: string; createdAt?: string;
updateAt?: string; updateAt?: string;
groupId: string; groupId: string;
userId: string;
id: string; id: string;
listItems?: ShoppingListItemOut[]; listItems?: ShoppingListItemOut[];
recipeReferences: ShoppingListRecipeRefOut[]; recipeReferences: ShoppingListRecipeRefOut[];
@ -568,6 +569,7 @@ export interface ShoppingListSave {
createdAt?: string; createdAt?: string;
updateAt?: string; updateAt?: string;
groupId: string; groupId: string;
userId: string;
} }
export interface ShoppingListSummary { export interface ShoppingListSummary {
name?: string; name?: string;
@ -577,6 +579,7 @@ export interface ShoppingListSummary {
createdAt?: string; createdAt?: string;
updateAt?: string; updateAt?: string;
groupId: string; groupId: string;
userId: string;
id: string; id: string;
recipeReferences: ShoppingListRecipeRefOut[]; recipeReferences: ShoppingListRecipeRefOut[];
labelSettings: ShoppingListMultiPurposeLabelOut[]; labelSettings: ShoppingListMultiPurposeLabelOut[];
@ -589,6 +592,7 @@ export interface ShoppingListUpdate {
createdAt?: string; createdAt?: string;
updateAt?: string; updateAt?: string;
groupId: string; groupId: string;
userId: string;
id: string; id: string;
listItems?: ShoppingListItemOut[]; listItems?: ShoppingListItemOut[];
} }

View File

@ -68,6 +68,27 @@
</v-card> </v-card>
</BaseDialog> </BaseDialog>
<!-- Settings -->
<BaseDialog
v-model="settingsDialog"
:icon="$globals.icons.cog"
:title="$t('general.settings')"
@confirm="updateSettings"
>
<v-container>
<v-form>
<v-select
v-model="currentUserId"
:items="allUsers"
item-text="fullName"
item-value="id"
:label="$t('general.owner')"
:prepend-icon="$globals.icons.user"
/>
</v-form>
</v-container>
</BaseDialog>
<!-- Create Item --> <!-- Create Item -->
<div v-if="createEditorOpen"> <div v-if="createEditorOpen">
<ShoppingListItemEditor <ShoppingListItemEditor
@ -82,7 +103,7 @@
/> />
</div> </div>
<div v-else class="mt-4 d-flex justify-end"> <div v-else class="mt-4 d-flex justify-end">
<BaseButton v-if="preferences.viewByLabel" color="info" class="mr-2" @click="reorderLabelsDialog = true"> <BaseButton v-if="preferences.viewByLabel" edit class="mr-2" @click="reorderLabelsDialog = true">
<template #icon> {{ $globals.icons.tags }} </template> <template #icon> {{ $globals.icons.tags }} </template>
{{ $t('shopping-list.reorder-labels') }} {{ $t('shopping-list.reorder-labels') }}
</BaseButton> </BaseButton>
@ -197,6 +218,15 @@
</section> </section>
</v-lazy> </v-lazy>
<v-lazy>
<div class="d-flex justify-end">
<BaseButton edit @click="toggleSettingsDialog">
<template #icon> {{ $globals.icons.cog }} </template>
{{ $t('general.settings') }}
</BaseButton>
</div>
</v-lazy>
<v-lazy> <v-lazy>
<div class="d-flex justify-end mt-10"> <div class="d-flex justify-end mt-10">
<ButtonLink :to="`/group/data/labels`" :text="$tc('shopping-list.manage-labels')" :icon="$globals.icons.tags" /> <ButtonLink :to="`/group/data/labels`" :text="$tc('shopping-list.manage-labels')" :icon="$globals.icons.tags" />
@ -215,6 +245,7 @@ import { useUserApi } from "~/composables/api";
import MultiPurposeLabelSection from "~/components/Domain/ShoppingList/MultiPurposeLabelSection.vue" import MultiPurposeLabelSection from "~/components/Domain/ShoppingList/MultiPurposeLabelSection.vue"
import ShoppingListItem from "~/components/Domain/ShoppingList/ShoppingListItem.vue"; import ShoppingListItem from "~/components/Domain/ShoppingList/ShoppingListItem.vue";
import { ShoppingListItemCreate, ShoppingListItemOut, ShoppingListMultiPurposeLabelOut, ShoppingListOut } from "~/lib/api/types/group"; import { ShoppingListItemCreate, ShoppingListItemOut, ShoppingListMultiPurposeLabelOut, ShoppingListOut } from "~/lib/api/types/group";
import { UserOut } from "~/lib/api/types/user";
import RecipeList from "~/components/Domain/Recipe/RecipeList.vue"; import RecipeList from "~/components/Domain/Recipe/RecipeList.vue";
import ShoppingListItemEditor from "~/components/Domain/ShoppingList/ShoppingListItemEditor.vue"; import ShoppingListItemEditor from "~/components/Domain/ShoppingList/ShoppingListItemEditor.vue";
import { useFoodStore, useLabelStore, useUnitStore } from "~/composables/store"; import { useFoodStore, useLabelStore, useUnitStore } from "~/composables/store";
@ -247,6 +278,7 @@ export default defineComponent({
const edit = ref(false); const edit = ref(false);
const reorderLabelsDialog = ref(false); const reorderLabelsDialog = ref(false);
const settingsDialog = ref(false);
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 || "");
@ -464,6 +496,13 @@ export default defineComponent({
reorderLabelsDialog.value = !reorderLabelsDialog.value reorderLabelsDialog.value = !reorderLabelsDialog.value
} }
async function toggleSettingsDialog() {
if (!settingsDialog.value) {
await fetchAllUsers();
}
settingsDialog.value = !settingsDialog.value;
}
async function updateLabelOrder(labelSettings: ShoppingListMultiPurposeLabelOut[]) { async function updateLabelOrder(labelSettings: ShoppingListMultiPurposeLabelOut[]) {
if (!shoppingList.value) { if (!shoppingList.value) {
return; return;
@ -775,6 +814,39 @@ export default defineComponent({
} }
} }
// ===============================================================
// Shopping List Settings
const allUsers = ref<UserOut[]>([]);
const currentUserId = ref<string | undefined>();
async function fetchAllUsers() {
const { data } = await userApi.users.getAll(1, -1, { orderBy: "full_name", orderDirection: "asc" });
if (!data) {
return;
}
// update current user
allUsers.value = data.items;
currentUserId.value = shoppingList.value?.userId;
}
async function updateSettings() {
if (!shoppingList.value || !currentUserId.value) {
return;
}
loadingCounter.value += 1;
const { data } = await userApi.shopping.lists.updateOne(
shoppingList.value.id,
{...shoppingList.value, userId: currentUserId.value},
);
loadingCounter.value -= 1;
if (data) {
refresh();
}
}
return { return {
addRecipeReferenceToList, addRecipeReferenceToList,
updateListItems, updateListItems,
@ -799,6 +871,8 @@ export default defineComponent({
removeRecipeReferenceToList, removeRecipeReferenceToList,
reorderLabelsDialog, reorderLabelsDialog,
toggleReorderLabelsDialog, toggleReorderLabelsDialog,
settingsDialog,
toggleSettingsDialog,
updateLabelOrder, updateLabelOrder,
saveListItem, saveListItem,
shoppingList, shoppingList,
@ -810,6 +884,9 @@ export default defineComponent({
updateIndexUncheckedByLabel, updateIndexUncheckedByLabel,
allUnits, allUnits,
allFoods, allFoods,
allUsers,
currentUserId,
updateSettings,
}; };
}, },
head() { head() {

View File

@ -1,5 +1,5 @@
<template> <template>
<v-container v-if="shoppingLists" class="narrow-container"> <v-container v-if="shoppingListChoices" class="narrow-container">
<BaseDialog v-model="createDialog" :title="$tc('shopping-list.create-shopping-list')" @submit="createOne"> <BaseDialog v-model="createDialog" :title="$tc('shopping-list.create-shopping-list')" @submit="createOne">
<v-card-text> <v-card-text>
<v-text-field v-model="createName" autofocus :label="$t('shopping-list.new-list')"> </v-text-field> <v-text-field v-model="createName" autofocus :label="$t('shopping-list.new-list')"> </v-text-field>
@ -15,10 +15,19 @@
</template> </template>
<template #title>{{ $t('shopping-list.shopping-lists') }}</template> <template #title>{{ $t('shopping-list.shopping-lists') }}</template>
</BasePageTitle> </BasePageTitle>
<BaseButton create @click="createDialog = true" />
<v-container class="d-flex justify-end px-0 pt-0 pb-4">
<v-checkbox v-model="preferences.viewAllLists" hide-details :label="$tc('general.show-all')" class="my-auto mr-4" />
<BaseButton create @click="createDialog = true" />
</v-container>
<section> <section>
<v-card v-for="list in shoppingLists" :key="list.id" class="my-2 left-border" :to="`/shopping-lists/${list.id}`"> <v-card
v-for="list in shoppingListChoices"
:key="list.id"
class="my-2 left-border"
:to="`/shopping-lists/${list.id}`"
>
<v-card-title> <v-card-title>
<v-icon left> <v-icon left>
{{ $globals.icons.cartCheck }} {{ $globals.icons.cartCheck }}
@ -42,6 +51,7 @@
import { computed, defineComponent, useAsync, useContext, reactive, toRefs, useRoute } from "@nuxtjs/composition-api"; import { computed, defineComponent, useAsync, useContext, reactive, toRefs, useRoute } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { useAsyncKey } from "~/composables/use-utils"; import { useAsyncKey } from "~/composables/use-utils";
import { useShoppingListPreferences } from "~/composables/use-users/preferences";
export default defineComponent({ export default defineComponent({
middleware: "auth", middleware: "auth",
@ -50,6 +60,7 @@ export default defineComponent({
const userApi = useUserApi(); const userApi = useUserApi();
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 preferences = useShoppingListPreferences();
const state = reactive({ const state = reactive({
createName: "", createName: "",
@ -62,8 +73,16 @@ export default defineComponent({
return await fetchShoppingLists(); return await fetchShoppingLists();
}, useAsyncKey()); }, useAsyncKey());
const shoppingListChoices = computed(() => {
if (!shoppingLists.value) {
return [];
}
return shoppingLists.value.filter((list) => preferences.value.viewAllLists || list.userId === $auth.user?.id);
});
async function fetchShoppingLists() { async function fetchShoppingLists() {
const { data } = await userApi.shopping.lists.getAll(); const { data } = await userApi.shopping.lists.getAll(1, -1, { orderBy: "name", orderDirection: "asc" });
if (!data) { if (!data) {
return []; return [];
@ -100,7 +119,8 @@ export default defineComponent({
return { return {
...toRefs(state), ...toRefs(state),
groupSlug, groupSlug,
shoppingLists, preferences,
shoppingListChoices,
createOne, createOne,
deleteOne, deleteOne,
openDelete, openDelete,

View File

@ -14,6 +14,7 @@ from ..recipe.ingredient import IngredientFoodModel, IngredientUnitModel
if TYPE_CHECKING: if TYPE_CHECKING:
from group import Group from group import Group
from users import User
from ..recipe import RecipeModel from ..recipe import RecipeModel
@ -122,6 +123,8 @@ class ShoppingList(SqlAlchemyBase, BaseMixins):
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True) group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
group: Mapped["Group"] = orm.relationship("Group", back_populates="shopping_lists") group: Mapped["Group"] = orm.relationship("Group", back_populates="shopping_lists")
user_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("users.id"), nullable=False, index=True)
user: Mapped["User"] = orm.relationship("User", back_populates="shopping_lists")
name: Mapped[str | None] = mapped_column(String) name: Mapped[str | None] = mapped_column(String)
list_items: Mapped[list[ShoppingListItem]] = orm.relationship( list_items: Mapped[list[ShoppingListItem]] = orm.relationship(

View File

@ -17,6 +17,7 @@ from .user_to_favorite import users_to_favorites
if TYPE_CHECKING: if TYPE_CHECKING:
from ..group import Group from ..group import Group
from ..group.mealplan import GroupMealPlan from ..group.mealplan import GroupMealPlan
from ..group.shopping_list import ShoppingList
from ..recipe import RecipeComment, RecipeModel, RecipeTimelineEvent from ..recipe import RecipeComment, RecipeModel, RecipeTimelineEvent
from .password_reset import PasswordResetModel from .password_reset import PasswordResetModel
@ -81,7 +82,7 @@ class User(SqlAlchemyBase, BaseMixins):
mealplans: Mapped[Optional["GroupMealPlan"]] = orm.relationship( mealplans: Mapped[Optional["GroupMealPlan"]] = orm.relationship(
"GroupMealPlan", order_by="GroupMealPlan.date", **sp_args "GroupMealPlan", order_by="GroupMealPlan.date", **sp_args
) )
shopping_lists: Mapped[Optional["ShoppingList"]] = orm.relationship("ShoppingList", **sp_args)
favorite_recipes: Mapped[list["RecipeModel"]] = orm.relationship( favorite_recipes: Mapped[list["RecipeModel"]] = orm.relationship(
"RecipeModel", secondary=users_to_favorites, back_populates="favorited_by" "RecipeModel", secondary=users_to_favorites, back_populates="favorited_by"
) )

View File

@ -89,7 +89,7 @@ def publish_list_item_events(publisher: Callable, items_collection: ShoppingList
class ShoppingListItemController(BaseCrudController): class ShoppingListItemController(BaseCrudController):
@cached_property @cached_property
def service(self): def service(self):
return ShoppingListService(self.repos, self.group) return ShoppingListService(self.repos, self.group, self.user)
@cached_property @cached_property
def repo(self): def repo(self):
@ -154,7 +154,7 @@ router = APIRouter(prefix="/groups/shopping/lists", tags=["Group: Shopping Lists
class ShoppingListController(BaseCrudController): class ShoppingListController(BaseCrudController):
@cached_property @cached_property
def service(self): def service(self):
return ShoppingListService(self.repos, self.group) return ShoppingListService(self.repos, self.group, self.user)
@cached_property @cached_property
def repo(self): def repo(self):

View File

@ -190,6 +190,7 @@ class ShoppingListRecipeRefOut(MealieModel):
class ShoppingListSave(ShoppingListCreate): class ShoppingListSave(ShoppingListCreate):
group_id: UUID4 group_id: UUID4
user_id: UUID4
class ShoppingListSummary(ShoppingListSave): class ShoppingListSummary(ShoppingListSave):

View File

@ -19,13 +19,14 @@ from mealie.schema.group.group_shopping_list import (
) )
from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUnit, RecipeIngredient from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUnit, RecipeIngredient
from mealie.schema.response.pagination import OrderDirection, PaginationQuery from mealie.schema.response.pagination import OrderDirection, PaginationQuery
from mealie.schema.user.user import GroupInDB from mealie.schema.user.user import GroupInDB, UserOut
class ShoppingListService: class ShoppingListService:
def __init__(self, repos: AllRepositories, group: GroupInDB): def __init__(self, repos: AllRepositories, group: GroupInDB, user: UserOut):
self.repos = repos self.repos = repos
self.group = group self.group = group
self.user = user
self.shopping_lists = repos.group_shopping_lists self.shopping_lists = repos.group_shopping_lists
self.list_items = repos.group_shopping_list_item self.list_items = repos.group_shopping_list_item
self.list_item_refs = repos.group_shopping_list_item_references self.list_item_refs = repos.group_shopping_list_item_references
@ -476,7 +477,7 @@ class ShoppingListService:
return self.shopping_lists.get_one(shopping_list.id), items # type: ignore return self.shopping_lists.get_one(shopping_list.id), items # type: ignore
def create_one_list(self, data: ShoppingListCreate): def create_one_list(self, data: ShoppingListCreate):
create_data = data.cast(ShoppingListSave, group_id=self.group.id) create_data = data.cast(ShoppingListSave, group_id=self.group.id, user_id=self.user.id)
new_list = self.shopping_lists.create(create_data) # type: ignore new_list = self.shopping_lists.create(create_data) # type: ignore
labels = self.repos.group_multi_purpose_labels.by_group(self.group.id).page_all( labels = self.repos.group_multi_purpose_labels.by_group(self.group.id).page_all(

View File

@ -60,7 +60,8 @@ def delete_old_checked_list_items(group_id: UUID4 | None = None):
for group in groups: for group in groups:
event_bus_service = EventBusService(session=session, group_id=group.id) event_bus_service = EventBusService(session=session, group_id=group.id)
shopping_list_service = ShoppingListService(repos, group) # user is passed as None since we don't use it here
shopping_list_service = ShoppingListService(repos, group, None) # type: ignore
shopping_list_data = repos.group_shopping_lists.by_group(group.id).page_all( shopping_list_data = repos.group_shopping_lists.by_group(group.id).page_all(
PaginationQuery(page=1, per_page=-1) PaginationQuery(page=1, per_page=-1)
) )

View File

@ -29,7 +29,7 @@ def shopping_lists(database: AllRepositories, unique_user: TestUser):
for _ in range(3): for _ in range(3):
model = database.group_shopping_lists.create( model = database.group_shopping_lists.create(
ShoppingListSave(name=random_string(10), group_id=unique_user.group_id), ShoppingListSave(name=random_string(10), group_id=unique_user.group_id, user_id=unique_user.user_id),
) )
models.append(model) models.append(model)
@ -46,7 +46,7 @@ def shopping_lists(database: AllRepositories, unique_user: TestUser):
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def shopping_list(database: AllRepositories, unique_user: TestUser): def shopping_list(database: AllRepositories, unique_user: TestUser):
model = database.group_shopping_lists.create( model = database.group_shopping_lists.create(
ShoppingListSave(name=random_string(10), group_id=unique_user.group_id), ShoppingListSave(name=random_string(10), group_id=unique_user.group_id, user_id=unique_user.user_id),
) )
yield model yield model
@ -60,7 +60,7 @@ def shopping_list(database: AllRepositories, unique_user: TestUser):
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def list_with_items(database: AllRepositories, unique_user: TestUser): def list_with_items(database: AllRepositories, unique_user: TestUser):
list_model = database.group_shopping_lists.create( list_model = database.group_shopping_lists.create(
ShoppingListSave(name=random_string(10), group_id=unique_user.group_id), ShoppingListSave(name=random_string(10), group_id=unique_user.group_id, user_id=unique_user.user_id),
) )
for _ in range(10): for _ in range(10):

View File

@ -34,6 +34,7 @@ def test_shopping_lists_create_one(api_client: TestClient, unique_user: TestUser
assert response_list["name"] == payload["name"] assert response_list["name"] == payload["name"]
assert response_list["groupId"] == str(unique_user.group_id) assert response_list["groupId"] == str(unique_user.group_id)
assert response_list["userId"] == str(unique_user.user_id)
def test_shopping_lists_get_one(api_client: TestClient, unique_user: TestUser, shopping_lists: list[ShoppingListOut]): def test_shopping_lists_get_one(api_client: TestClient, unique_user: TestUser, shopping_lists: list[ShoppingListOut]):
@ -47,6 +48,7 @@ def test_shopping_lists_get_one(api_client: TestClient, unique_user: TestUser, s
assert response_list["id"] == str(shopping_list.id) assert response_list["id"] == str(shopping_list.id)
assert response_list["name"] == shopping_list.name assert response_list["name"] == shopping_list.name
assert response_list["groupId"] == str(shopping_list.group_id) assert response_list["groupId"] == str(shopping_list.group_id)
assert response_list["userId"] == str(shopping_list.user_id)
def test_shopping_lists_update_one( def test_shopping_lists_update_one(
@ -58,6 +60,7 @@ def test_shopping_lists_update_one(
"name": random_string(10), "name": random_string(10),
"id": str(sample_list.id), "id": str(sample_list.id),
"groupId": str(sample_list.group_id), "groupId": str(sample_list.group_id),
"userId": str(sample_list.user_id),
"listItems": [], "listItems": [],
} }
@ -71,6 +74,7 @@ def test_shopping_lists_update_one(
assert response_list["id"] == str(sample_list.id) assert response_list["id"] == str(sample_list.id)
assert response_list["name"] == payload["name"] assert response_list["name"] == payload["name"]
assert response_list["groupId"] == str(sample_list.group_id) assert response_list["groupId"] == str(sample_list.group_id)
assert response_list["userId"] == str(sample_list.user_id)
def test_shopping_lists_delete_one( def test_shopping_lists_delete_one(

View File

@ -758,7 +758,7 @@ def test_pagination_order_by_nulls(
def test_pagination_shopping_list_items_with_labels(database: AllRepositories, unique_user: TestUser): def test_pagination_shopping_list_items_with_labels(database: AllRepositories, unique_user: TestUser):
# create a shopping list and populate it with some items with labels, and some without labels # create a shopping list and populate it with some items with labels, and some without labels
shopping_list = database.group_shopping_lists.create( shopping_list = database.group_shopping_lists.create(
ShoppingListSave(name=random_string(), group_id=unique_user.group_id) ShoppingListSave(name=random_string(), group_id=unique_user.group_id, user_id=unique_user.user_id)
) )
labels = database.group_multi_purpose_labels.create_many( labels = database.group_multi_purpose_labels.create_many(

View File

@ -14,7 +14,9 @@ def test_cleanup(database: AllRepositories, unique_user: TestUser):
list_repo = database.group_shopping_lists.by_group(unique_user.group_id) list_repo = database.group_shopping_lists.by_group(unique_user.group_id)
list_item_repo = database.group_shopping_list_item list_item_repo = database.group_shopping_list_item
shopping_list = list_repo.create(ShoppingListSave(name=random_string(), group_id=unique_user.group_id)) shopping_list = list_repo.create(
ShoppingListSave(name=random_string(), group_id=unique_user.group_id, user_id=unique_user.user_id)
)
unchecked_items = list_item_repo.create_many( unchecked_items = list_item_repo.create_many(
[ [
ShoppingListItemCreate(note=random_string(), shopping_list_id=shopping_list.id) ShoppingListItemCreate(note=random_string(), shopping_list_id=shopping_list.id)
@ -57,7 +59,9 @@ def test_no_cleanup(database: AllRepositories, unique_user: TestUser):
list_repo = database.group_shopping_lists.by_group(unique_user.group_id) list_repo = database.group_shopping_lists.by_group(unique_user.group_id)
list_item_repo = database.group_shopping_list_item list_item_repo = database.group_shopping_list_item
shopping_list = list_repo.create(ShoppingListSave(name=random_string(), group_id=unique_user.group_id)) shopping_list = list_repo.create(
ShoppingListSave(name=random_string(), group_id=unique_user.group_id, user_id=unique_user.user_id)
)
unchecked_items = list_item_repo.create_many( unchecked_items = list_item_repo.create_many(
[ [
ShoppingListItemCreate(note=random_string(), shopping_list_id=shopping_list.id) ShoppingListItemCreate(note=random_string(), shopping_list_id=shopping_list.id)