Skip to content

Commit

Permalink
feat: Additional Household Permissions (#4158)
Browse files Browse the repository at this point in the history
Co-authored-by: Kuchenpirat <[email protected]>
  • Loading branch information
michael-genson and Kuchenpirat authored Sep 17, 2024
1 parent b1820f9 commit fd0257c
Show file tree
Hide file tree
Showing 37 changed files with 686 additions and 181 deletions.
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

0 comments on commit fd0257c

Please sign in to comment.