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

Develop v2.5.0 #13

Merged
merged 3 commits into from
May 30, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
30 changes: 25 additions & 5 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ CSVファイルに記載された情報を読み込んで、Redmineにチケッ
入手したjarファイルを指定してアプリケーションを実行します。

```
java -jar redmine-issue-loader-2.4.1-all.jar config.json issues.csv
java -jar redmine-issue-loader-2.5.0-all.jar config.json issues.csv
```

第1引数が設定ファイル、第2引数がチケットの情報が書かれたCSVファイルとなります。
Expand Down Expand Up @@ -85,6 +85,15 @@ Processing is completed. 3 issues were loaded.
"type": "CUSTOM_FIELD",
"customFieldId": 2,
"multipleItemSeparator": ";"
},
{
"headerName": "Watchers",
"type": "WATCHER_USER_IDS",
"multipleItemSeparator": ";",
"mappings": {
"User A": 5,
"User B": 6
}
}
]
}
Expand All @@ -93,10 +102,10 @@ Processing is completed. 3 issues were loaded.
上記に対応するCSVファイルの例です。

```csv
Project,Tracker,Subject,Description,Field1,Field2,Field3
Project A,Bug,xxxx,yyyy,A,1;2,C
Project,Tracker,Subject,Description,Field1,Field2,Watchers
Project A,Bug,xxxx,yyyy,A,1;2,User A;User B
Project B,Feature,aaaa,bbbb,,,
Project B,Bug,zzzz,zzzz,1,2,3
Project B,Bug,zzzz,zzzz,1,2,User B
```

### 例: 更新時
Expand Down Expand Up @@ -158,7 +167,7 @@ Project B,Bug,zzzz,zzzz,1,2,3
* `headerName` : CSV内のヘッダ名。
* `type` : 種別。種別として指定可能なものは後述。
* `customFieldId` : カスタムフィールドのID。種別が`CUSTOM_FIELD`の場合に設定する。
* `multipleItemSeparator` : 値を分割する文字。複数選択のカスタムフィールドの場合に設定する
* `multipleItemSeparator` : 値を分割する文字。種別が`WATCHER_USER_IDS`、または`CUSTOM_FIELD`で複数選択の場合に設定する
* `primaryKey` : プライマリーキーか。更新時のみ有効な項目であり、`true`となっているフィールドの情報を使って更新対象のチケットを検索し、`false`となっているフィールドが更新されることとなる。
* `mappings` : CSV上の値とRedmine上での値のマッピングを記載することによって、CSVの内容を変換して登録できる。たとえば、プロジェクト名をプロジェクトIDに変換する場合など。

Expand All @@ -183,6 +192,7 @@ Project B,Bug,zzzz,zzzz,1,2,3
|`IS_PRIVATE`|○|○|プライベートか。`true`または`false`を指定。|-|
|`ESTIMATED_HOURS`|○|○|予定工数。|-|
|`CUSTOM_FIELD`|○|○|カスタムフィールド。更新時のプライマリーキーとしても利用できる。<br>この種別を指定する際には、`customFieldId`として対応するカスタムフィールドのIDを指定する必要がある。|`/custom_fields.xml`|
|`WATCHER_USER_IDS`|○|×|ウォッチャーのID。更新には対応していない。|`/users.xml`|

IDとして指定するものは、上記表のID確認URLでIDを確認することができます。

