2018年1月4日木曜日

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

勉強の為に転載しました。
https://qiita.com/xKxAxKx/items/14d81209ac195e485338

勉強がてら、Django REST FrameworkでAPIを実装し、Angular2でAPIを利用したTodoアプリを作ってみたので、メモ
できたものの様子です
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でやっていく
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にアプリケーションを反映

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サイトのセットアップ

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
ログイン後の様子
スクリーンショット 2017-05-01 16.11.43.png
TodosのAddから適当に何件かtodoを登録しておきましょう

Django REST Frameworkの設定をしてAPIを実装する

REST Frameworkの読み込み

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を作成する
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を編集
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パターンの定義

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は最初は作られてないので作る
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

XMLHTMLの許可設定

用意したAPIにAngularからアクセス可能にするための設定をsettings.pyに書いていく
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して、データを渡す、といった動きを実装していく
/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というファイルを作成
/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を作成
/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を作成
/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を下記の通り編集する
/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を編集
/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

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
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
チェックボタンで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のような動きをするはず

以上です

超ビギナーなので、こうしたほうがいいんじゃね?的なことがあったら、ガシガシ指摘してやってください
また、ソースはこちら。Pull-Request歓迎

今後

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

参考にしたもの




0 コメント:

コメントを投稿