mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -04:00
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:
commit
e84e5e2910
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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 ###
|
@ -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 ?? [];
|
||||||
}
|
}
|
||||||
|
@ -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 ?? [];
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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 }
|
||||||
|
@ -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",
|
||||||
|
@ -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[];
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
@ -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,
|
||||||
|
@ -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(
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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(
|
||||||
|
@ -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)
|
||||||
)
|
)
|
||||||
|
6
tests/fixtures/fixture_shopping_lists.py
vendored
6
tests/fixtures/fixture_shopping_lists.py
vendored
@ -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):
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user