Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Additional Household Permissions #4158

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e84df3e
added household settings for editing other households' recipes
michael-genson Sep 2, 2024
a880972
codegen
michael-genson Sep 2, 2024
8119e8b
updated permissions composable
michael-genson Sep 2, 2024
ef3fb96
added to household settings
michael-genson Sep 2, 2024
ff6692c
added can_manage_household permission to users
michael-genson Sep 2, 2024
fc501ac
codegen
michael-genson Sep 2, 2024
36a9d50
added migration
michael-genson Sep 2, 2024
0719e31
bugfixes
michael-genson Sep 3, 2024
d0b0627
fix migration to set permission to True for admins
michael-genson Sep 3, 2024
f351537
update household preferences editor
michael-genson Sep 3, 2024
4e3a533
replace household preferences page with component
michael-genson Sep 3, 2024
a6d2ac9
update permission ref and fix types
michael-genson Sep 3, 2024
b124859
add can-manage-household to user pages
michael-genson Sep 3, 2024
08b7920
update frontend to check recipe household prefs
michael-genson Sep 3, 2024
903d7b5
fix backend not being able to update recipes
michael-genson Sep 3, 2024
4b1d806
add api call for fetching single household
michael-genson Sep 3, 2024
8e161b5
update frontend to use new api call
michael-genson Sep 3, 2024
0b94df0
remove edit from recipe card
michael-genson Sep 3, 2024
3e9aaa8
updated frontend tests
michael-genson Sep 4, 2024
fa54ecd
fixed missing check for private household
michael-genson Sep 4, 2024
c00b433
updated frontend test
michael-genson Sep 4, 2024
f367ef2
added/updated backend tests
michael-genson Sep 4, 2024
dee2497
fix some recipe routes not supporting id
michael-genson Sep 4, 2024
7d42f4c
reverted is_private check because it doesn't make sense
michael-genson Sep 4, 2024
6fadc7f
fix for deletes
michael-genson Sep 4, 2024
23107cf
updated tests
michael-genson Sep 4, 2024
3c8e2e2
added test for new household route
michael-genson Sep 4, 2024
3235564
fixed missing test key
michael-genson Sep 4, 2024
e17b361
cleaned up migration script
michael-genson Sep 11, 2024
340b6c5
Update frontend/components/Domain/Recipe/RecipePage/RecipePageParts/R…
michael-genson Sep 17, 2024
9de41e6
made private text clearer
michael-genson Sep 17, 2024
64cae83
Merge remote-tracking branch 'upstream/mealie-next' into feat/additio…
michael-genson Sep 17, 2024
9aab87e
revert public recipe text to reference groups rather than households
michael-genson Sep 17, 2024
9fbfb8d
fix middleware for managing household
michael-genson Sep 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""added household recipe lock setting and household management user permission

Revision ID: be568e39ffdf
Revises: feecc8ffb956
Create Date: 2024-09-02 21:39:49.210355

