@@ -215,6 +245,7 @@ import { useUserApi } from "~/composables/api";
import MultiPurposeLabelSection from "~/components/Domain/ShoppingList/MultiPurposeLabelSection.vue"
import ShoppingListItem from "~/components/Domain/ShoppingList/ShoppingListItem.vue";
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 ShoppingListItemEditor from "~/components/Domain/ShoppingList/ShoppingListItemEditor.vue";
import { useFoodStore, useLabelStore, useUnitStore } from "~/composables/store";
@@ -247,6 +278,7 @@ export default defineComponent({
const edit = ref(false);
const reorderLabelsDialog = ref(false);
+ const settingsDialog = ref(false);
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
@@ -464,6 +496,13 @@ export default defineComponent({
reorderLabelsDialog.value = !reorderLabelsDialog.value
}
+ async function toggleSettingsDialog() {
+ if (!settingsDialog.value) {
+ await fetchAllUsers();
+ }
+ settingsDialog.value = !settingsDialog.value;
+ }
+
async function updateLabelOrder(labelSettings: ShoppingListMultiPurposeLabelOut[]) {
if (!shoppingList.value) {
return;
@@ -775,6 +814,39 @@ export default defineComponent({
}
}
+ // ===============================================================
+ // Shopping List Settings
+
+ const allUsers = ref([]);
+ const currentUserId = ref();
+ 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 {
addRecipeReferenceToList,
updateListItems,
@@ -799,6 +871,8 @@ export default defineComponent({
removeRecipeReferenceToList,
reorderLabelsDialog,
toggleReorderLabelsDialog,
+ settingsDialog,
+ toggleSettingsDialog,
updateLabelOrder,
saveListItem,
shoppingList,
@@ -810,6 +884,9 @@ export default defineComponent({
updateIndexUncheckedByLabel,
allUnits,
allFoods,
+ allUsers,
+ currentUserId,
+ updateSettings,
};
},
head() {
diff --git a/frontend/pages/shopping-lists/index.vue b/frontend/pages/shopping-lists/index.vue
index 9a504f7be1..bde98330a5 100644
--- a/frontend/pages/shopping-lists/index.vue
+++ b/frontend/pages/shopping-lists/index.vue
@@ -1,5 +1,5 @@
-
+
@@ -15,10 +15,19 @@
{{ $t('shopping-list.shopping-lists') }}
-
+
+
+
+
+
-
+
{{ $globals.icons.cartCheck }}
@@ -42,6 +51,7 @@
import { computed, defineComponent, useAsync, useContext, reactive, toRefs, useRoute } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api";
import { useAsyncKey } from "~/composables/use-utils";
+import { useShoppingListPreferences } from "~/composables/use-users/preferences";
export default defineComponent({
middleware: "auth",
@@ -50,6 +60,7 @@ export default defineComponent({
const userApi = useUserApi();
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
+ const preferences = useShoppingListPreferences();
const state = reactive({
createName: "",
@@ -62,8 +73,16 @@ export default defineComponent({
return await fetchShoppingLists();
}, useAsyncKey());
+ const shoppingListChoices = computed(() => {
+ if (!shoppingLists.value) {
+ return [];
+ }
+
+ return shoppingLists.value.filter((list) => preferences.value.viewAllLists || list.userId === $auth.user?.id);
+ });
+
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) {
return [];
@@ -100,7 +119,8 @@ export default defineComponent({
return {
...toRefs(state),
groupSlug,
- shoppingLists,
+ preferences,
+ shoppingListChoices,
createOne,
deleteOne,
openDelete,
diff --git a/mealie/db/models/group/shopping_list.py b/mealie/db/models/group/shopping_list.py
index c960ded171..9e76861c78 100644
--- a/mealie/db/models/group/shopping_list.py
+++ b/mealie/db/models/group/shopping_list.py
@@ -14,6 +14,7 @@
if TYPE_CHECKING:
from group import Group
+ from users import User
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: 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)
list_items: Mapped[list[ShoppingListItem]] = orm.relationship(
diff --git a/mealie/db/models/users/users.py b/mealie/db/models/users/users.py
index ca6bea91fa..83f96ef605 100644
--- a/mealie/db/models/users/users.py
+++ b/mealie/db/models/users/users.py
@@ -17,6 +17,7 @@
if TYPE_CHECKING:
from ..group import Group
from ..group.mealplan import GroupMealPlan
+ from ..group.shopping_list import ShoppingList
from ..recipe import RecipeComment, RecipeModel, RecipeTimelineEvent
from .password_reset import PasswordResetModel
@@ -81,7 +82,7 @@ class User(SqlAlchemyBase, BaseMixins):
mealplans: Mapped[Optional["GroupMealPlan"]] = orm.relationship(
"GroupMealPlan", order_by="GroupMealPlan.date", **sp_args
)
-
+ shopping_lists: Mapped[Optional["ShoppingList"]] = orm.relationship("ShoppingList", **sp_args)
favorite_recipes: Mapped[list["RecipeModel"]] = orm.relationship(
"RecipeModel", secondary=users_to_favorites, back_populates="favorited_by"
)
diff --git a/mealie/routes/groups/controller_shopping_lists.py b/mealie/routes/groups/controller_shopping_lists.py
index a61ad3f397..2b5a8174e7 100644
--- a/mealie/routes/groups/controller_shopping_lists.py
+++ b/mealie/routes/groups/controller_shopping_lists.py
@@ -89,7 +89,7 @@ def publish_list_item_events(publisher: Callable, items_collection: ShoppingList
class ShoppingListItemController(BaseCrudController):
@cached_property
def service(self):
- return ShoppingListService(self.repos, self.group)
+ return ShoppingListService(self.repos, self.group, self.user)
@cached_property
def repo(self):
@@ -154,7 +154,7 @@ def delete_one(self, item_id: UUID4):
class ShoppingListController(BaseCrudController):
@cached_property
def service(self):
- return ShoppingListService(self.repos, self.group)
+ return ShoppingListService(self.repos, self.group, self.user)
@cached_property
def repo(self):
diff --git a/mealie/schema/group/group_shopping_list.py b/mealie/schema/group/group_shopping_list.py
index 583976f903..511e87af7b 100644
--- a/mealie/schema/group/group_shopping_list.py
+++ b/mealie/schema/group/group_shopping_list.py
@@ -190,6 +190,7 @@ def loader_options(cls) -> list[LoaderOption]:
class ShoppingListSave(ShoppingListCreate):
group_id: UUID4
+ user_id: UUID4
class ShoppingListSummary(ShoppingListSave):
diff --git a/mealie/services/group_services/shopping_lists.py b/mealie/services/group_services/shopping_lists.py
index b477d9bb56..09350b6037 100644
--- a/mealie/services/group_services/shopping_lists.py
+++ b/mealie/services/group_services/shopping_lists.py
@@ -19,13 +19,14 @@
)
from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUnit, RecipeIngredient
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:
- def __init__(self, repos: AllRepositories, group: GroupInDB):
+ def __init__(self, repos: AllRepositories, group: GroupInDB, user: UserOut):
self.repos = repos
self.group = group
+ self.user = user
self.shopping_lists = repos.group_shopping_lists
self.list_items = repos.group_shopping_list_item
self.list_item_refs = repos.group_shopping_list_item_references
@@ -476,7 +477,7 @@ def remove_recipe_ingredients_from_list(
return self.shopping_lists.get_one(shopping_list.id), items # type: ignore
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
labels = self.repos.group_multi_purpose_labels.by_group(self.group.id).page_all(
diff --git a/mealie/services/scheduler/tasks/delete_old_checked_shopping_list_items.py b/mealie/services/scheduler/tasks/delete_old_checked_shopping_list_items.py
index cc532c8caa..1f33ec5801 100644
--- a/mealie/services/scheduler/tasks/delete_old_checked_shopping_list_items.py
+++ b/mealie/services/scheduler/tasks/delete_old_checked_shopping_list_items.py
@@ -60,7 +60,8 @@ def delete_old_checked_list_items(group_id: UUID4 | None = None):
for group in groups:
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(
PaginationQuery(page=1, per_page=-1)
)
diff --git a/tests/fixtures/fixture_shopping_lists.py b/tests/fixtures/fixture_shopping_lists.py
index 835ea0f51e..2eae671ffd 100644
--- a/tests/fixtures/fixture_shopping_lists.py
+++ b/tests/fixtures/fixture_shopping_lists.py
@@ -29,7 +29,7 @@ def shopping_lists(database: AllRepositories, unique_user: TestUser):
for _ in range(3):
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)
@@ -46,7 +46,7 @@ def shopping_lists(database: AllRepositories, unique_user: TestUser):
@pytest.fixture(scope="function")
def shopping_list(database: AllRepositories, unique_user: TestUser):
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
@@ -60,7 +60,7 @@ def shopping_list(database: AllRepositories, unique_user: TestUser):
@pytest.fixture(scope="function")
def list_with_items(database: AllRepositories, unique_user: TestUser):
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):
diff --git a/tests/integration_tests/user_group_tests/test_group_shopping_lists.py b/tests/integration_tests/user_group_tests/test_group_shopping_lists.py
index b3cdd434cf..dd64810890 100644
--- a/tests/integration_tests/user_group_tests/test_group_shopping_lists.py
+++ b/tests/integration_tests/user_group_tests/test_group_shopping_lists.py
@@ -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["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]):
@@ -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["name"] == shopping_list.name
assert response_list["groupId"] == str(shopping_list.group_id)
+ assert response_list["userId"] == str(shopping_list.user_id)
def test_shopping_lists_update_one(
@@ -58,6 +60,7 @@ def test_shopping_lists_update_one(
"name": random_string(10),
"id": str(sample_list.id),
"groupId": str(sample_list.group_id),
+ "userId": str(sample_list.user_id),
"listItems": [],
}
@@ -71,6 +74,7 @@ def test_shopping_lists_update_one(
assert response_list["id"] == str(sample_list.id)
assert response_list["name"] == payload["name"]
assert response_list["groupId"] == str(sample_list.group_id)
+ assert response_list["userId"] == str(sample_list.user_id)
def test_shopping_lists_delete_one(
diff --git a/tests/unit_tests/repository_tests/test_pagination.py b/tests/unit_tests/repository_tests/test_pagination.py
index 8d46134de8..6007878bf9 100644
--- a/tests/unit_tests/repository_tests/test_pagination.py
+++ b/tests/unit_tests/repository_tests/test_pagination.py
@@ -758,7 +758,7 @@ def test_pagination_order_by_nulls(
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
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(
diff --git a/tests/unit_tests/services_tests/scheduler/tasks/test_delete_old_checked_shopping_list_items.py b/tests/unit_tests/services_tests/scheduler/tasks/test_delete_old_checked_shopping_list_items.py
index b462745dab..34705dee05 100644
--- a/tests/unit_tests/services_tests/scheduler/tasks/test_delete_old_checked_shopping_list_items.py
+++ b/tests/unit_tests/services_tests/scheduler/tasks/test_delete_old_checked_shopping_list_items.py
@@ -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_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(
[
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_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(
[
ShoppingListItemCreate(note=random_string(), shopping_list_id=shopping_list.id)