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:
commit
704d0a8392
8 changed files with 173 additions and 41 deletions
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue