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

@@ -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 = []