LoginSignup
13
10

More than 5 years have passed since last update.

AngularからRedmineのチケット一覧を取得する

Last updated at Posted at 2017-08-12

はじめに

この記事では、AngularからRedmineのREST APIにアクセスして、チケット一覧を取得する方法について説明します。
(環境は、この記事の執筆時点の最新バージョンである Angular 4.3.4 と Redmine 3.4.2 です)

解説は、「恋に落ちるコード.js」に登場するJavaScript好きの女子高生、絵子と樹里がお送りします。

人物紹介

瀬尾絵子:「恋に落ちるコード.js」の登場人物。JavaScript勉強中の女子高生。
篠宮樹里: 同じく「恋に落ちるコード.js」の登場人物。JavaScriptに詳しい女子高生。
藤倉紅子:「Redmineで始める異世界人心掌握術」の登場人物。自称美人SE。二人の高校の先輩でRedmineに詳しい。

ひな形を作成する

樹里「ではさっそく、Angular CLIを使って、RedmineのREST APIにアクセスするWebアプリを作ってみよう」
絵子「ホントにいきなりだね。Angularとは?とかRedmineとは?とか、その辺は説明しないの?」
樹里「その辺は、Qiitaに有益な記事がすでにいくつも公開されているので、そちらを読んでほしい」
絵子「……確かに。お世話になってます」
樹里「とりあえずnode.jsとAngular CLIはセットアップしておこう。乙女の新常識だ」
絵子「そうなんだ。いつの間に」
樹里「あと、Redmineが同一のホスト上で稼働しているという前提で話を進めるが……Redmine側にも準備が必要だ」
絵子「どんな?」
樹里「まず、APIを有効にするため、設定画面で『RESTによるWebサービスを有効にする』と『JSONPを有効にする』の機能をオンにしておく必要がある。また、今回は公開されたプロジェクトにアクセスするが、認証が必要なプロジェクトのチケットにアクセスするには、事前に認証用のキーを発行してパラメータに付与する必要がある」
絵子「うーん、わかった。詳しいところはあとで紅子さんに聞いてみよう」
樹里「じゃ、準備も整ったところで、サクッとひな形を作るぞ」
絵子「らじゃー」

> ng new sample

絵子「……これでひな形が出来たの?便利だねー」
樹里「今回は『sample』という名前のアプリにしたが、名前はなんでも良い。では、これをベースに作っていくぞ」

JSONデータを取得するサービスのひな形を作成する

樹里「では、出来上がったディレクトリに移動しよう」

> cd sample

絵子「樹里!なんだか山のようにファイルが出来てるんだけど!」
樹里「とりあえず必要なのはsrcディレクトリ、さらにその中のappディレクトリだ。それ以外のファイルの役割については徐々に覚えていけばいい」
絵子「……良かった。早々に挫折するところだった」
樹里「ではさっそく、出来上がったファイルを修正していくのだが……」
絵子「まだ準備がいるの?」
樹里「今出来上がったのは、コンポーネントやモジュールといった基本的な機能を持つファイルなのだが……JSONファイルを取得する、といった独立した機能については、『サービス』として分けて管理する方が都合が良い。今後のためにも分けておこう」
絵子「はーい」
樹里「ではサービスのひな形を作成する。これも便利なコマンドがある」

> ng g service issues

樹里「これで、appディレクトリ内に『issues.service.spec.ts』と『issues.service.ts』が、必要なコードがすでに記述された状態で作成される」
絵子「便利すぎて逆に不安になるね」

データを格納するクラスを作る

樹里「それから、もう一つ……」
絵子「まだあるの?早く作ろうよ」
樹里「まあ聞きなさい。JSONを取得するリクエストを投げた時、返ってくるデータは?」
絵子「……JSON、だよね……?」
樹里「そう。JSON形式のデータが返ってくる。しかし、その構造は自由自在だし、どんなデータが返ってくるかわからないのでは不安だ。なので、ちゃんと事前にクラスを定義しておき、こういう構造のデータが返ってくるよ……という受け入れ態勢を作っておく方がWebアプリとしては望ましい」
絵子「確かに、事前の心構えは大事だよね」
樹里「なので、データの構造を定義するクラスファイルをappディレクトリ内に作成しておく。名前は『issues.ts』にしよう」
絵子「はーい」
樹里「絵子も、『オレンジジュースをあげるよ』と渡されたら美味しく飲めるが、『何かしらの汁をあげるよ』と渡されたら飲みづらかろう」
絵子「渡す側に悪意があるよね」

