feat: add user recipe export functionality (#845)

* feat(frontend):  add user recipe export functionality

* remove depreciated folders

* change/remove depreciated folders

* add testing variable in config

* add GUID support for group_id

* improve testing feedback on 422 errors

* remove/cleanup files/folders

* initial user export support

* delete unused css

* update backup page UI

* remove depreciated settings

* feat:  export download links

* fix #813

* remove top level statements

* show footer

* add export purger to scheduler

* update purge glob

* fix meal-planner lockout

* feat:  add bulk delete/purge exports

* style(frontend): 💄 update UI for site settings

* feat:  add version checker

* update documentation

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden
2021-12-04 14:18:46 -09:00
committed by GitHub
parent 2ce195a0d4
commit c32d7d7486
84 changed files with 1329 additions and 667 deletions

View File

@@ -37,6 +37,7 @@ def start_scheduler():
tasks.purge_group_registration,
tasks.auto_backup,
tasks.purge_password_reset_tokens,
tasks.purge_group_data_exports,
)
SchedulerRegistry.register_hourly()

View File

@@ -0,0 +1,41 @@
import datetime
from functools import lru_cache
import requests
_LAST_RESET = None
@lru_cache(maxsize=1)
def get_latest_github_release() -> str:
"""
Gets the latest release from GitHub.
Returns:
str: The latest release from GitHub.
"""
url = "https://api.github.com/repos/hay-kot/mealie/releases/latest"
response = requests.get(url)
response.raise_for_status()
return response.json()["tag_name"]
def get_latest_version() -> str:
"""
Gets the latest release version.
Returns:
str: The latest release version.
"""
MAX_DAYS_OLD = 1 # reset cache after 1 day
global _LAST_RESET
now = datetime.datetime.now()
if not _LAST_RESET or now - _LAST_RESET > datetime.timedelta(days=MAX_DAYS_OLD):
_LAST_RESET = now
get_latest_github_release.cache_clear()
return get_latest_github_release()

View File

@@ -2,30 +2,33 @@ from pathlib import Path
class AppDirectories:
def __init__(self, data_dir) -> None:
self.DATA_DIR: Path = data_dir
self.IMG_DIR: Path = data_dir.joinpath("img")
self.BACKUP_DIR: Path = data_dir.joinpath("backups")
self.DEBUG_DIR: Path = data_dir.joinpath("debug")
self.MIGRATION_DIR: Path = data_dir.joinpath("migration")
self.NEXTCLOUD_DIR: Path = self.MIGRATION_DIR.joinpath("nextcloud")
self.CHOWDOWN_DIR: Path = self.MIGRATION_DIR.joinpath("chowdown")
self.TEMPLATE_DIR: Path = data_dir.joinpath("templates")
self.USER_DIR: Path = data_dir.joinpath("users")
self.RECIPE_DATA_DIR: Path = data_dir.joinpath("recipes")
self.TEMP_DIR: Path = data_dir.joinpath(".temp")
def __init__(self, data_dir: Path) -> None:
self.DATA_DIR = data_dir
self.BACKUP_DIR = data_dir.joinpath("backups")
self.USER_DIR = data_dir.joinpath("users")
self.RECIPE_DATA_DIR = data_dir.joinpath("recipes")
self.TEMPLATE_DIR = data_dir.joinpath("templates")
self.GROUPS_DIR = self.DATA_DIR.joinpath("groups")
# Deprecated
self._TEMP_DIR = data_dir.joinpath(".temp")
self._IMG_DIR = data_dir.joinpath("img")
self.ensure_directories()
@property
def IMG_DIR(self):
return self._IMG_DIR
@property
def TEMP_DIR(self):
return self._TEMP_DIR
def ensure_directories(self):
required_dirs = [
self.IMG_DIR,
self.GROUPS_DIR,
self.BACKUP_DIR,
self.DEBUG_DIR,
self.MIGRATION_DIR,
self.TEMPLATE_DIR,
self.NEXTCLOUD_DIR,
self.CHOWDOWN_DIR,
self.RECIPE_DATA_DIR,
self.USER_DIR,
]

View File

@@ -102,6 +102,11 @@ class AppSettings(BaseSettings):
not_none = None not in required
return self.LDAP_AUTH_ENABLED and not_none
# ===============================================
# Testing Config
TESTING: bool = False
class Config:
arbitrary_types_allowed = True

View File

@@ -5,6 +5,7 @@ from sqlalchemy.orm import Session
from mealie.db.models.event import Event, EventNotification
from mealie.db.models.group import Group, GroupMealPlan, ReportEntryModel, ReportModel
from mealie.db.models.group.cookbook import CookBook
from mealie.db.models.group.exports import GroupDataExportsModel
from mealie.db.models.group.invite_tokens import GroupInviteToken
from mealie.db.models.group.preferences import GroupPreferencesModel
from mealie.db.models.group.webhooks import GroupWebhooksModel
@@ -21,6 +22,7 @@ from mealie.db.models.users.password_reset import PasswordResetModel
from mealie.schema.cookbook.cookbook import ReadCookBook
from mealie.schema.events import Event as EventSchema
from mealie.schema.events import EventNotificationIn
from mealie.schema.group.group_exports import GroupDataExport
from mealie.schema.group.group_preferences import ReadGroupPreferences
from mealie.schema.group.invite_token import ReadInviteToken
from mealie.schema.group.webhook import ReadWebhook
@@ -42,6 +44,7 @@ from .user_access_model import UserDataAccessModel
pk_id = "id"
pk_slug = "slug"
pk_token = "token"
pk_group_id = "group_id"
class CategoryDataAccessModel(AccessModel):
@@ -143,7 +146,11 @@ class Database:
@cached_property
def group_preferences(self) -> AccessModel[ReadGroupPreferences, GroupPreferencesModel]:
return AccessModel(self.session, "group_id", GroupPreferencesModel, ReadGroupPreferences)
return AccessModel(self.session, pk_group_id, GroupPreferencesModel, ReadGroupPreferences)
@cached_property
def group_exports(self) -> AccessModel[GroupDataExport, GroupDataExportsModel]:
return AccessModel(self.session, pk_id, GroupDataExportsModel, GroupDataExport)
@cached_property
def meals(self) -> MealDataAccessModel:

View File

