Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(commands): add creation timestamp and user name inside commands #2252

Merged
merged 16 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
35 changes: 31 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
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
18 changes: 18 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,23 @@ 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)
assert commands_res.status_code == 200
assert commands_res.json()
maugde marked this conversation as resolved.
Show resolved Hide resolved

for command in commands_res.json():
# 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
maugde marked this conversation as resolved.
Show resolved Hide resolved
# 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
Loading