refactor: update timestamp handling to use timezone-aware datetime

This commit is contained in:
JSC
2025-07-04 19:20:56 +02:00
parent 4375718c2f
commit 1cd43a670d
5 changed files with 94 additions and 58 deletions

View File

@@ -3,11 +3,13 @@
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum
from typing import Optional from typing import Optional
from zoneinfo import ZoneInfo
from app.database import db
from sqlalchemy import Boolean, DateTime, Integer, String from sqlalchemy import Boolean, DateTime, Integer, String
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.database import db
class SoundType(Enum): class SoundType(Enum):
"""Sound type enumeration.""" """Sound type enumeration."""
@@ -75,13 +77,13 @@ class Sound(db.Model):
# Timestamps # Timestamps
created_at: Mapped[datetime] = mapped_column( created_at: Mapped[datetime] = mapped_column(
DateTime, DateTime,
default=datetime.utcnow, default=lambda: datetime.now(tz=ZoneInfo("UTC")),
nullable=False, nullable=False,
) )
updated_at: Mapped[datetime] = mapped_column( updated_at: Mapped[datetime] = mapped_column(
DateTime, DateTime,
default=datetime.utcnow, default=lambda: datetime.now(tz=ZoneInfo("UTC")),
onupdate=datetime.utcnow, onupdate=lambda: datetime.now(tz=ZoneInfo("UTC")),
nullable=False, nullable=False,
) )
@@ -114,7 +116,7 @@ class Sound(db.Model):
def increment_play_count(self) -> None: def increment_play_count(self) -> None:
"""Increment the play count for this sound.""" """Increment the play count for this sound."""
self.play_count += 1 self.play_count += 1
self.updated_at = datetime.utcnow() self.updated_at = datetime.now(tz=ZoneInfo("UTC"))
db.session.commit() db.session.commit()
def set_normalized_info( def set_normalized_info(
@@ -130,7 +132,7 @@ class Sound(db.Model):
self.normalized_size = normalized_size self.normalized_size = normalized_size
self.normalized_hash = normalized_hash self.normalized_hash = normalized_hash
self.is_normalized = True self.is_normalized = True
self.updated_at = datetime.utcnow() self.updated_at = datetime.now(tz=ZoneInfo("UTC"))
def clear_normalized_info(self) -> None: def clear_normalized_info(self) -> None:
"""Clear normalized sound information.""" """Clear normalized sound information."""
@@ -139,7 +141,7 @@ class Sound(db.Model):
self.normalized_hash = None self.normalized_hash = None
self.normalized_size = None self.normalized_size = None
self.is_normalized = False self.is_normalized = False
self.updated_at = datetime.utcnow() self.updated_at = datetime.now(tz=ZoneInfo("UTC"))
def update_file_info( def update_file_info(
self, self,
@@ -153,7 +155,7 @@ class Sound(db.Model):
self.duration = duration self.duration = duration
self.size = size self.size = size
self.hash = hash_value self.hash = hash_value
self.updated_at = datetime.utcnow() self.updated_at = datetime.now(tz=ZoneInfo("UTC"))
@classmethod @classmethod
def find_by_hash(cls, hash_value: str) -> Optional["Sound"]: def find_by_hash(cls, hash_value: str) -> Optional["Sound"]:

View File

@@ -1,11 +1,13 @@
"""Sound played tracking model.""" """Sound played tracking model."""
from datetime import datetime from datetime import datetime
from zoneinfo import ZoneInfo
from app.database import db
from sqlalchemy import DateTime, ForeignKey, Integer, func, text from sqlalchemy import DateTime, ForeignKey, Integer, func, text
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import db
class SoundPlayed(db.Model): class SoundPlayed(db.Model):
"""Model to track when users play sounds.""" """Model to track when users play sounds."""
@@ -16,15 +18,21 @@ class SoundPlayed(db.Model):
# Foreign keys # Foreign keys
user_id: Mapped[int] = mapped_column( user_id: Mapped[int] = mapped_column(
Integer, ForeignKey("users.id"), nullable=False, Integer,
ForeignKey("users.id"),
nullable=False,
) )
sound_id: Mapped[int] = mapped_column( sound_id: Mapped[int] = mapped_column(
Integer, ForeignKey("sounds.id"), nullable=False, Integer,
ForeignKey("sounds.id"),
nullable=False,
) )
# Timestamp # Timestamp
played_at: Mapped[datetime] = mapped_column( played_at: Mapped[datetime] = mapped_column(
DateTime, default=datetime.utcnow, nullable=False, DateTime,
default=lambda: datetime.now(tz=ZoneInfo("UTC")),
nullable=False,
) )
# Relationships # Relationships
@@ -45,17 +53,25 @@ class SoundPlayed(db.Model):
"user_id": self.user_id, "user_id": self.user_id,
"sound_id": self.sound_id, "sound_id": self.sound_id,
"played_at": self.played_at.isoformat(), "played_at": self.played_at.isoformat(),
"user": { "user": (
{
"id": self.user.id, "id": self.user.id,
"name": self.user.name, "name": self.user.name,
"email": self.user.email, "email": self.user.email,
} if self.user else None, }
"sound": { if self.user
else None
),
"sound": (
{
"id": self.sound.id, "id": self.sound.id,
"name": self.sound.name, "name": self.sound.name,
"filename": self.sound.filename, "filename": self.sound.filename,
"type": self.sound.type, "type": self.sound.type,
} if self.sound else None, }
if self.sound
else None
),
} }
@classmethod @classmethod
@@ -79,7 +95,10 @@ class SoundPlayed(db.Model):
@classmethod @classmethod
def get_user_plays( def get_user_plays(
cls, user_id: int, limit: int = 50, offset: int = 0, cls,
user_id: int,
limit: int = 50,
offset: int = 0,
) -> list["SoundPlayed"]: ) -> list["SoundPlayed"]:
"""Get recent plays for a specific user.""" """Get recent plays for a specific user."""
return ( return (
@@ -92,7 +111,10 @@ class SoundPlayed(db.Model):
@classmethod @classmethod
def get_sound_plays( def get_sound_plays(
cls, sound_id: int, limit: int = 50, offset: int = 0, cls,
sound_id: int,
limit: int = 50,
offset: int = 0,
) -> list["SoundPlayed"]: ) -> list["SoundPlayed"]:
"""Get recent plays for a specific sound.""" """Get recent plays for a specific sound."""
return ( return (
@@ -105,7 +127,9 @@ class SoundPlayed(db.Model):
@classmethod @classmethod
def get_recent_plays( def get_recent_plays(
cls, limit: int = 100, offset: int = 0, cls,
limit: int = 100,
offset: int = 0,
) -> list["SoundPlayed"]: ) -> list["SoundPlayed"]:
"""Get recent plays across all users and sounds.""" """Get recent plays across all users and sounds."""
return ( return (
@@ -127,7 +151,9 @@ class SoundPlayed(db.Model):
@classmethod @classmethod
def get_popular_sounds( def get_popular_sounds(
cls, limit: int = 10, days: int | None = None, cls,
limit: int = 10,
days: int | None = None,
) -> list[dict]: ) -> list[dict]:
"""Get most popular sounds with play counts.""" """Get most popular sounds with play counts."""
from app.models.sound import Sound from app.models.sound import Sound
@@ -144,7 +170,7 @@ class SoundPlayed(db.Model):
if days: if days:
query = query.filter( query = query.filter(
cls.played_at >= text(f"datetime('now', '-{days} days')") cls.played_at >= text(f"datetime('now', '-{days} days')"),
) )
results = query.limit(limit).all() results = query.limit(limit).all()
@@ -154,7 +180,8 @@ class SoundPlayed(db.Model):
for result in results: for result in results:
sound = Sound.query.get(result.sound_id) sound = Sound.query.get(result.sound_id)
if sound: if sound:
popular_sounds.append({ popular_sounds.append(
{
"sound": sound.to_dict(), "sound": sound.to_dict(),
"play_count": result.play_count, "play_count": result.play_count,
"last_played": ( "last_played": (
@@ -162,7 +189,8 @@ class SoundPlayed(db.Model):
if result.last_played if result.last_played
else None else None
), ),
}) }
)
return popular_sounds return popular_sounds
@@ -193,7 +221,8 @@ class SoundPlayed(db.Model):
# Get favorite sound # Get favorite sound
favorite_query = ( favorite_query = (
db.session.query( db.session.query(
cls.sound_id, func.count(cls.id).label("play_count") cls.sound_id,
func.count(cls.id).label("play_count"),
) )
.filter_by(user_id=user_id) .filter_by(user_id=user_id)
.group_by(cls.sound_id) .group_by(cls.sound_id)

View File

@@ -3,6 +3,7 @@
import secrets import secrets
from datetime import datetime from datetime import datetime
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from zoneinfo import ZoneInfo
from sqlalchemy import DateTime, ForeignKey, Integer, String from sqlalchemy import DateTime, ForeignKey, Integer, String
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
@@ -63,13 +64,13 @@ class User(db.Model):
# Timestamps # Timestamps
created_at: Mapped[datetime] = mapped_column( created_at: Mapped[datetime] = mapped_column(
DateTime, DateTime,
default=datetime.utcnow, default=lambda: datetime.now(tz=ZoneInfo("UTC")),
nullable=False, nullable=False,
) )
updated_at: Mapped[datetime] = mapped_column( updated_at: Mapped[datetime] = mapped_column(
DateTime, DateTime,
default=datetime.utcnow, default=lambda: datetime.now(tz=ZoneInfo("UTC")),
onupdate=datetime.utcnow, onupdate=lambda: datetime.now(tz=ZoneInfo("UTC")),
nullable=False, nullable=False,
) )
@@ -103,9 +104,11 @@ class User(db.Model):
"role": self.role, "role": self.role,
"is_active": self.is_active, "is_active": self.is_active,
"api_token": self.api_token, "api_token": self.api_token,
"api_token_expires_at": self.api_token_expires_at.isoformat() "api_token_expires_at": (
self.api_token_expires_at.isoformat()
if self.api_token_expires_at if self.api_token_expires_at
else None, else None
),
"providers": providers, "providers": providers,
"plan": self.plan.to_dict() if self.plan else None, "plan": self.plan.to_dict() if self.plan else None,
"credits": self.credits, "credits": self.credits,
@@ -129,13 +132,13 @@ class User(db.Model):
self.email = provider_data.get("email", self.email) self.email = provider_data.get("email", self.email)
self.name = provider_data.get("name", self.name) self.name = provider_data.get("name", self.name)
self.picture = provider_data.get("picture", self.picture) self.picture = provider_data.get("picture", self.picture)
self.updated_at = datetime.utcnow() self.updated_at = datetime.now(tz=ZoneInfo("UTC"))
db.session.commit() db.session.commit()
def set_password(self, password: str) -> None: def set_password(self, password: str) -> None:
"""Hash and set user password.""" """Hash and set user password."""
self.password_hash = generate_password_hash(password) self.password_hash = generate_password_hash(password)
self.updated_at = datetime.utcnow() self.updated_at = datetime.now(tz=ZoneInfo("UTC"))
def check_password(self, password: str) -> bool: def check_password(self, password: str) -> bool:
"""Check if provided password matches user's password.""" """Check if provided password matches user's password."""
@@ -151,7 +154,7 @@ class User(db.Model):
"""Generate a new API token for the user.""" """Generate a new API token for the user."""
self.api_token = secrets.token_urlsafe(32) self.api_token = secrets.token_urlsafe(32)
self.api_token_expires_at = None # No expiration by default self.api_token_expires_at = None # No expiration by default
self.updated_at = datetime.utcnow() self.updated_at = datetime.now(tz=ZoneInfo("UTC"))
return self.api_token return self.api_token
def is_api_token_valid(self) -> bool: def is_api_token_valid(self) -> bool:
@@ -162,23 +165,23 @@ class User(db.Model):
if self.api_token_expires_at is None: if self.api_token_expires_at is None:
return True # No expiration return True # No expiration
return datetime.utcnow() < self.api_token_expires_at return datetime.now(tz=ZoneInfo("UTC")) < self.api_token_expires_at
def revoke_api_token(self) -> None: def revoke_api_token(self) -> None:
"""Revoke the user's API token.""" """Revoke the user's API token."""
self.api_token = None self.api_token = None
self.api_token_expires_at = None self.api_token_expires_at = None
self.updated_at = datetime.utcnow() self.updated_at = datetime.now(tz=ZoneInfo("UTC"))
def activate(self) -> None: def activate(self) -> None:
"""Activate the user account.""" """Activate the user account."""
self.is_active = True self.is_active = True
self.updated_at = datetime.utcnow() self.updated_at = datetime.now(tz=ZoneInfo("UTC"))
def deactivate(self) -> None: def deactivate(self) -> None:
"""Deactivate the user account.""" """Deactivate the user account."""
self.is_active = False self.is_active = False
self.updated_at = datetime.utcnow() self.updated_at = datetime.now(tz=ZoneInfo("UTC"))
@classmethod @classmethod
def find_by_email(cls, email: str) -> Optional["User"]: def find_by_email(cls, email: str) -> Optional["User"]:
@@ -218,7 +221,7 @@ class User(db.Model):
oauth_provider.email = email oauth_provider.email = email
oauth_provider.name = name oauth_provider.name = name
oauth_provider.picture = picture oauth_provider.picture = picture
oauth_provider.updated_at = datetime.utcnow() oauth_provider.updated_at = datetime.now(tz=ZoneInfo("UTC"))
# Update user info with latest data # Update user info with latest data
user.update_from_provider( user.update_from_provider(

View File

@@ -2,6 +2,7 @@
from datetime import datetime from datetime import datetime
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from zoneinfo import ZoneInfo
from sqlalchemy import DateTime, ForeignKey, String, Text from sqlalchemy import DateTime, ForeignKey, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
@@ -34,13 +35,13 @@ class UserOAuth(db.Model):
# Timestamps # Timestamps
created_at: Mapped[datetime] = mapped_column( created_at: Mapped[datetime] = mapped_column(
DateTime, DateTime,
default=datetime.utcnow, default=lambda: datetime.now(tz=ZoneInfo("UTC")),
nullable=False, nullable=False,
) )
updated_at: Mapped[datetime] = mapped_column( updated_at: Mapped[datetime] = mapped_column(
DateTime, DateTime,
default=datetime.utcnow, default=lambda: datetime.now(tz=ZoneInfo("UTC")),
onupdate=datetime.utcnow, onupdate=lambda: datetime.now(tz=ZoneInfo("UTC")),
nullable=False, nullable=False,
) )
@@ -107,7 +108,7 @@ class UserOAuth(db.Model):
oauth_provider.email = email oauth_provider.email = email
oauth_provider.name = name oauth_provider.name = name
oauth_provider.picture = picture oauth_provider.picture = picture
oauth_provider.updated_at = datetime.utcnow() oauth_provider.updated_at = datetime.now(tz=ZoneInfo("UTC"))
else: else:
# Create new provider # Create new provider
oauth_provider = cls( oauth_provider = cls(

View File

@@ -2,6 +2,7 @@
import logging import logging
from datetime import datetime from datetime import datetime
from zoneinfo import ZoneInfo
from app.database import db from app.database import db
from app.models.user import User from app.models.user import User
@@ -62,7 +63,7 @@ class CreditService:
if credits_added > 0: if credits_added > 0:
user.credits = new_credits user.credits = new_credits
user.updated_at = datetime.utcnow() user.updated_at = datetime.now(tz=ZoneInfo("UTC"))
total_credits_added += credits_added total_credits_added += credits_added
logger.debug( logger.debug(