@@ -1,4 +1,5 @@
from datetime import date
from uuid import UUID
from mealie.db.models.group import GroupMealPlan
from mealie.schema.meal_plan.new_meal import ReadPlanEntry
@@ -7,7 +8,7 @@ from ._access_model import AccessModel
class MealDataAccessModel(AccessModel[ReadPlanEntry, GroupMealPlan]):
def get_slice(self, start: date, end: date, group_id: int) -> list[ReadPlanEntry]:
def get_slice(self, start: date, end: date, group_id: UUID) -> list[ReadPlanEntry]:
start = start.strftime("%Y-%m-%d")
end = end.strftime("%Y-%m-%d")
qry = self.session.query(GroupMealPlan).filter(
@@ -17,7 +18,7 @@ class MealDataAccessModel(AccessModel[ReadPlanEntry, GroupMealPlan]):
return [self.schema.from_orm(x) for x in qry.all()]
def get_today(self, group_id: int) -> list[ReadPlanEntry]:
def get_today(self, group_id: UUID) -> list[ReadPlanEntry]:
today = date.today()
qry = self.session.query(GroupMealPlan).filter(GroupMealPlan.date == today, GroupMealPlan.group_id == group_id)

View File

@@ -62,14 +62,19 @@ class RecipeDataAccessModel(AccessModel[Recipe, RecipeModel]):
override_schema=override_schema,
)
def summary(self, group_id, start=0, limit=99999) -> Any:
def summary(self, group_id, start=0, limit=99999, load_foods=False) -> Any:
args = [
joinedload(RecipeModel.recipe_category),
joinedload(RecipeModel.tags),
joinedload(RecipeModel.tools),
]
if load_foods:
args.append(joinedload(RecipeModel.recipe_ingredient).options(joinedload(RecipeIngredient.food)))
return (
self.session.query(RecipeModel)
.options(
joinedload(RecipeModel.recipe_category),
joinedload(RecipeModel.tags),
joinedload(RecipeModel.recipe_ingredient).options(joinedload(RecipeIngredient.food)),
)
.options(*args)
.filter(RecipeModel.group_id == group_id)
.offset(start)
.limit(limit)

View File

@@ -1 +1,2 @@
from .auto_init import auto_init
from .guid import GUID

View File

@@ -1,4 +1,5 @@
from .cookbook import *
from .exports import *
from .group import *
from .invite_tokens import *
from .mealplan import *

View File

@@ -1,7 +1,7 @@
from sqlalchemy import Column, ForeignKey, Integer, String, orm
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import auto_init
from .._model_utils import auto_init, guid
from ..recipe.category import Category, cookbooks_to_categories
@@ -14,7 +14,7 @@ class CookBook(SqlAlchemyBase, BaseMixins):
slug = Column(String, nullable=False)
categories = orm.relationship(Category, secondary=cookbooks_to_categories, single_parent=True)
group_id = Column(Integer, ForeignKey("groups.id"))
group_id = Column(guid.GUID, ForeignKey("groups.id"))
group = orm.relationship("Group", back_populates="cookbooks")
@auto_init()

View File

@@ -0,0 +1,24 @@
from uuid import uuid4
from sqlalchemy import Column, ForeignKey, String, orm
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import GUID, auto_init
class GroupDataExportsModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "group_data_exports"
id = Column(GUID, primary_key=True, default=uuid4)
group = orm.relationship("Group", back_populates="data_exports", single_parent=True)
group_id = Column(GUID, ForeignKey("groups.id"), index=True)
name = Column(String, nullable=False)
filename = Column(String, nullable=False)
path = Column(String, nullable=False)
size = Column(String, nullable=False)
expires = Column(String, nullable=False)
@auto_init()
def __init__(self, **_) -> None:
pass

View File

@@ -1,15 +1,17 @@
import uuid
import sqlalchemy as sa
import sqlalchemy.orm as orm
from sqlalchemy.orm.session import Session
from mealie.core.config import get_app_settings
from mealie.db.models.group.invite_tokens import GroupInviteToken
from mealie.db.models.server.task import ServerTaskModel
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import auto_init
from .._model_utils import GUID, auto_init
from ..group.invite_tokens import GroupInviteToken
from ..group.webhooks import GroupWebhooksModel
from ..recipe.category import Category, group2categories
from ..server.task import ServerTaskModel
from .cookbook import CookBook
from .mealplan import GroupMealPlan
from .preferences import GroupPreferencesModel
@@ -19,6 +21,7 @@ settings = get_app_settings()
class Group(SqlAlchemyBase, BaseMixins):
__tablename__ = "groups"
id = sa.Column(GUID, primary_key=True, default=uuid.uuid4)
name = sa.Column(sa.String, index=True, nullable=False, unique=True)
users = orm.relationship("User", back_populates="group")
categories = orm.relationship(Category, secondary=group2categories, single_parent=True, uselist=True)
@@ -48,11 +51,21 @@ class Group(SqlAlchemyBase, BaseMixins):
webhooks = orm.relationship(GroupWebhooksModel, **common_args)
cookbooks = orm.relationship(CookBook, **common_args)
server_tasks = orm.relationship(ServerTaskModel, **common_args)
data_exports = orm.relationship("GroupDataExportsModel", **common_args)
shopping_lists = orm.relationship("ShoppingList", **common_args)
group_reports = orm.relationship("ReportModel", **common_args)
class Config:
exclude = {"users", "webhooks", "shopping_lists", "cookbooks", "preferences", "invite_tokens", "mealplans"}
exclude = {
"users",
"webhooks",
"shopping_lists",
"cookbooks",
"preferences",
"invite_tokens",
"mealplans",
"data_exports",
}
@auto_init()
def __init__(self, **_) -> None:

View File

@@ -1,7 +1,7 @@
from sqlalchemy import Column, ForeignKey, Integer, String, orm
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import auto_init
from .._model_utils import auto_init, guid
class GroupInviteToken(SqlAlchemyBase, BaseMixins):
@@ -9,7 +9,7 @@ class GroupInviteToken(SqlAlchemyBase, BaseMixins):
token = Column(String, index=True, nullable=False, unique=True)
uses_left = Column(Integer, nullable=False, default=1)
group_id = Column(Integer, ForeignKey("groups.id"))
group_id = Column(guid.GUID, ForeignKey("groups.id"))
group = orm.relationship("Group", back_populates="invite_tokens")
@auto_init()

View File

@@ -2,7 +2,7 @@ from sqlalchemy import Column, Date, ForeignKey, String, orm
from sqlalchemy.sql.sqltypes import Integer
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import auto_init
from .._model_utils import GUID, auto_init
class GroupMealPlan(SqlAlchemyBase, BaseMixins):
@@ -13,7 +13,7 @@ class GroupMealPlan(SqlAlchemyBase, BaseMixins):
title = Column(String, index=True, nullable=False)
text = Column(String, nullable=False)
group_id = Column(Integer, ForeignKey("groups.id"), index=True)
group_id = Column(GUID, ForeignKey("groups.id"), index=True)
group = orm.relationship("Group", back_populates="mealplans")
recipe_id = Column(Integer, ForeignKey("recipes.id"))

View File

@@ -1,13 +1,15 @@
import sqlalchemy as sa
import sqlalchemy.orm as orm
from mealie.db.models._model_utils.guid import GUID
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import auto_init
class GroupPreferencesModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "group_preferences"
group_id = sa.Column(sa.Integer, sa.ForeignKey("groups.id"))
group_id = sa.Column(GUID, sa.ForeignKey("groups.id"))
group = orm.relationship("Group", back_populates="preferences")
private_group: bool = sa.Column(sa.Boolean, default=True)

View File

@@ -1,7 +1,7 @@
from datetime import datetime
from uuid import uuid4
from sqlalchemy import Column, ForeignKey, Integer, orm
from sqlalchemy import Column, ForeignKey, orm
from sqlalchemy.sql.sqltypes import Boolean, DateTime, String
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
@@ -12,14 +12,14 @@ from .._model_utils.guid import GUID
class ReportEntryModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "report_entries"
id = Column(GUID(), primary_key=True, default=uuid4)
id = Column(GUID, primary_key=True, default=uuid4)
success = Column(Boolean, default=False)
message = Column(String, nullable=True)
exception = Column(String, nullable=True)
timestamp = Column(DateTime, nullable=False, default=datetime.utcnow)
report_id = Column(GUID(), ForeignKey("group_reports.id"), nullable=False)
report_id = Column(GUID, ForeignKey("group_reports.id"), nullable=False)
report = orm.relationship("ReportModel", back_populates="entries")
@auto_init()
@@ -29,7 +29,7 @@ class ReportEntryModel(SqlAlchemyBase, BaseMixins):
class ReportModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "group_reports"
id = Column(GUID(), primary_key=True, default=uuid4)
id = Column(GUID, primary_key=True, default=uuid4)
name = Column(String, nullable=False)
status = Column(String, nullable=False)
@@ -39,7 +39,7 @@ class ReportModel(SqlAlchemyBase, BaseMixins):
entries = orm.relationship(ReportEntryModel, back_populates="report", cascade="all, delete-orphan")
# Relationships
group_id = Column(Integer, ForeignKey("groups.id"))
group_id = Column(GUID, ForeignKey("groups.id"))
group = orm.relationship("Group", back_populates="group_reports", single_parent=True)
class Config:

View File

@@ -4,6 +4,7 @@ from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
from sqlalchemy.ext.orderinglist import ordering_list
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils.guid import GUID
from .group import Group
@@ -29,7 +30,7 @@ class ShoppingList(SqlAlchemyBase, BaseMixins):
__tablename__ = "shopping_lists"
id = Column(Integer, primary_key=True)
group_id = Column(Integer, ForeignKey("groups.id"))
group_id = Column(GUID, ForeignKey("groups.id"))
group = orm.relationship("Group", back_populates="shopping_lists")
name = Column(String)

View File

@@ -1,8 +1,7 @@
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import auto_init
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import GUID, auto_init
class GroupWebhooksModel(SqlAlchemyBase, BaseMixins):
@@ -10,7 +9,7 @@ class GroupWebhooksModel(SqlAlchemyBase, BaseMixins):
id = Column(Integer, primary_key=True)
group = orm.relationship("Group", back_populates="webhooks", single_parent=True)
group_id = Column(Integer, ForeignKey("groups.id"), index=True)
group_id = Column(GUID, ForeignKey("groups.id"), index=True)
enabled = Column(Boolean, default=False)
name = Column(String)

View File

@@ -4,7 +4,9 @@ from slugify import slugify
from sqlalchemy.orm import validates
from mealie.core import root_logger
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils.guid import GUID
logger = root_logger.get_logger()
@@ -12,7 +14,7 @@ logger = root_logger.get_logger()
group2categories = sa.Table(
"group2categories",
SqlAlchemyBase.metadata,
sa.Column("group_id", sa.Integer, sa.ForeignKey("groups.id")),
sa.Column("group_id", GUID, sa.ForeignKey("groups.id")),
sa.Column("category_id", sa.Integer, sa.ForeignKey("categories.id")),
)

View File

@@ -9,7 +9,7 @@ from mealie.db.models._model_utils.guid import GUID
class RecipeComment(SqlAlchemyBase, BaseMixins):
__tablename__ = "recipe_comments"
id = Column(GUID(), primary_key=True, default=uuid4)
id = Column(GUID, primary_key=True, default=uuid4)
text = Column(String)
# Recipe Link

View File

@@ -49,7 +49,7 @@ class RecipeIngredient(SqlAlchemyBase, BaseMixins):
food = orm.relationship(IngredientFoodModel, uselist=False)
quantity = Column(Integer)
reference_id = Column(GUID()) # Reference Links
reference_id = Column(GUID) # Reference Links
# Extras

View File

@@ -10,7 +10,7 @@ from .._model_utils.guid import GUID
class RecipeIngredientRefLink(SqlAlchemyBase, BaseMixins):
__tablename__ = "recipe_ingredient_ref_link"
instruction_id = Column(Integer, ForeignKey("recipe_instructions.id"))
reference_id = Column(GUID())
reference_id = Column(GUID)
@auto_init()
def __init__(self, **_) -> None:
@@ -19,7 +19,7 @@ class RecipeIngredientRefLink(SqlAlchemyBase, BaseMixins):
class RecipeInstruction(SqlAlchemyBase):
__tablename__ = "recipe_instructions"
id = Column(GUID(), primary_key=True, default=uuid4)
id = Column(GUID, primary_key=True, default=uuid4)
parent_id = Column(Integer, ForeignKey("recipes.id"))
position = Column(Integer)
type = Column(String, default="")

View File

@@ -6,6 +6,8 @@ import sqlalchemy.orm as orm
from sqlalchemy.ext.orderinglist import ordering_list
from sqlalchemy.orm import validates
from mealie.db.models._model_utils.guid import GUID
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import auto_init
from ..users import users_to_favorites
@@ -43,13 +45,13 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
slug = sa.Column(sa.String, index=True)
# ID Relationships
group_id = sa.Column(sa.Integer, sa.ForeignKey("groups.id"))
group_id = sa.Column(GUID, sa.ForeignKey("groups.id"))
group = orm.relationship("Group", back_populates="recipes", foreign_keys=[group_id])
user_id = sa.Column(sa.Integer, sa.ForeignKey("users.id"))
user = orm.relationship("User", uselist=False, foreign_keys=[user_id])
meal_entries = orm.relationship("GroupMealPlan", back_populates="recipe")
meal_entries = orm.relationship("GroupMealPlan", back_populates="recipe", cascade="all, delete-orphan")
favorited_by = orm.relationship("User", secondary=users_to_favorites, back_populates="favorite_recipes")

View File

@@ -1,6 +1,7 @@
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, orm
from sqlalchemy import Column, DateTime, ForeignKey, String, orm
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
from mealie.db.models._model_utils.guid import GUID
from .._model_utils import auto_init
@@ -12,7 +13,7 @@ class ServerTaskModel(SqlAlchemyBase, BaseMixins):
status = Column(String, nullable=False)
log = Column(String, nullable=True)
group_id = Column(Integer, ForeignKey("groups.id"))
group_id = Column(GUID, ForeignKey("groups.id"))
group = orm.relationship("Group", back_populates="server_tasks")
@auto_init()

View File

@@ -1,14 +1,13 @@
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
from mealie.core.config import get_app_settings
from mealie.db.models._model_utils.guid import GUID
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import auto_init
from ..group import Group
from .user_to_favorite import users_to_favorites
settings = get_app_settings()
class LongLiveToken(SqlAlchemyBase, BaseMixins):
__tablename__ = "long_live_tokens"
@@ -32,7 +31,7 @@ class User(SqlAlchemyBase, BaseMixins):
admin = Column(Boolean, default=False)
advanced = Column(Boolean, default=False)
group_id = Column(Integer, ForeignKey("groups.id"))
group_id = Column(GUID, ForeignKey("groups.id"))
group = orm.relationship("Group", back_populates="users")
# Group Permissions
@@ -40,17 +39,15 @@ class User(SqlAlchemyBase, BaseMixins):
can_invite = Column(Boolean, default=False)
can_organize = Column(Boolean, default=False)
tokens: list[LongLiveToken] = orm.relationship(
LongLiveToken, back_populates="user", cascade="all, delete, delete-orphan", single_parent=True
)
sp_args = {
"back_populates": "user",
"cascade": "all, delete, delete-orphan",
"single_parent": True,
}
comments: list = orm.relationship(
"RecipeComment", back_populates="user", cascade="all, delete, delete-orphan", single_parent=True
)
password_reset_tokens = orm.relationship(
"PasswordResetModel", back_populates="user", cascade="all, delete, delete-orphan", single_parent=True
)
tokens = orm.relationship(LongLiveToken, **sp_args)
comments = orm.relationship("RecipeComment", **sp_args)
password_reset_tokens = orm.relationship("PasswordResetModel", **sp_args)
owned_recipes_id = Column(Integer, ForeignKey("recipes.id"))
owned_recipes = orm.relationship("RecipeModel", single_parent=True, foreign_keys=[owned_recipes_id])
@@ -65,11 +62,14 @@ class User(SqlAlchemyBase, BaseMixins):
"can_invite",
"can_organize",
"group",
"username",
}
@auto_init()
def __init__(self, session, full_name, password, group: str = settings.DEFAULT_GROUP, **kwargs) -> None:
def __init__(self, session, full_name, password, group: str = None, **kwargs) -> None:
if group is None:
settings = get_app_settings()
group = settings.DEFAULT_GROUP
self.group = Group.get_ref(session, group)
self.favorite_recipes = []