issues.ts
export class Issues {
  issues: Issue[];
  total_count: number;
  offset: number;
  limit: number;
}
export class Issue {
  id: number;
  project: FieldObject;
  tracker: FieldObject;
  status: FieldObject;
  priority: FieldObject;
  author: FieldObject;
  assigned_to: FieldObject;
  subject: string;
  description: string;
  due_date: string;
  done_ratio: number;
  custom_fields: CustomFieldObject[];
  created_on: string;
  updated_on: string;
}
export class FieldObject {
  id: number;
  name: string;
}
export class CustomFieldObject {
  id: number;
  name: string;
  value: string;
}

樹里「細かい説明は省くが……Redmineチケット一覧をJSON形式で取得すると、こういう構造のデータが返ってくる」
絵子「これもあとで紅子さんに聞いとこう」
樹里「概要だけ言うと……issuesの中にチケットの情報が配列で入っている。そのチケットの構造がIssueオブジェクトの通りだ。Issueには、例えばidの情報がnumber型で格納されている。他にはprojectの情報もあるが、これはプロジェクトのidnameが対になったオブジェクトとして格納されているので、それをFieldObjectという名前で定義している。他も同様だ」
絵子「うん。わかった気になった」

サービスのひな形を修正する

樹里「ファイルの作成は以上だ。あとは、出来上がったファイルを修正していく」
絵子「待ってました!」
樹里「まずはJSONを取得するサービスからだ。さっき作ったファイルのうち、今回はissues.service.spec.tsは使わない。issues.service.tsを修正していく」

issues.service.ts
import { Injectable } from '@angular/core';
/* ↓(1) 必要なモジュール等のインポート */
import { Jsonp }       from '@angular/http';
import { Observable }  from 'rxjs/Observable';
import { Issues }      from './issues';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
/* ↑ */

@Injectable()
export class IssuesService {
/* ↓(2) JSONファイルのURLを定義 */
  private issuesUrl = 'http://localhost/redmine/issues.json?callback=JSONP_CALLBACK';
/* ↑ */

/* ↓(3) コンストラクタを書き換え */
//constructor() { }
  constructor(private jsonp: Jsonp) { }
/* ↑ */

/* ↓(4) JSONを取得する処理 */
  getIssues(): Observable<Issues> {
    return this.jsonp
      .get(this.issuesUrl)
      .map(response => response.json() as Issues)
      .catch(this.handleError);
  }

  private handleError(error: any): Observable<any> {
    console.error('An error occurred', error);
    return Observable.throw(error.message || error);
  }
/* ↑ */
}

必要なモジュール等のインポート

樹里「ではさっそくコードの説明だが……最初は使用するモジュールや関数のインポートだ。コード内で使用するものは、最初にインポートする必要がある、とだけ覚えておけば良い。個々の役割についてはあとで説明する」
絵子「うん。あとで聞く」

JSONファイルのURLを定義

樹里「続いて、RedmineのREST APIにアクセスするURLの定義だが……まず、『localhost/redmine/』というのがRedmineのURLだ。別ホストのRedmineにアクセスする場合には各自直すように」
絵子「おっけー」
樹里「続いて『issues.json』が、チケット一覧を取得するためのAPIだ。この後ろにパラメータを付与することにより、特定のプロジェクトやトラッカーのみの一覧を取得することも可能だ。詳しいことはRedmine APIの仕様を確認しておくように」
絵子「紅子さんに聞くこといっぱいあるな……」
樹里「最後に、『?callback=JSONP_CALLBACK』。これは、AngularからJSONP形式のデータを取得するための約束事だと思えばいい。詳しい事は割愛する」
絵子「あ、逃げた」
樹里「ちなみに、なぜJSONじゃなくてJSONP形式でデータを取得するのかと言うと……JSONはクロスドメインがどうとか、セキュリティ上の制約が厳しくて色々あるので……」
絵子「色々あるよ、こんな世の中だもの」

コンストラクタを書き換え

樹里「これは、最初にJsonpモジュールを呼び出して変数に格納しているだけだ」

JSONを取得する処理

樹里「このgetIssues()関数でJSONデータを取得している。ちなみに戻り値のObservableとは……JSONのようなHTTPリクエストは、このObservableオブジェクトの形式で返ってくる……らしい」
絵子「あんまり深く聞くと可哀想だからやめておこう」
樹里「……処理の流れは、

  • get()関数で指定したURLからデータを取得し、
  • map()関数で実データ部分を先ほど定義したIssuesクラスにマッピングして返す。
  • エラーが発生したらcatch()関数でエラー処理に飛ぶ。

