diff --git a/README.md b/README.md index 62917c79..b69e6811 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ annofabapiを使ったCLI(Command Line Interface)ツールです。 # 注意 * 作者または著作権者は、ソフトウェアに関してなんら責任を負いません。 * 予告なく互換性のない変更がある可能性をご了承ください。 -* AnnoFabプロジェクトに大きな変更を及ぼすツールも存在します。間違えて実行してしまわないよう、注意してご利用ください。 +* AnnoFabプロジェクトに大きな変更を及ぼすコマンドも存在します。間違えて実行してしまわないよう、注意してご利用ください。 ## 廃止予定 @@ -33,7 +33,7 @@ AnnoFabの認証情報を設定する方法は2つあります。 AnnoFabの認証情報が設定されていない状態で`annofabcli`コマンドを実行すると、標準入力からAnnoFabの認証情報を入力できるようになります。 ``` -$ annofabcli diff_projects aaa bbb +$ annofabcli project diff aaa bbb Enter AnnoFab User ID: XXXXXX Enter AnnoFab Password: ``` @@ -67,9 +67,10 @@ $ docker run -it -e ANNOFAB_USER_ID=XXXX -e ANNOFAB_PASSWORD=YYYYY annofab-cli a |----|-------------------------------|----------------------------------------------------------------------------------------------------------|------------| |input_data|list | 入力データ一覧を出力する。 |-| |instruction| upload | HTMLファイルを作業ガイドとして登録する。 |チェッカー/オーナ| -|task|list | タスク一覧を出力する。 |-| |task| cancel_acceptance | 受け入れ完了タスクを、受け入れ取り消しする。 |オーナ| +|task| change_operator | タスクの担当者を変更する。 |チェッカー/オーナ| |task| complete | 未処置の検査コメントを適切な状態に変更して、タスクを受け入れ完了にする。 |チェッカー/オーナ| +|task|list | タスク一覧を出力する。 |-| |task| reject | 検査コメントを付与してタスクを差し戻す。 |チェッカー/オーナ| |project| diff | プロジェクト間の差分を表示する |チェッカー/オーナ| |project| download | タスクや検査コメント、アノテーションなどをダウンロードします。 |オーナ| @@ -104,13 +105,13 @@ CSVのフォーマットをJSON形式で指定します。`--format`が`csv`で ### `--disable_log` -ログを無効化する。 +ログを無効化にします。 ### `f` / `--format` 出力フォーマットを指定します。基本的に以下のフォーマットを指定できます。 -* csv : CSV(デフォルとはカンマ区切り) -* json : インデントや空白がないJSON -* pretty_json : インデントされたJSON +* `csv` : CSV(デフォルとはカンマ区切り) +* `json` : インデントや空白がないJSON +* `pretty_json` : インデントされたJSON list系のコマンドで利用できます。 @@ -130,7 +131,7 @@ $ annofabcli project diff -h ログファイルを保存するディレクトリを指定します。指定しない場合、`.log`ディレクトリにログファイルを出力します。 ### `--logging_yaml` -ロギグングの設定ファイル(YAML)を指定します。指定した場合、`--logdir`オプションは無視されます。指定しない場合、デフォルトのロギング設定ファイルが読み込まれます。 +以下のような、ロギグングの設定ファイル(YAML)を指定します。指定した場合、`--logdir`オプションは無視されます。指定しない場合、デフォルトのロギング設定ファイルが読み込まれます。 設定ファイルの書き方は https://docs.python.org/ja/3/howto/logging.html を参照してください。 ```yaml:logging-sample.yaml @@ -320,6 +321,18 @@ $ annofabcli task cancel_acceptance --project_id prj1 --task_id file://task.txt ``` +### task change_operator +タスクの担当者を変更します。 + + +``` +# 指定されたタスクの担当者を 'user1' に変更する。 +$ annofabcli task change_operator --project_id prj1 --task_id file://task.txt --user_id usr1 + +# 指定されたタスクの担当者を未割り当てに変更する。 +$ annofabcli task change_operator --project_id prj1 --task_id file://task.txt --not_assign +``` + ### task complete 未処置の検査コメントを適切な状態に変更して、タスクを受け入れ完了にします。 diff --git a/annofabcli/__version__.py b/annofabcli/__version__.py index 51ed7c48..bcd8d54e 100644 --- a/annofabcli/__version__.py +++ b/annofabcli/__version__.py @@ -1 +1 @@ -__version__ = '1.5.1' +__version__ = '1.6.0' diff --git a/annofabcli/common/facade.py b/annofabcli/common/facade.py index 32331089..05c56b88 100644 --- a/annofabcli/common/facade.py +++ b/annofabcli/common/facade.py @@ -287,14 +287,15 @@ def download_latest_simple_annotation_archive_with_waiting(self, project_id: str # operateTaskのfacade ################## - def change_operator_of_task(self, project_id: str, task_id: str, account_id: str) -> Dict[str, Any]: + def change_operator_of_task(self, project_id: str, task_id: str, + account_id: Optional[str] = None) -> Dict[str, Any]: """ タスクの担当者を変更する Args: self: project_id: task_id: - account_id: + account_id: 新しい担当者のuser_id. Noneの場合未割り当てになる。 Returns: 変更後のtask情報 diff --git a/annofabcli/task/change_operator.py b/annofabcli/task/change_operator.py new file mode 100644 index 00000000..ae1e435b --- /dev/null +++ b/annofabcli/task/change_operator.py @@ -0,0 +1,129 @@ +""" +検査コメントを付与してタスクを差し戻します。 +""" + +import argparse +import logging +from typing import Any, Dict, List, Optional # pylint: disable=unused-import + +import requests +from annofabapi.dataclass.task import Task +from annofabapi.models import ProjectMemberRole, TaskStatus + +import annofabcli +import annofabcli.common.cli +from annofabcli import AnnofabApiFacade +from annofabcli.common.cli import AbstractCommandLineInterface, ArgumentParser, build_annofabapi_resource_and_login + +logger = logging.getLogger(__name__) + + +class ChangeOperator(AbstractCommandLineInterface): + def confirm_change_operator(self, task: Task, user_id: Optional[str] = None) -> bool: + if user_id is None: + str_user_id = "未割り当て" + else: + str_user_id = user_id + + confirm_message = f"task_id = {task.task_id} のタスクの担当者を '{str_user_id}' にしますか?" + return self.confirm_processing(confirm_message) + + def change_operator(self, project_id: str, task_id_list: List[str], user_id: Optional[str] = None): + """ + 検査コメントを付与して、タスクを差し戻す + Args: + project_id: + task_id_list: + user_id: タスクを担当するユーザのuser_id。Noneの場合タスクの担当者は未割り当て + + """ + + super().validate_project(project_id, [ProjectMemberRole.OWNER, ProjectMemberRole.ACCEPTER]) + + if user_id is not None: + account_id = self.facade.get_account_id_from_user_id(project_id, user_id) + if account_id is None: + logger.error(f"ユーザ '{user_id}' のaccount_idが見つかりませんでした。終了します。") + return + else: + account_id = None + + logger.info(f"タスクの担当者を変更する件数: {len(task_id_list)}") + success_count = 0 + + for task_index, task_id in enumerate(task_id_list): + str_progress = annofabcli.utils.progress_msg(task_index + 1, len(task_id_list)) + + dict_task, _ = self.service.api.get_task(project_id, task_id) + task: Task = Task.from_dict(dict_task) + + logger.debug(f"{str_progress} : task_id = {task.task_id}, " + f"status = {task.status.value}, " + f"phase = {task.phase.value}, " + f"user_id = {self.facade.get_user_id_from_account_id(project_id, task.account_id)}") + + if task.status in [TaskStatus.COMPLETE, TaskStatus.WORKING]: + logger.warning( + f"{str_progress} : task_id = {task_id} : タスクのstatusがworking or complete なので、担当者を変更できません。") + continue + + if not self.confirm_change_operator(task, user_id): + continue + + try: + # 担当者を変更する + self.facade.change_operator_of_task(project_id, task_id, account_id) + success_count += 1 + logger.debug(f"{str_progress} : task_id = {task_id}, phase={dict_task['phase']}, {user_id}に担当者を変更しました。") + + except requests.exceptions.HTTPError as e: + logger.warning(e) + logger.warning(f"{str_progress} : task_id = {task_id} の担当者を変更するのに失敗しました。") + continue + + logger.info(f"{success_count} / {len(task_id_list)} 件 タスクの担当者を変更しました。") + + def main(self): + args = self.args + + task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id) + if args.user_id is not None: + user_id = args.user_id + elif args.not_assign: + user_id = None + else: + logger.error(f"タスクの担当者の指定方法が正しくありません。") + return + + self.change_operator(args.project_id, task_id_list, user_id) + + +def main(args: argparse.Namespace): + service = build_annofabapi_resource_and_login() + facade = AnnofabApiFacade(service) + ChangeOperator(service, facade, args).main() + + +def parse_args(parser: argparse.ArgumentParser): + argument_parser = ArgumentParser(parser) + + argument_parser.add_project_id() + argument_parser.add_task_id() + + assign_group = parser.add_mutually_exclusive_group(required=True) + + assign_group.add_argument('-u', '--user_id', type=str, help='タスクを新しく担当するユーザのuser_idを指定してください。') + + assign_group.add_argument('--not_assign', action="store_true", help='指定した場合、タスクの担当者は未割り当てになります。') + + parser.set_defaults(subcommand_func=main) + + +def add_parser(subparsers: argparse._SubParsersAction): + subcommand_name = "change_operator" + subcommand_help = "タスクの担当者を変更します。" + description = ("タスクの担当者を変更します。ただし、作業中のタスクに対しては変更しません。") + epilog = "チェッカーまたはオーナロールを持つユーザで実行してください。" + + parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog) + parse_args(parser) diff --git a/annofabcli/task/subcommand_task.py b/annofabcli/task/subcommand_task.py index ff10b9a5..cc2e3156 100644 --- a/annofabcli/task/subcommand_task.py +++ b/annofabcli/task/subcommand_task.py @@ -3,6 +3,7 @@ import annofabcli import annofabcli.common.cli import annofabcli.task.cancel_acceptance +import annofabcli.task.change_operator import annofabcli.task.complete_tasks import annofabcli.task.list_tasks import annofabcli.task.reject_tasks @@ -13,10 +14,11 @@ def parse_args(parser: argparse.ArgumentParser): subparsers = parser.add_subparsers(dest='subcommand_name') # サブコマンドの定義 + annofabcli.task.cancel_acceptance.add_parser(subparsers) + annofabcli.task.change_operator.add_parser(subparsers) + annofabcli.task.complete_tasks.add_parser(subparsers) annofabcli.task.list_tasks.add_parser(subparsers) annofabcli.task.reject_tasks.add_parser(subparsers) - annofabcli.task.complete_tasks.add_parser(subparsers) - annofabcli.task.cancel_acceptance.add_parser(subparsers) def add_parser(subparsers: argparse._SubParsersAction): diff --git a/tests/test_main.py b/tests/test_main.py index a09bfaca..aecbea98 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -3,7 +3,6 @@ """ import configparser import datetime -import json import os from pathlib import Path @@ -35,16 +34,37 @@ def get_organization_name(project_id: str) -> str: return organization["organization_name"] -def test_task(): - main([ - 'task', 'list', '--project_id', project_id, '--task_query', - f'{{"user_id": "{user_id}", "phase":"acceptance", "status": "complete"}}', '--format', 'csv' - ]) +class TestTask: + command_name = "task" + + def test_list(self): + main([ + self.command_name, 'list', '--project_id', project_id, '--task_query', + f'{{"user_id": "{user_id}", "phase":"acceptance", "status": "complete"}}', '--format', 'csv' + ]) + + def test_cancel_acceptance(self): + main([self.command_name, 'cancel_acceptance', '--project_id', project_id, '--task_id', task_id, '--yes']) + + def test_reject_task(self): + inspection_comment = datetime.datetime.now().isoformat() + main([ + self.command_name, 'reject', '--project_id', project_id, '--task_id', task_id, '--comment', + inspection_comment, '--yes' + ]) - main(['task', 'cancel_acceptance', '--project_id', project_id, '--task_id', task_id, '--yes']) + def test_change_operator(self): + # user指定 + main([ + self.command_name, 'change_operator', '--project_id', project_id, '--task_id', task_id, '--user_id', + user_id, '--yes' + ]) - inspection_comment = datetime.datetime.now().isoformat() - main(['task', 'reject', '--project_id', project_id, '--task_id', task_id, '--comment', inspection_comment, '--yes']) + # 未割り当て + main([ + self.command_name, 'change_operator', '--project_id', project_id, '--task_id', task_id, '--not_assign', + '--yes' + ]) def test_project(): @@ -93,8 +113,8 @@ def test_filesystem(): main([ 'filesystem', 'write_annotation_image', '--annotation', str(zip_path), '--output_dir', - str(output_image_dir), '--image_size', '64x64', '--label_color', - f"file://{str(label_color_file)}", '--image_extension', 'jpg' + str(output_image_dir), '--image_size', '64x64', '--label_color', f"file://{str(label_color_file)}", + '--image_extension', 'jpg' ])