View File

@@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends
from sqlalchemy.orm.session import Session
from mealie.core.config import get_app_settings
from mealie.core.release_checker import get_latest_version
from mealie.core.settings.static import APP_VERSION
from mealie.db.database import get_database
from mealie.db.db_setup import generate_session
@@ -18,6 +19,7 @@ async def get_app_info():
return AdminAboutInfo(
production=settings.PRODUCTION,
version=APP_VERSION,
versionLatest=get_latest_version(),
demo_status=settings.IS_DEMO,
api_port=settings.API_PORT,
api_docs=settings.API_DOCS,
@@ -49,4 +51,5 @@ async def check_app_config():
email_ready=settings.SMTP_ENABLE,
ldap_ready=settings.LDAP_ENABLED,
base_url_set=url_set,
is_up_to_date=get_latest_version() == APP_VERSION,
)

View File

@@ -1,70 +0,0 @@
from fastapi import BackgroundTasks, Depends, HTTPException, status
from sqlalchemy.orm.session import Session
from mealie.core.dependencies import get_current_user
from mealie.db.database import get_database
from mealie.db.db_setup import generate_session
from mealie.routes.routers import AdminAPIRouter
from mealie.schema.user import GroupBase, GroupInDB, PrivateUser, UpdateGroup
from mealie.services.events import create_group_event
router = AdminAPIRouter(prefix="/groups")
@router.get("", response_model=list[GroupInDB])
async def get_all_groups(session: Session = Depends(generate_session)):
"""Returns a list of all groups in the database"""
db = get_database(session)
return db.groups.get_all()
@router.post("", status_code=status.HTTP_201_CREATED, response_model=GroupInDB)
async def create_group(
background_tasks: BackgroundTasks,
group_data: GroupBase,
session: Session = Depends(generate_session),
):
"""Creates a Group in the Database"""
db = get_database(session)
try:
new_group = db.groups.create(group_data.dict())
background_tasks.add_task(create_group_event, "Group Created", f"'{group_data.name}' created", session)
return new_group
except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST)
@router.put("/{id}")
async def update_group_data(id: int, group_data: UpdateGroup, session: Session = Depends(generate_session)):
"""Updates a User Group"""
db = get_database(session)
db.groups.update(id, group_data.dict())
@router.delete("/{id}")
async def delete_user_group(
background_tasks: BackgroundTasks,
id: int,
current_user: PrivateUser = Depends(get_current_user),
session: Session = Depends(generate_session),
):
"""Removes a user group from the database"""
db = get_database(session)
if id == 1:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="DEFAULT_GROUP")
group: GroupInDB = db.groups.get(id)
if not group:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="GROUP_NOT_FOUND")
if group.users != []:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="GROUP_WITH_USERS")
background_tasks.add_task(
create_group_event, "Group Deleted", f"'{group.name}' deleted by {current_user.full_name}", session
)
db.groups.delete(id)

