mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 22:43:34 -07:00
Merge branch 'mealie-next' into fix--remove-unused-deps
This commit is contained in:
commit
a847ced1a2
11 changed files with 125 additions and 149 deletions
|
@ -12,7 +12,7 @@ repos:
|
||||||
exclude: ^tests/data/
|
exclude: ^tests/data/
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: v0.12.0
|
rev: v0.12.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
|
|
|
@ -1,101 +1,104 @@
|
||||||
<template>
|
<template>
|
||||||
|
<!-- Wrap v-hover with a div to provide a proper DOM element for the transition -->
|
||||||
<v-lazy>
|
<v-lazy>
|
||||||
<v-hover
|
<div>
|
||||||
v-slot="{ isHovering, props }"
|
<v-hover
|
||||||
:open-delay="50"
|
v-slot="{ isHovering, props }"
|
||||||
>
|
:open-delay="50"
|
||||||
<v-card
|
|
||||||
v-bind="props"
|
|
||||||
:class="{ 'on-hover': isHovering }"
|
|
||||||
:style="{ cursor }"
|
|
||||||
:elevation="isHovering ? 12 : 2"
|
|
||||||
:to="recipeRoute"
|
|
||||||
:min-height="imageHeight + 75"
|
|
||||||
@click.self="$emit('click')"
|
|
||||||
>
|
>
|
||||||
<RecipeCardImage
|
<v-card
|
||||||
:icon-size="imageHeight"
|
v-bind="props"
|
||||||
:height="imageHeight"
|
:class="{ 'on-hover': isHovering }"
|
||||||
:slug="slug"
|
:style="{ cursor }"
|
||||||
:recipe-id="recipeId"
|
:elevation="isHovering ? 12 : 2"
|
||||||
size="small"
|
:to="recipeRoute"
|
||||||
:image-version="image"
|
:min-height="imageHeight + 75"
|
||||||
|
@click.self="$emit('click')"
|
||||||
>
|
>
|
||||||
<v-expand-transition v-if="description">
|
<RecipeCardImage
|
||||||
<div
|
:icon-size="imageHeight"
|
||||||
v-if="isHovering"
|
:height="imageHeight"
|
||||||
class="d-flex transition-fast-in-fast-out bg-secondary v-card--reveal"
|
:slug="slug"
|
||||||
style="height: 100%"
|
:recipe-id="recipeId"
|
||||||
>
|
size="small"
|
||||||
<v-card-text class="v-card--text-show white--text">
|
:image-version="image"
|
||||||
<div class="descriptionWrapper">
|
|
||||||
<SafeMarkdown :source="description" />
|
|
||||||
</div>
|
|
||||||
</v-card-text>
|
|
||||||
</div>
|
|
||||||
</v-expand-transition>
|
|
||||||
</RecipeCardImage>
|
|
||||||
<v-card-title class="mb-n3 px-4">
|
|
||||||
<div class="headerClass">
|
|
||||||
{{ name }}
|
|
||||||
</div>
|
|
||||||
</v-card-title>
|
|
||||||
|
|
||||||
<slot name="actions">
|
|
||||||
<v-card-actions
|
|
||||||
v-if="showRecipeContent"
|
|
||||||
class="px-1"
|
|
||||||
>
|
>
|
||||||
<RecipeFavoriteBadge
|
<v-expand-transition v-if="description">
|
||||||
v-if="isOwnGroup"
|
<div
|
||||||
class="absolute"
|
v-if="isHovering"
|
||||||
:recipe-id="recipeId"
|
class="d-flex transition-fast-in-fast-out bg-secondary v-card--reveal"
|
||||||
show-always
|
style="height: 100%"
|
||||||
/>
|
>
|
||||||
<div v-else class="px-1" /> <!-- Empty div to keep the layout consistent -->
|
<v-card-text class="v-card--text-show white--text">
|
||||||
|
<div class="descriptionWrapper">
|
||||||
|
<SafeMarkdown :source="description" />
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</div>
|
||||||
|
</v-expand-transition>
|
||||||
|
</RecipeCardImage>
|
||||||
|
<v-card-title class="mb-n3 px-4">
|
||||||
|
<div class="headerClass">
|
||||||
|
{{ name }}
|
||||||
|
</div>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
<RecipeRating
|
<slot name="actions">
|
||||||
class="ml-n2"
|
<v-card-actions
|
||||||
:value="rating"
|
v-if="showRecipeContent"
|
||||||
:recipe-id="recipeId"
|
class="px-1"
|
||||||
:slug="slug"
|
>
|
||||||
small
|
<RecipeFavoriteBadge
|
||||||
/>
|
v-if="isOwnGroup"
|
||||||
<v-spacer />
|
class="absolute"
|
||||||
<RecipeChips
|
:recipe-id="recipeId"
|
||||||
:truncate="true"
|
show-always
|
||||||
:items="tags"
|
/>
|
||||||
:title="false"
|
<div v-else class="px-1" /> <!-- Empty div to keep the layout consistent -->
|
||||||
:limit="2"
|
|
||||||
small
|
|
||||||
url-prefix="tags"
|
|
||||||
v-bind="$attrs"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- If we're not logged-in, no items display, so we hide this menu -->
|
<RecipeRating
|
||||||
<RecipeContextMenu
|
class="ml-n2"
|
||||||
v-if="isOwnGroup"
|
:value="rating"
|
||||||
color="grey-darken-2"
|
:recipe-id="recipeId"
|
||||||
:slug="slug"
|
:slug="slug"
|
||||||
:name="name"
|
small
|
||||||
:recipe-id="recipeId"
|
/>
|
||||||
:use-items="{
|
<v-spacer />
|
||||||
delete: false,
|
<RecipeChips
|
||||||
edit: false,
|
:truncate="true"
|
||||||
download: true,
|
:items="tags"
|
||||||
mealplanner: true,
|
:title="false"
|
||||||
shoppingList: true,
|
:limit="2"
|
||||||
print: false,
|
small
|
||||||
printPreferences: false,
|
url-prefix="tags"
|
||||||
share: true,
|
v-bind="$attrs"
|
||||||
}"
|
/>
|
||||||
@delete="$emit('delete', slug)"
|
|
||||||
/>
|
<!-- If we're not logged-in, no items display, so we hide this menu -->
|
||||||
</v-card-actions>
|
<RecipeContextMenu
|
||||||
</slot>
|
v-if="isOwnGroup"
|
||||||
<slot />
|
color="grey-darken-2"
|
||||||
</v-card>
|
:slug="slug"
|
||||||
</v-hover>
|
:name="name"
|
||||||
|
:recipe-id="recipeId"
|
||||||
|
:use-items="{
|
||||||
|
delete: false,
|
||||||
|
edit: false,
|
||||||
|
download: true,
|
||||||
|
mealplanner: true,
|
||||||
|
shoppingList: true,
|
||||||
|
print: false,
|
||||||
|
printPreferences: false,
|
||||||
|
share: true,
|
||||||
|
}"
|
||||||
|
@delete="$emit('delete', slug)"
|
||||||
|
/>
|
||||||
|
</v-card-actions>
|
||||||
|
</slot>
|
||||||
|
<slot />
|
||||||
|
</v-card>
|
||||||
|
</v-hover>
|
||||||
|
</div>
|
||||||
</v-lazy>
|
</v-lazy>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,8 @@
|
||||||
:style="maxHeight ? `max-height: ${maxHeight}; overflow-y: auto;` : ''"
|
:style="maxHeight ? `max-height: ${maxHeight}; overflow-y: auto;` : ''"
|
||||||
>
|
>
|
||||||
<v-timeline
|
<v-timeline
|
||||||
:dense="$vuetify.display.smAndDown"
|
:density="$vuetify.display.smAndDown ? ($vuetify.display.xs ? 'compact' : 'comfortable') : undefined"
|
||||||
|
justify="center"
|
||||||
class="timeline"
|
class="timeline"
|
||||||
>
|
>
|
||||||
<RecipeTimelineItem
|
<RecipeTimelineItem
|
||||||
|
@ -78,6 +79,7 @@
|
||||||
:event="event"
|
:event="event"
|
||||||
:recipe="recipes.get(event.recipeId)"
|
:recipe="recipes.get(event.recipeId)"
|
||||||
:show-recipe-cards="showRecipeCards"
|
:show-recipe-cards="showRecipeCards"
|
||||||
|
:width="$vuetify.display.smAndDown ? '100%' : undefined"
|
||||||
@update="updateTimelineEvent(index)"
|
@update="updateTimelineEvent(index)"
|
||||||
@delete="deleteTimelineEvent(index)"
|
@delete="deleteTimelineEvent(index)"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useAdminApi } from "~/composables/api";
|
||||||
import type { UserIn, UserOut } from "~/lib/api/types/user";
|
import type { UserIn, UserOut } from "~/lib/api/types/user";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -8,7 +8,7 @@ to control whether the object is substantiated... but some of the others rely on
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const useAllUsers = function () {
|
export const useAllUsers = function () {
|
||||||
const api = useUserApi();
|
const api = useAdminApi();
|
||||||
const asyncKey = String(Date.now());
|
const asyncKey = String(Date.now());
|
||||||
const { data: users, refresh: refreshAllUsers } = useLazyAsyncData(asyncKey, async () => {
|
const { data: users, refresh: refreshAllUsers } = useLazyAsyncData(asyncKey, async () => {
|
||||||
const { data } = await api.users.getAll();
|
const { data } = await api.users.getAll();
|
||||||
|
@ -24,7 +24,7 @@ export const useAllUsers = function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUser = function (refreshFunc: CallableFunction | null = null) {
|
export const useUser = function (refreshFunc: CallableFunction | null = null) {
|
||||||
const api = useUserApi();
|
const api = useAdminApi();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
function getUser(id: string) {
|
function getUser(id: string) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
|
|
||||||
from mealie.core import security
|
from mealie.core import security
|
||||||
|
@ -42,6 +42,11 @@ class AdminUserManagementRoutes(BaseAdminController):
|
||||||
|
|
||||||
@router.post("", response_model=UserOut, status_code=201)
|
@router.post("", response_model=UserOut, status_code=201)
|
||||||
def create_one(self, data: UserIn):
|
def create_one(self, data: UserIn):
|
||||||
|
if self.repos.users.get_by_username(data.username):
|
||||||
|
raise HTTPException(status.HTTP_409_CONFLICT, {"message": self.t("exceptions.username-conflict-error")})
|
||||||
|
elif self.repos.users.get_one(data.email, "email"):
|
||||||
|
raise HTTPException(status.HTTP_409_CONFLICT, {"message": self.t("exceptions.email-conflict-error")})
|
||||||
|
|
||||||
data.password = security.hash_password(data.password)
|
data.password = security.hash_password(data.password)
|
||||||
return self.mixins.create_one(data)
|
return self.mixins.create_one(data)
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@ router = APIRouter()
|
||||||
|
|
||||||
router.include_router(registration.router, prefix=user_prefix, tags=["Users: Registration"])
|
router.include_router(registration.router, prefix=user_prefix, tags=["Users: Registration"])
|
||||||
router.include_router(crud.user_router)
|
router.include_router(crud.user_router)
|
||||||
router.include_router(crud.admin_router)
|
|
||||||
router.include_router(forgot_password.router, prefix=user_prefix, tags=["Users: Passwords"])
|
router.include_router(forgot_password.router, prefix=user_prefix, tags=["Users: Passwords"])
|
||||||
router.include_router(images.router, prefix=user_prefix, tags=["Users: Images"])
|
router.include_router(images.router, prefix=user_prefix, tags=["Users: Images"])
|
||||||
router.include_router(api_tokens.router)
|
router.include_router(api_tokens.router)
|
||||||
|
|
|
@ -1,52 +1,17 @@
|
||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
|
|
||||||
from mealie.core.security import hash_password
|
from mealie.core.security import hash_password
|
||||||
from mealie.core.security.providers.credentials_provider import CredentialsProvider
|
from mealie.core.security.providers.credentials_provider import CredentialsProvider
|
||||||
from mealie.db.models.users.users import AuthMethod
|
from mealie.db.models.users.users import AuthMethod
|
||||||
from mealie.routes._base import BaseAdminController, BaseUserController, controller
|
from mealie.routes._base import BaseUserController, controller
|
||||||
from mealie.routes._base.mixins import HttpRepo
|
from mealie.routes._base.routers import UserAPIRouter
|
||||||
from mealie.routes._base.routers import AdminAPIRouter, UserAPIRouter
|
|
||||||
from mealie.routes.users._helpers import assert_user_change_allowed
|
from mealie.routes.users._helpers import assert_user_change_allowed
|
||||||
from mealie.schema.response import ErrorResponse, SuccessResponse
|
from mealie.schema.response import ErrorResponse, SuccessResponse
|
||||||
from mealie.schema.response.pagination import PaginationQuery
|
from mealie.schema.user import ChangePassword, UserBase, UserOut
|
||||||
from mealie.schema.user import ChangePassword, UserBase, UserIn, UserOut
|
from mealie.schema.user.user import UserRatings, UserRatingSummary
|
||||||
from mealie.schema.user.user import UserPagination, UserRatings, UserRatingSummary
|
|
||||||
|
|
||||||
user_router = UserAPIRouter(prefix="/users", tags=["Users: CRUD"])
|
user_router = UserAPIRouter(prefix="/users", tags=["Users: CRUD"])
|
||||||
admin_router = AdminAPIRouter(prefix="/users", tags=["Users: Admin CRUD"])
|
|
||||||
|
|
||||||
|
|
||||||
@controller(admin_router)
|
|
||||||
class AdminUserController(BaseAdminController):
|
|
||||||
@property
|
|
||||||
def mixins(self) -> HttpRepo:
|
|
||||||
return HttpRepo[UserIn, UserOut, UserBase](self.repos.users, self.logger)
|
|
||||||
|
|
||||||
@admin_router.get("", response_model=UserPagination)
|
|
||||||
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
|
|
||||||
"""Returns all users from all groups"""
|
|
||||||
|
|
||||||
response = self.repos.users.page_all(
|
|
||||||
pagination=q,
|
|
||||||
override=UserOut,
|
|
||||||
)
|
|
||||||
|
|
||||||
response.set_pagination_guides(admin_router.url_path_for("get_all"), q.model_dump())
|
|
||||||
return response
|
|
||||||
|
|
||||||
@admin_router.post("", response_model=UserOut, status_code=201)
|
|
||||||
def create_user(self, new_user: UserIn):
|
|
||||||
new_user.password = hash_password(new_user.password)
|
|
||||||
return self.mixins.create_one(new_user)
|
|
||||||
|
|
||||||
@admin_router.get("/{item_id}", response_model=UserOut)
|
|
||||||
def get_user(self, item_id: UUID4):
|
|
||||||
return self.mixins.get_one(item_id)
|
|
||||||
|
|
||||||
@admin_router.delete("/{item_id}")
|
|
||||||
def delete_user(self, item_id: UUID4):
|
|
||||||
self.mixins.delete_one(item_id)
|
|
||||||
|
|
||||||
|
|
||||||
@controller(user_router)
|
@controller(user_router)
|
||||||
|
|
6
poetry.lock
generated
6
poetry.lock
generated
|
@ -1855,14 +1855,14 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openai"
|
name = "openai"
|
||||||
version = "1.92.2"
|
version = "1.93.0"
|
||||||
description = "The official Python library for the openai API"
|
description = "The official Python library for the openai API"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "openai-1.92.2-py3-none-any.whl", hash = "sha256:abb64bee7f2571709edf9a856f598ffe871730129a7d807a8a4d8d2958f5c842"},
|
{file = "openai-1.93.0-py3-none-any.whl", hash = "sha256:3d746fe5498f0dd72e0d9ab706f26c91c0f646bf7459e5629af8ba7c9dbdf090"},
|
||||||
{file = "openai-1.92.2.tar.gz", hash = "sha256:b571a79fc7e165e7d00e6963a8a95eb5f42b60ac89fd316f1dc0a2dac5c6fae1"},
|
{file = "openai-1.93.0.tar.gz", hash = "sha256:988f31ade95e1ff0585af11cc5a64510225e4f5cd392698c675d0a9265b8e337"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
8
tests/fixtures/fixture_users.py
vendored
8
tests/fixtures/fixture_users.py
vendored
|
@ -76,7 +76,7 @@ def h2_user(session: Session, admin_token, api_client: TestClient, unique_user:
|
||||||
"admin": False,
|
"admin": False,
|
||||||
"tokens": [],
|
"tokens": [],
|
||||||
}
|
}
|
||||||
response = api_client.post(api_routes.users, json=user_data, headers=admin_token)
|
response = api_client.post(api_routes.admin_users, json=user_data, headers=admin_token)
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
|
|
||||||
# Log in as this user
|
# Log in as this user
|
||||||
|
@ -135,7 +135,7 @@ def g2_user(session: Session, admin_token, api_client: TestClient):
|
||||||
}
|
}
|
||||||
|
|
||||||
api_client.post(api_routes.admin_groups, json={"name": group}, headers=admin_token)
|
api_client.post(api_routes.admin_groups, json={"name": group}, headers=admin_token)
|
||||||
response = api_client.post(api_routes.users, json=create_data, headers=admin_token)
|
response = api_client.post(api_routes.admin_users, json=create_data, headers=admin_token)
|
||||||
|
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
@ -258,7 +258,7 @@ def user_tuple(session: Session, admin_token, api_client: TestClient) -> Generat
|
||||||
users_out = []
|
users_out = []
|
||||||
|
|
||||||
for usr in [create_data_1, create_data_2]:
|
for usr in [create_data_1, create_data_2]:
|
||||||
response = api_client.post(api_routes.users, json=usr, headers=admin_token)
|
response = api_client.post(api_routes.admin_users, json=usr, headers=admin_token)
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
|
|
||||||
# Log in as this user
|
# Log in as this user
|
||||||
|
@ -312,7 +312,7 @@ def user_token(admin_token, api_client: TestClient):
|
||||||
"tokens": [],
|
"tokens": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
response = api_client.post(api_routes.users, json=create_data, headers=admin_token)
|
response = api_client.post(api_routes.admin_users, json=create_data, headers=admin_token)
|
||||||
|
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ def test_api_token_private(api_client: TestClient, admin_token):
|
||||||
response = api_client.post(api_routes.users_api_tokens, json={"name": "Test API Token"}, headers=admin_token)
|
response = api_client.post(api_routes.users_api_tokens, json={"name": "Test API Token"}, headers=admin_token)
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
|
|
||||||
response = api_client.get(api_routes.users, headers=admin_token, params={"perPage": -1})
|
response = api_client.get(api_routes.admin_users, headers=admin_token, params={"perPage": -1})
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
for user in response.json()["items"]:
|
for user in response.json()["items"]:
|
||||||
for user_token in user["tokens"] or []:
|
for user_token in user["tokens"] or []:
|
||||||
|
@ -39,7 +39,7 @@ def test_api_token_private(api_client: TestClient, admin_token):
|
||||||
|
|
||||||
|
|
||||||
def test_use_token(api_client: TestClient, long_live_token):
|
def test_use_token(api_client: TestClient, long_live_token):
|
||||||
response = api_client.get(api_routes.users, headers=long_live_token)
|
response = api_client.get(api_routes.admin_users, headers=long_live_token)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
|
@ -88,9 +88,11 @@ def test_user_update(api_client: TestClient, unique_user: TestUser, admin_user:
|
||||||
|
|
||||||
|
|
||||||
def test_admin_updates(api_client: TestClient, admin_user: TestUser, unique_user: TestUser):
|
def test_admin_updates(api_client: TestClient, admin_user: TestUser, unique_user: TestUser):
|
||||||
response = api_client.get(api_routes.users_item_id(unique_user.user_id), headers=admin_user.token)
|
response = api_client.get(api_routes.admin_users_item_id(unique_user.user_id), headers=admin_user.token)
|
||||||
|
assert response.status_code == 200
|
||||||
user = response.json()
|
user = response.json()
|
||||||
response = api_client.get(api_routes.users_item_id(admin_user.user_id), headers=admin_user.token)
|
|
||||||
|
response = api_client.get(api_routes.admin_users_item_id(admin_user.user_id), headers=admin_user.token)
|
||||||
admin = response.json()
|
admin = response.json()
|
||||||
|
|
||||||
# admin updating themselves
|
# admin updating themselves
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue