LoginSignup
82
74

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-05-01

勉強がてら、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歓迎

今後

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

参考にしたもの

Django REST Frameworkを使って爆速でAPIを実装する - Qiita
Rails 5 + Angular2 + TypeScript でTodoアプリを作った。 - DC4

82
74
0

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
82
74