Feature/shareable recipes (#866)

* simplify context menu

* move computed to comp-api

* feat:  create share tokens for recipes for sharing recieps to non-users

* feat:  shareable recipe links with og tags
This commit is contained in:
Hayden
2021-12-05 11:55:46 -09:00
committed by GitHub
parent ba4107348f
commit b2673d75bf
25 changed files with 914 additions and 199 deletions

View File

@@ -13,6 +13,7 @@ from mealie.db.models.recipe.category import Category
from mealie.db.models.recipe.comment import RecipeComment
from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUnitModel
from mealie.db.models.recipe.recipe import RecipeModel
from mealie.db.models.recipe.shared import RecipeShareTokenModel
from mealie.db.models.recipe.tag import Tag
from mealie.db.models.recipe.tool import Tool
from mealie.db.models.server.task import ServerTaskModel
@@ -29,6 +30,7 @@ from mealie.schema.group.webhook import ReadWebhook
from mealie.schema.meal_plan.new_meal import ReadPlanEntry
from mealie.schema.recipe import Recipe, RecipeCategoryResponse, RecipeCommentOut, RecipeTagResponse
from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUnit
from mealie.schema.recipe.recipe_share_token import RecipeShareToken
from mealie.schema.recipe.recipe_tool import RecipeTool
from mealie.schema.reports.reports import ReportEntryOut, ReportOut
from mealie.schema.server import ServerTask
@@ -99,6 +101,10 @@ class Database:
def tags(self) -> TagsDataAccessModel:
return TagsDataAccessModel(self.session, pk_slug, Tag, RecipeTagResponse)
@cached_property
def recipe_share_tokens(self) -> AccessModel[RecipeShareToken, RecipeShareTokenModel]:
return AccessModel(self.session, pk_id, RecipeShareTokenModel, RecipeShareToken)
# ================================================================
# Site Items

View File

@@ -19,6 +19,7 @@ from .instruction import RecipeInstruction
from .note import Note
from .nutrition import Nutrition
from .settings import RecipeSettings
from .shared import RecipeShareTokenModel
from .tag import Tag, recipes2tags
from .tool import recipes_to_tools
@@ -87,6 +88,8 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
collection_class=ordering_list("position"),
)
share_tokens = orm.relationship(RecipeShareTokenModel, back_populates="recipe")
comments: list = orm.relationship("RecipeComment", back_populates="recipe", cascade="all, delete, delete-orphan")
# Mealie Specific

View File

@@ -0,0 +1,27 @@
from datetime import datetime, timedelta
from uuid import uuid4
import sqlalchemy as sa
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
from mealie.db.models._model_utils import GUID, auto_init
def defaut_expires_at_time() -> datetime:
return datetime.utcnow() + timedelta(days=30)
class RecipeShareTokenModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "recipe_share_tokens"
id = sa.Column(GUID, primary_key=True, default=uuid4)
group_id = sa.Column(GUID, sa.ForeignKey("groups.id"))
recipe_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id"), nullable=False)
recipe = sa.orm.relationship("RecipeModel", back_populates="share_tokens", uselist=False)
expires_at = sa.Column(sa.DateTime, nullable=False)
@auto_init()
def __init__(self, **_) -> None:
pass

View File