Expand Down Expand Up @@ -241,3 +251,13 @@ gradlew shadowJar
```

`build/libs/redmine-issue-loader-x.x.x-all.jar`という実行ファイルが出来上がります。(`x.x.x`はバージョン番号)

## ライセンス

MIT

## 作者

[onozaty](https://github.com/onozaty)

[スポンサー](https://github.com/sponsors/onozaty) となり、本プロジェクトを維持することに貢献していただける方を募集しています。
28 changes: 24 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,15 @@ An example of a configuration file when creating a new issue.
"type": "CUSTOM_FIELD",
"customFieldId": 2,
"multipleItemSeparator": ";"
},
{
"headerName": "Watchers",
"type": "WATCHER_USER_IDS",
"multipleItemSeparator": ";",
"mappings": {
"User A": 5,
"User B": 6
}
}
]
}
Expand All @@ -92,10 +101,10 @@ An example of a configuration file when creating a new issue.
An example of a CSV file corresponding to the above configuration file.

```csv
Project,Tracker,Subject,Description,Field1,Field2,Field3
Project A,Bug,xxxx,yyyy,A,1;2,C
Project,Tracker,Subject,Description,Field1,Field2,Watchers
Project A,Bug,xxxx,yyyy,A,1;2,User A;User B
Project B,Feature,aaaa,bbbb,,,
Project B,Bug,zzzz,zzzz,1,2,3
Project B,Bug,zzzz,zzzz,1,2,User B
```

### Ex: update issue
Expand Down Expand Up @@ -157,7 +166,7 @@ The contents of each item are as follows.
* `headerName` : Header name in CSV.
* `type` : Type. What can be specified as a type is described later.
* `customFieldId` : ID of the custom field. Set if the type is `CUSTOM_FIELD`.
* `multipleItemSeparator` : The character to separate the values. Set for multiple selection custom fields.
* `multipleItemSeparator` : The character to separate the values. Set if the type is `WATCHER_USER_IDS` or `CUSTOM_FIELD` and multiple selection.
* `primaryKey` : Primary key? Search for issues to be updated using the information of the field set to `true`, and the field` false` will be updated. It is not necessary to specify when mode is `CREATE`.
* `mappings` : By describing the mapping between the value on CSV and the value on Redmine, contents of CSV can be converted and registered. For example, to convert a project name to a project ID.

Expand All @@ -182,6 +191,7 @@ Items that can be specified as a type of field are as follows.
|`IS_PRIVATE`|○|○|Private. `true` or `false`.|-|
|`ESTIMATED_HOURS`|○|○|Estimated time.|-|
|`CUSTOM_FIELD`|○|○|Custom field. It can also be used as a primary key for updating.<br>When specifying this type, you need to specify the ID of the corresponding custom field as `customFieldId`.|`/custom_fields.xml`|
|`WATCHER_USER_IDS`|○|×|Watcher ID. It does not support update mode.|`/users.xml`|

Items specified as ID can be confirmed with the ID confirmation URL in the table above.

Expand Down Expand Up @@ -240,3 +250,13 @@ gradlew shadowJar
```

`build/libs/redmine-issue-loader-x.x.x-all.jar` will be created. (`x.x.x` is version number)

## License

MIT

## Author

