mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-12-13 05:45:22 -05:00
feat: Improve Public URL Readability (#2482)
* added support for group slugs * modified frontend to use links with group slug * fixed test refs * unused import --------- Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
This commit is contained in:
@@ -32,6 +32,7 @@ class Group(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "groups"
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
name: Mapped[str] = mapped_column(sa.String, index=True, nullable=False, unique=True)
|
||||
slug: Mapped[str | None] = mapped_column(sa.String, index=True, unique=True)
|
||||
users: Mapped[list["User"]] = orm.relationship("User", back_populates="group")
|
||||
categories: Mapped[Category] = orm.relationship(
|
||||
Category, secondary=group_to_categories, single_parent=True, uselist=True
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
from collections.abc import Iterable
|
||||
from typing import cast
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import UUID4
|
||||
from slugify import slugify
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from mealie.db.models.group import Group
|
||||
from mealie.db.models.recipe.category import Category
|
||||
@@ -8,19 +14,55 @@ from mealie.db.models.recipe.tag import Tag
|
||||
from mealie.db.models.recipe.tool import Tool
|
||||
from mealie.db.models.users.users import User
|
||||
from mealie.schema.group.group_statistics import GroupStatistics
|
||||
from mealie.schema.user.user import GroupInDB
|
||||
from mealie.schema.user.user import GroupBase, GroupInDB
|
||||
|
||||
from ..db.models._model_base import SqlAlchemyBase
|
||||
from .repository_generic import RepositoryGeneric
|
||||
|
||||
|
||||
class RepositoryGroup(RepositoryGeneric[GroupInDB, Group]):
|
||||
def create(self, data: GroupBase | dict) -> GroupInDB:
|
||||
if isinstance(data, GroupBase):
|
||||
data = data.dict()
|
||||
|
||||
max_attempts = 10
|
||||
original_name = cast(str, data["name"])
|
||||
|
||||
attempts = 0
|
||||
while True:
|
||||
try:
|
||||
data["slug"] = slugify(data["name"])
|
||||
return super().create(data)
|
||||
except IntegrityError:
|
||||
self.session.rollback()
|
||||
attempts += 1
|
||||
if attempts >= max_attempts:
|
||||
raise
|
||||
|
||||
data["name"] = f"{original_name} ({attempts})"
|
||||
|
||||
def create_many(self, data: Iterable[GroupInDB | dict]) -> list[GroupInDB]:
|
||||
# since create uses special logic for resolving slugs, we don't want to use the standard create_many method
|
||||
return [self.create(new_group) for new_group in data]
|
||||
|
||||
def get_by_name(self, name: str) -> GroupInDB | None:
|
||||
dbgroup = self.session.execute(select(self.model).filter_by(name=name)).scalars().one_or_none()
|
||||
if dbgroup is None:
|
||||
return None
|
||||
return self.schema.from_orm(dbgroup)
|
||||
|
||||
def get_by_slug_or_id(self, slug_or_id: str | UUID) -> GroupInDB | None:
|
||||
if isinstance(slug_or_id, str):
|
||||
try:
|
||||
slug_or_id = UUID(slug_or_id)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if isinstance(slug_or_id, UUID):
|
||||
return self.get_one(slug_or_id)
|
||||
else:
|
||||
return self.get_one(slug_or_id, key="slug")
|
||||
|
||||
def statistics(self, group_id: UUID4) -> GroupStatistics:
|
||||
def model_count(model: type[SqlAlchemyBase]) -> int:
|
||||
stmt = select(func.count(model.id)).filter_by(group_id=group_id)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.routes._base import controller
|
||||
from mealie.routes._base.base_controllers import BasePublicController
|
||||
@@ -10,14 +9,14 @@ router = APIRouter(prefix="/explore", tags=["Explore: Recipes"])
|
||||
|
||||
@controller(router)
|
||||
class PublicRecipesController(BasePublicController):
|
||||
@router.get("/recipes/{group_id}/{recipe_slug}", response_model=Recipe)
|
||||
def get_recipe(self, group_id: UUID4, recipe_slug: str) -> Recipe:
|
||||
group = self.repos.groups.get_one(group_id)
|
||||
@router.get("/recipes/{group_slug}/{recipe_slug}", response_model=Recipe)
|
||||
def get_recipe(self, group_slug: str, recipe_slug: str) -> Recipe:
|
||||
group = self.repos.groups.get_by_slug_or_id(group_slug)
|
||||
|
||||
if not group or group.preferences.private_group:
|
||||
raise HTTPException(404, "group not found")
|
||||
|
||||
recipe = self.repos.recipes.by_group(group_id).get_one(recipe_slug)
|
||||
recipe = self.repos.recipes.by_group(group.id).get_one(recipe_slug)
|
||||
|
||||
if not recipe or not recipe.settings.public:
|
||||
raise HTTPException(404, "recipe not found")
|
||||
|
||||
@@ -181,6 +181,7 @@ class PrivateUser(UserOut):
|
||||
class UpdateGroup(GroupBase):
|
||||
id: UUID4
|
||||
name: str
|
||||
slug: str
|
||||
categories: list[CategoryBase] | None = []
|
||||
|
||||
webhooks: list[Any] = []
|
||||
|
||||
Reference in New Issue
Block a user