Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Make Mealie Timezone-Aware #3847

Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

"""

from datetime import datetime
from datetime import datetime, timezone
from textwrap import dedent
from typing import Any
from uuid import uuid4
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 2 additions & 6 deletions frontend/components/Domain/Recipe/RecipeLastMade.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions frontend/components/Domain/Recipe/RecipeTimelineItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion frontend/composables/use-shopping-list-item-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
1 change: 0 additions & 1 deletion frontend/pages/shopping-lists/_id.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
boc-the-git marked this conversation as resolved.
Show resolved Hide resolved

// make updates reflect immediately
Expand Down
2 changes: 1 addition & 1 deletion frontend/pages/user/profile/api-tokens.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion mealie/core/release_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions mealie/db/models/_model_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
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:
Expand Down
7 changes: 0 additions & 7 deletions mealie/db/models/_model_utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +0,0 @@
from .auto_init import auto_init
from .guid import GUID

__all__ = [
"auto_init",
"GUID",
]
15 changes: 15 additions & 0 deletions mealie/db/models/_model_utils/datetime.py
Original file line number Diff line number Diff line change
@@ -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()
5 changes: 3 additions & 2 deletions mealie/db/models/group/cookbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
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):
Expand Down
5 changes: 3 additions & 2 deletions mealie/db/models/group/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
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):
Expand Down
5 changes: 3 additions & 2 deletions mealie/db/models/group/exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
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):
Expand Down
3 changes: 2 additions & 1 deletion mealie/db/models/group/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
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
Expand Down
5 changes: 3 additions & 2 deletions mealie/db/models/group/invite_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
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):
Expand Down
6 changes: 3 additions & 3 deletions mealie/db/models/group/mealplan.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
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):
Expand Down
4 changes: 2 additions & 2 deletions mealie/db/models/group/preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
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):
Expand Down
5 changes: 3 additions & 2 deletions mealie/db/models/group/recipe_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
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):
Expand Down
9 changes: 5 additions & 4 deletions mealie/db/models/group/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@

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):
Expand All @@ -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")
Expand All @@ -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"
Expand Down
7 changes: 4 additions & 3 deletions mealie/db/models/group/shopping_list.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,7 +11,8 @@
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:
Expand Down Expand Up @@ -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()
Expand Down
9 changes: 5 additions & 4 deletions mealie/db/models/group/webhooks.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions mealie/db/models/labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@

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):
Expand Down
2 changes: 1 addition & 1 deletion mealie/db/models/recipe/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
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:
Expand Down
2 changes: 1 addition & 1 deletion mealie/db/models/recipe/ingredient.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
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:
Expand Down
Loading
Loading