mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-05-24 01:12:54 -04:00
fix: Make Mealie Timezone-Aware (#3847)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
This commit is contained in:
parent
17f9eef551
commit
d5f7a883df
@ -27,6 +27,7 @@
|
||||
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
|
||||
},
|
||||
"extensions": [
|
||||
"charliermarsh.ruff",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"matangover.mypy",
|
||||
"ms-python.black-formatter",
|
||||
|
@ -13,7 +13,7 @@ from text_unidecode import unidecode
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
from mealie.db.models._model_utils import GUID
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "5ab195a474eb"
|
||||
|
@ -6,7 +6,7 @@ Create Date: 2024-03-18 02:28:15.896959
|
||||
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from textwrap import dedent
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
@ -34,7 +34,7 @@ def new_user_rating(user_id: Any, recipe_id: Any, rating: float | None = None, i
|
||||
else:
|
||||
id = "%.32x" % uuid4().int
|
||||
|
||||
now = datetime.now().isoformat()
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
return {
|
||||
"id": id,
|
||||
"user_id": user_id,
|
||||
|
@ -102,7 +102,7 @@
|
||||
<v-icon left>
|
||||
{{ $globals.icons.calendar }}
|
||||
</v-icon>
|
||||
{{ $t('recipe.last-made-date', { date: value ? new Date(value+"Z").toLocaleDateString($i18n.locale) : $t("general.never") } ) }}
|
||||
{{ $t('recipe.last-made-date', { date: value ? new Date(value).toLocaleDateString($i18n.locale) : $t("general.never") } ) }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
@ -199,11 +199,7 @@ export default defineComponent({
|
||||
await userApi.recipes.updateLastMade(props.recipe.slug, newTimelineEvent.value.timestamp);
|
||||
|
||||
// update recipe in parent so the user can see it
|
||||
// we remove the trailing "Z" since this is how the API returns it
|
||||
context.emit(
|
||||
"input", newTimelineEvent.value.timestamp
|
||||
.substring(0, newTimelineEvent.value.timestamp.length - 1)
|
||||
);
|
||||
context.emit("input", newTimelineEvent.value.timestamp);
|
||||
}
|
||||
|
||||
// update the image, if provided
|
||||
|
@ -8,7 +8,7 @@
|
||||
<template v-if="!useMobileFormat" #opposite>
|
||||
<v-chip v-if="event.timestamp" label large>
|
||||
<v-icon class="mr-1"> {{ $globals.icons.calendar }} </v-icon>
|
||||
{{ new Date(event.timestamp+"Z").toLocaleDateString($i18n.locale) }}
|
||||
{{ new Date(event.timestamp).toLocaleDateString($i18n.locale) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<v-card
|
||||
@ -25,7 +25,7 @@
|
||||
<v-col v-if="useMobileFormat" align-self="center" class="pr-0">
|
||||
<v-chip label>
|
||||
<v-icon> {{ $globals.icons.calendar }} </v-icon>
|
||||
{{ new Date(event.timestamp+"Z").toLocaleDateString($i18n.locale) }}
|
||||
{{ new Date(event.timestamp || "").toLocaleDateString($i18n.locale) }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col v-else cols="9" style="margin: auto; text-align: center;">
|
||||
|
@ -75,7 +75,7 @@
|
||||
<v-row v-if="listItem.checked" no-gutters class="mb-2">
|
||||
<v-col cols="auto">
|
||||
<div class="text-caption font-weight-light font-italic">
|
||||
{{ $t("shopping-list.completed-on", {date: new Date(listItem.updateAt+"Z").toLocaleDateString($i18n.locale)}) }}
|
||||
{{ $t("shopping-list.completed-on", {date: new Date(listItem.updateAt || "").toLocaleDateString($i18n.locale)}) }}
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
@ -4,7 +4,7 @@ import { useUserApi } from "~/composables/api";
|
||||
import { ShoppingListItemOut } from "~/lib/api/types/group";
|
||||
|
||||
const localStorageKey = "shopping-list-queue";
|
||||
const queueTimeout = 48 * 60 * 60 * 1000; // 48 hours
|
||||
const queueTimeout = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
type ItemQueueType = "create" | "update" | "delete";
|
||||
|
||||
|
@ -868,7 +868,6 @@ export default defineComponent({
|
||||
|
||||
// set a temporary updatedAt timestamp prior to refresh so it appears at the top of the checked items
|
||||
item.updateAt = new Date().toISOString();
|
||||
item.updateAt = item.updateAt.substring(0, item.updateAt.length-1);
|
||||
}
|
||||
|
||||
// make updates reflect immediately
|
||||
|
@ -51,7 +51,7 @@
|
||||
<v-list-item-title>
|
||||
{{ token.name }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle> {{ $t('general.created-on-date', [$d(new Date(token.createdAt+"Z"))]) }} </v-list-item-subtitle>
|
||||
<v-list-item-subtitle> {{ $t('general.created-on-date', [$d(new Date(token.createdAt))]) }} </v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<BaseButton delete small @click="deleteToken(token.id)"></BaseButton>
|
||||
|
@ -32,7 +32,7 @@ def get_latest_version() -> str:
|
||||
|
||||
global _LAST_RESET
|
||||
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
if not _LAST_RESET or now - _LAST_RESET > datetime.timedelta(days=MAX_DAYS_OLD):
|
||||
_LAST_RESET = now
|
||||
|
@ -4,11 +4,13 @@ from sqlalchemy import DateTime, Integer
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
from text_unidecode import unidecode
|
||||
|
||||
from ._model_utils.datetime import get_utc_now
|
||||
|
||||
|
||||
class SqlAlchemyBase(DeclarativeBase):
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
created_at: Mapped[datetime | None] = mapped_column(DateTime, default=datetime.now, index=True)
|
||||
update_at: Mapped[datetime | None] = mapped_column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
created_at: Mapped[datetime | None] = mapped_column(DateTime, default=get_utc_now, index=True)
|
||||
update_at: Mapped[datetime | None] = mapped_column(DateTime, default=get_utc_now, onupdate=get_utc_now)
|
||||
|
||||
@classmethod
|
||||
def normalize(cls, val: str) -> str:
|
||||
|
@ -1,7 +0,0 @@
|
||||
from .auto_init import auto_init
|
||||
from .guid import GUID
|
||||
|
||||
__all__ = [
|
||||
"auto_init",
|
||||
"GUID",
|
||||
]
|
15
mealie/db/models/_model_utils/datetime.py
Normal file
15
mealie/db/models/_model_utils/datetime.py
Normal file
@ -0,0 +1,15 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
def get_utc_now():
|
||||
"""
|
||||
Returns the current time in UTC.
|
||||
"""
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
def get_utc_today():
|
||||
"""
|
||||
Returns the current date in UTC.
|
||||
"""
|
||||
return datetime.now(timezone.utc).date()
|
@ -4,13 +4,14 @@ from sqlalchemy import Boolean, ForeignKey, Integer, String, orm
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import auto_init, guid
|
||||
from .._model_utils import guid
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from ..recipe.category import Category, cookbooks_to_categories
|
||||
from ..recipe.tag import Tag, cookbooks_to_tags
|
||||
from ..recipe.tool import Tool, cookbooks_to_tools
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from group import Group
|
||||
from .group import Group
|
||||
|
||||
|
||||
class CookBook(SqlAlchemyBase, BaseMixins):
|
||||
|
@ -4,10 +4,11 @@ from sqlalchemy import Boolean, ForeignKey, String, orm
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import GUID, auto_init
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from group import Group
|
||||
from .group import Group
|
||||
|
||||
|
||||
class GroupEventNotifierOptionsModel(SqlAlchemyBase, BaseMixins):
|
||||
|
@ -4,10 +4,11 @@ from sqlalchemy import ForeignKey, String, orm
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import GUID, auto_init
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from group import Group
|
||||
from .group import Group
|
||||
|
||||
|
||||
class GroupDataExportsModel(SqlAlchemyBase, BaseMixins):
|
||||
|
@ -11,7 +11,8 @@ from mealie.core.config import get_app_settings
|
||||
from mealie.db.models.labels import MultiPurposeLabel
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import GUID, auto_init
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
from ..group.invite_tokens import GroupInviteToken
|
||||
from ..group.webhooks import GroupWebhooksModel
|
||||
from ..recipe.category import Category, group_to_categories
|
||||
|
@ -4,10 +4,11 @@ from sqlalchemy import ForeignKey, Integer, String, orm
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import auto_init, guid
|
||||
from .._model_utils import guid
|
||||
from .._model_utils.auto_init import auto_init
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from group import Group
|
||||
from .group import Group
|
||||
|
||||
|
||||
class GroupInviteToken(SqlAlchemyBase, BaseMixins):
|
||||
|
@ -7,14 +7,14 @@ from sqlalchemy.orm import Mapped, mapped_column
|
||||
from mealie.db.models.recipe.tag import Tag, plan_rules_to_tags
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import GUID, auto_init
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
from ..recipe.category import Category, plan_rules_to_categories
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from group import Group
|
||||
|
||||
from ..recipe import RecipeModel
|
||||
from ..users import User
|
||||
from .group import Group
|
||||
|
||||
|
||||
class GroupMealPlanRules(BaseMixins, SqlAlchemyBase):
|
||||
|
@ -5,11 +5,11 @@ import sqlalchemy.orm as orm
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import auto_init
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from group import Group
|
||||
from .group import Group
|
||||
|
||||
|
||||
class GroupPreferencesModel(SqlAlchemyBase, BaseMixins):
|
||||
|
@ -4,10 +4,11 @@ from sqlalchemy import ForeignKey, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import GUID, auto_init
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from group import Group
|
||||
from .group import Group
|
||||
|
||||
|
||||
class GroupRecipeAction(SqlAlchemyBase, BaseMixins):
|
||||
|
@ -8,11 +8,12 @@ from sqlalchemy.sql.sqltypes import Boolean, DateTime, String
|
||||
|
||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||
|
||||
from .._model_utils import auto_init
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.datetime import get_utc_now
|
||||
from .._model_utils.guid import GUID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from group import Group
|
||||
from .group import Group
|
||||
|
||||
|
||||
class ReportEntryModel(SqlAlchemyBase, BaseMixins):
|
||||
@ -22,7 +23,7 @@ class ReportEntryModel(SqlAlchemyBase, BaseMixins):
|
||||
success: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||
message: Mapped[str] = mapped_column(String, nullable=True)
|
||||
exception: Mapped[str] = mapped_column(String, nullable=True)
|
||||
timestamp: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
timestamp: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=get_utc_now)
|
||||
|
||||
report_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("group_reports.id"), nullable=False, index=True)
|
||||
report: Mapped["ReportModel"] = orm.relationship("ReportModel", back_populates="entries")
|
||||
@ -39,7 +40,7 @@ class ReportModel(SqlAlchemyBase, BaseMixins):
|
||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
status: Mapped[str] = mapped_column(String, nullable=False)
|
||||
category: Mapped[str] = mapped_column(String, index=True, nullable=False)
|
||||
timestamp: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
timestamp: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=get_utc_now)
|
||||
|
||||
entries: Mapped[list[ReportEntryModel]] = orm.relationship(
|
||||
ReportEntryModel, back_populates="report", cascade="all, delete-orphan"
|
||||
|
@ -1,5 +1,5 @@
|
||||
from contextvars import ContextVar
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from pydantic import ConfigDict
|
||||
@ -11,7 +11,8 @@ from mealie.db.models.labels import MultiPurposeLabel
|
||||
from mealie.db.models.recipe.api_extras import ShoppingListExtras, ShoppingListItemExtras, api_extras
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import GUID, auto_init
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
from ..recipe.ingredient import IngredientFoodModel, IngredientUnitModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -203,7 +204,7 @@ def update_shopping_lists(session: orm.Session, _):
|
||||
if not shopping_list:
|
||||
continue
|
||||
|
||||
shopping_list.update_at = datetime.now()
|
||||
shopping_list.update_at = datetime.now(timezone.utc)
|
||||
local_session.commit()
|
||||
except Exception:
|
||||
local_session.rollback()
|
||||
|
@ -1,14 +1,15 @@
|
||||
from datetime import datetime, time
|
||||
from datetime import datetime, time, timezone
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from sqlalchemy import Boolean, ForeignKey, String, Time, orm
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import GUID, auto_init
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from group import Group
|
||||
from .group import Group
|
||||
|
||||
|
||||
class GroupWebhooksModel(SqlAlchemyBase, BaseMixins):
|
||||
@ -24,7 +25,7 @@ class GroupWebhooksModel(SqlAlchemyBase, BaseMixins):
|
||||
|
||||
# New Fields
|
||||
webhook_type: Mapped[str | None] = mapped_column(String, default="") # Future use for different types of webhooks
|
||||
scheduled_time: Mapped[time | None] = mapped_column(Time, default=lambda: datetime.now().time())
|
||||
scheduled_time: Mapped[time | None] = mapped_column(Time, default=lambda: datetime.now(timezone.utc).time())
|
||||
|
||||
# Columne is no longer used but is kept for since it's super annoying to
|
||||
# delete a column in SQLite and it's not a big deal to keep it around
|
||||
|
@ -5,13 +5,13 @@ from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||
|
||||
from ._model_utils import auto_init
|
||||
from ._model_utils.auto_init import auto_init
|
||||
from ._model_utils.guid import GUID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from group import Group
|
||||
from group.shopping_list import ShoppingListItem, ShoppingListMultiPurposeLabel
|
||||
from recipe import IngredientFoodModel
|
||||
from .group.group import Group
|
||||
from .group.shopping_list import ShoppingListItem, ShoppingListMultiPurposeLabel
|
||||
from .recipe import IngredientFoodModel
|
||||
|
||||
|
||||
class MultiPurposeLabel(SqlAlchemyBase, BaseMixins):
|
||||
|
@ -4,7 +4,7 @@ from sqlalchemy import ForeignKey, String, orm
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||
from mealie.db.models._model_utils import auto_init
|
||||
from mealie.db.models._model_utils.auto_init import auto_init
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -9,7 +9,7 @@ from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||
from mealie.db.models.labels import MultiPurposeLabel
|
||||
from mealie.db.models.recipe.api_extras import IngredientFoodExtras, api_extras
|
||||
|
||||
from .._model_utils import auto_init
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -3,7 +3,7 @@ from sqlalchemy import ForeignKey, Integer, String, orm
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import auto_init
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
from datetime import date, datetime
|
||||
from datetime import date, datetime, timezone
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import sqlalchemy as sa
|
||||
@ -10,10 +10,11 @@ from sqlalchemy.orm import Mapped, mapped_column, validates
|
||||
from sqlalchemy.orm.attributes import get_history
|
||||
from sqlalchemy.orm.session import object_session
|
||||
|
||||
from mealie.db.models._model_utils.auto_init import auto_init
|
||||
from mealie.db.models._model_utils.datetime import get_utc_today
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import auto_init
|
||||
from ..users.user_to_recipe import UserToRecipe
|
||||
from .api_extras import ApiExtras, api_extras
|
||||
from .assets import RecipeAsset
|
||||
@ -125,7 +126,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
is_ocr_recipe: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||
|
||||
# Time Stamp Properties
|
||||
date_added: Mapped[date | None] = mapped_column(sa.Date, default=date.today)
|
||||
date_added: Mapped[date | None] = mapped_column(sa.Date, default=get_utc_today)
|
||||
date_updated: Mapped[datetime | None] = mapped_column(sa.DateTime)
|
||||
last_made: Mapped[datetime | None] = mapped_column(sa.DateTime)
|
||||
|
||||
@ -194,7 +195,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
if notes:
|
||||
self.notes = [Note(**n) for n in notes]
|
||||
|
||||
self.date_updated = datetime.now()
|
||||
self.date_updated = datetime.now(timezone.utc)
|
||||
|
||||
# SQLAlchemy events do not seem to register things that are set during auto_init
|
||||
if name is not None:
|
||||
|
@ -1,11 +1,11 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import auto_init
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -42,4 +42,4 @@ class RecipeTimelineEvent(SqlAlchemyBase, BaseMixins):
|
||||
timestamp=None,
|
||||
**_,
|
||||
) -> None:
|
||||
self.timestamp = timestamp or datetime.now()
|
||||
self.timestamp = timestamp or datetime.now(timezone.utc)
|
||||
|
@ -1,4 +1,4 @@
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import uuid4
|
||||
|
||||
@ -6,14 +6,15 @@ import sqlalchemy as sa
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||
from mealie.db.models._model_utils import GUID, auto_init
|
||||
from mealie.db.models._model_utils.auto_init import auto_init
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import RecipeModel
|
||||
|
||||
|
||||
def defaut_expires_at_time() -> datetime:
|
||||
return datetime.utcnow() + timedelta(days=30)
|
||||
return datetime.now(timezone.utc) + timedelta(days=30)
|
||||
|
||||
|
||||
class RecipeShareTokenModel(SqlAlchemyBase, BaseMixins):
|
||||
|
@ -5,7 +5,7 @@ from sqlalchemy import Boolean, Column, ForeignKey, String, Table, UniqueConstra
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||
from mealie.db.models._model_utils import auto_init
|
||||
from mealie.db.models._model_utils.auto_init import auto_init
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -7,7 +7,7 @@ from sqlalchemy.orm import Mapped, mapped_column
|
||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
|
||||
from .._model_utils import auto_init
|
||||
from .._model_utils.auto_init import auto_init
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..group import Group
|
||||
|
@ -4,7 +4,7 @@ from sqlalchemy import ForeignKey, String, orm
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import GUID
|
||||
from .._model_utils.guid import GUID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .users import User
|
||||
|
@ -4,7 +4,8 @@ from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import GUID, auto_init
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
|
||||
|
||||
class UserToRecipe(SqlAlchemyBase, BaseMixins):
|
||||
|
@ -8,10 +8,10 @@ from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from mealie.core.config import get_app_settings
|
||||
from mealie.db.models._model_utils.auto_init import auto_init
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import auto_init
|
||||
from .user_to_recipe import UserToRecipe
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -1,4 +1,4 @@
|
||||
from datetime import date
|
||||
from datetime import datetime, timezone
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import select
|
||||
@ -14,7 +14,7 @@ class RepositoryMeals(RepositoryGeneric[ReadPlanEntry, GroupMealPlan]):
|
||||
return super().by_group(group_id)
|
||||
|
||||
def get_today(self, group_id: UUID) -> list[ReadPlanEntry]:
|
||||
today = date.today()
|
||||
today = datetime.now(tz=timezone.utc).date()
|
||||
stmt = select(GroupMealPlan).filter(GroupMealPlan.date == today, GroupMealPlan.group_id == group_id)
|
||||
plans = self.session.execute(stmt).scalars().all()
|
||||
return [self.schema.model_validate(x) for x in plans]
|
||||
|
@ -1,4 +1,4 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from functools import cached_property
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends
|
||||
@ -45,7 +45,7 @@ class ReadWebhookController(BaseUserController):
|
||||
"""Manually re-fires all previously scheduled webhooks for today"""
|
||||
|
||||
start_time = datetime.min.time()
|
||||
start_dt = datetime.combine(datetime.utcnow().date(), start_time)
|
||||
start_dt = datetime.combine(datetime.now(timezone.utc).date(), start_time)
|
||||
post_group_webhooks(start_dt=start_dt, group_id=self.group.id)
|
||||
|
||||
@router.get("/{item_id}", response_model=ReadWebhook)
|
||||
|
@ -39,7 +39,7 @@ iso8601_duration_re = re.compile(
|
||||
r"$"
|
||||
)
|
||||
|
||||
EPOCH = datetime(1970, 1, 1)
|
||||
EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
||||
# if greater than this, the number is in ms, if less than or equal it's in seconds
|
||||
# (in seconds this is 11th October 2603, in ms it's 20th August 1970)
|
||||
MS_WATERSHED = int(2e10)
|
||||
@ -209,7 +209,7 @@ def parse_datetime(value: datetime | str | bytes | int | float) -> datetime:
|
||||
kw_["tzinfo"] = tzinfo
|
||||
|
||||
try:
|
||||
return datetime(**kw_) # type: ignore
|
||||
return datetime(**kw_) # type: ignore # noqa DTZ001
|
||||
except ValueError as e:
|
||||
raise DateTimeError() from e
|
||||
|
||||
|
@ -1,19 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections.abc import Sequence
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from typing import ClassVar, Protocol, TypeVar
|
||||
|
||||
from humps.main import camelize
|
||||
from pydantic import UUID4, BaseModel, ConfigDict
|
||||
from pydantic import UUID4, BaseModel, ConfigDict, model_validator
|
||||
from sqlalchemy import Select, desc, func, or_, text
|
||||
from sqlalchemy.orm import InstrumentedAttribute, Session
|
||||
from sqlalchemy.orm.interfaces import LoaderOption
|
||||
from typing_extensions import Self
|
||||
|
||||
from mealie.db.models._model_base import SqlAlchemyBase
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
|
||||
HOUR_ONLY_TZ_PATTERN = re.compile(r"[+-]\d{2}$")
|
||||
|
||||
|
||||
class SearchType(Enum):
|
||||
fuzzy = "fuzzy"
|
||||
@ -30,6 +35,43 @@ class MealieModel(BaseModel):
|
||||
"""
|
||||
model_config = ConfigDict(alias_generator=camelize, populate_by_name=True)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def fix_hour_only_tz(cls, data: T) -> T:
|
||||
"""
|
||||
Fixes datetimes with timezones that only have the hour portion.
|
||||
|
||||
Pydantic assumes timezones are in the format +HH:MM, but postgres returns +HH.
|
||||
https://github.com/pydantic/pydantic/issues/8609
|
||||
"""
|
||||
for field, field_info in cls.model_fields.items():
|
||||
if field_info.annotation != datetime:
|
||||
continue
|
||||
try:
|
||||
if not isinstance(val := getattr(data, field), str):
|
||||
continue
|
||||
except AttributeError:
|
||||
continue
|
||||
if re.search(HOUR_ONLY_TZ_PATTERN, val):
|
||||
setattr(data, field, val + ":00")
|
||||
|
||||
return data
|
||||
|
||||
@model_validator(mode="after")
|
||||
def set_tz_info(self) -> Self:
|
||||
"""
|
||||
Adds UTC timezone information to all datetimes in the model.
|
||||
The server stores everything in UTC without timezone info.
|
||||
"""
|
||||
for field in self.model_fields:
|
||||
val = getattr(self, field)
|
||||
if not isinstance(val, datetime):
|
||||
continue
|
||||
if not val.tzinfo:
|
||||
setattr(self, field, val.replace(tzinfo=timezone.utc))
|
||||
|
||||
return self
|
||||
|
||||
def cast(self, cls: type[T], **kwargs) -> T:
|
||||
"""
|
||||
Cast the current model to another with additional arguments. Useful for
|
||||
|
@ -1,4 +1,4 @@
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from pydantic import UUID4, ConfigDict, Field
|
||||
from sqlalchemy.orm import selectinload
|
||||
@ -11,7 +11,7 @@ from .recipe import Recipe
|
||||
|
||||
|
||||
def defaut_expires_at_time() -> datetime:
|
||||
return datetime.utcnow() + timedelta(days=30)
|
||||
return datetime.now(timezone.utc) + timedelta(days=30)
|
||||
|
||||
|
||||
class RecipeShareTokenCreate(MealieModel):
|
||||
|
@ -1,4 +1,4 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
@ -35,7 +35,7 @@ class RecipeTimelineEventIn(MealieModel):
|
||||
message: str | None = Field(None, alias="eventMessage")
|
||||
image: Annotated[TimelineEventImage | None, Field(validate_default=True)] = TimelineEventImage.does_not_have_image
|
||||
|
||||
timestamp: datetime = datetime.now()
|
||||
timestamp: datetime = datetime.now(timezone.utc)
|
||||
model_config = ConfigDict(use_enum_values=True)
|
||||
|
||||
|
||||
|
@ -6,6 +6,7 @@ from pydantic.types import UUID4
|
||||
from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy.orm.interfaces import LoaderOption
|
||||
|
||||
from mealie.db.models._model_utils.datetime import get_utc_now
|
||||
from mealie.db.models.group import ReportModel
|
||||
from mealie.schema._mealie import MealieModel
|
||||
|
||||
@ -26,7 +27,7 @@ class ReportSummaryStatus(str, enum.Enum):
|
||||
|
||||
class ReportEntryCreate(MealieModel):
|
||||
report_id: UUID4
|
||||
timestamp: datetime.datetime = Field(default_factory=datetime.datetime.utcnow)
|
||||
timestamp: datetime.datetime = Field(default_factory=get_utc_now)
|
||||
success: bool = True
|
||||
message: str
|
||||
exception: str = ""
|
||||
@ -38,7 +39,7 @@ class ReportEntryOut(ReportEntryCreate):
|
||||
|
||||
|
||||
class ReportCreate(MealieModel):
|
||||
timestamp: datetime.datetime = Field(default_factory=datetime.datetime.utcnow)
|
||||
timestamp: datetime.datetime = Field(default_factory=get_utc_now)
|
||||
category: ReportCategory
|
||||
group_id: UUID4
|
||||
name: str
|
||||
|
@ -1,4 +1,4 @@
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any, Generic, TypeVar
|
||||
from uuid import UUID
|
||||
@ -186,7 +186,7 @@ class PrivateUser(UserOut):
|
||||
return False
|
||||
|
||||
lockout_expires_at = self.locked_at + timedelta(hours=get_app_settings().SECURITY_USER_LOCKOUT_TIME)
|
||||
return lockout_expires_at > datetime.now()
|
||||
return lockout_expires_at > datetime.now(timezone.utc)
|
||||
|
||||
def directory(self) -> Path:
|
||||
return PrivateUser.get_directory(self.id)
|
||||
|
@ -13,7 +13,7 @@ from sqlalchemy.orm import sessionmaker
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from mealie.db import init_db
|
||||
from mealie.db.models._model_utils import GUID
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
from mealie.services._base_service import BaseService
|
||||
|
||||
PROJECT_DIR = Path(__file__).parent.parent.parent.parent
|
||||
|
@ -25,7 +25,7 @@ class BackupV2(BaseService):
|
||||
db_file = self.settings.DB_URL.removeprefix("sqlite:///") # type: ignore
|
||||
|
||||
# Create a backup of the SQLite database
|
||||
timestamp = datetime.datetime.now().strftime("%Y.%m.%d")
|
||||
timestamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y.%m.%d")
|
||||
shutil.copy(db_file, self.directories.DATA_DIR.joinpath(f"mealie_{timestamp}.bak.db"))
|
||||
|
||||
def _postgres(self) -> None:
|
||||
@ -37,7 +37,7 @@ class BackupV2(BaseService):
|
||||
exclude_ext = {".zip"}
|
||||
exclude_dirs = {"backups", ".temp"}
|
||||
|
||||
timestamp = datetime.datetime.now().strftime("%Y.%m.%d.%H.%M.%S")
|
||||
timestamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y.%m.%d.%H.%M.%S")
|
||||
|
||||
backup_name = f"mealie_{timestamp}.zip"
|
||||
backup_file = self.directories.BACKUP_DIR / backup_name
|
||||
|
@ -1,5 +1,5 @@
|
||||
import uuid
|
||||
from datetime import date, datetime
|
||||
from datetime import date, datetime, timezone
|
||||
from enum import Enum, auto
|
||||
from typing import Any
|
||||
|
||||
@ -188,4 +188,4 @@ class Event(MealieModel):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.event_id = uuid.uuid4()
|
||||
self.timestamp = datetime.now()
|
||||
self.timestamp = datetime.now(timezone.utc)
|
||||
|
@ -43,7 +43,7 @@ class Exporter(BaseService):
|
||||
name="Data Export",
|
||||
size=pretty_size(export_path.stat().st_size),
|
||||
filename=export_path.name,
|
||||
expires=datetime.datetime.now() + datetime.timedelta(days=1),
|
||||
expires=datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=1),
|
||||
)
|
||||
|
||||
db.group_exports.create(group_data_export)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import tempfile
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
@ -35,7 +35,7 @@ class CopyMeThatMigrator(BaseMigrator):
|
||||
self.name = "copymethat"
|
||||
|
||||
self.key_aliases = [
|
||||
MigrationAlias(key="last_made", alias="made_this", func=lambda x: datetime.now()),
|
||||
MigrationAlias(key="last_made", alias="made_this", func=lambda x: datetime.now(timezone.utc)),
|
||||
MigrationAlias(key="notes", alias="recipeNotes"),
|
||||
MigrationAlias(key="orgURL", alias="original_link"),
|
||||
MigrationAlias(key="rating", alias="ratingValue"),
|
||||
|
@ -1,6 +1,6 @@
|
||||
import json
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from shutil import copytree, rmtree
|
||||
from typing import Any
|
||||
@ -157,7 +157,7 @@ class RecipeService(BaseService):
|
||||
recipe_id=new_recipe.id,
|
||||
subject="Recipe Created",
|
||||
event_type=TimelineEventType.system,
|
||||
timestamp=new_recipe.created_at or datetime.now(),
|
||||
timestamp=new_recipe.created_at or datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
self.repos.recipe_timeline_events.create(timeline_event_data)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from mealie.core import root_logger
|
||||
@ -28,7 +28,7 @@ class SchedulerService:
|
||||
|
||||
|
||||
async def schedule_daily():
|
||||
now = datetime.now()
|
||||
now = datetime.now(timezone.utc)
|
||||
daily_schedule_time = get_app_settings().DAILY_SCHEDULE_TIME
|
||||
logger.debug(
|
||||
"Current time is %s and DAILY_SCHEDULE_TIME is %s",
|
||||
|
@ -16,7 +16,7 @@ def purge_group_data_exports(max_minutes_old=ONE_DAY_AS_MINUTES):
|
||||
logger = root_logger.get_logger()
|
||||
|
||||
logger.debug("purging group data exports")
|
||||
limit = datetime.datetime.now() - datetime.timedelta(minutes=max_minutes_old)
|
||||
limit = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(minutes=max_minutes_old)
|
||||
|
||||
with session_context() as session:
|
||||
stmt = select(GroupDataExportsModel).filter(cast(GroupDataExportsModel.expires, DateTime) <= limit)
|
||||
@ -38,7 +38,7 @@ def purge_excess_files() -> None:
|
||||
directories = get_app_dirs()
|
||||
logger = root_logger.get_logger()
|
||||
|
||||
limit = datetime.datetime.now() - datetime.timedelta(minutes=ONE_DAY_AS_MINUTES * 2)
|
||||
limit = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(minutes=ONE_DAY_AS_MINUTES * 2)
|
||||
|
||||
for file in directories.GROUPS_DIR.glob("**/export/*.zip"):
|
||||
# TODO: fix comparison types
|
||||
|
@ -14,7 +14,7 @@ MAX_DAYS_OLD = 2
|
||||
def purge_password_reset_tokens():
|
||||
"""Purges all events after x days"""
|
||||
logger.debug("purging password reset tokens")
|
||||
limit = datetime.datetime.now() - datetime.timedelta(days=MAX_DAYS_OLD)
|
||||
limit = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=MAX_DAYS_OLD)
|
||||
|
||||
with session_context() as session:
|
||||
stmt = delete(PasswordResetModel).filter(PasswordResetModel.created_at <= limit)
|
||||
|
@ -14,7 +14,7 @@ MAX_DAYS_OLD = 4
|
||||
def purge_group_registration():
|
||||
"""Purges all events after x days"""
|
||||
logger.debug("purging expired registration tokens")
|
||||
limit = datetime.datetime.now() - datetime.timedelta(days=MAX_DAYS_OLD)
|
||||
limit = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=MAX_DAYS_OLD)
|
||||
|
||||
with session_context() as session:
|
||||
stmt = delete(GroupInviteToken).filter(GroupInviteToken.created_at <= limit)
|
||||
|
@ -1,4 +1,4 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from mealie.repos.repository_factory import AllRepositories
|
||||
from mealie.schema.user.user import PrivateUser
|
||||
@ -30,7 +30,7 @@ class UserService(BaseService):
|
||||
return unlocked
|
||||
|
||||
def lock_user(self, user: PrivateUser) -> PrivateUser:
|
||||
user.locked_at = datetime.now()
|
||||
user.locked_at = datetime.now(timezone.utc)
|
||||
return self.repos.users.update(user.id, user)
|
||||
|
||||
def unlock_user(self, user: PrivateUser) -> PrivateUser:
|
||||
|
4
poetry.lock
generated
4
poetry.lock
generated
@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiofiles"
|
||||
@ -3475,4 +3475,4 @@ pgsql = ["psycopg2-binary"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "a3013c99f7e125bab3566192fe93d7b808eb6b837e4ae3d0e42a344673963950"
|
||||
content-hash = "d2b389e15570fa45314e20d80bce9e47a52a087c17864fb079d90f2028f69efe"
|
||||
|
@ -48,6 +48,7 @@ pydantic-settings = "^2.1.0"
|
||||
pillow-heif = "^0.17.0"
|
||||
pyjwt = "^2.8.0"
|
||||
openai = "^1.27.0"
|
||||
typing-extensions = "^4.12.2"
|
||||
|
||||
[tool.poetry.group.postgres.dependencies]
|
||||
psycopg2-binary = { version = "^2.9.1" }
|
||||
@ -144,6 +145,7 @@ select = [
|
||||
"T", # flake8-print
|
||||
"UP", # pyupgrade
|
||||
"B", # flake8-bugbear
|
||||
"DTZ", # flake8-datetimez
|
||||
# "ANN", # flake8-annotations
|
||||
# "C", # McCabe complexity
|
||||
# "RUF", # Ruff specific
|
||||
|
@ -1,4 +1,4 @@
|
||||
from datetime import date, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
@ -15,8 +15,10 @@ def route_all_slice(page: int, perPage: int, start_date: str, end_date: str):
|
||||
def test_create_mealplan_no_recipe(api_client: TestClient, unique_user: TestUser):
|
||||
title = random_string(length=25)
|
||||
text = random_string(length=25)
|
||||
new_plan = CreatePlanEntry(date=date.today(), entry_type="breakfast", title=title, text=text).model_dump()
|
||||
new_plan["date"] = date.today().strftime("%Y-%m-%d")
|
||||
new_plan = CreatePlanEntry(
|
||||
date=datetime.now(timezone.utc).date(), entry_type="breakfast", title=title, text=text
|
||||
).model_dump()
|
||||
new_plan["date"] = datetime.now(timezone.utc).date().strftime("%Y-%m-%d")
|
||||
|
||||
response = api_client.post(api_routes.groups_mealplans, json=new_plan, headers=unique_user.token)
|
||||
|
||||
@ -36,8 +38,10 @@ def test_create_mealplan_with_recipe(api_client: TestClient, unique_user: TestUs
|
||||
recipe = response.json()
|
||||
recipe_id = recipe["id"]
|
||||
|
||||
new_plan = CreatePlanEntry(date=date.today(), entry_type="dinner", recipe_id=recipe_id).model_dump(by_alias=True)
|
||||
new_plan["date"] = date.today().strftime("%Y-%m-%d")
|
||||
new_plan = CreatePlanEntry(
|
||||
date=datetime.now(timezone.utc).date(), entry_type="dinner", recipe_id=recipe_id
|
||||
).model_dump(by_alias=True)
|
||||
new_plan["date"] = datetime.now(timezone.utc).date().strftime("%Y-%m-%d")
|
||||
new_plan["recipeId"] = str(recipe_id)
|
||||
|
||||
response = api_client.post(api_routes.groups_mealplans, json=new_plan, headers=unique_user.token)
|
||||
@ -49,14 +53,14 @@ def test_create_mealplan_with_recipe(api_client: TestClient, unique_user: TestUs
|
||||
|
||||
def test_crud_mealplan(api_client: TestClient, unique_user: TestUser):
|
||||
new_plan = CreatePlanEntry(
|
||||
date=date.today(),
|
||||
date=datetime.now(timezone.utc).date(),
|
||||
entry_type="breakfast",
|
||||
title=random_string(),
|
||||
text=random_string(),
|
||||
).model_dump()
|
||||
|
||||
# Create
|
||||
new_plan["date"] = date.today().strftime("%Y-%m-%d")
|
||||
new_plan["date"] = datetime.now(timezone.utc).date().strftime("%Y-%m-%d")
|
||||
response = api_client.post(api_routes.groups_mealplans, json=new_plan, headers=unique_user.token)
|
||||
response_json = response.json()
|
||||
assert response.status_code == 201
|
||||
@ -87,13 +91,13 @@ def test_crud_mealplan(api_client: TestClient, unique_user: TestUser):
|
||||
def test_get_all_mealplans(api_client: TestClient, unique_user: TestUser):
|
||||
for _ in range(3):
|
||||
new_plan = CreatePlanEntry(
|
||||
date=date.today(),
|
||||
date=datetime.now(timezone.utc).date(),
|
||||
entry_type="breakfast",
|
||||
title=random_string(),
|
||||
text=random_string(),
|
||||
).model_dump()
|
||||
|
||||
new_plan["date"] = date.today().strftime("%Y-%m-%d")
|
||||
new_plan["date"] = datetime.now(timezone.utc).date().strftime("%Y-%m-%d")
|
||||
response = api_client.post(api_routes.groups_mealplans, json=new_plan, headers=unique_user.token)
|
||||
assert response.status_code == 201
|
||||
|
||||
@ -105,7 +109,7 @@ def test_get_all_mealplans(api_client: TestClient, unique_user: TestUser):
|
||||
|
||||
def test_get_slice_mealplans(api_client: TestClient, unique_user: TestUser):
|
||||
# Make List of 10 dates from now to +10 days
|
||||
dates = [date.today() + timedelta(days=x) for x in range(10)]
|
||||
dates = [datetime.now(timezone.utc).date() + timedelta(days=x) for x in range(10)]
|
||||
|
||||
# Make a list of 10 meal plans
|
||||
meal_plans = [
|
||||
@ -139,7 +143,7 @@ def test_get_mealplan_today(api_client: TestClient, unique_user: TestUser):
|
||||
# Create Meal Plans for today
|
||||
test_meal_plans = [
|
||||
CreatePlanEntry(
|
||||
date=date.today(), entry_type="breakfast", title=random_string(), text=random_string()
|
||||
date=datetime.now(timezone.utc).date(), entry_type="breakfast", title=random_string(), text=random_string()
|
||||
).model_dump()
|
||||
for _ in range(3)
|
||||
]
|
||||
@ -158,4 +162,4 @@ def test_get_mealplan_today(api_client: TestClient, unique_user: TestUser):
|
||||
response_json = response.json()
|
||||
|
||||
for meal_plan in response_json:
|
||||
assert meal_plan["date"] == date.today().strftime("%Y-%m-%d")
|
||||
assert meal_plan["date"] == datetime.now(timezone.utc).date().strftime("%Y-%m-%d")
|
||||
|
@ -14,7 +14,7 @@ def webhook_data():
|
||||
"name": "Test-Name",
|
||||
"url": "https://my-fake-url.com",
|
||||
"time": "00:00",
|
||||
"scheduledTime": datetime.now(),
|
||||
"scheduledTime": datetime.now(timezone.utc),
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
@ -106,7 +106,7 @@ def test_user_update_last_made(api_client: TestClient, user_tuple: list[TestUser
|
||||
response = api_client.put(api_routes.recipes + f"/{recipe_name}", json=recipe, headers=usr_1.token)
|
||||
|
||||
# User 2 should be able to update the last made timestamp
|
||||
last_made_json = {"timestamp": datetime.now().isoformat()}
|
||||
last_made_json = {"timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")}
|
||||
response = api_client.patch(
|
||||
api_routes.recipes_slug_last_made(recipe_name), json=last_made_json, headers=usr_2.token
|
||||
)
|
||||
|
@ -1,7 +1,7 @@
|
||||
import random
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from datetime import date, datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from random import randint
|
||||
from urllib.parse import parse_qsl, urlsplit
|
||||
|
||||
@ -233,7 +233,7 @@ def test_pagination_filter_null(database: AllRepositories, unique_user: TestUser
|
||||
user_id=unique_user.user_id,
|
||||
group_id=unique_user.group_id,
|
||||
name=random_string(),
|
||||
last_made=datetime.now(),
|
||||
last_made=datetime.now(timezone.utc),
|
||||
)
|
||||
)
|
||||
|
||||
@ -619,7 +619,7 @@ def test_pagination_filter_datetimes(
|
||||
def test_pagination_order_by_multiple(
|
||||
database: AllRepositories, unique_user: TestUser, order_direction: OrderDirection
|
||||
):
|
||||
current_time = datetime.now()
|
||||
current_time = datetime.now(timezone.utc)
|
||||
|
||||
alphabet = ["a", "b", "c", "d", "e"]
|
||||
abbreviations = alphabet.copy()
|
||||
@ -681,7 +681,7 @@ def test_pagination_order_by_multiple_directions(
|
||||
order_by_str: str,
|
||||
order_direction: OrderDirection,
|
||||
):
|
||||
current_time = datetime.now()
|
||||
current_time = datetime.now(timezone.utc)
|
||||
|
||||
alphabet = ["a", "b", "c", "d", "e"]
|
||||
abbreviations = alphabet.copy()
|
||||
@ -729,7 +729,7 @@ def test_pagination_order_by_multiple_directions(
|
||||
def test_pagination_order_by_nested_model(
|
||||
database: AllRepositories, unique_user: TestUser, order_direction: OrderDirection
|
||||
):
|
||||
current_time = datetime.now()
|
||||
current_time = datetime.now(timezone.utc)
|
||||
|
||||
alphabet = ["a", "b", "c", "d", "e"]
|
||||
labels = database.group_multi_purpose_labels.create_many(
|
||||
@ -759,7 +759,7 @@ def test_pagination_order_by_nested_model(
|
||||
|
||||
|
||||
def test_pagination_order_by_doesnt_filter(database: AllRepositories, unique_user: TestUser):
|
||||
current_time = datetime.now()
|
||||
current_time = datetime.now(timezone.utc)
|
||||
|
||||
label = database.group_multi_purpose_labels.create(
|
||||
MultiPurposeLabelSave(name=random_string(), group_id=unique_user.group_id)
|
||||
@ -805,7 +805,7 @@ def test_pagination_order_by_nulls(
|
||||
null_position: OrderByNullPosition,
|
||||
order_direction: OrderDirection,
|
||||
):
|
||||
current_time = datetime.now()
|
||||
current_time = datetime.now(timezone.utc)
|
||||
|
||||
label = database.group_multi_purpose_labels.create(
|
||||
MultiPurposeLabelSave(name=random_string(), group_id=unique_user.group_id)
|
||||
@ -909,10 +909,11 @@ def test_pagination_shopping_list_items_with_labels(database: AllRepositories, u
|
||||
|
||||
|
||||
def test_pagination_filter_dates(api_client: TestClient, unique_user: TestUser):
|
||||
yesterday = date.today() - timedelta(days=1)
|
||||
today = date.today()
|
||||
tomorrow = date.today() + timedelta(days=1)
|
||||
day_after_tomorrow = date.today() + timedelta(days=2)
|
||||
today = datetime.now(timezone.utc).date()
|
||||
|
||||
yesterday = today - timedelta(days=1)
|
||||
tomorrow = today + timedelta(days=1)
|
||||
day_after_tomorrow = today + timedelta(days=2)
|
||||
|
||||
mealplan_today = CreatePlanEntry(date=today, entry_type="breakfast", title=random_string(), text=random_string())
|
||||
mealplan_tomorrow = CreatePlanEntry(
|
||||
|
@ -1,4 +1,4 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import cast
|
||||
from uuid import UUID
|
||||
|
||||
@ -298,12 +298,12 @@ def test_recipe_repo_pagination_by_categories(database: AllRepositories, unique_
|
||||
page=1,
|
||||
per_page=-1,
|
||||
order_by="random",
|
||||
pagination_seed=str(datetime.now()),
|
||||
pagination_seed=str(datetime.now(timezone.utc)),
|
||||
order_direction=OrderDirection.asc,
|
||||
)
|
||||
random_ordered = []
|
||||
for i in range(5):
|
||||
pagination_query.pagination_seed = str(datetime.now())
|
||||
pagination_query.pagination_seed = str(datetime.now(timezone.utc))
|
||||
random_ordered.append(database.recipes.page_all(pagination_query, categories=[category_slug]).items)
|
||||
assert not all(i == random_ordered[0] for i in random_ordered)
|
||||
|
||||
@ -391,12 +391,12 @@ def test_recipe_repo_pagination_by_tags(database: AllRepositories, unique_user:
|
||||
page=1,
|
||||
per_page=-1,
|
||||
order_by="random",
|
||||
pagination_seed=str(datetime.now()),
|
||||
pagination_seed=str(datetime.now(timezone.utc)),
|
||||
order_direction=OrderDirection.asc,
|
||||
)
|
||||
random_ordered = []
|
||||
for i in range(5):
|
||||
pagination_query.pagination_seed = str(datetime.now())
|
||||
pagination_query.pagination_seed = str(datetime.now(timezone.utc))
|
||||
random_ordered.append(database.recipes.page_all(pagination_query, tags=[tag_slug]).items)
|
||||
assert len(random_ordered[0]) == 15
|
||||
assert not all(i == random_ordered[0] for i in random_ordered)
|
||||
@ -487,12 +487,12 @@ def test_recipe_repo_pagination_by_tools(database: AllRepositories, unique_user:
|
||||
page=1,
|
||||
per_page=-1,
|
||||
order_by="random",
|
||||
pagination_seed=str(datetime.now()),
|
||||
pagination_seed=str(datetime.now(timezone.utc)),
|
||||
order_direction=OrderDirection.asc,
|
||||
)
|
||||
random_ordered = []
|
||||
for i in range(5):
|
||||
pagination_query.pagination_seed = str(datetime.now())
|
||||
pagination_query.pagination_seed = str(datetime.now(timezone.utc))
|
||||
random_ordered.append(database.recipes.page_all(pagination_query, tools=[tool_id]).items)
|
||||
assert len(random_ordered[0]) == 15
|
||||
assert not all(i == random_ordered[0] for i in random_ordered)
|
||||
@ -571,12 +571,12 @@ def test_recipe_repo_pagination_by_foods(database: AllRepositories, unique_user:
|
||||
page=1,
|
||||
per_page=-1,
|
||||
order_by="random",
|
||||
pagination_seed=str(datetime.now()),
|
||||
pagination_seed=str(datetime.now(timezone.utc)),
|
||||
order_direction=OrderDirection.asc,
|
||||
)
|
||||
random_ordered = []
|
||||
for i in range(5):
|
||||
pagination_query.pagination_seed = str(datetime.now())
|
||||
pagination_query.pagination_seed = str(datetime.now(timezone.utc))
|
||||
random_ordered.append(database.recipes.page_all(pagination_query, foods=[food_id]).items)
|
||||
assert len(random_ordered[0]) == 15
|
||||
assert not all(i == random_ordered[0] for i in random_ordered)
|
||||
@ -651,12 +651,12 @@ def test_random_order_recipe_search(
|
||||
page=1,
|
||||
per_page=-1,
|
||||
order_by="random",
|
||||
pagination_seed=str(datetime.now()),
|
||||
pagination_seed=str(datetime.now(timezone.utc)),
|
||||
order_direction=OrderDirection.asc,
|
||||
)
|
||||
random_ordered = []
|
||||
for _ in range(5):
|
||||
pagination.pagination_seed = str(datetime.now())
|
||||
pagination.pagination_seed = str(datetime.now(timezone.utc))
|
||||
random_ordered.append(repo.page_all(pagination, search="soup").items)
|
||||
assert not all(i == random_ordered[0] for i in random_ordered)
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
@ -125,11 +125,11 @@ def test_random_order_search(
|
||||
page=1,
|
||||
per_page=-1,
|
||||
order_by="random",
|
||||
pagination_seed=str(datetime.now()),
|
||||
pagination_seed=str(datetime.now(timezone.utc)),
|
||||
order_direction=OrderDirection.asc,
|
||||
)
|
||||
random_ordered = []
|
||||
for _ in range(5):
|
||||
pagination.pagination_seed = str(datetime.now())
|
||||
pagination.pagination_seed = str(datetime.now(timezone.utc))
|
||||
random_ordered.append(repo.page_all(pagination, search="unit").items)
|
||||
assert not all(i == random_ordered[0] for i in random_ordered)
|
||||
|
@ -9,7 +9,7 @@ from sqlalchemy.orm import Session
|
||||
import tests.data as test_data
|
||||
from mealie.core.config import get_app_settings
|
||||
from mealie.db.db_setup import session_context
|
||||
from mealie.db.models._model_utils import GUID
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
from mealie.db.models.group import Group
|
||||
from mealie.db.models.group.shopping_list import ShoppingList
|
||||
from mealie.db.models.labels import MultiPurposeLabel
|
||||
|
@ -1,4 +1,4 @@
|
||||
from datetime import date, datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import UUID4
|
||||
@ -34,8 +34,10 @@ def test_new_mealplan_event(api_client: TestClient, unique_user: TestUser):
|
||||
response_json = response.json()
|
||||
initial_event_count = len(response_json["items"])
|
||||
|
||||
new_plan = CreatePlanEntry(date=date.today(), entry_type="dinner", recipe_id=recipe_id).model_dump(by_alias=True)
|
||||
new_plan["date"] = date.today().isoformat()
|
||||
new_plan = CreatePlanEntry(
|
||||
date=datetime.now(timezone.utc).date(), entry_type="dinner", recipe_id=recipe_id
|
||||
).model_dump(by_alias=True)
|
||||
new_plan["date"] = datetime.now(timezone.utc).date().isoformat()
|
||||
new_plan["recipeId"] = str(recipe_id)
|
||||
|
||||
response = api_client.post(api_routes.groups_mealplans, json=new_plan, headers=unique_user.token)
|
||||
@ -63,7 +65,7 @@ def test_new_mealplan_event(api_client: TestClient, unique_user: TestUser):
|
||||
response = api_client.get(api_routes.recipes_slug(recipe_name), headers=unique_user.token)
|
||||
new_recipe_data: dict = response.json()
|
||||
recipe = RecipeSummary.model_validate(new_recipe_data)
|
||||
assert recipe.last_made.date() == date.today() # type: ignore
|
||||
assert recipe.last_made.date() == datetime.now(timezone.utc).date() # type: ignore
|
||||
|
||||
# make sure nothing else was updated
|
||||
for data in [original_recipe_data, new_recipe_data]:
|
||||
@ -99,8 +101,10 @@ def test_new_mealplan_event_duplicates(api_client: TestClient, unique_user: Test
|
||||
response_json = response.json()
|
||||
initial_event_count = len(response_json["items"])
|
||||
|
||||
new_plan = CreatePlanEntry(date=date.today(), entry_type="dinner", recipe_id=recipe_id).model_dump(by_alias=True)
|
||||
new_plan["date"] = date.today().isoformat()
|
||||
new_plan = CreatePlanEntry(
|
||||
date=datetime.now(timezone.utc).date(), entry_type="dinner", recipe_id=recipe_id
|
||||
).model_dump(by_alias=True)
|
||||
new_plan["date"] = datetime.now(timezone.utc).date().isoformat()
|
||||
new_plan["recipeId"] = str(recipe_id)
|
||||
|
||||
response = api_client.post(api_routes.groups_mealplans, json=new_plan, headers=unique_user.token)
|
||||
@ -143,10 +147,10 @@ def test_new_mealplan_events_with_multiple_recipes(api_client: TestClient, uniqu
|
||||
for recipe in recipes:
|
||||
mealplan_count_by_recipe_id[recipe.id] = 0 # type: ignore
|
||||
for _ in range(random_int(1, 5)):
|
||||
new_plan = CreatePlanEntry(date=date.today(), entry_type="dinner", recipe_id=str(recipe.id)).model_dump(
|
||||
by_alias=True
|
||||
)
|
||||
new_plan["date"] = date.today().isoformat()
|
||||
new_plan = CreatePlanEntry(
|
||||
date=datetime.now(timezone.utc).date(), entry_type="dinner", recipe_id=str(recipe.id)
|
||||
).model_dump(by_alias=True)
|
||||
new_plan["date"] = datetime.now(timezone.utc).date().isoformat()
|
||||
new_plan["recipeId"] = str(recipe.id)
|
||||
|
||||
response = api_client.post(api_routes.groups_mealplans, json=new_plan, headers=unique_user.token)
|
||||
@ -196,15 +200,17 @@ def test_preserve_future_made_date(api_client: TestClient, unique_user: TestUser
|
||||
recipe = RecipeSummary.model_validate(response.json())
|
||||
recipe_id = str(recipe.id)
|
||||
|
||||
future_dt = datetime.now() + timedelta(days=random_int(1, 10))
|
||||
future_dt = datetime.now(timezone.utc) + timedelta(days=random_int(1, 10))
|
||||
recipe.last_made = future_dt
|
||||
response = api_client.put(
|
||||
api_routes.recipes_slug(recipe.slug), json=utils.jsonify(recipe), headers=unique_user.token
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
new_plan = CreatePlanEntry(date=date.today(), entry_type="dinner", recipe_id=recipe_id).model_dump(by_alias=True)
|
||||
new_plan["date"] = date.today().isoformat()
|
||||
new_plan = CreatePlanEntry(
|
||||
date=datetime.now(timezone.utc).date(), entry_type="dinner", recipe_id=recipe_id
|
||||
).model_dump(by_alias=True)
|
||||
new_plan["date"] = datetime.now(timezone.utc).date().isoformat()
|
||||
new_plan["recipeId"] = str(recipe_id)
|
||||
|
||||
response = api_client.post(api_routes.groups_mealplans, json=new_plan, headers=unique_user.token)
|
||||
|
@ -1,4 +1,4 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from mealie.repos.repository_factory import AllRepositories
|
||||
from mealie.schema.group.group_shopping_list import ShoppingListItemCreate, ShoppingListItemOut, ShoppingListSave
|
||||
@ -40,7 +40,7 @@ def test_cleanup(database: AllRepositories, unique_user: TestUser):
|
||||
for item in unchecked_items + checked_items:
|
||||
assert item in shopping_list.list_items
|
||||
|
||||
checked_items.sort(key=lambda x: x.update_at or datetime.now(), reverse=True)
|
||||
checked_items.sort(key=lambda x: x.update_at or datetime.now(timezone.utc), reverse=True)
|
||||
expected_kept_items = unchecked_items + checked_items[:MAX_CHECKED_ITEMS]
|
||||
expected_deleted_items = checked_items[MAX_CHECKED_ITEMS:]
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from pydantic import UUID4
|
||||
|
||||
@ -23,7 +23,7 @@ def webhook_factory(
|
||||
name=name or random_string(),
|
||||
url=url or random_string(),
|
||||
webhook_type=webhook_type,
|
||||
scheduled_time=scheduled_time.time() if scheduled_time else datetime.now().time(),
|
||||
scheduled_time=scheduled_time.time() if scheduled_time else datetime.now(timezone.utc).time(),
|
||||
group_id=group_id,
|
||||
)
|
||||
|
||||
@ -35,7 +35,7 @@ def test_get_scheduled_webhooks_filter_query(database: AllRepositories, unique_u
|
||||
|
||||
expected: list[SaveWebhook] = []
|
||||
|
||||
start = datetime.now()
|
||||
start = datetime.now(timezone.utc)
|
||||
|
||||
for _ in range(5):
|
||||
new_item = webhook_factory(group_id=unique_user.group_id, enabled=random_bool())
|
||||
@ -52,7 +52,7 @@ def test_get_scheduled_webhooks_filter_query(database: AllRepositories, unique_u
|
||||
expected.append(new_item)
|
||||
|
||||
event_bus_listener = WebhookEventListener(unique_user.group_id) # type: ignore
|
||||
results = event_bus_listener.get_scheduled_webhooks(start, datetime.now() + timedelta(minutes=5))
|
||||
results = event_bus_listener.get_scheduled_webhooks(start, datetime.now(timezone.utc) + timedelta(minutes=5))
|
||||
|
||||
assert len(results) == len(expected)
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from mealie.repos.repository_factory import AllRepositories
|
||||
from mealie.services.user_services.user_service import UserService
|
||||
@ -59,7 +59,7 @@ def test_lock_unlocker_user(database: AllRepositories, unique_user: TestUser) ->
|
||||
assert not unlocked_user.is_locked
|
||||
|
||||
# Sanity check that the is_locked property is working
|
||||
user.locked_at = datetime.now() - timedelta(days=2)
|
||||
user.locked_at = datetime.now(timezone.utc) - timedelta(days=2)
|
||||
assert not user.is_locked
|
||||
|
||||
|
||||
@ -85,7 +85,7 @@ def test_reset_locked_users(database: AllRepositories, unique_user: TestUser) ->
|
||||
assert user.login_attemps == 5
|
||||
|
||||
# Test that the locked user is unlocked by reset
|
||||
user.locked_at = datetime.now() - timedelta(days=2)
|
||||
user.locked_at = datetime.now(timezone.utc) - timedelta(days=2)
|
||||
database.users.update(user.id, user)
|
||||
unlocked = user_service.reset_locked_users()
|
||||
user = database.users.get_one(unique_user.user_id)
|
||||
|
@ -1,4 +1,4 @@
|
||||
from datetime import date
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
@ -7,7 +7,7 @@ from mealie.schema.meal_plan.new_meal import CreatePlanEntry
|
||||
|
||||
|
||||
def test_create_plan_with_title():
|
||||
entry = CreatePlanEntry(date=date.today(), title="Test Title")
|
||||
entry = CreatePlanEntry(date=datetime.now(timezone.utc).date(), title="Test Title")
|
||||
|
||||
assert entry.title == "Test Title"
|
||||
assert entry.recipe_id is None
|
||||
@ -15,7 +15,7 @@ def test_create_plan_with_title():
|
||||
|
||||
def test_create_plan_with_slug():
|
||||
uuid = uuid4()
|
||||
entry = CreatePlanEntry(date=date.today(), recipe_id=uuid)
|
||||
entry = CreatePlanEntry(date=datetime.now(timezone.utc).date(), recipe_id=uuid)
|
||||
|
||||
assert entry.recipe_id == uuid
|
||||
assert entry.title == ""
|
||||
@ -23,4 +23,4 @@ def test_create_plan_with_slug():
|
||||
|
||||
def test_slug_or_title_validation():
|
||||
with pytest.raises(ValueError):
|
||||
CreatePlanEntry(date=date.today(), slug="", title="")
|
||||
CreatePlanEntry(date=datetime.now(timezone.utc).date(), slug="", title="")
|
||||
|
Loading…
x
Reference in New Issue
Block a user