[onozaty](https://github.com/onozaty)

I am looking for people who are willing to become [sponsors](https://github.com/sponsors/onozaty) and contribute to maintaining this project.
9 changes: 9 additions & 0 deletions sample/config-create.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@
"headerName": "Field1",
"type": "CUSTOM_FIELD",
"customFieldId": 1
},
{
"headerName": "Watchers",
"type": "WATCHER_USER_IDS",
"multipleItemSeparator": ";",
"mappings": {
"User A": 5,
"User B": 6
}
}
]
}
8 changes: 4 additions & 4 deletions sample/issues.csv
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#,Project,Tracker,Status,Priority,Assignee,Category,Target version,Parent #,Subject,Description,Start date,Due date,% Done,Private,Estimated time,Field1,Field2,Field3
1,Project A,Bug,New,Normal,User A,Category1,v1.0,,xxx,aaa,2019/2/1,2019/2/20,10,true,2.5,A,1,A
2,Project B,Bug,In Progress,Low,User B,,,,yyy,,2019/3/2,,,false,,B,1;2,B
3,Project A,Support,Closed,High,,Category2,v2.0,1,zzz,ccc,,2019/10/30,90,false,10,C,,C
#,Project,Tracker,Status,Priority,Assignee,Category,Target version,Parent #,Subject,Description,Start date,Due date,% Done,Private,Estimated time,Field1,Field2,Field3,Watchers
1,Project A,Bug,New,Normal,User A,Category1,v1.0,,xxx,aaa,2019/2/1,2019/2/20,10,true,2.5,A,1,A,User B
2,Project B,Bug,In Progress,Low,User B,,,,yyy,,2019/3/2,,,false,,B,1;2,B,
3,Project A,Support,Closed,High,,Category2,v2.0,1,zzz,ccc,,2019/10/30,90,false,10,C,,C,User A;User B
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ public enum FieldType {

ESTIMATED_HOURS("estimated_hours"),

CUSTOM_FIELD("custom_field");
CUSTOM_FIELD("custom_field"),

WATCHER_USER_IDS("watcher_user_ids");

@Getter
private final String fieldName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand Down Expand Up @@ -111,6 +114,33 @@ private IssueRecord toIssueRecord(CSVRecord csvRecord) {

break;

case WATCHER_USER_IDS:

// ウォッチャーはリスト

List<Integer> watcherUserIds;

if (StringUtils.isEmpty(value)) {

watcherUserIds = Collections.emptyList();

} else if (StringUtils.isNotEmpty(fieldSetting.getMultipleItemSeparator())) {

watcherUserIds = Stream.of(StringUtils.split(value, fieldSetting.getMultipleItemSeparator()))
.map(v -> convertValue(v, fieldSetting))
.map(Integer::valueOf)
.collect(Collectors.toList());

} else {

// 区切り文字が無い場合、1ユーザとして登録
watcherUserIds = Arrays.asList(Integer.valueOf(convertValue(value, fieldSetting)));
}

targetFieldsBuilder.field(fieldType, watcherUserIds);

break;

default:
// その他の項目は更新対象フィールドとして利用
value = convertValue(value, fieldSetting);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class IssueTargetFieldsBuilder {

private Map<String, Object> updateTargetFields = new LinkedHashMap<>(); // テスト時に順序を保証したいので

public IssueTargetFieldsBuilder field(FieldType type, String value) {
public IssueTargetFieldsBuilder field(FieldType type, Object value) {

updateTargetFields.put(type.getFieldName(), value);
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public class IssueLoadRunnerTest {
assertThat(request.getHeader("X-Redmine-API-Key")).isEqualTo("apikey1234567890");
assertThat(request.getPath()).isEqualTo("/issues.json");
assertThat(request.getBody().readUtf8()).isEqualTo(
"{\"issue\":{\"project_id\":\"1\",\"tracker_id\":\"2\",\"status_id\":\"1\",\"priority_id\":\"2\",\"assigned_to_id\":\"5\",\"category_id\":\"2\",\"fixed_version_id\":\"2\",\"parent_issue_id\":\"\",\"subject\":\"xxx\",\"description\":\"説明1\",\"start_date\":\"2019-02-01\",\"due_date\":\"2019-02-20\",\"done_ratio\":\"10\",\"is_private\":\"true\",\"estimated_hours\":\"2.5\",\"custom_fields\":[{\"id\":1,\"value\":\"A\"},{\"id\":2,\"value\":\"a\"},{\"id\":3,\"value\":[\"C\"]}]}}");
"{\"issue\":{\"project_id\":\"1\",\"tracker_id\":\"2\",\"status_id\":\"1\",\"priority_id\":\"2\",\"assigned_to_id\":\"5\",\"category_id\":\"2\",\"fixed_version_id\":\"2\",\"parent_issue_id\":\"\",\"subject\":\"xxx\",\"description\":\"説明1\",\"start_date\":\"2019-02-01\",\"due_date\":\"2019-02-20\",\"done_ratio\":\"10\",\"is_private\":\"true\",\"estimated_hours\":\"2.5\",\"custom_fields\":[{\"id\":1,\"value\":\"A\"},{\"id\":2,\"value\":\"a\"},{\"id\":3,\"value\":[\"C\"]}],\"watcher_user_ids\":[6]}}");
}

// 2レコード目
Expand All @@ -61,7 +61,7 @@ public class IssueLoadRunnerTest {
assertThat(request.getHeader("X-Redmine-API-Key")).isEqualTo("apikey1234567890");
assertThat(request.getPath()).isEqualTo("/issues.json");
assertThat(request.getBody().readUtf8()).isEqualTo(
"{\"issue\":{\"project_id\":\"2\",\"tracker_id\":\"2\",\"status_id\":\"2\",\"priority_id\":\"1\",\"assigned_to_id\":\"\",\"category_id\":\"2\",\"fixed_version_id\":\"\",\"parent_issue_id\":\"\",\"subject\":\"yyy\",\"description\":\"説明2\",\"start_date\":\"2019-03-02\",\"due_date\":\"\",\"done_ratio\":\"\",\"is_private\":\"false\",\"estimated_hours\":\"\",\"custom_fields\":[{\"id\":1,\"value\":\"B\"},{\"id\":2,\"value\":\"b\"},{\"id\":3,\"value\":[\"A\",\"B\"]}]}}");
"{\"issue\":{\"project_id\":\"2\",\"tracker_id\":\"2\",\"status_id\":\"2\",\"priority_id\":\"1\",\"assigned_to_id\":\"\",\"category_id\":\"2\",\"fixed_version_id\":\"\",\"parent_issue_id\":\"\",\"subject\":\"yyy\",\"description\":\"説明2\",\"start_date\":\"2019-03-02\",\"due_date\":\"\",\"done_ratio\":\"\",\"is_private\":\"false\",\"estimated_hours\":\"\",\"custom_fields\":[{\"id\":1,\"value\":\"B\"},{\"id\":2,\"value\":\"b\"},{\"id\":3,\"value\":[\"A\",\"B\"]}],\"watcher_user_ids\":[5,6]}}");
}

// 3レコード目
Expand All @@ -71,7 +71,7 @@ public class IssueLoadRunnerTest {
assertThat(request.getHeader("X-Redmine-API-Key")).isEqualTo("apikey1234567890");
assertThat(request.getPath()).isEqualTo("/issues.json");
assertThat(request.getBody().readUtf8()).isEqualTo(
"{\"issue\":{\"project_id\":\"1\",\"tracker_id\":\"3\",\"status_id\":\"3\",\"priority_id\":\"3\",\"assigned_to_id\":\"6\",\"category_id\":\"1\",\"fixed_version_id\":\"1\",\"parent_issue_id\":\"1\",\"subject\":\"zzz\",\"description\":\"説明3\",\"start_date\":\"2019-03-12\",\"due_date\":\"2019-10-30\",\"done_ratio\":\"90\",\"is_private\":\"false\",\"estimated_hours\":\"10\",\"custom_fields\":[{\"id\":1,\"value\":\"C\"},{\"id\":2,\"value\":\"c\"},{\"id\":3,\"value\":[]}]}}");
"{\"issue\":{\"project_id\":\"1\",\"tracker_id\":\"3\",\"status_id\":\"3\",\"priority_id\":\"3\",\"assigned_to_id\":\"6\",\"category_id\":\"1\",\"fixed_version_id\":\"1\",\"parent_issue_id\":\"1\",\"subject\":\"zzz\",\"description\":\"説明3\",\"start_date\":\"2019-03-12\",\"due_date\":\"2019-10-30\",\"done_ratio\":\"90\",\"is_private\":\"false\",\"estimated_hours\":\"10\",\"custom_fields\":[{\"id\":1,\"value\":\"C\"},{\"id\":2,\"value\":\"c\"},{\"id\":3,\"value\":[]}],\"watcher_user_ids\":[]}}");
}
}
}
Expand Down Expand Up @@ -134,14 +134,14 @@ public class IssueLoadRunnerTest {
server.start();

Path configPath =
Paths.get(IssueLoadRunnerTest.class.getResource("create-multiple_custom_fields.json").toURI());
Paths.get(IssueLoadRunnerTest.class.getResource("create-multiple-custom_fields.json").toURI());
Config config = Config.of(configPath);

// Mockに対してリクエスト送信するよう設定
config.setReadmineUrl(server.url("/").toString());

Path csvPath =
Paths.get(IssueLoadRunnerTest.class.getResource("issues-multiple_custom_fields.csv").toURI());
Paths.get(IssueLoadRunnerTest.class.getResource("issues-multiple-custom_fields.csv").toURI());

IssueLoadRunner runner = new IssueLoadRunner(System.out);
runner.execute(config, csvPath);
Expand Down Expand Up @@ -180,6 +180,53 @@ public class IssueLoadRunnerTest {
}
}

