1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-04 13:05:21 +02:00

Merge branch 'mealie-next' into feat-frontend-access-controll

This commit is contained in:
Kuchenpirat 2024-02-07 18:22:55 +01:00 committed by GitHub
commit 704d0a8392
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 173 additions and 41 deletions

View file

@ -33,6 +33,7 @@ export interface AppInfo {
version: string; version: string;
demoStatus: boolean; demoStatus: boolean;
allowSignup: boolean; allowSignup: boolean;
defaultGroupSlug?: string;
} }
export interface AppStartupInfo { export interface AppStartupInfo {
isFirstLogin: boolean; isFirstLogin: boolean;

View file

@ -4,17 +4,28 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, useContext, useRouter } from "@nuxtjs/composition-api"; import { computed, defineComponent, useContext, useRouter } from "@nuxtjs/composition-api";
import { AppInfo } from "~/lib/api/types/admin";
export default defineComponent({ export default defineComponent({
layout: "blank", layout: "blank",
setup() { setup() {
const { $auth } = useContext(); const { $auth, $axios } = useContext();
const router = useRouter(); const router = useRouter();
const groupSlug = computed(() => $auth.user?.groupSlug); const groupSlug = computed(() => $auth.user?.groupSlug);
async function redirectPublicUserToDefaultGroup() {
const { data } = await $axios.get<AppInfo>("/api/app/about");
if (data?.defaultGroupSlug) {
router.push(`/g/${data.defaultGroupSlug}`);
} else {
router.push("/login");
}
}
if (groupSlug.value) { if (groupSlug.value) {
router.push(`/g/${groupSlug.value}`); router.push(`/g/${groupSlug.value}`);
} else { } else {
router.push("/login"); redirectPublicUserToDefaultGroup();
} }
} }
}); });

View file

@ -5,21 +5,30 @@ from mealie.core.config import get_app_settings
from mealie.core.settings.static import APP_VERSION from mealie.core.settings.static import APP_VERSION
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.db.models.users.users import User from mealie.db.models.users.users import User
from mealie.repos.all_repositories import get_repositories
from mealie.schema.admin.about import AppInfo, AppStartupInfo, AppTheme from mealie.schema.admin.about import AppInfo, AppStartupInfo, AppTheme
router = APIRouter(prefix="/about") router = APIRouter(prefix="/about")
@router.get("", response_model=AppInfo) @router.get("", response_model=AppInfo)
def get_app_info(): def get_app_info(session: Session = Depends(generate_session)):
"""Get general application information""" """Get general application information"""
settings = get_app_settings() settings = get_app_settings()
repos = get_repositories(session)
default_group = repos.groups.get_by_name(settings.DEFAULT_GROUP)
if default_group and default_group.preferences and not default_group.preferences.private_group:
default_group_slug = default_group.slug
else:
default_group_slug = None
return AppInfo( return AppInfo(
version=APP_VERSION, version=APP_VERSION,
demo_status=settings.IS_DEMO, demo_status=settings.IS_DEMO,
production=settings.PRODUCTION, production=settings.PRODUCTION,
allow_signup=settings.ALLOW_SIGNUP, allow_signup=settings.ALLOW_SIGNUP,
default_group_slug=default_group_slug,
) )

View file

@ -14,6 +14,7 @@ class AppInfo(MealieModel):
version: str version: str
demo_status: bool demo_status: bool
allow_signup: bool allow_signup: bool
default_group_slug: str | None = None
class AppTheme(MealieModel): class AppTheme(MealieModel):

View file

@ -132,7 +132,7 @@ def parse_ingredient(tokens) -> tuple[str, str]:
return ingredient, note return ingredient, note
def parse(ing_str) -> BruteParsedIngredient: def parse(ing_str, parser) -> BruteParsedIngredient:
amount = 0.0 amount = 0.0
unit = "" unit = ""
ingredient = "" ingredient = ""
@ -192,12 +192,20 @@ def parse(ing_str) -> BruteParsedIngredient:
# which means this is the ingredient # which means this is the ingredient
ingredient = tokens[1] ingredient = tokens[1]
except ValueError: except ValueError:
try: # can't parse first argument as amount
# can't parse first argument as amount # try to parse as unit and ingredient (e.g. "a tblsp salt"), with unit in first three tokens
# -> no unit -> parse everything as ingredient # won't work for units that have spaces
ingredient, note = parse_ingredient(tokens) for index, token in enumerate(tokens[:3]):
except ValueError: if parser.find_unit_match(token):
ingredient = " ".join(tokens[1:]) unit = token
ingredient, note = parse_ingredient(tokens[index + 1 :])
break
if not unit:
try:
# no unit -> parse everything as ingredient
ingredient, note = parse_ingredient(tokens)
except ValueError:
ingredient = " ".join(tokens[1:])
if unit_note not in note: if unit_note not in note:
note += " " + unit_note note += " " + unit_note

View file

@ -126,22 +126,24 @@ class ABCIngredientParser(ABC):
return store_map[fuzz_result[0]] return store_map[fuzz_result[0]]
def find_food_match(self, food: IngredientFood | CreateIngredientFood) -> IngredientFood | None: def find_food_match(self, food: IngredientFood | CreateIngredientFood | str) -> IngredientFood | None:
if isinstance(food, IngredientFood): if isinstance(food, IngredientFood):
return food return food
match_value = IngredientFoodModel.normalize(food.name) food_name = food if isinstance(food, str) else food.name
match_value = IngredientFoodModel.normalize(food_name)
return self.find_match( return self.find_match(
match_value, match_value,
store_map=self.foods_by_alias, store_map=self.foods_by_alias,
fuzzy_match_threshold=self.food_fuzzy_match_threshold, fuzzy_match_threshold=self.food_fuzzy_match_threshold,
) )
def find_unit_match(self, unit: IngredientUnit | CreateIngredientUnit) -> IngredientUnit | None: def find_unit_match(self, unit: IngredientUnit | CreateIngredientUnit | str) -> IngredientUnit | None:
if isinstance(unit, IngredientUnit): if isinstance(unit, IngredientUnit):
return unit return unit
match_value = IngredientUnitModel.normalize(unit.name) unit_name = unit if isinstance(unit, str) else unit.name
match_value = IngredientUnitModel.normalize(unit_name)
return self.find_match( return self.find_match(
match_value, match_value,
store_map=self.units_by_alias, store_map=self.units_by_alias,
@ -155,6 +157,16 @@ class ABCIngredientParser(ABC):
if ingredient.ingredient.unit and (unit_match := self.find_unit_match(ingredient.ingredient.unit)): if ingredient.ingredient.unit and (unit_match := self.find_unit_match(ingredient.ingredient.unit)):
ingredient.ingredient.unit = unit_match ingredient.ingredient.unit = unit_match
# Parser might have wrongly split a food into a unit and food.
if isinstance(ingredient.ingredient.food, CreateIngredientFood) and isinstance(
ingredient.ingredient.unit, CreateIngredientUnit
):
if food_match := self.find_food_match(
f"{ingredient.ingredient.unit.name} {ingredient.ingredient.food.name}"
):
ingredient.ingredient.food = food_match
ingredient.ingredient.unit = None
return ingredient return ingredient
@ -164,7 +176,7 @@ class BruteForceParser(ABCIngredientParser):
""" """
def parse_one(self, ingredient: str) -> ParsedIngredient: def parse_one(self, ingredient: str) -> ParsedIngredient:
bfi = brute.parse(ingredient) bfi = brute.parse(ingredient, self)
parsed_ingredient = ParsedIngredient( parsed_ingredient = ParsedIngredient(
input=ingredient, input=ingredient,

View file

@ -1,11 +1,36 @@
import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from mealie.core.config import get_app_settings from mealie.core.config import get_app_settings
from mealie.core.settings.static import APP_VERSION from mealie.core.settings.static import APP_VERSION
from mealie.repos.repository_factory import AllRepositories
from tests.utils import api_routes from tests.utils import api_routes
from tests.utils.fixture_schemas import TestUser from tests.utils.fixture_schemas import TestUser
@pytest.mark.parametrize("is_private_group", [True, False], ids=["private group", "public group"])
def test_public_about_get_app_info(api_client: TestClient, is_private_group: bool, database: AllRepositories):
settings = get_app_settings()
group = database.groups.get_by_name(settings.DEFAULT_GROUP)
assert group and group.preferences
group.preferences.private_group = is_private_group
database.group_preferences.update(group.id, group.preferences)
response = api_client.get(api_routes.app_about)
as_dict = response.json()
assert as_dict["production"] == settings.PRODUCTION
assert as_dict["version"] == APP_VERSION
assert as_dict["demoStatus"] == settings.IS_DEMO
assert as_dict["allowSignup"] == settings.ALLOW_SIGNUP
if is_private_group:
assert as_dict["defaultGroupSlug"] == None
else:
assert as_dict["defaultGroupSlug"] == group.slug
def test_admin_about_get_app_info(api_client: TestClient, admin_user: TestUser): def test_admin_about_get_app_info(api_client: TestClient, admin_user: TestUser):
response = api_client.get(api_routes.admin_about, headers=admin_user.token) response = api_client.get(api_routes.admin_about, headers=admin_user.token)

View file

@ -135,7 +135,7 @@ test_ingredients = [
@pytest.mark.skipif(not crf_exists(), reason="CRF++ not installed") @pytest.mark.skipif(not crf_exists(), reason="CRF++ not installed")
def test_nlp_parser(): def test_nlp_parser() -> None:
models: list[CRFIngredient] = convert_list_to_crf_model([x.input for x in test_ingredients]) models: list[CRFIngredient] = convert_list_to_crf_model([x.input for x in test_ingredients])
# Iterate over models and test_ingredients to gather # Iterate over models and test_ingredients to gather
@ -147,37 +147,102 @@ def test_nlp_parser():
assert model.unit == test_ingredient.unit assert model.unit == test_ingredient.unit
def test_brute_parser(unique_user: TestUser): @pytest.mark.parametrize(
# input: (quantity, unit, food, comments) "input, quantity, unit, food, comment",
expectations = { [
# Dutch pytest.param("1 theelepel koffie", 1, "theelepel", "koffie", "", id="1 theelepel koffie"),
"1 theelepel koffie": (1, "theelepel", "koffie", ""), pytest.param("3 theelepels koffie", 3, "theelepels", "koffie", "", id="3 theelepels koffie"),
"3 theelepels koffie": (3, "theelepels", "koffie", ""), pytest.param("1 eetlepel tarwe", 1, "eetlepel", "tarwe", "", id="1 eetlepel tarwe"),
"1 eetlepel tarwe": (1, "eetlepel", "tarwe", ""), pytest.param("20 eetlepels bloem", 20, "eetlepels", "bloem", "", id="20 eetlepels bloem"),
"20 eetlepels bloem": (20, "eetlepels", "bloem", ""), pytest.param("1 mespunt kaneel", 1, "mespunt", "kaneel", "", id="1 mespunt kaneel"),
"1 mespunt kaneel": (1, "mespunt", "kaneel", ""), pytest.param("1 snuf(je) zout", 1, "snuf(je)", "zout", "", id="1 snuf(je) zout"),
"1 snuf(je) zout": (1, "snuf(je)", "zout", ""), pytest.param(
"2 tbsp minced cilantro, leaves and stems": (2, "tbsp", "minced cilantro", "leaves and stems"), "2 tbsp minced cilantro, leaves and stems",
"1 large yellow onion, coarsely chopped": (1, "large", "yellow onion", "coarsely chopped"),
"1 1/2 tsp garam masala": (1.5, "tsp", "garam masala", ""),
"2 cups mango chunks, (2 large mangoes) (fresh or frozen)": (
2, 2,
"cups", "tbsp",
"minced cilantro",
"leaves and stems",
id="2 tbsp minced cilantro, leaves and stems",
),
pytest.param(
"1 large yellow onion, coarsely chopped",
1,
"large",
"yellow onion",
"coarsely chopped",
id="1 large yellow onion, coarsely chopped",
),
pytest.param("1 1/2 tsp garam masala", 1.5, "tsp", "garam masala", "", id="1 1/2 tsp garam masala"),
pytest.param(
"2 cups mango chunks, (2 large mangoes) (fresh or frozen)",
2,
"Cups",
"mango chunks, (2 large mangoes)", "mango chunks, (2 large mangoes)",
"fresh or frozen", "fresh or frozen",
id="2 cups mango chunks, (2 large mangoes) (fresh or frozen)",
), ),
} pytest.param("stalk onion", 0, "Stalk", "onion", "", id="stalk onion"),
pytest.param("a stalk bell peppers", 0, "Stalk", "bell peppers", "", id="a stalk bell peppers"),
pytest.param("a tablespoon unknownFood", 0, "Tablespoon", "unknownFood", "", id="a tablespoon unknownFood"),
pytest.param(
"stalk bell peppers, cut in pieces",
0,
"Stalk",
"bell peppers",
"cut in pieces",
id="stalk bell peppers, cut in pieces",
),
pytest.param(
"a stalk bell peppers, cut in pieces",
0,
"Stalk",
"bell peppers",
"cut in pieces",
id="stalk bell peppers, cut in pieces",
),
pytest.param("red pepper flakes", 0, "", "red pepper flakes", "", id="red pepper flakes"),
pytest.param("1 red pepper flakes", 1, "", "red pepper flakes", "", id="1 red pepper flakes"),
pytest.param("1 bell peppers", 1, "", "bell peppers", "", id="1 bell peppers"),
pytest.param("1 stalk bell peppers", 1, "Stalk", "bell peppers", "", id="1 big stalk bell peppers"),
pytest.param("a big stalk bell peppers", 0, "Stalk", "bell peppers", "", id="a big stalk bell peppers"),
pytest.param(
"1 bell peppers, cut in pieces", 1, "", "bell peppers", "cut in pieces", id="1 bell peppers, cut in pieces"
),
pytest.param(
"bell peppers, cut in pieces", 0, "", "bell peppers", "cut in pieces", id="bell peppers, cut in pieces"
),
],
)
def test_brute_parser(
unique_local_group_id: UUID4,
parsed_ingredient_data: tuple[list[IngredientFood], list[IngredientUnit]], # required so database is populated
input: str,
quantity: int | float,
unit: str,
food: str,
comment: str,
):
with session_context() as session: with session_context() as session:
parser = get_parser(RegisteredParser.brute, unique_user.group_id, session) parser = get_parser(RegisteredParser.brute, unique_local_group_id, session)
parsed = parser.parse_one(input)
ing = parsed.ingredient
for key, val in expectations.items(): if ing.quantity:
parsed = parser.parse_one(key) assert ing.quantity == quantity
else:
assert parsed.ingredient.quantity == val[0] assert not quantity
assert parsed.ingredient.unit.name == val[1] if ing.unit:
assert parsed.ingredient.food.name == val[2] assert ing.unit.name == unit
assert parsed.ingredient.note in {val[3], None} else:
assert not unit
if ing.food:
assert ing.food.name == food
else:
assert not food
if ing.note:
assert ing.note == comment
else:
assert not comment
@pytest.mark.parametrize( @pytest.mark.parametrize(