Django REST Framework + AngularでTodoアプリを作る

勉強がてら、Django REST FrameworkでAPIを実装し、Angular2でAPIを利用したTodoアプリを作ってみたので、メモ

できたものの様子です
![todo_django_angular.gif](https://qiita-image-store.s3.amazonaws.com/0/80787/2abf885f-83fd-d681-cc17-dd2bdf0f3514.gif "todo_django_angular.gif")


#環境
- python 3.6.0
- node 7.5.0
- npm 4.1.2

#Django
##Djangoのセットアップ
###必要なライブラリのインストール
```
$ pip install django
$ pip install djangorestframework
$ pip install django-filter
$ pip install django-cors-headers
```


###プロジェクトの作成
```
$ django-admin startproject django_app
```

###アプリケーションの作成
```
$ cd django_app
$ python manage.py startapp to_do
```

###modelの作成
シンプルにtitleと作成日時だけのModelでやっていく

```python:django_app/to_do/models.py
from django.db import models

class Todo(models.Model):
    title = models.CharField(max_length=140, blank=False)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title
```

### settings.pyにアプリケーションを反映
```python:django_app/django_app/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'to_do', # 追加
]
```

###マイグレーション
```
$ python manage.py makemigrations
$ python manage.py migrate
```
DBは準備の必要のないsqliteを利用

###adminユーザの作成
```
$ python manage.py createsuperuser
```
適当にユーザ名、メールアドレス、パスワードを設定する

###adminサイトのセットアップ
```python:django_app/to_do/admin.py
from django.contrib import admin

from .models import Todo

@admin.register(Todo)
class Todo(admin.ModelAdmin):
    pass
```

### adminサイトの確認
```
python manage.py runserver
```

`http://localhost:8000/admin`にアクセス&ログインしてみる

ログイン前の様子
![スクリーンショット 2017-05-01 16.11.01.png](https://qiita-image-store.s3.amazonaws.com/0/80787/2affd612-d8d4-93bb-d490-3b7f245b104a.png)


ログイン後の様子
![スクリーンショット 2017-05-01 16.11.43.png](https://qiita-image-store.s3.amazonaws.com/0/80787/d38e0cb4-eda7-f864-abe0-8730fcf9c409.png)

	
TodosのAddから適当に何件かtodoを登録しておきましょう


##Django REST Frameworkの設定をしてAPIを実装する
###REST Frameworkの読み込み
```python:django_app/django_app/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'to_do',
    'rest_framework', #追記
]
```

###Serializerの定義
django_app/to_doにserializer.pyを作成する

```python:django_app/to_do/serializer.py
from rest_framework import serializers

from .models import Todo

class TodoSerializer(serializers.ModelSerializer):
    class Meta:
        model = Todo
        fields = ('id', 'title', 'created_at')
```

###Viewの定義
django_app/to_doのviews.pyを編集

```python:django_app/to_do/views.py
from django.shortcuts import render
import django_filters
from rest_framework import viewsets, filters

from .models import Todo
from .serializer import TodoSerializer

from rest_framework.decorators import api_view

class TodoViewSet(viewsets.ModelViewSet):
    queryset = Todo.objects.all().order_by('-created_at')
    serializer_class = TodoSerializer
```

###URLパターンの定義
```python:django_app/django_app/urls.py
from django.conf.urls import url, include //includeを追記
from django.contrib import admin

from to_do.urls import router as to_do_router　//追記

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^api/', include(to_do_router.urls)), //追記
]
```

アプリケーション側のurls.pyは最初は作られてないので作る

```python:django_app/to_do/urls.py
from rest_framework import routers
from .views import TodoViewSet

router = routers.DefaultRouter()
router.register(r'todo', TodoViewSet)
```

###API動作確認
```
$ python manage.py runserver
```
`http://localhost:8000/api`にアクセス

こういう画面が出るはず
![スクリーンショット 2017-05-01 15.13.38.png](https://qiita-image-store.s3.amazonaws.com/0/80787/213f3db6-1eb3-3eda-7c86-135be2480507.png "スクリーンショット 2017-05-01 15.13.38.png")


## XMLHTMLの許可設定
用意したAPIにAngularからアクセス可能にするための設定をsettings.pyに書いていく

```python:django_app/django_app/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'to_do',
    'rest_framework',
    'corsheaders',　//追記
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'django.middleware.locale.LocaleMiddleware', //追記
    'corsheaders.middleware.CorsMiddleware', //追記
]

〜中略〜

// 追記
CORS_ALLOW_CREDENTIALS = True
CORS_ORIGIN_ALLOW_ALL = True
```


#Angular
##Angular CLIのインストール
```
$ npm install -g @angular/cli
```

##Angularのセットアップ
###プロジェクトの作成
```
$ ng new ng2app
```
ちょっと時間かかる

###build
```
$ cd ng2app
$ ng build
```

###動作確認
```
$ ng serve
```
`http://localhost:4200`にアクセスしてみる
app works!という画面が出ているはず

## Todo一覧を表示させる
まずは単純にDjango REST Frameworkで生成されたAPIを取得し、表示させるといった機能を実装する

### モデルの定義
/ng2app/src/appにmodelsというフォルダを作成
そのなかにtodo.model.tsというファイルを作成する

```/ng2app/src/app/models/todo.model.ts
export class Todo {
  id: number;
  title: string;
}
```
Djangoのmodelではcreated_atを定義していましたが、フロント側では不要なので、idとtitleのみを定義

### serviceの作成
/ng2app/src/appにservicesというフォルダを作成
そのなかにtodo.service.tsというファイルを作成

ここではAPIをGETして、データを渡す、といった動きを実装していく

```typescript:/ng2app/src/app/services/todo.service.ts
import { Injectable } from "@angular/core";
import { Http, Headers } from '@angular/http';
import 'rxjs/add/operator/toPromise';

import { Todo } from '../models/todo.model';


@Injectable()
export class TodoService {
  todo: Todo[] = [];
  private Url = `http://127.0.0.1:8000/api/todo/`
  private headers = new Headers({'Content-Type': 'application/json'});

  constructor(
    private http: Http
  ){}

  // 全てのtodoをGETする
  getAllTodo(): Promise<Todo[]> {
    return this.http
      .get(this.Url)
      .toPromise()
      .then(response => response.json() as Todo[])
      .catch(this.handleError)
  }
}
```

### componentの作成
/ng2app/src/appにcomponentsというフォルダを作成
そのなかにtodo-list.component.tsというファイルを作成

```typescript:/ng2app/src/app/components/todo-list.component.ts
import { Component,Input } from '@angular/core';
import { Router, ActivatedRoute, Params }   from '@angular/router';

import { TodoService } from '../services/todo.service';
import { Todo } from '../models/todo.model';

@Component({
  selector: 'todo-list',
  templateUrl: '../templates/todo-list.component.html',
  styleUrls: ['../static/todo-list.component.css']
})
export class TodoListComponent {
  todos: Todo[] = [];

  constructor(
    private todoService: TodoService,
  ){}
  ngOnInit(): void {
    this.todoService.getAllTodo()
      .then(todos => this.todos = todos);
  }
}
```

### htmlの作成
/ng2app/src/appにtemplatesというフォルダを作成
そのなかにtodo-list.component.htmlを作成

```html:/ng2app/src/app/templates/todo-list.component.html
<div class="container">
    <div class="todo-list row">
      <div *ngFor="let todo of todos" class="col-sm-8 col-sm-offset-2">
         <div class="panel panel-default">
            <div class="panel-body">
              <span from="name">{{todo.title}}</span>
          </div>
      </div>
    </div>
</div>
```

### ルーティングの設定
/ng2app/src/appにapp-routing.module.tsを作成

```typescript:/ng2app/src/app/app-routing.module.ts
import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { TodoListComponent }    from './components/todo-list.component';

const routes: Routes = [
  { path: '',  component: TodoListComponent }
];
@NgModule({
  imports: [ RouterModule.forRoot(routes) ],
  exports: [ RouterModule ]
})
export class AppRoutingModule {}
```

`http://localhost:4200`にアクセスがあったら、TodoListComponentを見にいかせる

### app.componentの編集
/ng2app/src/app/app.component.tsを下記の通り編集する

```typescript:/ng2app/src/app/app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <h1 class="text-center">
      <span class="title">{{ title }}</span>
      <p class="sub-title">{{ subtitle }}</p>
    </h1>
    <router-outlet></router-outlet>
    `,
  styles: [
    '.title { color: #ee6e73;}',
    '.sub-title { font-size: small; }'
  ],
})
export class AppComponent {
  title = 'Simple Todo';
  subtitle = 'Angular2 + Django Rest Framework'
}
```

### 各種モジュールの読み込み
/ng2app/src/app/app.module.tsを編集

```typescript:/ng2app/src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { AppRoutingModule }   from './app-routing.module';
import { AppComponent } from './app.component';
import { TodoListComponent }      from './components/todo-list.component';
import { TodoService } from './services/todo.service';

@NgModule({
  declarations: [
    AppComponent,
    TodoListComponent,
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    AppRoutingModule
  ],
  providers: [TodoService],
  bootstrap: [AppComponent]
})
export class AppModule { }
```

### cssファイルの配置
/ng2app/src/appにstaticというフォルダを作成
todo-list.component.cssを作成する
とりあえず、下記の通り記載

```/ng2app/src/app/static/todo-list.component.css
.todo-list {
  padding-top: 10px;
}
.todo {
  min-height: 130px;
}

.btn-circle {
  width: 30px;
  height: 30px;
  text-align: center;
  padding: 6px 0;
  font-size: 12px;
  line-height: 1.428571429;
  border-radius: 15px;
}

.add-todo {
  margin-top: 10px;
}
```

### bootstrapモジュールのインストール
下記を実行

```
$ npm install --save bootstrap ng2-bootstrap
```

/ng2app/.angular-cli.json(隠しファイルになっているので注意)のstylesの部分を編集

```
"styles": [
        "styles.css",
        "../node_modules/bootstrap/dist/css/bootstrap.css", //追記
      ],
```

### 動作確認
下記コマンドでアプリケーション起動

```
$ ng serve
```

`http://localhost:4200/`にアクセスすると、下記のような状態になっているはず
(DjangoのAdminからtodoを登録していれば)
![スクリーンショット 2017-05-01 16.57.51.png](https://qiita-image-store.s3.amazonaws.com/0/80787/0d00632f-c5ce-6cf5-ff78-e4aafaa5cb43.png)


## Todoを新規作成する機能を追加
### serviceの編集
todo.service.tsのTodoService内に下記を追記

```/ng2app/src/services/todo.service.ts
  // 追加時の挙動
  create(todo: Todo): Promise<Todo> {
    return this.http
      .post(this.Url, JSON.stringify(todo), {headers: this.headers})
      .toPromise()
      .then(res => res.json())
      .catch(this.handleError);
　}
　
  // 追加された最新のtodoを一件取得する
  getNewTodo(): Promise<Todo> {
    return this.http
      .get(this.Url+"?limit=1")
      .toPromise()
      .then(res => res.json().results)
      .catch(this.handleError)
  }　
```

### componentの編集
todo.component.tsのTodoListComponent内に下記を追記

```/ng2app/src/components/todo.component.ts
export class TodoListComponent {
  todos: Todo[] = []; 
  newtodos: Todo[] = []; //追記
  @Input() todo: Todo = new Todo(); //追記

  〜中略〜

  // 保存ボタンを押した時の挙動
  save(): void {
    this.todoService
      .create(this.todo)
      .then(data => {this.getNewTodo()});
    this.todo = new Todo();
  }

  // 最新の一件を呼び出す挙動
  getNewTodo(): void {
    this.todoService
      .getNewTodo()
      .then(res => {this.pushData(res)});
  }

  // htmlに渡すnewtodosにデータをpushする
  pushData(data: Todo): void {
    this.newtodos.unshift(data);
  }
}
```
新規todoを追加すると、save()でTodoService内のcreateを実行し、新規TodoをPOST
その後、getNewTodo()でTodoService内のgetNewTodoを実行し、最新の一件(=POSTされたtodo)を呼び出す
呼び出した一件をpushData()でnewtodosに格納、といった動きをさせています

### htmlの編集
todo-list.component.htmlを編集

```/ng2app/src/templates/todo-list.component.html
<div class="container">
  <!-- 　ここから　-->
  <div class="center">
    <div class="row">
      <div class="col-sm-8 col-sm-offset-2">
        <input [(ngModel)]="todo.title"
          id="input_text"
          type="text"
          length="140"
          class="form-control add-todo"
          placeholder="add-todo"
          (keydown.enter)="save()"
        >
        <button (click)="save()" class="btn btn-success pull-right add-todo">Add</button>
      </div>
    </div>
  </div>
  <div class="newtodo-list row" style="margin-top:10px;">
    <div *ngFor="let newtodo of newtodos" class="col-sm-8 col-sm-offset-2">
        <div class="panel panel-default">
          <div class="panel-body">
            <span from="name">{{newtodo[0].title}}</span>
          </div>
        </div>
    </div>
  </div>
  <hr class="col-sm-8 col-sm-offset-2">
  <!-- 　ここまで　-->
    <div class="todo-list row">
      <div *ngFor="let todo of todos" class="col-sm-8 col-sm-offset-2">
         <div class="panel panel-default">
            <div class="panel-body">
              <span from="name">{{todo.title}}</span>
          </div>
      </div>
    </div>
</div>
```

### 動作確認
下記コマンドでアプリケーション起動

```
$ ng serve
```

下記のような状態になっているはず
![スクリーンショット 2017-05-01 17.20.19.png](https://qiita-image-store.s3.amazonaws.com/0/80787/2d1908eb-dcb6-e9fa-523d-977155d85df2.png)

input部分で新規todoを追加ができる

## Todoを削除する機能を追加
### serviceの編集
todo.service.tsのTodoService内に下記を追記

```/ng2app/src/services/todo.service.ts
  // 削除時の挙動
  delete(id: number): Promise<void> {
    const url = `${this.Url}${id}/`;
    return this.http
      .delete(url, {headers: this.headers})
      .toPromise()
      .then(() => null)
      .catch(this.handleError);
  }
```

### componentの編集
todo.component.tsのTodoListComponent内に下記を追記

```/ng2app/src/components/todo.component.ts
  // 削除ボタンを押した時の挙動
  delete(id): void {
    this.todoService
      .delete(id);
  }
```

### htmlの編集
todo-list.component.htmlを編集

```/ng2app/src/templates/todo-list.component.html
<div class="container">
    <div class="center">
      <div class="row">
        <div class="col-sm-8 col-sm-offset-2">
          <input [(ngModel)]="todo.title"
            id="input_text"
            type="text"
            length="140"
            class="form-control add-todo"
            placeholder="add-todo"
            (keydown.enter)="save()"
          >
          <button (click)="save()" class="btn btn-success pull-right add-todo">Add</button>
        </div>
      </div>
    </div>
    <div class="newtodo-list row" style="margin-top:10px;">
      <div *ngFor="let newtodo of newtodos" class="col-sm-8 col-sm-offset-2">
        <div [style.display]="!newtodo.hideElement ? 'inline':'none'">
          <div class="panel panel-default">
            <div class="panel-body">
              <span from="name">{{newtodo[0].title}}</span>
              <button (click)="delete(newtodo[0].id) || newtodo.hideElement=true"
                type="button"
                class="btn btn-success btn-circle pull-right"
              >
                <i class="glyphicon glyphicon-ok"></i>
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
    <hr class="col-sm-8 col-sm-offset-2">
    <div class="todo-list row">
      <div *ngFor="let todo of todos" class="col-sm-8 col-sm-offset-2">
        <div [style.display]="!todo.hideElement ? 'inline':'none'">
          <div class="panel panel-default">
            <div class="panel-body">
              <span from="name">{{todo.title}}</span>
              <button (click)="delete(todo.id) || todo.hideElement=true"
                type="button"
                class="btn btn-success
                btn-circle pull-right"
              >
                <i class="glyphicon glyphicon-ok"></i>
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
</div>
```
ボタンの追加、ボタンが押されると、deleteメソッドを呼び出す&要素がdisplay=noneとなるようにしています
(ここあんまりイケてない気がするので直したい)

### 動作確認
下記コマンドでアプリケーション起動

```
$ ng serve
```

下記のような状態になっているはず
![スクリーンショット 2017-05-01 17.36.41.png](https://qiita-image-store.s3.amazonaws.com/0/80787/7677b2ce-6d6b-4d43-99e3-58c4b33ac1f5.png)

チェックボタンでtodoを削除ができる

## Todoを編集する機能を追加
### serviceの編集
todo.service.tsのTodoService内に下記を追記

```/ng2app/src/services/todo.service.ts
  // 更新時の挙動
  update(todo: Todo): Promise<Todo> {
    const url = `${this.Url}${todo.id}/`;
    return this.http
      .put(url, JSON.stringify(todo), {headers: this.headers})
      .toPromise()
      .then(res => res.json())
      .catch(this.handleError);
  }
```

### componentの編集
todo.component.tsのTodoListComponent内に下記を追記

```/ng2app/src/components/todo.component.ts
  // todoを更新した時の挙動
  update(id: number, title: string): void {
    let todo = {
      id: id,
      title: title
    }
    this.todoService.update(todo);
  }
```
(ここもあんまりイケてないので直したい)

### htmlの編集
todo-list.component.htmlを編集

```/ng2app/src/templates/todo-list.component.html
<div class="container">
    <div class="center">
      <div class="row">
        <div class="col-sm-8 col-sm-offset-2">
          <input [(ngModel)]="todo.title"
            id="input_text"
            type="text"
            length="140"
            class="form-control add-todo"
            placeholder="add-todo"
            (keydown.enter)="save()"
          >
          <button (click)="save()" class="btn btn-success pull-right add-todo">Add</button>
        </div>
      </div>
    </div>
    <div class="newtodo-list row" style="margin-top:10px;">
      <div *ngFor="let newtodo of newtodos" class="col-sm-8 col-sm-offset-2">
        <div [style.display]="!newtodo.hideElement ? 'inline':'none'">
          <div class="panel panel-default">
            <div class="panel-body">
              <span *ngIf="!newtodo.isEdit" (click)="newtodo.isEdit=true" from="name">{{newtodo[0].title}}</span>
              <input *ngIf="newtodo.isEdit"
                (focusout)="newtodo.isEdit=false || update(newtodo[0].id, newtodo[0].title)"
                [(ngModel)]="newtodo[0].title"
                id="input_text"
                type="text"
                length="140"
                style="border:none; width:70%"
              >
              <button (click)="delete(newtodo[0].id) || newtodo.hideElement=true"
                type="button"
                class="btn btn-success btn-circle pull-right"
              >
                <i class="glyphicon glyphicon-ok"></i>
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
    <hr class="col-sm-8 col-sm-offset-2">
    <div class="todo-list row">
      <div *ngFor="let todo of todos" class="col-sm-8 col-sm-offset-2">
        <div [style.display]="!todo.hideElement ? 'inline':'none'">
          <div class="panel panel-default">
            <div class="panel-body">
              <span *ngIf="!todo.isEdit" (click)="todo.isEdit=true" from="name">{{todo.title}}</span>
              <input *ngIf="todo.isEdit"
                (focusout)="todo.isEdit=false || update(todo.id, todo.title)"
                [(ngModel)]="todo.title"
                id="input_text"
                type="text"
                length="140"
                style="border:none; width:70%"
              >
              <button (click)="delete(todo.id) || todo.hideElement=true"
                type="button"
                class="btn btn-success
                btn-circle pull-right"
              >
                <i class="glyphicon glyphicon-ok"></i>
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
</div>
```

todoの文字列をクリックしたら、input要素に切り替わり、inputからフォーカスが外れたら、updateが実行される
(これもイケてな(ry

### 動作確認
下記コマンドでアプリケーション起動

```
$ ng serve
```

冒頭のgifのような動きをするはず

#以上です
超ビギナーなので、こうしたほうがいいんじゃね？的なことがあったら、ガシガシ指摘してやってください

また、ソースは[こちら](https://github.com/xKxAxKx/todo_django_and_angular2)。Pull-Request歓迎

#今後
とりあえず、このアプリケーションにユーザ認証とかを取り入れてそこら辺をやっていく気持ちがある

#参考にしたもの
[Django REST Frameworkを使って爆速でAPIを実装する - Qiita]
(http://qiita.com/kimihiro_n/items/86e0a9e619720e57ecd8)
[Rails 5 + Angular2 + TypeScript でTodoアプリを作った。 - DC4](http://yasun.hatenablog.jp/entry/2016/12/21/235334)