@@ -9,6 +9,7 @@ from . import (
groups,
parser,
recipe,
shared,
shopping_lists,
tags,
tools,
@@ -23,6 +24,7 @@ router.include_router(auth.router)
router.include_router(users.router)
router.include_router(groups.router)
router.include_router(recipe.router)
router.include_router(shared.router)
router.include_router(comments.router)
router.include_router(parser.router)
router.include_router(unit_and_foods.router)

View File

@@ -1,6 +1,14 @@
from fastapi import APIRouter
from . import all_recipe_routes, bulk_actions, comments, image_and_assets, recipe_crud_routes, recipe_export
from . import (
all_recipe_routes,
bulk_actions,
comments,
image_and_assets,
recipe_crud_routes,
recipe_export,
shared_routes,
)
prefix = "/recipes"
@@ -14,3 +22,4 @@ router.include_router(image_and_assets.user_router, prefix=prefix, tags=["Recipe
router.include_router(comments.router, prefix=prefix, tags=["Recipe: Comments"])
router.include_router(bulk_actions.router, prefix=prefix, tags=["Recipe: Bulk Actions"])
router.include_router(bulk_actions.export_router, prefix=prefix, tags=["Recipe: Bulk Exports"])
router.include_router(shared_routes.router, prefix=prefix, tags=["Recipe: Shared"])

View File

@@ -0,0 +1,21 @@
from fastapi import APIRouter, Depends
from pydantic import UUID4
from sqlalchemy.orm.session import Session
from mealie.db.database import get_database
from mealie.db.db_setup import generate_session
from mealie.schema.recipe import Recipe
router = APIRouter()
@router.get("/shared/{token_id}", response_model=Recipe)
def get_shared_recipe(token_id: UUID4, session: Session = Depends(generate_session)):
db = get_database(session)
token_summary = db.recipe_share_tokens.get_one(token_id)
if token_summary is None:
return None
return token_summary.recipe

View File

@@ -0,0 +1,23 @@
from fastapi import Depends
from mealie.routes.routers import UserAPIRouter
from mealie.services._base_http_service.router_factory import RouterFactory
from mealie.services.shared.recipe_shared_service import RecipeShareTokenSummary, SharedRecipeService
router = UserAPIRouter(prefix="/shared")
shared_router = RouterFactory(SharedRecipeService, prefix="/recipes", tags=["Shared: Recipes"])
@shared_router.get("", response_model=list[RecipeShareTokenSummary])
def get_all_shared(
recipe_id: int = None,
shared_recipe_service: SharedRecipeService = Depends(SharedRecipeService.private),
):
"""
Get all shared recipes
"""
return shared_recipe_service.get_all(recipe_id)
router.include_router(shared_router)

View File

@@ -0,0 +1,34 @@
from datetime import datetime, timedelta
from fastapi_camelcase import CamelModel
from pydantic import UUID4, Field
from .recipe import Recipe
def defaut_expires_at_time() -> datetime:
return datetime.utcnow() + timedelta(days=30)
class RecipeShareTokenCreate(CamelModel):
recipe_id: int
expires_at: datetime = Field(default_factory=defaut_expires_at_time)
class RecipeShareTokenSave(RecipeShareTokenCreate):
group_id: UUID4
class RecipeShareTokenSummary(RecipeShareTokenSave):
id: UUID4
created_at: datetime
class Config:
orm_mode = True
class RecipeShareToken(RecipeShareTokenSummary):
recipe: Recipe
class Config:
orm_mode = True

View File

View File

@@ -0,0 +1,51 @@
from functools import cached_property
from pydantic import UUID4
from mealie.schema.recipe.recipe_share_token import (
RecipeShareToken,
RecipeShareTokenCreate,
RecipeShareTokenSave,
RecipeShareTokenSummary,
)
from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins
from mealie.services._base_http_service.http_services import UserHttpService
from mealie.services.events import create_recipe_event
class SharedRecipeService(
CrudHttpMixins[RecipeShareToken, RecipeShareTokenCreate, RecipeShareTokenCreate],
UserHttpService[UUID4, RecipeShareToken],
):
event_func = create_recipe_event
_restrict_by_group = False
_schema = RecipeShareToken
@cached_property
def dal(self):
return self.db.recipe_share_tokens
def populate_item(self, id: UUID4) -> RecipeShareToken:
self.item = self.dal.get_one(id)
return self.item
def get_all(self, recipe_id=None) -> list[RecipeShareTokenSummary]:
# sourcery skip: assign-if-exp, inline-immediately-returned-variable
if recipe_id:
return self.db.recipe_share_tokens.multi_query(
{"group_id": self.group_id, "recipe_id": recipe_id},
override_schema=RecipeShareTokenSummary,
)
else:
return self.db.recipe_share_tokens.multi_query(
{"group_id": self.group_id}, override_schema=RecipeShareTokenSummary
)
def create_one(self, data: RecipeShareTokenCreate) -> RecipeShareToken:
save_data = RecipeShareTokenSave(**data.dict(), group_id=self.group_id)
return self._create_one(save_data)
def delete_one(self, item_id: UUID4 = None) -> None:
item_id = item_id or self.item.id
return self.dal.delete(item_id)