Skip to content

Commit

Permalink
Merge pull request #3213 from michael-genson/feat/filter-shopping-lists
Browse files Browse the repository at this point in the history
feat: Filter Out Shopping Lists That Aren't Yours
  • Loading branch information
boc-the-git committed Mar 6, 2024
2 parents d577978 + 5e6f5bc commit e84e5e2
Show file tree
Hide file tree
Showing 20 changed files with 253 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from uuid import uuid4

import sqlalchemy as sa
from sqlalchemy.orm.session import Session
from sqlalchemy import orm

import mealie.db.migration_types
from alembic import op
Expand All @@ -23,8 +23,10 @@
depends_on = None


def populate_shopping_lists_multi_purpose_labels(shopping_lists_multi_purpose_labels_table: sa.Table, session: Session):
shopping_lists = session.query(ShoppingList).all()
def populate_shopping_lists_multi_purpose_labels(
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] = []
for shopping_list in shopping_lists:
Expand Down Expand Up @@ -60,7 +62,7 @@ def upgrade():
)
# ### 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)


Expand Down
Original file line number Diff line number Diff line change
@@ -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 ###
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export default defineComponent({
})
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) {
shoppingLists.value = data.items ?? [];
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/Domain/Recipe/RecipeContextMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ export default defineComponent({
const recipeRefWithScale = computed(() => recipeRef.value ? { scale: props.recipeScale, ...recipeRef.value } : undefined);
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) {
shoppingLists.value = data.items ?? [];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<BaseDialog v-if="shoppingListDialog" v-model="dialog" :title="$t('recipe.add-to-list')" :icon="$globals.icons.cartCheck">
<v-card-text>
<v-card
v-for="list in shoppingLists"
v-for="list in shoppingListChoices"
:key="list.id"
hover
class="my-2 left-border"
Expand All @@ -14,6 +14,18 @@
</v-card-title>
</v-card>
</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
v-if="shoppingListIngredientDialog"
Expand Down Expand Up @@ -120,6 +132,7 @@ import { toRefs } from "@vueuse/core";
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
import { useUserApi } from "~/composables/api";
import { alert } from "~/composables/use-toast";
import { useShoppingListPreferences } from "~/composables/use-users/preferences";
import { ShoppingListSummary } from "~/lib/api/types/group";
import { Recipe, RecipeIngredient } from "~/lib/api/types/recipe";
Expand Down Expand Up @@ -164,8 +177,9 @@ export default defineComponent({
},
},
setup(props, context) {
const { i18n } = useContext();
const { $auth, i18n } = useContext();
const api = useUserApi();
const preferences = useShoppingListPreferences();
// v-model support
const dialog = computed({
Expand All @@ -183,6 +197,10 @@ export default defineComponent({
shoppingListIngredientDialog: false,
});
const shoppingListChoices = computed(() => {
return props.shoppingLists.filter((list) => preferences.value.viewAllLists || list.userId === $auth.user?.id);
});
const recipeIngredientSections = ref<ShoppingListRecipeIngredientSection[]>([]);
const selectedShoppingList = ref<ShoppingListSummary | null>(null);
Expand Down Expand Up @@ -334,6 +352,8 @@ export default defineComponent({
return {
dialog,
preferences,
shoppingListChoices,
...toRefs(state),
addRecipesToList,
bulkCheckIngredients,
Expand Down
2 changes: 2 additions & 0 deletions frontend/composables/use-users/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface UserRecipePreferences {
}

export interface UserShoppingListPreferences {
viewAllLists: boolean;
viewByLabel: boolean;
}

Expand Down Expand Up @@ -70,6 +71,7 @@ export function useShoppingListPreferences(): Ref<UserShoppingListPreferences> {
const fromStorage = useLocalStorage(
"shopping-list-preferences",
{
viewAllLists: false,
viewByLabel: false,
},
{ mergeDefaults: true }
Expand Down
1 change: 1 addition & 0 deletions frontend/lang/messages/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
"save": "Save",
"settings": "Settings",
"share": "Share",
"show-all": "Show All",
"shuffle": "Shuffle",
"sort": "Sort",
"sort-alphabetically": "Alphabetical",
Expand Down
4 changes: 4 additions & 0 deletions frontend/lib/api/types/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ export interface ShoppingListOut {
createdAt?: string;
updateAt?: string;
groupId: string;
userId: string;
id: string;
listItems?: ShoppingListItemOut[];
recipeReferences: ShoppingListRecipeRefOut[];
Expand Down Expand Up @@ -568,6 +569,7 @@ export interface ShoppingListSave {
createdAt?: string;
updateAt?: string;
groupId: string;
userId: string;
}
export interface ShoppingListSummary {
name?: string;
Expand All @@ -577,6 +579,7 @@ export interface ShoppingListSummary {
createdAt?: string;
updateAt?: string;
groupId: string;
userId: string;
id: string;
recipeReferences: ShoppingListRecipeRefOut[];
labelSettings: ShoppingListMultiPurposeLabelOut[];
Expand All @@ -589,6 +592,7 @@ export interface ShoppingListUpdate {
createdAt?: string;
updateAt?: string;
groupId: string;
userId: string;
id: string;
listItems?: ShoppingListItemOut[];
}
Expand Down
79 changes: 78 additions & 1 deletion frontend/pages/shopping-lists/_id.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,27 @@
</v-card>
</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 -->
<div v-if="createEditorOpen">
<ShoppingListItemEditor
Expand All @@ -82,7 +103,7 @@
/>
</div>
<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>
{{ $t('shopping-list.reorder-labels') }}
</BaseButton>
Expand Down Expand Up @@ -197,6 +218,15 @@
</section>
</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>
<div class="d-flex justify-end mt-10">
<ButtonLink :to="`/group/data/labels`" :text="$tc('shopping-list.manage-labels')" :icon="$globals.icons.tags" />
Expand All @@ -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";
Expand Down Expand Up @@ -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 || "");
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
addRecipeReferenceToList,
updateListItems,
Expand All @@ -799,6 +871,8 @@ export default defineComponent({
removeRecipeReferenceToList,
reorderLabelsDialog,
toggleReorderLabelsDialog,
settingsDialog,
toggleSettingsDialog,
updateLabelOrder,
saveListItem,
shoppingList,
Expand All @@ -810,6 +884,9 @@ export default defineComponent({
updateIndexUncheckedByLabel,
allUnits,
allFoods,
allUsers,
currentUserId,
updateSettings,
};
},
head() {
Expand Down

0 comments on commit e84e5e2

Please sign in to comment.