mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-30 17:53:31 -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:
		| @@ -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="") | ||||
|   | ||||
		Reference in New Issue
	
	Block a user