tortoise / tortoise-orm

Familiar asyncio ORM for python, built with relations in mind
https://tortoise.github.io
Apache License 2.0
4.54k stars 374 forks source link

Version 0.19.1 is not fetching datetime field #1171

Open kraghuprasad opened 2 years ago

kraghuprasad commented 2 years ago

Describe the bug I am using version 0.19.1 of tortoise-orm. It is not fetching the datetime fields of models correctly from from postgres database. This problem is not present in version 0.19.0.

To Reproduce Following is the collated code from multiple files in my project. I have tried to put relevant sections of models and mixins here. I am making a FastAPI app.

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from tortoise.contrib.fastapi import register_tortoise
from tortoise import Tortoise
from typing import Any
from typing import Optional, List
from datetime import datetime
from tortoise.fields import IntField, CharField, DatetimeField, BooleanField
from tortoise.fields.relational import ForeignKeyField
from tortoise.fields.base import RESTRICT
from tortoise.models import Model
from typing import List
from fastapi import APIRouter, Query, HTTPException
from tortoise.contrib.fastapi import HTTPNotFoundError
from tortoise.exceptions import DoesNotExist
from tortoise.contrib.pydantic import pydantic_model_creator

# These are my application specific. Code not included.
# from ..utils.hashing import Hasher
# from ..core.config import settings

class EntityMixin(Model):
    """
    Entity (common entity) class for other entities to inherit from.
    """
    id: int = IntField(
        pk=True, index=True, null=False,
        description="The primary identifier of this entity.")
    name: str = CharField(64, index=True, null=False,
                          description="The name of this entity.")
    status: str = CharField(1, default='A', index=True,
                            description="Status of this entity: A, I, or D.")

    def __str__(self):
        return self.name

    def is_active(self):
        """
        Function to check if the Entity object is in active state.
        """
        return self.status == "A"

    class Meta:
        abstract = True

class CreatorMixin():
    """
    Mixin class to define creator and creation time related attributes for
    Entity type objects.
    """
    created_at: Optional[datetime] = DatetimeField(
        auto_now_add=True, null=False)

    class Meta:
        abstract = True

class LastModifierMixin():
    """
    Mixin class to define modifier and modification time related attributes for
    Entity type objects.
    """
    last_modified_at: Optional[datetime] = DatetimeField(
        null=True, index=True, auto_now=True)

    class Meta:
        abstract = True

class Authuser(EntityMixin, CreatorMixin, LastModifierMixin):
    """
    Authuser data model.
    """
    login_name: Optional[str] = CharField(
        80, description="Login identifier string.", unique=True, null=False)
    email_address: Optional[str] = CharField(
        80, description="E-mail address of the user.", unique=True, null=False)
    password: Optional[str] = CharField(
        512, description="Password associated with the user.", null=False)
    is_superuser: bool = BooleanField(
        description="Indicates if the user is super-user.", null=False)

    class Meta:
        table = "authusers"
        table_description = "Stores records of users who can authenticate."
        ordering = ["name", "created_at"]

AuthuserIn = pydantic_model_creator(
    Authuser, name="AuthuserIn", include=(
        "name", "login_name", "email_address", "password", "is_superuser",
        "status"))

AuthuserOut = pydantic_model_creator(
    Authuser, name="AuthuserOut", exclude=("password", ),
    exclude_readonly=False)

err_response = {404: {"model": HTTPNotFoundError}}

router = APIRouter(
    prefix='/authusers',
    tags=['Users'],
    responses={404: {"description": "Authuser not found."}}
)

@router.get("/", response_model=List[AuthuserOut])
async def get_authuser_list(
        skip: int = 0, pick: int = 10) -> [AuthuserOut]:
    """
    Fetches and returns a sequence of AuthuserOut objects having status A.
    """
    return await AuthuserOut.from_queryset(Authuser.all())

@router.get("/{id}", response_model=AuthuserOut, responses=err_response)
async def get_authuser_single(id: int) -> AuthuserOut:
    """
    Fetches and returns an AuthuserOut object for a non-deleted Auther object
    of supplied id.
    """
    try:
        return await AuthuserOut.from_queryset_single(Authuser.get(id=id))
    except DoesNotExist:
        raise HTTPException(
            status_code=404, detail=f"User with id {id} not found")

@router.post("/", response_model=AuthuserOut)
async def create_authuser(
        #   user: str = Depends(get_current_active_user),
        authuser_in: AuthuserIn) -> AuthuserOut:
    """
    Create a new authuser from supplied AuthuserIn object, persist it in DB,
    and then return it along with its id.
    """
    # authuser_in.password = Hasher.hash_create(authuser_in.password)
    attrs = authuser_in.dict()
    user = await m.Authuser.create(**attrs)
    userout = AuthuserOut.from_orm(user)
    print(userout.dict())  # Here created_at and last_modified_at fields are found to be null. *BUG*
    return userout

# Some values are being picked-up from an environment file .env which gets loaded by the settings module.
# I am not including its code here.
orm_config = {'connections': {'default': settings.DATABASE_URL},
              'apps': {
                  'models': {
                      'models': ["backend.db.models"],
                      'default_connection': 'default', }},
              'use_tz': True,
              'timezone': 'UTC',
              'generate_schemas': False,
              'add_exception_handlers': True, }

app = FastAPI(
    title=settings.PROJECT_TITLE,
    description=settings.APP_DESCRIPTION,
    version=settings.PROJECT_VERSION,
    root_path='/8000',
    docs_url='/docs',
    redoc_url='/redoc',
    openapi_url='/openapi.json')

app.add_middleware(CORSMiddleware, allow_origins=[settings.APP_HOST_URL, ],
                   allow_credentials=True, allow_methods=["*"],
                   allow_headers=["*"])

register_tortoise(app, orm_config, generate_schemas=True)

@app.get("/version")
async def get_version() -> Any:
    """
    Returns the version number of the application.
    """
    return {"version": settings.PROJECT_VERSION}

Expected behavior After a POST request is made in /authusers, the object returned (AuthuserOut) has its created_at and last_modified_at attributes set to null if I use version 0.19.1 of tortoise-orm. Correct date-time is shown if I use version 0.19.0.

Additional context I am using Tortoise ORM for building a FastAPI application. There are multiple files in the project. I have tried to collect only relevant sections of the code to showcase the issue. May be some parts or modules missing from the supplied code.

long2ice commented 2 years ago

Show me the code

kraghuprasad commented 2 years ago

Show me the code

Sorry for the mess-up. The full text of the issue I had written got lost when github directed me towards the code submission guideline which opened in the same window. While posting it, I didn't notice that it was lost. Now I have rewritten the same by modifying this bug report.

waketzheng commented 1 year ago

My test result: 0.19.1 can reproduce the bug And 0.19.3 fixed

kraghuprasad commented 1 year ago

Thanks. I'll check again.