"""

from textwrap import dedent

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision = "be568e39ffdf"
down_revision = "feecc8ffb956"
branch_labels: str | tuple[str, ...] | None = None
depends_on: str | tuple[str, ...] | None = None


def populate_defaults():
if op.get_context().dialect.name == "postgresql":
TRUE = "TRUE"
FALSE = "FALSE"
else:
TRUE = "1"
FALSE = "0"

op.execute(
dedent(
f"""
UPDATE household_preferences
SET lock_recipe_edits_from_other_households = {TRUE}
WHERE lock_recipe_edits_from_other_households IS NULL
"""
)
)
op.execute(
dedent(
f"""
UPDATE users
SET can_manage_household = {FALSE}
WHERE can_manage_household IS NULL AND admin = {FALSE}
"""
)
)
op.execute(
dedent(
f"""
UPDATE users
SET can_manage_household = {TRUE}
WHERE can_manage_household IS NULL AND admin = {TRUE}
"""
)
)


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"household_preferences",
sa.Column("lock_recipe_edits_from_other_households", sa.Boolean(), nullable=True),
)
op.add_column("users", sa.Column("can_manage_household", sa.Boolean(), nullable=True))
# ### end Alembic commands ###

populate_defaults()


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("users", "can_manage_household")
op.drop_column("household_preferences", "lock_recipe_edits_from_other_households")
# ### end Alembic commands ###
109 changes: 88 additions & 21 deletions frontend/components/Domain/Household/HouseholdPreferencesEditor.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,33 @@
<template>
<div v-if="preferences">
<BaseCardSectionTitle :title="$tc('group.general-preferences')"></BaseCardSectionTitle>
<v-checkbox v-model="preferences.privateHousehold" class="mt-n4" :label="$t('household.private-household')"></v-checkbox>
<BaseCardSectionTitle class="mt-10" :title="$tc('household.household-preferences')"></BaseCardSectionTitle>
<div class="mb-6">
<v-checkbox
v-model="preferences.privateHousehold"
hide-details
dense
:label="$t('household.private-household')"
/>
<div class="ml-8">
<p class="text-subtitle-2 my-0 py-0">
{{ $t("household.private-household-description") }}
</p>
<DocLink class="mt-2" link="/documentation/getting-started/faq/#how-do-private-groups-and-recipes-work" />
</div>
</div>
<div class="mb-6">
<v-checkbox
v-model="preferences.lockRecipeEditsFromOtherHouseholds"
hide-details
dense
:label="$t('household.lock-recipe-edits-from-other-households')"
/>
<div class="ml-8">
<p class="text-subtitle-2 my-0 py-0">
{{ $t("household.lock-recipe-edits-from-other-households-description") }}
</p>
</div>
</div>
<v-select
v-model="preferences.firstDayOfWeek"
:prepend-icon="$globals.icons.calendarWeekBegin"
Expand All @@ -12,20 +38,25 @@
/>

<BaseCardSectionTitle class="mt-5" :title="$tc('household.household-recipe-preferences')"></BaseCardSectionTitle>
<template v-for="(_, key) in preferences">
<v-checkbox
v-if="labels[key]"
:key="key"
v-model="preferences[key]"
class="mt-n4"
:label="labels[key]"
></v-checkbox>
</template>
<div class="preference-container">
<div v-for="p in recipePreferences" :key="p.key">
<v-checkbox
v-model="preferences[p.key]"
hide-details
dense
:label="p.label"
/>
<p class="ml-8 text-subtitle-2 my-0 py-0">
{{ p.description }}
</p>
</div>
</div>
</div>
</template>

<script lang="ts">
import { defineComponent, computed, useContext } from "@nuxtjs/composition-api";
import { ReadHouseholdPreferences } from "~/lib/api/types/household";

export default defineComponent({
props: {
Expand All @@ -37,14 +68,44 @@ export default defineComponent({
setup(props, context) {
const { i18n } = useContext();

const labels = {
recipePublic: i18n.tc("household.allow-users-outside-of-your-household-to-see-your-recipes"),
recipeShowNutrition: i18n.tc("group.show-nutrition-information"),
recipeShowAssets: i18n.tc("group.show-recipe-assets"),
recipeLandscapeView: i18n.tc("group.default-to-landscape-view"),
recipeDisableComments: i18n.tc("group.disable-users-from-commenting-on-recipes"),
recipeDisableAmount: i18n.tc("group.disable-organizing-recipe-ingredients-by-units-and-food"),
};
type Preference = {
key: keyof ReadHouseholdPreferences;
label: string;
description: string;
}

const recipePreferences: Preference[] = [
{
key: "recipePublic",
label: i18n.tc("group.allow-users-outside-of-your-group-to-see-your-recipes"),
description: i18n.tc("group.allow-users-outside-of-your-group-to-see-your-recipes-description"),
},
{
key: "recipeShowNutrition",
label: i18n.tc("group.show-nutrition-information"),
description: i18n.tc("group.show-nutrition-information-description"),
},
{
key: "recipeShowAssets",
label: i18n.tc("group.show-recipe-assets"),
description: i18n.tc("group.show-recipe-assets-description"),
},
{
key: "recipeLandscapeView",
label: i18n.tc("group.default-to-landscape-view"),
description: i18n.tc("group.default-to-landscape-view-description"),
},
{
key: "recipeDisableComments",
label: i18n.tc("group.disable-users-from-commenting-on-recipes"),
description: i18n.tc("group.disable-users-from-commenting-on-recipes-description"),
},
{
key: "recipeDisableAmount",
label: i18n.tc("group.disable-organizing-recipe-ingredients-by-units-and-food"),
description: i18n.tc("group.disable-organizing-recipe-ingredients-by-units-and-food-description"),
},
];

const allDays = [
{
Expand Down Expand Up @@ -88,12 +149,18 @@ export default defineComponent({

return {
allDays,
labels,
preferences,
recipePreferences,
};
},
});
</script>

<style lang="scss" scoped>
<style lang="css">
.preference-container {
display: flex;
flex-direction: column;
gap: 0.5rem;
max-width: 600px;
}
</style>
2 changes: 1 addition & 1 deletion frontend/components/Domain/Recipe/RecipeCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
:recipe-id="recipeId"
:use-items="{
delete: false,
edit: true,
edit: false,
download: true,
mealplanner: true,
shoppingList: true,
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/Domain/Recipe/RecipeCardMobile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
:recipe-id="recipeId"
:use-items="{
delete: false,
edit: true,
edit: false,
download: true,
mealplanner: true,
shoppingList: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ import RecipeRating from "~/components/Domain/Recipe/RecipeRating.vue";
import RecipeLastMade from "~/components/Domain/Recipe/RecipeLastMade.vue";
import RecipeActionMenu from "~/components/Domain/Recipe/RecipeActionMenu.vue";
import RecipeTimeCard from "~/components/Domain/Recipe/RecipeTimeCard.vue";
import { useStaticRoutes } from "~/composables/api";
import { useStaticRoutes, useUserApi } from "~/composables/api";
import { HouseholdSummary } from "~/lib/api/types/household";
import { Recipe } from "~/lib/api/types/recipe";
import { NoUndefinedField } from "~/lib/api/types/non-generated";
import { usePageState, usePageUser, PageMode, EditorMode } from "~/composables/recipe-page/shared-state";
Expand Down Expand Up @@ -100,7 +101,15 @@ export default defineComponent({
const { imageKey, pageMode, editMode, setMode, toggleEditMode, isEditMode } = usePageState(props.recipe.slug);
const { user } = usePageUser();
const { isOwnGroup } = useLoggedInState();
const { canEditRecipe } = useRecipePermissions(props.recipe, user);

const recipeHousehold = ref<HouseholdSummary>();
if (user) {
const userApi = useUserApi();
userApi.groups.fetchHousehold(props.recipe.householdId).then(({ data }) => {
recipeHousehold.value = data || undefined;
});
}
const { canEditRecipe } = useRecipePermissions(props.recipe, recipeHousehold, user);

function printRecipe() {
window.print();
Expand Down
66 changes: 55 additions & 11 deletions frontend/composables/recipes/use-recipe-permissions.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { describe, test, expect } from "vitest";
import { ref, Ref } from "@nuxtjs/composition-api";
import { useRecipePermissions } from "./use-recipe-permissions";
import { HouseholdSummary } from "~/lib/api/types/household";
import { Recipe } from "~/lib/api/types/recipe";
import { UserOut } from "~/lib/api/types/user";

Expand Down Expand Up @@ -32,35 +34,76 @@ describe("test use recipe permissions", () => {
...overrides,
});

const createRecipeHousehold = (overrides: Partial<HouseholdSummary>, lockRecipeEdits = false): Ref<HouseholdSummary> => (
ref({
id: commonHouseholdId,
groupId: commonGroupId,
name: "My Household",
slug: "my-household",
preferences: {
id: "my-household-preferences-id",
lockRecipeEditsFromOtherHouseholds: lockRecipeEdits,
},
...overrides,
})
);

test("when user is null, cannot edit", () => {
const result = useRecipePermissions(createRecipe({}), null);
const result = useRecipePermissions(createRecipe({}), createRecipeHousehold({}), null);
expect(result.canEditRecipe.value).toBe(false);
});

test("when user is recipe owner, can edit", () => {
const result = useRecipePermissions(createRecipe({}), createUser({}));
const result = useRecipePermissions(createRecipe({}), ref(), createUser({}));
expect(result.canEditRecipe.value).toBe(true);
});

test("when user is not recipe owner, is correct group and household, and recipe is unlocked, can edit", () => {
test(
"when user is not recipe owner, is correct group and household, recipe is unlocked, and household is unlocked, can edit",
() => {
const result = useRecipePermissions(
createRecipe({}),
createRecipeHousehold({}),
createUser({ id: "other-user-id" }),
);
expect(result.canEditRecipe.value).toBe(true);
}
);

test(
"when user is not recipe owner, is correct group and household, recipe is unlocked, but household is locked, can edit",
() => {
const result = useRecipePermissions(
createRecipe({}),
createRecipeHousehold({}, true),
createUser({ id: "other-user-id" }),
);
expect(result.canEditRecipe.value).toBe(true);
}
);

test("when user is not recipe owner, and user is other group, cannot edit", () => {
const result = useRecipePermissions(
createRecipe({}),
createUser({ id: "other-user-id" }),
createRecipeHousehold({}),
createUser({ id: "other-user-id", groupId: "other-group-id"}),
);
expect(result.canEditRecipe.value).toBe(true);
expect(result.canEditRecipe.value).toBe(false);
});

test("when user is not recipe owner, and user is other group, cannot edit", () => {
test("when user is not recipe owner, and user is other household, and household is unlocked, can edit", () => {
const result = useRecipePermissions(
createRecipe({}),
createUser({ id: "other-user-id", groupId: "other-group-id"}),
createRecipeHousehold({}),
createUser({ id: "other-user-id", householdId: "other-household-id" }),
);
expect(result.canEditRecipe.value).toBe(false);
expect(result.canEditRecipe.value).toBe(true);
});

test("when user is not recipe owner, and user is other household, cannot edit", () => {
test("when user is not recipe owner, and user is other household, and household is locked, cannot edit", () => {
const result = useRecipePermissions(
createRecipe({}),
createRecipeHousehold({}, true),
createUser({ id: "other-user-id", householdId: "other-household-id" }),
);
expect(result.canEditRecipe.value).toBe(false);
Expand All @@ -69,13 +112,14 @@ describe("test use recipe permissions", () => {
test("when user is not recipe owner, and recipe is locked, cannot edit", () => {
const result = useRecipePermissions(
createRecipe({}, true),
createRecipeHousehold({}),
createUser({ id: "other-user-id"}),
);
expect(result.canEditRecipe.value).toBe(false);
});

test("when user is recipe owner, and recipe is locked, can edit", () => {
const result = useRecipePermissions(createRecipe({}, true), createUser({}));
test("when user is recipe owner, and recipe is locked, and household is locked, can edit", () => {
const result = useRecipePermissions(createRecipe({}, true), createRecipeHousehold({}, true), createUser({}));
expect(result.canEditRecipe.value).toBe(true);
});
});
Loading
Loading