……と言った具合だ」
絵子「ま、これで意図したデータが取れるならそれでオッケーだね」

コンポーネントを修正する

樹里「次は、ページの動作を定義するコンポーネント、app.component.tsを修正する」
絵子「一番キモの部分ね」

app.component.ts
/* ↓(1) 必要なモジュール等のインポート */
// import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { Issues }            from './issues';
import { IssuesService }     from './issues.service';
/* ↑ */

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
/*
export class AppComponent {
  title = 'app';
}
*/
/* ↓(2) JSONを取得する処理 */
export class AppComponent implements OnInit {
  issues: Issues;

  constructor(private issuesService: IssuesService) { }

  ngOnInit(): void {
    this
      .issuesService
      .getIssues()
      .subscribe( issues => this.issues = issues );
  }
}
/* ↑ */

必要なモジュール等のインポート

樹里「さっきと似たような感じだが……OnInitモジュール、さっき作ったIssuesクラスとIssuesServiceサービスをインポートしている」

JSONを取得する処理

樹里「ここがメインとなる処理だが……まずIssuesクラスを変数に格納し、IssuesServiceサービスのインスタンスを生成する。それから、OnInitモジュールをインポートすることでngOnInit()メソッドを使用できるようになる」
絵子「ngOnInit()メソッドって?」
樹里「データバインド後に一度だけ呼び出される処理なのだが……初期化の処理はこういう風に書くことが推奨されてるらしいので、ここに書く」
絵子「はーい」
樹里「で、さっき作ったIssuesServiceのgetIssues()を実行して、取得したデータを変数issuesに格納する。やっている事はこれだけだ」
絵子「ほう。じゃあ、取得したデータはどう使うの?」
樹里「それは、次に説明するテンプレートの中で使われる」

app.component.html
<h1>チケット一覧</h1>
<div *ngIf="issues">
  <p>
    total_count:<span>{{issues.total_count}}</span>
  </p>
  <ul>
    <li *ngFor="let issue of issues.issues">
      <span>{{issue.id}}</span> <span>{{issue.subject}}</span>
    </li>
  </ul>
</div>

樹里「これがテンプレートだ。見慣れたHTMLと、Angularで使用される構文が混在している」
絵子「ほんとだ」
樹里「この『{{』と『}}』に囲まれた部分に変数を記述すると、展開して表示してくれる」
絵子「『カーリーブラケット』だね。こないだ覚えた」
樹里「また、ngForは、いわゆるfor文と同じく反復処理だ。データの数だけ繰り返し表示される」
絵子「なるほどー。表示させるのもカンタンだね」

モジュールを修正する

樹里「最後に、app.module.tsの修正だ。……最後というか、本来はこのファイルが最初に呼び出される。このアプリケーションではどのようなモジュールやサービスが使われるか、という事を最初に宣言するためだ」
絵子「登場人物紹介みたいなもんね」

app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { JsonpModule } from '@angular/http';  /* 追加 */
import { IssuesService } from './issues.service';  /* 追加 */

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    JsonpModule  /* 追加 */
  ],
  providers: [
    IssuesService  /* 追加 */
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

樹里「このように、JsonpModuleimportsの配列に、IssuesServiceprovidersの配列にそれぞれ追加する」
絵子「とりあえず言われた通りにやっておこう」

動作確認

樹里「これで完成だ。さっそく動かしてみよう」

> ng serve

絵子「……おー、チケットのIDと題名が表示されてる」
樹里「たったこれだけのコードで、しかもすべてJavaScript……まあ正確にはTypeScriptだが、ともかくこれだけでRedmineのデータを表示するWebアプリケーションを作ることが出来る。便利な時代になったものだ」
絵子「ところで……Redmineの画面を直接表示するのに比べて、どんなメリットがあるの?」
樹里「それを言われると言葉に詰まるが……せっかく便利なAPIが用意されてて、データの入出力が自由に出来るんだ。フロントエンドは自分の好きな言語で書いてみたいじゃないか」
絵子「確かに、JavaScriptでもフロントエンドが作れる!ってのは嬉しいよねー。私達からしたら」

おわりに

というわけで、AngularからRedmineのチケット一覧を取得する方法についてお話ししました。
ほぼ必要最小限の事にしか触れていませんが、個々のファイルやメソッド等の詳細については、Angularの公式ドキュメント等で調べてみてください。(私も勉強します……)

13
10
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
10