View File

@@ -18,7 +18,7 @@ def log_wrapper(request: Request, e):
def register_debug_handler(app: FastAPI):
settings = get_app_settings()
if settings.PRODUCTION:
if settings.PRODUCTION and not settings.TESTING:
return
@app.exception_handler(RequestValidationError)

View File

@@ -13,3 +13,4 @@ router.include_router(recipe_crud_routes.user_router, prefix=prefix, tags=["Reci
router.include_router(image_and_assets.user_router, prefix=prefix, tags=["Recipe: Images and Assets"])
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"])

View File

@@ -1,7 +1,10 @@
from pathlib import Path
from fastapi import APIRouter, Depends
from fastapi.responses import FileResponse
from mealie.core.dependencies.dependencies import temporary_zip_path
from mealie.core.security import create_file_token
from mealie.schema.group.group_exports import GroupDataExport
from mealie.schema.recipe.recipe_bulk_actions import (
AssignCategories,
AssignTags,
@@ -38,7 +41,10 @@ def bulk_delete_recipes(
bulk_service.delete_recipes(delete_recipes.recipes)
@router.post("/export", response_class=FileResponse)
export_router = APIRouter(prefix="/bulk-actions")
@export_router.post("/export")
def bulk_export_recipes(
export_recipes: ExportRecipes,
temp_path=Depends(temporary_zip_path),
@@ -46,4 +52,26 @@ def bulk_export_recipes(
):
bulk_service.export_recipes(temp_path, export_recipes.recipes)
return FileResponse(temp_path, filename="recipes.zip")
# return FileResponse(temp_path, filename="recipes.zip")
@export_router.get("/export/download")
def get_exported_data_token(path: Path, _: RecipeBulkActions = Depends(RecipeBulkActions.private)):
# return FileResponse(temp_path, filename="recipes.zip")
"""Returns a token to download a file"""
return {"fileToken": create_file_token(path)}
@export_router.get("/export", response_model=list[GroupDataExport])
def get_exported_data(bulk_service: RecipeBulkActions = Depends(RecipeBulkActions.private)):
return bulk_service.get_exports()
# return FileResponse(temp_path, filename="recipes.zip")
@export_router.delete("/export/purge")
def purge_export_data(bulk_service: RecipeBulkActions = Depends(RecipeBulkActions.private)):
"""Remove all exports data, including items on disk without database entry"""
amountDelete = bulk_service.purge_exports()
return {"message": f"{amountDelete} exports deleted"}

View File

@@ -21,8 +21,13 @@ logger = get_logger()
@user_router.get("", response_model=list[RecipeSummary])
async def get_all(start=0, limit=None, service: RecipeService = Depends(RecipeService.private)):
json_compatible_item_data = jsonable_encoder(service.get_all(start, limit))
async def get_all(
start: int = 0,
limit: int = None,
load_foods: bool = False,
service: RecipeService = Depends(RecipeService.private),
):
json_compatible_item_data = jsonable_encoder(service.get_all(start, limit, load_foods))
return JSONResponse(content=json_compatible_item_data)

View File

@@ -18,6 +18,7 @@ class AppInfo(CamelModel):
class AdminAboutInfo(AppInfo):
versionLatest: str
api_port: int
api_docs: bool
db_type: str

View File

@@ -1,3 +1,5 @@
from uuid import UUID
from fastapi_camelcase import CamelModel
from pydantic import validator
from slugify import slugify
@@ -28,11 +30,11 @@ class UpdateCookBook(CreateCookBook):
class SaveCookBook(CreateCookBook):
group_id: int
group_id: UUID
class ReadCookBook(UpdateCookBook):
group_id: int
group_id: UUID
categories: list[CategoryBase] = []
class Config:
@@ -40,7 +42,7 @@ class ReadCookBook(UpdateCookBook):
class RecipeCookBook(ReadCookBook):
group_id: int
group_id: UUID
categories: list[RecipeCategoryResponse]
class Config:

View File

@@ -1,9 +1,10 @@
from fastapi_camelcase import CamelModel
from pydantic import UUID4
from .group_preferences import UpdateGroupPreferences
class GroupAdminUpdate(CamelModel):
id: int
id: UUID4
name: str
preferences: UpdateGroupPreferences

View File

@@ -0,0 +1,17 @@
from datetime import datetime
from fastapi_camelcase import CamelModel
from pydantic import UUID4
class GroupDataExport(CamelModel):
id: UUID4
group_id: UUID4
name: str
filename: str
path: str
size: str
expires: datetime
class Config:
orm_mode = True

View File

@@ -1,3 +1,5 @@
from uuid import UUID
from fastapi_camelcase import CamelModel
@@ -15,7 +17,7 @@ class UpdateGroupPreferences(CamelModel):
class CreateGroupPreferences(UpdateGroupPreferences):
group_id: int
group_id: UUID
class ReadGroupPreferences(CreateGroupPreferences):

View File

@@ -1,3 +1,5 @@
from uuid import UUID
from fastapi_camelcase import CamelModel
@@ -7,14 +9,14 @@ class CreateInviteToken(CamelModel):
class SaveInviteToken(CamelModel):
uses_left: int
group_id: int
group_id: UUID
token: str
class ReadInviteToken(CamelModel):
token: str
uses_left: int
group_id: int
group_id: UUID
class Config:
orm_mode = True

View File

@@ -1,3 +1,5 @@
from uuid import UUID
from fastapi_camelcase import CamelModel
@@ -9,7 +11,7 @@ class CreateWebhook(CamelModel):
class SaveWebhook(CreateWebhook):
group_id: int
group_id: UUID
class ReadWebhook(SaveWebhook):

View File

@@ -1,6 +1,7 @@
from datetime import date
from enum import Enum
from typing import Optional
from uuid import UUID
from fastapi_camelcase import CamelModel
from pydantic import validator
@@ -33,11 +34,11 @@ class CreatePlanEntry(CamelModel):
class UpdatePlanEntry(CreatePlanEntry):
id: int
group_id: int
group_id: UUID
class SavePlanEntry(CreatePlanEntry):
group_id: int
group_id: UUID
class Config:
orm_mode = True

View File

@@ -1,6 +1,7 @@
import datetime
from pathlib import Path
from typing import Any, Optional
from uuid import UUID, uuid4
from fastapi_camelcase import CamelModel
from pydantic import BaseModel, Field, validator
@@ -59,7 +60,7 @@ class RecipeSummary(CamelModel):
id: Optional[int]
user_id: int = 0
group_id: int = 0
group_id: UUID = Field(default_factory=uuid4)
name: Optional[str]
slug: str = ""
@@ -74,6 +75,7 @@ class RecipeSummary(CamelModel):
description: Optional[str] = ""
recipe_category: Optional[list[RecipeTag]] = []
tags: Optional[list[RecipeTag]] = []
tools: list[RecipeTool] = []
rating: Optional[int]
org_url: Optional[str] = Field(None, alias="orgURL")
@@ -86,23 +88,28 @@ class RecipeSummary(CamelModel):
orm_mode = True
@validator("tags", always=True, pre=True)
def validate_tags(cats: list[Any], values):
def validate_tags(cats: list[Any]):
if isinstance(cats, list) and cats and isinstance(cats[0], str):
return [RecipeTag(name=c, slug=slugify(c)) for c in cats]
return cats
@validator("recipe_category", always=True, pre=True)
def validate_categories(cats: list[Any], values):
def validate_categories(cats: list[Any]):
if isinstance(cats, list) and cats and isinstance(cats[0], str):
return [RecipeCategory(name=c, slug=slugify(c)) for c in cats]
return cats
@validator("group_id", always=True, pre=True)
def validate_group_id(group_id: list[Any]):
if isinstance(group_id, int):
return uuid4()
return group_id
class Recipe(RecipeSummary):
recipe_ingredient: Optional[list[RecipeIngredient]] = []
recipe_instructions: Optional[list[RecipeStep]] = []
nutrition: Optional[Nutrition]
tools: list[RecipeTool] = []
# Mealie Specific
settings: Optional[RecipeSettings] = RecipeSettings()

View File

@@ -37,7 +37,7 @@ class ReportEntryOut(ReportEntryCreate):
class ReportCreate(CamelModel):
timestamp: datetime.datetime = Field(default_factory=datetime.datetime.utcnow)
category: ReportCategory
group_id: int
group_id: UUID4
name: str
status: ReportSummaryStatus = ReportSummaryStatus.in_progress

View File

@@ -1,5 +1,6 @@
import datetime
import enum
from uuid import UUID
from fastapi_camelcase import CamelModel
from pydantic import Field
@@ -18,7 +19,7 @@ class ServerTaskStatus(str, enum.Enum):
class ServerTaskCreate(CamelModel):
group_id: int
group_id: UUID
name: ServerTaskNames = ServerTaskNames.default
created_at: datetime.datetime = Field(default_factory=datetime.datetime.now)
status: ServerTaskStatus = ServerTaskStatus.running

View File

@@ -1,11 +1,14 @@
from datetime import datetime
from pathlib import Path
from typing import Any, Optional
from uuid import UUID
from fastapi_camelcase import CamelModel
from pydantic import UUID4
from pydantic.types import constr
from pydantic.utils import GetterDict
from mealie.core.config import get_app_settings
from mealie.core.config import get_app_dirs, get_app_settings
from mealie.db.models.users import User
from mealie.schema.group.group_preferences import ReadGroupPreferences
from mealie.schema.recipe import RecipeSummary
@@ -87,7 +90,7 @@ class UserIn(UserBase):
class UserOut(UserBase):
id: int
group: str
group_id: int
group_id: UUID4
tokens: Optional[list[LongLiveTokenOut]]
favorite_recipes: Optional[list[str]] = []
@@ -119,14 +122,14 @@ class UserFavorites(UserBase):
class PrivateUser(UserOut):
password: str
group_id: int
group_id: UUID4
class Config:
orm_mode = True
class UpdateGroup(GroupBase):
id: int
id: UUID4
name: str
categories: Optional[list[CategoryBase]] = []
@@ -141,6 +144,26 @@ class GroupInDB(UpdateGroup):
class Config:
orm_mode = True
@staticmethod
def get_directory(id: UUID4) -> Path:
dir = get_app_dirs().GROUPS_DIR / str(id)
dir.mkdir(parents=True, exist_ok=True)
return dir
@staticmethod
def get_export_directory(id: UUID) -> Path:
dir = GroupInDB.get_directory(id) / "export"
dir.mkdir(parents=True, exist_ok=True)
return dir
@property
def directory(self) -> Path:
return GroupInDB.get_directory(self.id)
@property
def exports(self) -> Path:
return GroupInDB.get_export_directory(self.id)
class LongLiveTokenInDB(CreateToken):
id: int

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
from functools import cached_property
from fastapi import HTTPException, status
from pydantic import UUID4
from mealie.schema.group.group import GroupAdminUpdate
from mealie.schema.mapper import mapper
@@ -16,7 +17,7 @@ from mealie.services.group_services.group_utils import create_new_group
class AdminGroupService(
CrudHttpMixins[GroupBase, GroupInDB, GroupAdminUpdate],
AdminHttpService[int, GroupInDB],
AdminHttpService[UUID4, GroupInDB],
):
event_func = create_group_event
_schema = GroupInDB
@@ -25,7 +26,7 @@ class AdminGroupService(
def dal(self):
return self.db.groups
def populate_item(self, id: int) -> GroupInDB:
def populate_item(self, id: UUID4) -> GroupInDB:
self.item = self.dal.get_one(id)
return self.item
@@ -35,13 +36,13 @@ class AdminGroupService(
def create_one(self, data: GroupBase) -> GroupInDB:
return create_new_group(self.db, data)
def update_one(self, data: GroupAdminUpdate, item_id: int = None) -> GroupInDB:
def update_one(self, data: GroupAdminUpdate, item_id: UUID4 = None) -> GroupInDB:
target_id = item_id or data.id
if data.preferences:
preferences = self.db.group_preferences.get_one(value=target_id, key="group_id")
preferences = mapper(data.preferences, preferences)
self.item.preferences = self.db.group_preferences.update(preferences.id, preferences)
self.item.preferences = self.db.group_preferences.update(target_id, preferences)
if data.name not in ["", self.item.name]:
self.item.name = data.name
@@ -49,11 +50,13 @@ class AdminGroupService(
return self.item
def delete_one(self, id: int = None) -> GroupInDB:
def delete_one(self, id: UUID4 = None) -> GroupInDB:
target_id = id or self.item.id
if len(self.item.users) > 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ErrorResponse(message="Cannot delete group with users").dict(),
)
return self._delete_one(id)
return self._delete_one(target_id)

View File

@@ -0,0 +1,2 @@
from .exporter import *
from .recipe_exporter import *

View File

@@ -0,0 +1,91 @@
import zipfile
from abc import abstractmethod, abstractproperty
from dataclasses import dataclass
from pathlib import Path
from typing import Callable, Iterator, Optional
from uuid import UUID
from pydantic import BaseModel
from mealie.core.root_logger import get_logger
from mealie.db.database import Database
from mealie.schema.reports.reports import ReportEntryCreate
from .._base_service import BaseService
@dataclass
class ExportedItem:
"""
Exported items are the items provided by items() call in an concrete exporter class
Where the items are used to write data to the zip file. Models should derive from the
BaseModel class OR provide a .json method that returns a json string.
"""
model: BaseModel
name: str
class ABCExporter(BaseService):
write_dir_to_zip: Callable[[Path, str, Optional[list[str]]], None]
def __init__(self, db: Database, group_id: UUID) -> None:
self.logger = get_logger()
self.db = db
self.group_id = group_id
super().__init__()
@abstractproperty
def destination_dir(self) -> str:
...
@abstractmethod
def items(self) -> Iterator[ExportedItem]:
...
def _post_export_hook(self, _: BaseModel) -> None:
pass
@abstractmethod
def export(self, zip: zipfile.ZipFile) -> list[ReportEntryCreate]:
"""
Export takes in a zip file and exports the recipes to it. Note that the zip
file open/close is NOT handled by this method. You must handle it yourself.
Args:
zip (zipfile.ZipFile): Zip file destination
Returns:
list[ReportEntryCreate]: [description] ???!?!
"""
self.write_dir_to_zip = self.write_dir_to_zip_func(zip)
for item in self.items():
if item is None:
self.logger.error("Failed to export item. no item found")
continue
zip.writestr(f"{self.destination_dir}/{item.name}/{item.name}.json", item.model.json())
self._post_export_hook(item.model)
self.write_dir_to_zip = None
def write_dir_to_zip_func(self, zip: zipfile.ZipFile):
"""Returns a recursive function that writes a directory to a zip file.
Args:
zip (zipfile.ZipFile):
"""
def func(source_dir: Path, dest_dir: str, ignore_ext: set[str] = None) -> None:
ignore_ext = ignore_ext or set()
for source_file in source_dir.iterdir():
if source_file.is_dir():
func(source_file, f"{dest_dir}/{source_file.name}")
elif source_file.suffix not in ignore_ext:
zip.write(source_file, f"{dest_dir}/{source_file.name}")
return func

View File

@@ -0,0 +1,51 @@
import datetime
import shutil
import zipfile
from pathlib import Path
from uuid import UUID, uuid4
from mealie.db.database import Database
from mealie.schema.group.group_exports import GroupDataExport
from mealie.schema.user import GroupInDB
from mealie.utils.fs_stats import pretty_size
from .._base_service import BaseService
from ._abc_exporter import ABCExporter
class Exporter(BaseService):
def __init__(self, group_id: UUID, temp_zip: Path, exporters: list[ABCExporter]) -> None:
super().__init__()
self.group_id = group_id
self.temp_path = temp_zip
self.exporters = exporters
def run(self, db: Database) -> GroupDataExport:
# Create Zip File
self.temp_path.touch()
# Open Zip File
with zipfile.ZipFile(self.temp_path, "w") as zip:
for exporter in self.exporters:
exporter.export(zip)
export_id = uuid4()
export_path = GroupInDB.get_export_directory(self.group_id) / f"{export_id}.zip"
shutil.copy(self.temp_path, export_path)
group_data_export = GroupDataExport(
id=export_id,
group_id=self.group_id,
path=str(export_path),
name="Data Export",
size=pretty_size(export_path.stat().st_size),
filename=export_path.name,
expires=datetime.datetime.now() + datetime.timedelta(days=1),
)
db.group_exports.create(group_data_export)
return group_data_export

View File

@@ -0,0 +1,41 @@
from typing import Iterator
from uuid import UUID
from mealie.db.database import Database
from mealie.schema.recipe import Recipe
from ._abc_exporter import ABCExporter, ExportedItem
class RecipeExporter(ABCExporter):
def __init__(self, db: Database, group_id: UUID, recipes: list[str]) -> None:
"""
RecipeExporter is used to export a list of recipes to a zip file. The zip
file is then saved to a temporary directory and then available for a one-time
download.
Args:
db (Database):
group_id (int):
recipes (list[str]): Recipe Slugs
"""
super().__init__(db, group_id)
self.recipes = recipes
@property
def destination_dir(self) -> str:
return "recipes"
def items(self) -> Iterator[ExportedItem]:
for slug in self.recipes:
yield ExportedItem(
name=slug,
model=self.db.recipes.multi_query({"slug": slug, "group_id": self.group_id}, limit=1)[0],
)
def _post_export_hook(self, item: Recipe) -> None:
"""Copy recipe directory contents into the zip folder"""
recipe_dir = item.directory
if recipe_dir.exists():
self.write_dir_to_zip(recipe_dir, f"{self.destination_dir}/{item.slug}", {".json"})

View File

@@ -1,3 +1,5 @@
from uuid import uuid4
from mealie.db.data_access_layer.access_model_factory import Database
from mealie.schema.group.group_preferences import CreateGroupPreferences
from mealie.schema.user.user import GroupBase, GroupInDB
@@ -6,7 +8,8 @@ from mealie.schema.user.user import GroupBase, GroupInDB
def create_new_group(db: Database, g_base: GroupBase, g_preferences: CreateGroupPreferences = None) -> GroupInDB:
created_group = db.groups.create(g_base)
g_preferences = g_preferences or CreateGroupPreferences(group_id=0) # Assign Temporary ID before group is created
# Assign Temporary ID before group is created
g_preferences = g_preferences or CreateGroupPreferences(group_id=uuid4())
g_preferences.group_id = created_group.id

View File

@@ -99,6 +99,9 @@ def sizeof_fmt(file_path: Path, decimal_places=2):
def move_all_images():
if not app_dirs.IMG_DIR.exists():
return
for image_file in app_dirs.IMG_DIR.iterdir():
if image_file.is_file():
if image_file.name == ".DS_Store":

View File

@@ -1,5 +1,6 @@
from pathlib import Path
from typing import Tuple
from uuid import UUID
from mealie.core import root_logger
from mealie.db.database import Database
@@ -25,7 +26,7 @@ class BaseMigrator(BaseService):
report_id: int
report: ReportOut
def __init__(self, archive: Path, db: Database, session, user_id: int, group_id: int):
def __init__(self, archive: Path, db: Database, session, user_id: int, group_id: UUID):
self.archive = archive
self.db = db
self.session = session

View File

@@ -1,6 +1,7 @@
import tempfile
import zipfile
from pathlib import Path
from uuid import UUID
from mealie.db.database import Database
@@ -10,7 +11,7 @@ from .utils.migration_helpers import MigrationReaders, import_image, split_by_co
class ChowdownMigrator(BaseMigrator):
def __init__(self, archive: Path, db: Database, session, user_id: int, group_id: int):
def __init__(self, archive: Path, db: Database, session, user_id: int, group_id: UUID):
super().__init__(archive, db, session, user_id, group_id)
self.key_aliases = [

View File

@@ -3,6 +3,7 @@ import zipfile
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
from uuid import UUID
from slugify import slugify
@@ -39,7 +40,7 @@ class NextcloudDir:
class NextcloudMigrator(BaseMigrator):
def __init__(self, archive: Path, db: Database, session, user_id: int, group_id: int):
def __init__(self, archive: Path, db: Database, session, user_id: int, group_id: UUID):
super().__init__(archive, db, session, user_id, group_id)
self.key_aliases = [

View File

@@ -3,10 +3,12 @@ from __future__ import annotations
from pathlib import Path
from mealie.core.root_logger import get_logger
from mealie.schema.group.group_exports import GroupDataExport
from mealie.schema.recipe import CategoryBase, Recipe
from mealie.schema.recipe.recipe_category import TagBase
from mealie.services._base_http_service.http_services import UserHttpService
from mealie.services.events import create_recipe_event
from mealie.services.exporter import Exporter, RecipeExporter
logger = get_logger(__name__)
@@ -18,8 +20,36 @@ class RecipeBulkActions(UserHttpService[int, Recipe]):
def populate_item(self, _: int) -> Recipe:
return
def export_recipes(self, temp_path: Path, recipes: list[str]) -> None:
return
def export_recipes(self, temp_path: Path, slugs: list[str]) -> None:
recipe_exporter = RecipeExporter(self.db, self.group_id, slugs)
exporter = Exporter(self.group_id, temp_path, [recipe_exporter])
exporter.run(self.db)
def get_exports(self) -> list[GroupDataExport]:
return self.db.group_exports.multi_query({"group_id": self.group_id})
def purge_exports(self) -> int:
all_exports = self.get_exports()
exports_deleted = 0
for export in all_exports:
try:
Path(export.path).unlink(missing_ok=True)
self.db.group_exports.delete(export.id)
exports_deleted += 1
except Exception as e:
logger.error(f"Failed to delete export {export.id}")
logger.error(e)
group = self.db.groups.get_one(self.group_id)
for match in group.directory.glob("**/export/*zip"):
if match.is_file():
match.unlink()
exports_deleted += 1
return exports_deleted
def assign_tags(self, recipes: list[str], tags: list[TagBase]) -> None:
for slug in recipes:

View File

@@ -59,14 +59,18 @@ class RecipeService(CrudHttpMixins[CreateRecipe, Recipe, Recipe], UserHttpServic
if not self.item.settings.public and not self.user:
raise HTTPException(status.HTTP_403_FORBIDDEN)
def get_all(self, start=0, limit=None):
items = self.db.recipes.summary(self.user.group_id, start=start, limit=limit)
def get_all(self, start=0, limit=None, load_foods=False) -> list[RecipeSummary]:
items = self.db.recipes.summary(self.user.group_id, start=start, limit=limit, load_foods=load_foods)
new_items = []
for item in items:
# Pydantic/FastAPI can't seem to serialize the ingredient field on thier own.
new_item = item.__dict__
new_item["recipe_ingredient"] = [x.__dict__ for x in item.recipe_ingredient]
if load_foods:
new_item["recipe_ingredient"] = [x.__dict__ for x in item.recipe_ingredient]
new_items.append(new_item)
return [RecipeSummary.construct(**x) for x in new_items]

View File

@@ -15,7 +15,7 @@ CWD = Path(__file__).parent
app_dirs = get_app_dirs()
TEMP_DATA = app_dirs.DATA_DIR / ".temp"
SCHEDULER_DB = TEMP_DATA / "scheduler.db"
SCHEDULER_DB = CWD / ".scheduler.db"
SCHEDULER_DATABASE = f"sqlite:///{SCHEDULER_DB}"
MINUTES_DAY = 1440

View File

@@ -1,5 +1,6 @@
from .auto_backup import *
from .purge_events import *
from .purge_group_exports import *
from .purge_password_reset import *
from .purge_registration import *
from .webhooks import *

View File

@@ -0,0 +1,46 @@
import datetime
from pathlib import Path
from mealie.core import root_logger
from mealie.core.config import get_app_dirs
from mealie.db.db_setup import create_session
from mealie.db.models.group.exports import GroupDataExportsModel
ONE_DAY_AS_MINUTES = 1440
def purge_group_data_exports(max_minutes_old=ONE_DAY_AS_MINUTES):
"""Purges all group exports after x days"""
logger = root_logger.get_logger()
logger.info("purging group data exports")
limit = datetime.datetime.now() - datetime.timedelta(minutes=max_minutes_old)
session = create_session()
results = session.query(GroupDataExportsModel).filter(GroupDataExportsModel.expires <= limit)
total_removed = 0
for result in results:
session.delete(result)
Path(result.path).unlink(missing_ok=True)
total_removed += 1
session.commit()
session.close()
logger.info(f"finished purging group data exports. {total_removed} exports removed from group data")
def purge_excess_files() -> None:
"""Purges all files in the uploads directory that are older than 2 days"""
directories = get_app_dirs()
logger = root_logger.get_logger()
limit = datetime.datetime.now() - datetime.timedelta(minutes=ONE_DAY_AS_MINUTES * 2)
for file in directories.GROUPS_DIR.glob("**/export/*.zip"):
if file.stat().st_mtime < limit:
file.unlink()
logger.info(f"excess group file removed '{file}'")
logger.info("finished purging excess files")

View File

@@ -1,3 +1,5 @@
from uuid import uuid4
from fastapi import HTTPException, status
from mealie.core.root_logger import get_logger
@@ -69,7 +71,7 @@ class RegistrationService(PublicHttpService[int, str]):
group_data = GroupBase(name=self.registration.group)
group_preferences = CreateGroupPreferences(
group_id=0,
group_id=uuid4(),
private_group=self.registration.private,
first_day_of_week=0,
recipe_public=not self.registration.private,