mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 22:43:34 -07:00
Merge branch 'mealie-next' into fix-recipe-timeline-nuxt-3
This commit is contained in:
commit
5fbf102a03
8 changed files with 118 additions and 144 deletions
|
@ -1,101 +1,104 @@
|
|||
<template>
|
||||
<!-- Wrap v-hover with a div to provide a proper DOM element for the transition -->
|
||||
<v-lazy>
|
||||
<v-hover
|
||||
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')"
|
||||
<div>
|
||||
<v-hover
|
||||
v-slot="{ isHovering, props }"
|
||||
:open-delay="50"
|
||||
>
|
||||
<RecipeCardImage
|
||||
:icon-size="imageHeight"
|
||||
:height="imageHeight"
|
||||
:slug="slug"
|
||||
:recipe-id="recipeId"
|
||||
size="small"
|
||||
:image-version="image"
|
||||
<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')"
|
||||
>
|
||||
<v-expand-transition v-if="description">
|
||||
<div
|
||||
v-if="isHovering"
|
||||
class="d-flex transition-fast-in-fast-out bg-secondary v-card--reveal"
|
||||
style="height: 100%"
|
||||
>
|
||||
<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>
|
||||
|
||||
<slot name="actions">
|
||||
<v-card-actions
|
||||
v-if="showRecipeContent"
|
||||
class="px-1"
|
||||
<RecipeCardImage
|
||||
:icon-size="imageHeight"
|
||||
:height="imageHeight"
|
||||
:slug="slug"
|
||||
:recipe-id="recipeId"
|
||||
size="small"
|
||||
:image-version="image"
|
||||
>
|
||||
<RecipeFavoriteBadge
|
||||
v-if="isOwnGroup"
|
||||
class="absolute"
|
||||
:recipe-id="recipeId"
|
||||
show-always
|
||||
/>
|
||||
<div v-else class="px-1" /> <!-- Empty div to keep the layout consistent -->
|
||||
<v-expand-transition v-if="description">
|
||||
<div
|
||||
v-if="isHovering"
|
||||
class="d-flex transition-fast-in-fast-out bg-secondary v-card--reveal"
|
||||
style="height: 100%"
|
||||
>
|
||||
<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
|
||||
class="ml-n2"
|
||||
:value="rating"
|
||||
:recipe-id="recipeId"
|
||||
:slug="slug"
|
||||
small
|
||||
/>
|
||||
<v-spacer />
|
||||
<RecipeChips
|
||||
:truncate="true"
|
||||
:items="tags"
|
||||
:title="false"
|
||||
:limit="2"
|
||||
small
|
||||
url-prefix="tags"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
<slot name="actions">
|
||||
<v-card-actions
|
||||
v-if="showRecipeContent"
|
||||
class="px-1"
|
||||
>
|
||||
<RecipeFavoriteBadge
|
||||
v-if="isOwnGroup"
|
||||
class="absolute"
|
||||
:recipe-id="recipeId"
|
||||
show-always
|
||||
/>
|
||||
<div v-else class="px-1" /> <!-- Empty div to keep the layout consistent -->
|
||||
|
||||
<!-- If we're not logged-in, no items display, so we hide this menu -->
|
||||
<RecipeContextMenu
|
||||
v-if="isOwnGroup"
|
||||
color="grey-darken-2"
|
||||
:slug="slug"
|
||||
: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>
|
||||
<RecipeRating
|
||||
class="ml-n2"
|
||||
:value="rating"
|
||||
:recipe-id="recipeId"
|
||||
:slug="slug"
|
||||
small
|
||||
/>
|
||||
<v-spacer />
|
||||
<RecipeChips
|
||||
:truncate="true"
|
||||
:items="tags"
|
||||
:title="false"
|
||||
:limit="2"
|
||||
small
|
||||
url-prefix="tags"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
|
||||
<!-- If we're not logged-in, no items display, so we hide this menu -->
|
||||
<RecipeContextMenu
|
||||
v-if="isOwnGroup"
|
||||
color="grey-darken-2"
|
||||
:slug="slug"
|
||||
: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>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useUserApi } from "~/composables/api";
|
||||
import { useAdminApi } from "~/composables/api";
|
||||
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 () {
|
||||
const api = useUserApi();
|
||||
const api = useAdminApi();
|
||||
const asyncKey = String(Date.now());
|
||||
const { data: users, refresh: refreshAllUsers } = useLazyAsyncData(asyncKey, async () => {
|
||||
const { data } = await api.users.getAll();
|
||||
|
@ -24,7 +24,7 @@ export const useAllUsers = function () {
|
|||
};
|
||||
|
||||
export const useUser = function (refreshFunc: CallableFunction | null = null) {
|
||||
const api = useUserApi();
|
||||
const api = useAdminApi();
|
||||
const loading = ref(false);
|
||||
|
||||
function getUser(id: string) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from functools import cached_property
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.core import security
|
||||
|
@ -42,6 +42,11 @@ class AdminUserManagementRoutes(BaseAdminController):
|
|||
|
||||
@router.post("", response_model=UserOut, status_code=201)
|
||||
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)
|
||||
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(crud.user_router)
|
||||
router.include_router(crud.admin_router)
|
||||
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(api_tokens.router)
|
||||
|
|
|
@ -1,52 +1,17 @@
|
|||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi import HTTPException, status
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.core.security import hash_password
|
||||
from mealie.core.security.providers.credentials_provider import CredentialsProvider
|
||||
from mealie.db.models.users.users import AuthMethod
|
||||
from mealie.routes._base import BaseAdminController, BaseUserController, controller
|
||||
from mealie.routes._base.mixins import HttpRepo
|
||||
from mealie.routes._base.routers import AdminAPIRouter, UserAPIRouter
|
||||
from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.routes._base.routers import UserAPIRouter
|
||||
from mealie.routes.users._helpers import assert_user_change_allowed
|
||||
from mealie.schema.response import ErrorResponse, SuccessResponse
|
||||
from mealie.schema.response.pagination import PaginationQuery
|
||||
from mealie.schema.user import ChangePassword, UserBase, UserIn, UserOut
|
||||
from mealie.schema.user.user import UserPagination, UserRatings, UserRatingSummary
|
||||
from mealie.schema.user import ChangePassword, UserBase, UserOut
|
||||
from mealie.schema.user.user import UserRatings, UserRatingSummary
|
||||
|
||||
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)
|
||||
|
|
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,
|
||||
"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
|
||||
|
||||
# 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)
|
||||
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
|
||||
|
||||
|
@ -258,7 +258,7 @@ def user_tuple(session: Session, admin_token, api_client: TestClient) -> Generat
|
|||
users_out = []
|
||||
|
||||
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
|
||||
|
||||
# Log in as this user
|
||||
|
@ -312,7 +312,7 @@ def user_token(admin_token, api_client: TestClient):
|
|||
"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
|
||||
|
||||
|
|
|
@ -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)
|
||||
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
|
||||
for user in response.json()["items"]:
|
||||
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):
|
||||
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
|
||||
|
||||
|
|
|
@ -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):
|
||||
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()
|
||||
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 updating themselves
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue