Skip to content

Commit

Permalink
feat(commands): add creation timestamp and user name inside commands (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
maugde authored Dec 11, 2024
1 parent a5a2e1e commit 93884de
Show file tree
Hide file tree
Showing 12 changed files with 251 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""add_user_name_and_updated_at_to_commands
Revision ID: bae9c99bc42d
Revises: 00a9ceb38842
Create Date: 2024-11-29 09:13:04.292874
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'bae9c99bc42d'
down_revision = '00a9ceb38842'
branch_labels = None
depends_on = None

USER_ID_FKEY = 'commandblock_user_id_fk'


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('commandblock', schema=None) as batch_op:
batch_op.add_column(sa.Column('user_id', sa.Integer(), nullable=True))
batch_op.add_column(sa.Column('updated_at', sa.DateTime(), nullable=True))
batch_op.create_foreign_key(USER_ID_FKEY, 'identities', ['user_id'], ['id'], ondelete='SET NULL')
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('commandblock', schema=None) as batch_op:
batch_op.drop_constraint(USER_ID_FKEY, type_='foreignkey')
batch_op.drop_column('updated_at')
batch_op.drop_column('user_id')
# ### end Alembic commands ###
6 changes: 6 additions & 0 deletions antarest/study/storage/variantstudy/model/dbmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ class CommandBlock(Base): # type: ignore
version: int = Column(Integer)
args: str = Column(String())
study_version: str = Column(String(36))
user_id: int = Column(Integer, ForeignKey("identities.id", ondelete="SET NULL"), nullable=True)
updated_at: datetime.datetime = Column(DateTime, nullable=True)

def to_dto(self) -> CommandDTO:
# Database may lack a version number, defaulting to 1 if so.
Expand All @@ -76,6 +78,8 @@ def to_dto(self) -> CommandDTO:
args=from_json(self.args),
version=version,
study_version=self.study_version,
user_id=self.user_id,
updated_at=self.updated_at,
)

def __str__(self) -> str:
Expand All @@ -87,6 +91,8 @@ def __str__(self) -> str:
f" version={self.version!r},"
f" args={self.args!r})"
f" study_version={self.study_version!r}"
f" user_id={self.user_id!r}"
f" updated_at={self.updated_at!r}"
)


Expand Down
14 changes: 11 additions & 3 deletions antarest/study/storage/variantstudy/model/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# SPDX-License-Identifier: MPL-2.0
#
# This file is part of the Antares project.

import datetime
import typing as t
import uuid

Expand Down Expand Up @@ -73,6 +73,8 @@ class CommandDTOAPI(AntaresBaseModel):
action: str
args: t.Union[t.MutableSequence[JSON], JSON]
version: int = 1
user_name: t.Optional[str] = None
updated_at: t.Optional[datetime.datetime] = None


class CommandDTO(AntaresBaseModel):
Expand All @@ -85,16 +87,22 @@ class CommandDTO(AntaresBaseModel):
args: The arguments for the command action.
version: The version of the command.
study_version: The version of the study associated to the command.
user_id: id of the author of the command.
updated_at: The time the command was last updated.
"""

id: t.Optional[str] = None
action: str
args: t.Union[t.MutableSequence[JSON], JSON]
version: int = 1
study_version: StudyVersionStr
user_id: t.Optional[int] = None
updated_at: t.Optional[datetime.datetime] = None

def to_api(self) -> CommandDTOAPI:
return CommandDTOAPI.model_validate(self.model_dump(mode="json", exclude={"study_version"}))
def to_api(self, user_name: t.Optional[str] = None) -> CommandDTOAPI:
data = self.model_dump(mode="json", exclude={"study_version", "user_id"})
data["user_name"] = user_name
return CommandDTOAPI.model_validate(data)


class CommandResultDTO(AntaresBaseModel):
Expand Down
39 changes: 35 additions & 4 deletions antarest/study/storage/variantstudy/variant_study_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,15 @@
from antarest.core.filetransfer.model import FileDownloadTaskDTO
from antarest.core.interfaces.cache import ICache
from antarest.core.interfaces.eventbus import Event, EventChannelDirectory, EventType, IEventBus
from antarest.core.jwt import DEFAULT_ADMIN_USER
from antarest.core.jwt import DEFAULT_ADMIN_USER, JWTUser
from antarest.core.model import JSON, PermissionInfo, PublicMode, StudyPermissionType
from antarest.core.requests import RequestParameters, UserHasNotPermissionError
from antarest.core.serialization import to_json_string
from antarest.core.tasks.model import CustomTaskEventMessages, TaskDTO, TaskResult, TaskType
from antarest.core.tasks.service import DEFAULT_AWAIT_MAX_TIMEOUT, ITaskNotifier, ITaskService, NoopNotifier
from antarest.core.utils.fastapi_sqlalchemy import db
from antarest.core.utils.utils import assert_this, suppress_exception
from antarest.login.model import Identity
from antarest.matrixstore.service import MatrixService
from antarest.study.model import RawStudy, Study, StudyAdditionalData, StudyMetadataDTO, StudySimResultDTO
from antarest.study.repository import AccessPermissions, StudyFilter
Expand Down Expand Up @@ -106,6 +107,17 @@ def __init__(
self.command_factory = command_factory
self.generator = VariantCommandGenerator(self.study_factory)

@staticmethod
def _get_user_name_from_id(user_id: int) -> str:
"""
Utility method that retrieves a user's name based on their id.
Args:
user_id: user id (user must exist)
Returns: String representing the user's name
"""
user_obj: Identity = db.session.query(Identity).get(user_id)
return user_obj.name # type: ignore # `name` attribute is always a string

def get_command(self, study_id: str, command_id: str, params: RequestParameters) -> CommandDTOAPI:
"""
Get command lists
Expand All @@ -118,8 +130,10 @@ def get_command(self, study_id: str, command_id: str, params: RequestParameters)
study = self._get_variant_study(study_id, params)

try:
index = [command.id for command in study.commands].index(command_id) # Maybe add Try catch for this
return t.cast(CommandDTOAPI, study.commands[index].to_dto().to_api())
index = [command.id for command in study.commands].index(command_id)
command: CommandBlock = study.commands[index]
user_name = self._get_user_name_from_id(command.user_id) if command.user_id else None
return command.to_dto().to_api(user_name)
except ValueError:
raise CommandNotFoundError(f"Command with id {command_id} not found") from None

Expand All @@ -132,7 +146,16 @@ def get_commands(self, study_id: str, params: RequestParameters) -> t.List[Comma
Returns: List of commands
"""
study = self._get_variant_study(study_id, params)
return [command.to_dto().to_api() for command in study.commands]

id_to_name: t.Dict[int, str] = {}
command_list = []

for command in study.commands:
if command.user_id and command.user_id not in id_to_name.keys():
user_name: str = self._get_user_name_from_id(command.user_id)
id_to_name[command.user_id] = user_name
command_list.append(command.to_dto().to_api(id_to_name.get(command.user_id)))
return command_list

def convert_commands(
self, study_id: str, api_commands: t.List[CommandDTOAPI], params: RequestParameters
Expand Down Expand Up @@ -201,6 +224,7 @@ def append_commands(
command_objs = self._check_commands_validity(study_id, commands)
validated_commands = transform_command_to_dto(command_objs, commands)
first_index = len(study.commands)

# noinspection PyArgumentList
new_commands = [
CommandBlock(
Expand All @@ -209,6 +233,9 @@ def append_commands(
index=(first_index + i),
version=command.version,
study_version=str(command.study_version),
# params.user cannot be None, since previous checks were successful
user_id=params.user.id, # type: ignore
updated_at=datetime.utcnow(),
)
for i, command in enumerate(validated_commands)
]
Expand Down Expand Up @@ -249,6 +276,8 @@ def replace_commands(
index=i,
version=command.version,
study_version=str(command.study_version),
user_id=params.user.id, # type: ignore
updated_at=datetime.utcnow(),
)
for i, command in enumerate(validated_commands)
]
Expand Down Expand Up @@ -893,6 +922,8 @@ def copy(
index=command.index,
version=command.version,
study_version=str(command.study_version),
user_id=command.user_id,
updated_at=command.updated_at,
)
for command in src_meta.commands
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
# SPDX-License-Identifier: MPL-2.0
#
# This file is part of the Antares project.

from http import HTTPStatus
from unittest.mock import ANY

Expand Down Expand Up @@ -137,6 +136,8 @@ def test_get_inflow_structure(
"data": {"intermonthly-correlation": 0.9},
},
"version": 1,
"updated_at": ANY,
"user_name": ANY,
}
assert actual[1] == expected

Expand Down
6 changes: 6 additions & 0 deletions tests/integration/study_data_blueprint/test_st_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,8 @@ def test__default_values(
"inflows": ANY,
},
"version": 1,
"updated_at": ANY,
"user_name": ANY,
}
assert actual == expected

Expand All @@ -693,6 +695,8 @@ def test__default_values(
"target": "input/st-storage/clusters/fr/list/siemens battery/initiallevel",
},
"version": 1,
"updated_at": ANY,
"user_name": ANY,
}
assert actual == expected

Expand Down Expand Up @@ -723,6 +727,8 @@ def test__default_values(
},
],
"version": 1,
"updated_at": ANY,
"user_name": ANY,
}
assert actual == expected

Expand Down
16 changes: 16 additions & 0 deletions tests/integration/test_integration_token_end_to_end.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#
# This file is part of the Antares project.

import datetime
import io
import typing as t
from unittest.mock import ANY
Expand Down Expand Up @@ -173,6 +174,21 @@ def test_nominal_case_of_an_api_user(client: TestClient, admin_access_token: str
res = client.post(f"/v1/studies/{variant_id}/commands", headers=bot_headers, json=commands)
assert res.status_code == 200

# Check if the author's name and date of update are retrieved with commands created by a bot
commands_res = client.get(f"/v1/studies/{variant_id}/commands", headers=bot_headers)

for command in commands_res.json():
# FIXME: Some commands, such as those that modify study configurations, are run by admin user
# Thus the `user_name` for such type of command will be the admin's name
# Here we detect those commands by their `action` and their `target` values
if command["action"] == "update_playlist" or (
command["action"] == "update_config" and "settings/generaldata" in command["args"]["target"]
):
assert command["user_name"] == "admin"
else:
assert command["user_name"] == "admin_bot"
assert command["updated_at"]

# generate variant before running a simulation
res = client.put(f"/v1/studies/{variant_id}/generate", headers=bot_headers)
assert res.status_code == 200
Expand Down
7 changes: 6 additions & 1 deletion tests/study/storage/variantstudy/model/test_dbmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def test_init__with_command(self, db_session: Session, variant_study_id: str) ->


class TestCommandBlock:
def test_init(self, db_session: Session, variant_study_id: str) -> None:
def test_init(self, db_session: Session, variant_study_id: str, user_id: int) -> None:
"""
Check the creation of an instance of CommandBlock
"""
Expand All @@ -136,6 +136,7 @@ def test_init(self, db_session: Session, variant_study_id: str) -> None:
command = "dummy command"
version = 42
args = '{"foo": "bar"}'
updated_at = datetime.datetime.utcnow()

with db_session:
block = CommandBlock(
Expand All @@ -146,6 +147,8 @@ def test_init(self, db_session: Session, variant_study_id: str) -> None:
version=version,
args=args,
study_version="860",
updated_at=updated_at,
user_id=user_id,
)
db_session.add(block)
db_session.commit()
Expand All @@ -172,6 +175,8 @@ def test_init(self, db_session: Session, variant_study_id: str) -> None:
"args": json.loads(args),
"version": 42,
"study_version": StudyVersion.parse("860"),
"updated_at": updated_at,
"user_id": user_id,
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def test_generate_task(
) -> None:
## Prepare database objects
# noinspection PyArgumentList
user = User(id=0, name="admin")
user = User(id=1, name="admin")
db.session.add(user)
db.session.commit()

Expand Down
2 changes: 2 additions & 0 deletions tests/variantstudy/model/command/test_create_cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ def test_to_dto(self, command_context: CommandContext):
"id": None,
"version": 1,
"study_version": STUDY_VERSION_8_8,
"user_id": None,
"updated_at": None,
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ def test_to_dto(self, command_context: CommandContext) -> None:
"id": None,
"version": 1,
"study_version": STUDY_VERSION_8_8,
"updated_at": None,
"user_id": None,
}


Expand Down
Loading

0 comments on commit 93884de

Please sign in to comment.