@Test
public void execute_新規作成_ウォッチャー区切り文字無し() throws URISyntaxException, IOException, InterruptedException {

try (MockWebServer server = new MockWebServer()) {

server.enqueue(new MockResponse().setBody("{\"issue\":{\"id\":1}}"));
server.enqueue(new MockResponse().setBody("{\"issue\":{\"id\":2}}"));

server.start();

Path configPath =
Paths.get(IssueLoadRunnerTest.class.getResource("create-single-watchers.json").toURI());
Config config = Config.of(configPath);

// Mockに対してリクエスト送信するよう設定
config.setReadmineUrl(server.url("/").toString());

Path csvPath =
Paths.get(IssueLoadRunnerTest.class.getResource("issues-single-watchers.csv").toURI());

IssueLoadRunner runner = new IssueLoadRunner(System.out);
runner.execute(config, csvPath);

assertThat(server.getRequestCount()).isEqualTo(2);

// 1レコード目
{
RecordedRequest request = server.takeRequest();
assertThat(request.getMethod()).isEqualTo("POST");
assertThat(request.getHeader("X-Redmine-API-Key")).isEqualTo("apikey1234567890");
assertThat(request.getPath()).isEqualTo("/issues.json");
assertThat(request.getBody().readUtf8()).isEqualTo(
"{\"issue\":{\"project_id\":\"1\",\"subject\":\"xxx\",\"watcher_user_ids\":[1]}}");
}

// 2レコード目
{
RecordedRequest request = server.takeRequest();
assertThat(request.getMethod()).isEqualTo("POST");
assertThat(request.getHeader("X-Redmine-API-Key")).isEqualTo("apikey1234567890");
assertThat(request.getPath()).isEqualTo("/issues.json");
assertThat(request.getBody().readUtf8()).isEqualTo(
"{\"issue\":{\"project_id\":\"2\",\"subject\":\"yyy\",\"watcher_user_ids\":[]}}");
}
}
}

@Test
public void execute_Basic認証() throws URISyntaxException, IOException, InterruptedException {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@
"type": "CUSTOM_FIELD",
"customFieldId": 3,
"multipleItemSeparator": ";"
},
{
"headerName": "Watchers",
"type": "WATCHER_USER_IDS",
"multipleItemSeparator": ";",
"mappings": {
"ユーザA": 5,
"ユーザB": 6
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"mode": "CREATE",
"readmineUrl": "http://localhost",
"apiKey": "apikey1234567890",
"csvEncoding": "UTF-8",
"fields": [
{
"headerName": "Project",
"type": "PROJECT_ID",
"mappings": {
"プロジェクト1": 1,
"プロジェクト2": 2
}
},
{
"headerName": "Subject",
"type": "SUBJECT"
},
{
"headerName": "Watchers",
"type": "WATCHER_USER_IDS"
}
]
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#,Project,Tracker,Status,Priority,Assignee,Category,Target version,Parent #,Subject,Description,Start date,Due date,% Done,Private,Estimated time,Field1,Field2,Field3
1,プロジェクト1,トラッカー2,新規,通常,ユーザA,カテゴリ2,v2.0,,xxx,説明1,2019/02/01,2019/02/20,10,true,2.5,A,a,C
2,プロジェクト2,トラッカー2,進行中,低め,,カテゴリ2,,,yyy,説明2,2019/03/02,,,false,,B,b,A;B
3,プロジェクト1,トラッカー3,解決,高め,ユーザB,カテゴリ1,v1.0,1,zzz,説明3,2019/03/12,2019/10/30,90,false,10,C,c,
#,Project,Tracker,Status,Priority,Assignee,Category,Target version,Parent #,Subject,Description,Start date,Due date,% Done,Private,Estimated time,Field1,Field2,Field3,Watchers
1,プロジェクト1,トラッカー2,新規,通常,ユーザA,カテゴリ2,v2.0,,xxx,説明1,2019/02/01,2019/02/20,10,true,2.5,A,a,C,ユーザB
2,プロジェクト2,トラッカー2,進行中,低め,,カテゴリ2,,,yyy,説明2,2019/03/02,,,false,,B,b,A;B,ユーザA;ユーザB
3,プロジェクト1,トラッカー3,解決,高め,ユーザB,カテゴリ1,v1.0,1,zzz,説明3,2019/03/12,2019/10/30,90,false,10,C,c,,
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Project,Subject,Watchers
プロジェクト1,xxx,1
プロジェクト2,yyy,