├── note_client
├── build
│ └── .keep
├── src
│ ├── style
│ │ ├── navigation.scss
│ │ ├── font-awesome-config.scss
│ │ └── Note.scss
│ ├── resource
│ │ └── urls.js
│ ├── util
│ │ ├── csrf-token.js
│ │ ├── url-builder.js
│ │ └── rest-api.js
│ ├── components
│ │ ├── Editor.vue
│ │ ├── Index.vue
│ │ └── Note.vue
│ ├── common.js
│ ├── index.js
│ ├── model
│ │ └── page.js
│ └── controller
│ │ └── note.js
├── test
│ ├── mocha.opts
│ ├── util
│ │ ├── csrf-token.js
│ │ ├── url-builder.js
│ │ └── rest-api.js
│ ├── model
│ │ └── page.js
│ └── controller
│ │ └── note.js
├── .babelrc
├── .eslintrc.json
├── package.json
├── webpack.config.js
└── README.md
├── note_server
├── note
│ ├── __init__.py
│ ├── migrations
│ │ ├── __init__.py
│ │ └── 0001_initial.py
│ ├── tests.py
│ ├── admin.py
│ ├── apps.py
│ ├── urls.py
│ ├── serializers.py
│ ├── models.py
│ └── views.py
├── note_server
│ ├── __init__.py
│ ├── wsgi.py
│ ├── urls.py
│ └── settings.py
├── requirements.txt
├── templates
│ ├── index.html
│ ├── base.html
│ └── login.html
├── manage.py
└── README.md
├── .travis.yml
├── .gitignore
├── note-app.png
├── LICENSE
├── Vagrantfile
└── README.md
/note_client/build/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/note_server/note/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/note_server/note_server/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/note_server/note/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/note_client/src/style/navigation.scss:
--------------------------------------------------------------------------------
1 | #navigation {
2 | min-height: 54px;
3 | }
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "7"
4 | before_install: cd note_client
5 |
--------------------------------------------------------------------------------
/note_server/note/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | venv/
2 | __pycache__/
3 | *.sqlite3
4 | node_modules/
5 | build/
6 | .vagrant
7 | *.log
8 |
--------------------------------------------------------------------------------
/note-app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tokibito/note-app-django-vue-javascript/HEAD/note-app.png
--------------------------------------------------------------------------------
/note_client/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --recursive
2 | --require babel-register
3 | --require babel-polyfill
4 |
--------------------------------------------------------------------------------
/note_server/note/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from .models import Page
4 |
5 | admin.site.register(Page)
6 |
--------------------------------------------------------------------------------
/note_client/src/style/font-awesome-config.scss:
--------------------------------------------------------------------------------
1 | $fa-font-path: "~font-awesome/fonts";
2 | @import '~font-awesome/scss/font-awesome.scss';
3 |
--------------------------------------------------------------------------------
/note_server/note/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class NoteConfig(AppConfig):
5 | name = 'note'
6 | verbose_name = "ノート"
7 |
--------------------------------------------------------------------------------
/note_client/src/resource/urls.js:
--------------------------------------------------------------------------------
1 | /*
2 | * APIのパス定義
3 | */
4 | let API_URL = {
5 | NotePage: '/note/page/'
6 | }
7 |
8 | export {
9 | API_URL
10 | }
11 |
--------------------------------------------------------------------------------
/note_server/requirements.txt:
--------------------------------------------------------------------------------
1 | Django==2.0.4
2 | ipython==6.3.1
3 | djangorestframework==3.8.2
4 | coreapi==2.3.3
5 | django-debug-toolbar==1.9.1
6 | flake8==3.5.0
7 |
--------------------------------------------------------------------------------
/note_server/note/urls.py:
--------------------------------------------------------------------------------
1 | from rest_framework.routers import DefaultRouter
2 |
3 | from .views import PageViewSet
4 |
5 | router = DefaultRouter()
6 | router.register(r'page', PageViewSet, base_name='page')
7 | urlpatterns = router.urls
8 |
--------------------------------------------------------------------------------
/note_client/src/style/Note.scss:
--------------------------------------------------------------------------------
1 | #app {
2 | width: 100%
3 | }
4 |
5 | .index {
6 | padding: 5px;
7 | float: left;
8 | }
9 |
10 | .editor {
11 | padding: 5px;
12 | float: left;
13 | .btn {
14 | margin-bottom: 5px;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/note_client/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", {
4 | "targets": {
5 | "browsers": ["last 2 versions"]
6 | }
7 | }]
8 | ],
9 | "plugins": [
10 | ["module-resolver", {
11 | "root": ["./src"]
12 | }]
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/note_client/src/util/csrf-token.js:
--------------------------------------------------------------------------------
1 | import cookie from 'cookie'
2 |
3 | /*
4 | * DjangoフレームワークがCookieに格納したCSRFトークンを取り出す関数
5 | */
6 | const getCsrfTokenFromCookie = cookieString => {
7 | let cookies = cookie.parse(cookieString)
8 | return cookies.csrftoken || null
9 | }
10 |
11 | export default {
12 | getCsrfTokenFromCookie
13 | }
14 |
--------------------------------------------------------------------------------
/note_client/src/components/Editor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
--------------------------------------------------------------------------------
/note_client/src/common.js:
--------------------------------------------------------------------------------
1 | /*
2 | * 共通モジュール
3 | * BootstrapVueやFontAwesomeとCSSを適用するために使います
4 | */
5 | import 'bootstrap/dist/css/bootstrap.css'
6 | import 'bootstrap-vue/dist/bootstrap-vue.css'
7 | import './style/font-awesome-config.scss'
8 | import './style/navigation.scss'
9 | import Vue from 'vue'
10 | import BootstrapVue from 'bootstrap-vue'
11 |
12 | Vue.use(BootstrapVue)
13 |
--------------------------------------------------------------------------------
/note_client/test/util/csrf-token.js:
--------------------------------------------------------------------------------
1 | /*
2 | * util/csrf-token.jsのテスト
3 | */
4 | import * as assert from 'power-assert'
5 | import csrfToken from 'util/csrf-token'
6 |
7 | describe('csrfToken', () => {
8 | it('getCsrfTokenFromCookie', () => {
9 | let result = csrfToken.getCsrfTokenFromCookie(
10 | "foo=bar; csrftoken=token123; spam=egg")
11 | assert.equal('token123', result)
12 | })
13 | })
14 |
--------------------------------------------------------------------------------
/note_client/src/util/url-builder.js:
--------------------------------------------------------------------------------
1 | /*
2 | * APIのパス定義にprefixをつけるクラス
3 | */
4 | class UrlBuilder {
5 | constructor(urls, prefix=null) {
6 | this.urls = urls
7 | this.prefix = prefix || ''
8 | }
9 |
10 | build() {
11 | let result = {}
12 | for(let key of Object.keys(this.urls)) {
13 | result[key] = this.prefix + this.urls[key]
14 | }
15 | return result
16 | }
17 | }
18 |
19 | export {
20 | UrlBuilder
21 | }
22 |
--------------------------------------------------------------------------------
/note_server/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load static %}
3 |
4 | {% block title %}ノート{% endblock %}
5 |
6 | {% block content %}
7 |
8 |
9 | {% verbatim %}
10 |
11 |
12 |
13 | {% endverbatim %}
14 |
15 |
16 |
17 | {% endblock %}
18 |
--------------------------------------------------------------------------------
/note_server/note_server/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for note_server project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "note_server.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/note_client/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "es6": true
6 | },
7 | "extends": "eslint:recommended",
8 | "parserOptions": {
9 | "sourceType": "module"
10 | },
11 | "rules": {
12 | "linebreak-style": [
13 | "error",
14 | "unix"
15 | ],
16 | "semi": [
17 | "error",
18 | "never"
19 | ],
20 | "no-console": "off"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/note_client/src/components/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
8 | {{ page.title }}
9 |
10 |
11 |
12 |
13 |
23 |
--------------------------------------------------------------------------------
/note_server/note/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from rest_framework import fields
3 |
4 | from .models import Page
5 |
6 |
7 | class PageSerializer(serializers.ModelSerializer):
8 | """Pageモデルのシリアライザ
9 |
10 | 更新系は、titleとcontentのみ許可
11 | """
12 | class Meta:
13 | model = Page
14 | fields = [
15 | 'id', 'title', 'content', 'created_at', 'updated_at',
16 | ]
17 |
18 | id = fields.IntegerField(read_only=True)
19 | created_at = fields.DateTimeField(read_only=True)
20 | updated_at = fields.DateTimeField(read_only=True)
21 |
--------------------------------------------------------------------------------
/note_server/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "note_server.settings")
7 | try:
8 | from django.core.management import execute_from_command_line
9 | except ImportError as exc:
10 | raise ImportError(
11 | "Couldn't import Django. Are you sure it's installed and "
12 | "available on your PYTHONPATH environment variable? Did you "
13 | "forget to activate a virtual environment?"
14 | ) from exc
15 | execute_from_command_line(sys.argv)
16 |
--------------------------------------------------------------------------------
/note_client/test/util/url-builder.js:
--------------------------------------------------------------------------------
1 | /*
2 | * util/url-builder.jsのテスト
3 | */
4 | import * as assert from 'power-assert'
5 | import { UrlBuilder } from 'util/url-builder'
6 |
7 | describe('UrlBuilder', () => {
8 | it('build', () => {
9 | let target = new UrlBuilder({
10 | TestPath: '/foo/bar/'
11 | })
12 | let result = target.build()
13 | assert.equal('/foo/bar/', result.TestPath)
14 | })
15 |
16 | it('build with prefix', () => {
17 | let target = new UrlBuilder({
18 | TestPath: '/foo/bar/'
19 | }, 'https://example.com')
20 | let result = target.build()
21 | assert.equal(
22 | 'https://example.com/foo/bar/',
23 | result.TestPath,
24 | "prefix付きになること")
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/note_server/note/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.conf import settings
3 |
4 |
5 | class Page(models.Model):
6 | """ページモデル
7 | """
8 | class Meta:
9 | verbose_name = verbose_name_plural = "ページ"
10 | ordering = ('id',)
11 |
12 | user = models.ForeignKey(
13 | settings.AUTH_USER_MODEL,
14 | on_delete=models.CASCADE,
15 | verbose_name="ユーザー")
16 | title = models.CharField("タイトル", max_length=200)
17 | content = models.TextField("内容")
18 | created_at = models.DateTimeField("作成日時", auto_now_add=True)
19 | updated_at = models.DateTimeField("更新日時", auto_now=True)
20 |
21 | def __str__(self):
22 | return "{title}".format(title=self.title)
23 |
--------------------------------------------------------------------------------
/note_server/README.md:
--------------------------------------------------------------------------------
1 | # APIサーバー
2 |
3 | Djangoフレームワーク、Django REST Frameworkを使っています。
4 |
5 | ## virtualenvの作成、有効化
6 |
7 | ```
8 | python3.6 -m venv venv
9 | source venv/bin/activate
10 | ```
11 |
12 | ## Pythonモジュールのインストール
13 |
14 | virtualenvを有効にした状態で、pipコマンドでインストールします。
15 |
16 | ```
17 | pip install -r requirements.txt
18 | ```
19 |
20 | ## データベースファイルの作成とマイグレーション
21 |
22 | データベースはデフォルトのSQLite3を使います。 `migrate` コマンドでデータベースファイルが作成され、マイグレーションも実行されます。
23 |
24 | ```
25 | ./manage.py migrate
26 | ```
27 |
28 | ## ユーザーの作成
29 |
30 | このアプリケーションはDjangoの標準機能を使用したユーザー認証があります。管理者となるユーザーはcreatesuperuserコマンドで作成します。
31 |
32 | ```
33 | ./manage.py createsuperuser
34 | ```
35 |
36 | ## 開発用サーバーの起動
37 |
38 | 開発用サーバーは `runserver` コマンドで起動します。
39 |
40 | ```
41 | ./manage.py runserver
42 | ```
43 |
--------------------------------------------------------------------------------
/note_server/note/views.py:
--------------------------------------------------------------------------------
1 | from rest_framework.permissions import IsAuthenticated
2 | from rest_framework.viewsets import ModelViewSet
3 |
4 | from .models import Page
5 | from .serializers import PageSerializer
6 |
7 |
8 | class PageViewSet(ModelViewSet):
9 | """Pageモデルの一覧、作成、更新、削除のAPI
10 | """
11 | serializer_class = PageSerializer
12 | # ログイン済みの場合だけ許可
13 | permission_classes = (IsAuthenticated,)
14 |
15 | def get_queryset(self):
16 | # ログイン中のユーザーのページのみを返す
17 | return Page.objects.filter(
18 | user=self.request.user,
19 | ).order_by('-updated_at', '-id')
20 |
21 | def perform_create(self, serializer):
22 | """追加時のフック
23 |
24 | ログイン中のユーザー情報を記録します
25 | """
26 | serializer.save(user=self.request.user)
27 |
--------------------------------------------------------------------------------
/note_server/templates/base.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |
4 |
5 |
6 |
7 | {% block title %}{% endblock %}
8 |
9 |
10 |
21 | {% block content %}{% endblock %}
22 |
23 |
24 |
--------------------------------------------------------------------------------
/note_server/templates/login.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load static %}
3 |
4 | {% block title %}ログイン{% endblock %}
5 |
6 | {% block content %}
7 |
23 |
24 | {% endblock %}
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Shinya Okano
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/note_client/src/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * クライアントアプリケーションのエントリポイント
3 | */
4 | import 'bootstrap/dist/css/bootstrap.css'
5 | import 'bootstrap-vue/dist/bootstrap-vue.css'
6 | import './style/font-awesome-config.scss'
7 | import './style/navigation.scss'
8 | import Vue from 'vue'
9 | import BootstrapVue from 'bootstrap-vue'
10 |
11 | import { NoteController } from './controller/note'
12 | import { UrlBuilder } from './util/url-builder'
13 | import { API_URL } from './resource/urls'
14 |
15 | import Index from './components/Index.vue'
16 | import Editor from './components/Editor.vue'
17 | import Note from './components/Note.vue'
18 |
19 | Vue.use(BootstrapVue)
20 |
21 | let controller = new NoteController(
22 | (new UrlBuilder(API_URL)).build()
23 | )
24 |
25 | Vue.component('index', Index) // ページ一覧のコンポーネント
26 | Vue.component('editor', Editor) // エディタ部分のコンポーネント
27 | Vue.component('note', Note) // IndexとEditor、ボタンを含むコンポーネント(クライアントアプリケーション部分)
28 |
29 | new Vue({
30 | el: '#app',
31 | components: {
32 | Note
33 | },
34 | data: {
35 | controller: controller
36 | },
37 | mounted() {
38 | this.$nextTick(() => {
39 | controller.ready()
40 | })
41 | }
42 | })
43 |
--------------------------------------------------------------------------------
/note_client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "note-client",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "mocha",
8 | "webpack": "webpack --mode development",
9 | "webpack-production": "webpack --mode production",
10 | "clean": "rm build/*.js",
11 | "eslint": "eslint src/"
12 | },
13 | "author": "",
14 | "license": "ISC",
15 | "devDependencies": {
16 | "axios": "^0.18.0",
17 | "babel-core": "^6.26.0",
18 | "babel-loader": "^7.1.4",
19 | "babel-plugin-module-resolver": "^3.1.0",
20 | "babel-polyfill": "^6.26.0",
21 | "babel-preset-env": "^1.6.1",
22 | "bootstrap-vue": "^2.0.0-rc.2",
23 | "cookie": "^0.3.1",
24 | "css-loader": "^0.28.10",
25 | "eslint": "^4.19.1",
26 | "file-loader": "^1.1.11",
27 | "font-awesome": "^4.7.0",
28 | "mocha": "^5.0.4",
29 | "mocha-loader": "^1.1.3",
30 | "moxios": "^0.4.0",
31 | "node-sass": "^4.7.2",
32 | "power-assert": "^1.4.4",
33 | "sass-loader": "^6.0.7",
34 | "sinon": "^4.4.6",
35 | "style-loader": "^0.20.3",
36 | "vue": "^2.5.16",
37 | "vue-loader": "^14.2.1",
38 | "vue-style-loader": "^4.0.2",
39 | "vue-template-compiler": "^2.5.16",
40 | "webpack": "^4.1.1",
41 | "webpack-cli": "^2.0.12"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/note_server/note/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.2 on 2018-03-14 14:12
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='Page',
19 | fields=[
20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21 | ('title', models.CharField(max_length=200, verbose_name='タイトル')),
22 | ('content', models.TextField(verbose_name='内容')),
23 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='作成日時')),
24 | ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新日時')),
25 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='ユーザー')),
26 | ],
27 | options={
28 | 'verbose_name': 'ページ',
29 | 'verbose_name_plural': 'ページ',
30 | 'ordering': ('id',),
31 | },
32 | ),
33 | ]
34 |
--------------------------------------------------------------------------------
/note_server/note_server/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path, include
2 | from django.shortcuts import render
3 | from django.conf import settings
4 | from django.conf.urls.static import static
5 | from django.views.decorators.csrf import ensure_csrf_cookie
6 | from django.contrib import admin
7 | from django.contrib.auth import views as auth_views
8 | from django.contrib.auth.decorators import login_required
9 |
10 | from rest_framework.documentation import include_docs_urls
11 |
12 | urlpatterns = [
13 | # 管理画面
14 | path('admin/', admin.site.urls),
15 | # ログイン
16 | path(
17 | 'login',
18 | auth_views.LoginView.as_view(
19 | template_name='login.html',
20 | redirect_authenticated_user=True),
21 | name='login'),
22 | # ログアウト
23 | path(
24 | 'logout',
25 | auth_views.LogoutView.as_view(
26 | next_page='login'),
27 | name='logout'),
28 |
29 | # APIドキュメント
30 | path('docs/', include_docs_urls(title='API', public=False)),
31 | # ノートアプリケーションのAPI
32 | path('note/', include('note.urls')),
33 |
34 | # ノートアプリケーションの画面
35 | path('', ensure_csrf_cookie(login_required(render)),
36 | kwargs={'template_name': 'index.html'}, name='index'),
37 | ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # staticfilesのファイルを配信
38 |
39 | # デバッグ時のみdjango-debug-toolbarのURLを追加
40 | if settings.DEBUG:
41 | import debug_toolbar
42 | urlpatterns = [
43 | path(r'__debug__/', include(debug_toolbar.urls)),
44 | ] + urlpatterns
45 |
--------------------------------------------------------------------------------
/note_client/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var path = require('path');
3 |
4 | module.exports = {
5 | entry: {
6 | babel_polyfill: 'babel-polyfill',
7 | index: './src/index.js', // アプリケーションページ用
8 | common: './src/common.js' // 共通ページ用
9 | },
10 | output: {
11 | path: path.resolve('./build'), // ビルドしたファイルの出力先
12 | filename: '[name].js'
13 | },
14 | resolve: {
15 | extensions: ['.js'],
16 | alias: {
17 | 'vue$': 'vue/dist/vue.esm.js'
18 | }
19 | },
20 | module: {
21 | rules: [
22 | {
23 | test: /\.js$/,
24 | exclude: /node_modules/,
25 | include: [
26 | path.resolve(__dirname, 'src'),
27 | require.resolve('bootstrap-vue')
28 | ],
29 | use: [
30 | 'babel-loader'
31 | ]
32 | },
33 | {
34 | test: /\.vue$/,
35 | use: [
36 | 'vue-loader'
37 | ]
38 | },
39 | {
40 | test: /\.scss$/,
41 | use: [
42 | 'style-loader',
43 | 'css-loader',
44 | 'sass-loader'
45 | ]
46 | },
47 | {
48 | test: /\.css$/,
49 | use: [
50 | 'style-loader',
51 | 'css-loader'
52 | ]
53 | },
54 | {
55 | // FontAwesome
56 | test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
57 | use: [{
58 | loader: 'file-loader',
59 | options: {
60 | name: '[name].[ext]',
61 | outputPath: 'fonts/',
62 | publicPath: '/static/client/fonts/'
63 | }
64 | }]
65 | }
66 | ]
67 | }
68 | };
69 |
--------------------------------------------------------------------------------
/note_client/src/model/page.js:
--------------------------------------------------------------------------------
1 | class Page {
2 | constructor(
3 | id=null, title='', content='', createdAt=null,
4 | updatedAt=null, taint=false) {
5 | this.id = id
6 | this._title = title
7 | this._content = content
8 | this.createdAt = createdAt
9 | this.updatedAt = updatedAt
10 | this.taint = taint // 内容が変更されたことを保持するフラグ
11 | this.origin = null
12 | }
13 |
14 | /*
15 | * APIから受け取ったJSONからインスタンスを生成するメソッド
16 | */
17 | static fromData(data) {
18 | let instance = new Page(
19 | data.id || null,
20 | data.title || null,
21 | data.content || '',
22 | data.created_at || null,
23 | data.updated_at || null
24 | )
25 | // 変更を破棄する機能のために、元データを保持
26 | instance.origin = data
27 | return instance
28 | }
29 |
30 | /*
31 | * インスタンスを保存用にシリアライズする
32 | */
33 | toData() {
34 | return {
35 | id: this.id,
36 | title: this._title,
37 | content: this._content
38 | }
39 | }
40 |
41 | get title() {
42 | return this._title
43 | }
44 |
45 | set title(value) {
46 | this._title = value
47 | this.taint = true
48 | }
49 |
50 | get content() {
51 | return this._content
52 | }
53 |
54 | set content(value) {
55 | this._content = value
56 | this.taint = true
57 | }
58 |
59 | /*
60 | * 変更を破棄する
61 | * インスタンス構築時点のデータの状態に戻す
62 | */
63 | revert() {
64 | if (this.origin == null) {
65 | this._title = ''
66 | this._content = ''
67 | } else {
68 | this._title = this.origin.title
69 | this._content = this.origin.content
70 | }
71 | this.taint = false
72 | }
73 | }
74 |
75 | export {
76 | Page
77 | }
78 |
--------------------------------------------------------------------------------
/note_client/test/model/page.js:
--------------------------------------------------------------------------------
1 | /*
2 | * model/page.jsのテスト
3 | */
4 | import * as assert from 'power-assert'
5 | import { Page } from 'model/page'
6 |
7 | describe('Page', () => {
8 | it('fromData', () => {
9 | let result = Page.fromData({
10 | id: 123,
11 | title: 'テストタイトル',
12 | content: 'テスト本文'
13 | })
14 | assert.equal(123, result.id)
15 | assert.equal('テストタイトル', result.title)
16 | assert.equal('テスト本文', result.content)
17 | })
18 |
19 | it('toData', () => {
20 | let target = new Page(123, 'テストタイトル', 'テスト本文')
21 | let result = target.toData()
22 | assert.equal(123, result.id)
23 | assert.equal('テストタイトル', result.title)
24 | assert.equal('テスト本文', result.content)
25 | })
26 |
27 | it('set title:taint', () => {
28 | let target = new Page(123, 'テストタイトル', 'テスト本文')
29 | assert(!target.taint, '作成時は未変更')
30 | target.title = 'タイトル変更'
31 | assert(target.taint, 'タイトル変更後は変更')
32 | })
33 |
34 | it('set content:taint', () => {
35 | let target = new Page(123, 'テストタイトル', 'テスト本文')
36 | assert(!target.taint, '作成時は未変更')
37 | target.content = '本文変更'
38 | assert(target.taint, '本文変更後は変更')
39 | })
40 |
41 | it('revert', () => {
42 | let target = new Page(123, 'テストタイトル', 'テスト本文')
43 | target.origin = {
44 | title: target.title,
45 | content: target.content
46 | }
47 | target.title = 'タイトル変更'
48 | target.content = '本文変更'
49 | assert.equal(target.title, 'タイトル変更', '変更は保持されている')
50 | assert.equal(target.content, '本文変更', '変更は保持されている')
51 | target.revert()
52 | assert.equal(target.title, 'テストタイトル', '変更前に戻る')
53 | assert.equal(target.content, 'テスト本文', '変更前に戻る')
54 | })
55 | })
56 |
--------------------------------------------------------------------------------
/note_client/README.md:
--------------------------------------------------------------------------------
1 | # クライアントアプリケーション
2 |
3 | ## Node.JSのパッケージをインストールする
4 |
5 | ```
6 | npm i
7 | ```
8 |
9 | ## アプリケーションのビルド
10 |
11 | ```
12 | npm run webpack
13 | ```
14 |
15 | ## ファイルの変更を検知してビルド
16 |
17 | ```
18 | npm run webpack -- -w
19 | ```
20 |
21 | ## ビルドされたファイル群の削除
22 |
23 | ```
24 | npm run clean
25 | ```
26 |
27 | ## テストコードの実行
28 |
29 | ```
30 | npm t
31 | ```
32 |
33 | ## ファイルとディレクトリの説明
34 |
35 | ```
36 | .
37 | ├── README.md
38 | ├── build # webpackでビルドしたファイルが出力されるディレクトリ
39 | │ ├── babel_polyfill.js # ブラウザの機能の差異を吸収するためのpolyfillモジュール
40 | │ ├── common.js # 共通モジュール
41 | │ ├── fonts # Font Awesomeのフォント
42 | │ │ ├── fontawesome-webfont.eot
43 | │ │ ├── fontawesome-webfont.svg
44 | │ │ ├── fontawesome-webfont.ttf
45 | │ │ ├── fontawesome-webfont.woff
46 | │ │ └── fontawesome-webfont.woff2
47 | │ └── index.js # index.htmlから使われるノートアプリケーションのエントリポイント
48 | ├── package-lock.json # package.jsonに書かれたモジュールとその依存モジュールすべてのバージョンが書かれているファイル
49 | ├── package.json # このアプリケーションのパッケージ情報(依存パッケージの情報などが書かれている)
50 | ├── src # アプリケーションのソースコード
51 | │ ├── common.js
52 | │ ├── components # Vueコンポーネント
53 | │ │ ├── Editor.vue
54 | │ │ ├── Index.vue
55 | │ │ └── Note.vue
56 | │ ├── controller # コントローラ
57 | │ │ └── note.js
58 | │ ├── index.js # エントリポイント
59 | │ ├── model # モデル
60 | │ │ └── page.js
61 | │ ├── resource # URL定義やメッセージカタログなどの設定ファイル以外の固定値
62 | │ │ └── urls.js
63 | │ ├── style # アプリケーションで使うCSS
64 | │ │ ├── Note.scss
65 | │ │ ├── font-awesome-config.scss
66 | │ │ └── navigation.scss
67 | │ └── util # ユーティリティクラス
68 | │ ├── csrf-token.js
69 | │ ├── rest-api.js
70 | │ └── url-builder.js
71 | ├── test # テストコード
72 | │ ├── controller
73 | │ │ └── note.js
74 | │ ├── mocha.opts # テストランナーの設定
75 | │ ├── model
76 | │ │ └── page.js
77 | │ └── util
78 | │ ├── csrf-token.js
79 | │ ├── rest-api.js
80 | │ └── url-builder.js
81 | └── webpack.config.js
82 | ```
83 |
--------------------------------------------------------------------------------
/Vagrantfile:
--------------------------------------------------------------------------------
1 | # -*- mode: ruby -*-
2 | # vi: set ft=ruby :
3 |
4 | Vagrant.configure("2") do |config|
5 | config.vm.box = "ubuntu/xenial64"
6 | config.vm.network "private_network", ip: "192.168.33.10"
7 | config.vm.provider :virtualbox do |vb|
8 | vb.memory = "2048"
9 | end
10 | config.ssh.forward_agent = true
11 | # Add deadsnakes repository
12 | apt-key adv --recv-keys --keyserver keyserver.ubuntu.com 5BB92C09DB82666C
13 | add-apt-repository -y ppa:fkrull/deadsnakes
14 |
15 | # Add NodeJS repository
16 | curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
17 |
18 | # Update package list
19 | export DEBIAN_FRONTEND=noninteractive
20 | apt-get update --allow-unauthenticated
21 |
22 | # Generic development
23 | apt-get install -y \
24 | tree \
25 | zip \
26 | unzip \
27 | build-essential \
28 | language-pack-ja-base \
29 | language-pack-ja
30 |
31 | # Japanese locale
32 | update-locale LANG=ja_JP.UTF-8
33 |
34 | # Set timezone
35 | timedatectl set-timezone Asia/Tokyo
36 |
37 | # Python development
38 | apt-get install -y \
39 | python3.6 \
40 | python3.6-dev \
41 | python3.6-venv
42 |
43 | # NodeJS development
44 | apt-get install -y nodejs
45 |
46 | # Common Packages
47 | apt-get install -y \
48 | ca-certificates \
49 | curl \
50 | git \
51 | libcurl4-openssl-dev \
52 | libffi-dev \
53 | libjpeg-dev \
54 | libpng12-dev \
55 | libpq-dev \
56 | libsqlite3-dev \
57 | libssl-dev \
58 | libxml2-dev \
59 | libxslt1-dev \
60 | libz-dev \
61 | wget \
62 | zlib1g-dev
63 |
64 | # ngrok
65 | if [ ! -e /usr/local/bin/ngrok ]; then
66 | wget -q https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip -O /tmp/ngrok-stable-linux-amd64.zip
67 | unzip -o /tmp/ngrok-stable-linux-amd64.zip -d /usr/local/bin/
68 | fi
69 | EOS
70 | end
71 |
--------------------------------------------------------------------------------
/note_client/src/controller/note.js:
--------------------------------------------------------------------------------
1 | import { Page } from '../model/page'
2 | import { RestApi } from '../util/rest-api'
3 |
4 | /*
5 | * コントローラ
6 | * データも保持しているので、ViewModelとしても使っています
7 | */
8 | class NoteController {
9 | constructor(apiUrl) {
10 | // RestApiインスタンスの生成は呼び出し元に置くか悩みましたが、
11 | // 依存していてもさほど問題なさそうなのでここで呼んでいます
12 | this.pageApi = new RestApi(apiUrl.NotePage, Page)
13 | this.pages = []
14 | this.selectedPage = null
15 | this.loaded = false
16 | }
17 |
18 | /*
19 | * エントリポイントからロード完了時に呼ばれるメソッド
20 | */
21 | ready() {
22 | this.load()
23 | }
24 |
25 | /*
26 | * ページ一覧の取得
27 | */
28 | load() {
29 | this.pageApi.list()
30 | .then((instances) => {
31 | this.pages = instances
32 | this.loaded = true
33 | })
34 | }
35 |
36 | /*
37 | * ページを選択する
38 | */
39 | selectPage(page) {
40 | if (this.selectedPage && this.selectedPage.taint) {
41 | return [false, "変更が保存されていません。"]
42 | }
43 | this.selectedPage = page
44 | return [true, null]
45 | }
46 |
47 | /*
48 | * 現在のページを保存する
49 | */
50 | save(csrfToken=null) {
51 | if (!this.selectedPage.title || !this.selectedPage.content) {
52 | return [false, "タイトルと内容は必須です。", null]
53 | }
54 | let promise = this.pageApi.save(this.selectedPage, csrfToken)
55 | .then((instance) => {
56 | Object.assign(this.selectedPage, instance)
57 | })
58 | return [true, "保存しています...", promise]
59 | }
60 |
61 | /*
62 | * 変更を破棄する
63 | */
64 | revert() {
65 | if (this.selectedPage) {
66 | this.selectedPage.revert()
67 | }
68 | }
69 |
70 | /*
71 | * 現在のページを削除する
72 | */
73 | destroy(csrfToken=null) {
74 | if (this.selectedPage.id == null) {
75 | this.pages.pop()
76 | this.selectedPage = null
77 | return [true, null, null]
78 | }
79 | let promise = this.pageApi.destroy(this.selectedPage, csrfToken)
80 | .then(() => {
81 | this.selectedPage = null
82 | this.load()
83 | })
84 | return [true, "削除しています...", promise]
85 | }
86 |
87 | /*
88 | * 新規ページを作る
89 | * バックエンドへの保存はしません
90 | */
91 | create() {
92 | if (this.selectedPage && this.selectedPage.taint) {
93 | return
94 | }
95 | let page = new Page
96 | page.taint = true
97 | this.pages.push(page)
98 | this.selectedPage = page
99 | }
100 | }
101 |
102 | export {
103 | NoteController
104 | }
105 |
--------------------------------------------------------------------------------
/note_client/src/util/rest-api.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | /*
4 | * REST APIクライントのラッパー
5 | * API呼び出しの結果からモデルのインスタンスを生成します
6 | */
7 | class RestApi {
8 | constructor(endpoint, model) {
9 | this.endpoint = endpoint
10 | this.model = model
11 | }
12 |
13 | list(options={}) {
14 | return new Promise((resolve, reject) => {
15 | axios.get(this.endpoint, options)
16 | .then((response) => {
17 | resolve(response.data.map(this.model.fromData))
18 | })
19 | .catch((error) => {
20 | console.log(error)
21 | reject(error)
22 | })
23 | })
24 | }
25 |
26 | create(instance, csrfToken=null, options={}) {
27 | let sendOptions = {}
28 | Object.assign(sendOptions, options, {
29 | headers: {'X-CSRFToken': csrfToken}
30 | })
31 | return new Promise((resolve, reject) => {
32 | axios.post(
33 | this.endpoint,
34 | instance.toData(),
35 | sendOptions
36 | )
37 | .then((response) => {
38 | resolve(this.model.fromData(response.data))
39 | })
40 | .catch((error) => {
41 | console.log(error)
42 | reject(error)
43 | })
44 | })
45 | }
46 |
47 | update(instance, csrfToken=null, options={}) {
48 | let sendOptions = {}
49 | Object.assign(sendOptions, options, {
50 | headers: {'X-CSRFToken': csrfToken}
51 | })
52 | return new Promise((resolve, reject) => {
53 | axios.put(
54 | this.endpoint + `${instance.id}/`,
55 | instance.toData(),
56 | sendOptions
57 | )
58 | .then((response) => {
59 | resolve(this.model.fromData(response.data))
60 | })
61 | .catch((error) => {
62 | console.log(error)
63 | reject(error)
64 | })
65 | })
66 | }
67 |
68 | save(instance, csrfToken=null, options={}) {
69 | // idがない場合は新規、あれば更新
70 | if (instance.id == null) {
71 | return this.create(instance, csrfToken, options)
72 | } else {
73 | return this.update(instance, csrfToken, options)
74 | }
75 | }
76 |
77 | destroy(instance, csrfToken=null) {
78 | return new Promise((resolve, reject) => {
79 | axios.delete(
80 | this.endpoint + `${instance.id}/`,
81 | {
82 | headers: {'X-CSRFToken': csrfToken}
83 | })
84 | .then(() => {
85 | resolve()
86 | })
87 | .catch((error) => {
88 | console.log(error)
89 | reject(error)
90 | })
91 | })
92 | }
93 | }
94 |
95 | export {
96 | RestApi
97 | }
98 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ノートアプリケーション
2 |
3 | [](https://travis-ci.org/tokibito/note-app-django-vue-javascript)
4 |
5 | 
6 |
7 | ## これは何ですか?
8 |
9 | テキストを編集、保存できるシンプルなウェブアプリケーションです。
10 |
11 | バックエンドとなるAPIサーバーにはDjangoフレームワーク(Python)、フロントエンドにはVue.js(JavaScript)を使っています。
12 |
13 | DjangoフレームワークとJavaScriptでアプリケーションを作るサンプルコードとして作成しました。
14 |
15 | **このリポジトリのコードではVuexを使っていません。Vuex対応版は[note-app-django-vuex](https://github.com/tokibito/note-app-django-vuex)になります。**
16 |
17 | 以下の要素を含んでいます:
18 |
19 | * バックエンド
20 | * Python3
21 | * venv
22 | * プロジェクトで使うPython環境を作成します
23 | * Djangoフレームワーク
24 | * Django REST Framework
25 | * REST APIを作るのに便利な機能がたくさん含まれるモジュール
26 | * django-debug-toolbar
27 | * 各種デバッグ情報をサイドバーで表示します
28 | * フロントエンド
29 | * Babel
30 | * 最新の言語仕様の構文で書いたコードは、そのままだと古いブラウザなどで動かないので、Babelを使ってトランスパイルして動かせるようにします
31 | * webpack
32 | * JavaScriptコードやCSSなど、アプリケーションを構成するファイル群を配備用にまとめます
33 | * webpack.config.jsでビルドルールを設定します
34 | * loaderの仕組みによりいろいろな処理ができます(babelで変換したりとか)
35 | * Vue.js
36 | * データバインディングとコンポーネントの仕組みを提供するJavaScriptフレームワークです
37 | * Bootstrap
38 | * HTMLのUI部品(ナビゲーションやボタンなどいろいろ)を提供するUIフレームワークです
39 | * CSSと動きのある部分に必要なJavaScriptコードが提供されます
40 | * jQuery部分をVue.jsに置き換えたBootstrapVueと組み合わせて使っています
41 | * Font Awesome
42 | * Webフォント、CSSが提供されるので、HTMLでクラスを指定すれば使えます
43 | * axios
44 | * REST APIクライアントです
45 | * ユニットテスト
46 | * mocha
47 | * ユニットテストのフレームワークです(describe, itで記述)
48 | * power-assert
49 | * アサーション関数
50 | * Sinon.JS
51 | * モック
52 | * moxios
53 | * axiosに対応したモック(スタブ)
54 |
55 | ## 構成
56 |
57 | * `note_server`
58 | * バックエンド(Pythonで動作するAPIサーバー)
59 | * `note_client`
60 | * フロントエンド(webpackでビルドし、Djangoフレームワークのstaticfilesモジュールから配信される)
61 | * CSS(Sass)も含む
62 |
63 | ## 動かしてみる
64 |
65 | 1. `note_client` をビルドする
66 | 2. `note_server` を起動してブラウザでアクセスする
67 |
68 | ## 設計について
69 |
70 | アーキテクチャとモジュール構成に関して考えた点など:
71 |
72 | * シングルページアプリケーション(SPA)にはせず、DjangoのサーバーサイドレンダリングとVue.jsを組み合わせて使う
73 | * ビルドしたファイルの配信は、Djangoフレームワークのstaticfilesに任せる
74 | * jQueryは使わない
75 | * 表示制御のために、アプリケーションで大量のJavaScriptでコードを書くのを避けるため
76 | * Vue.jsのデータバインディングを使えば表示制御のコードはかなり減らせます
77 | * BootstrapではBootstrapVueを使います
78 | * Vuexはなるべく使わない
79 | * 依存をなるべく減らす気持ち
80 | * Vuexは学習コストもメンテコストも高いので、使わないで済むうちは使わない
81 | * 複雑になったら使ったほうが楽できるとは思います
82 | * vue-cliを使っていない
83 | * ごちゃっと余計なものが入るのを避ける
84 | * 使うツールスタックを合わせられるなら使ってもよいかな
85 | * Vueへの依存をなるべく広げない
86 | * Vueに依存しないほうがテストコードを書きやすいから
87 | * Vueインスタンスをエントリポイント(index.js)外のJavaScriptコードに渡さない
88 | * エントリポイントではVueに依存しないコントローラクラスのインスタンスを生成し、データはコントローラに持たせる
89 | * コンポーネントから外へVueインスタンスを渡さない
90 | * Bootstrapのモーダルダイアログを表示する場合は、コントローラから呼び出すのではなく、コントローラからの戻り値をコンポーネント側で使って表示制御する
91 | * django-webpack-loaderを使っていない
92 | * なるべく依存を増やさない
93 | * 必要になったら入れよう
94 | * DjangoのCSRF対策をフロントエンドからも利用する
95 | * Cookieに書き込まれたCSRFトークンをAPI呼び出し時に利用しています
96 | * Vueコンポーネントのテストはがんばらない
97 | * 表示部分は変更されやすいのであんまり頑張らない
98 | * Vueコンポーネントにアプリケーションロジックを書かないなら、テストもがんばらなくて済む
99 | * Vueの単一ファイルコンポーネントをテストする仕組みの用意がそもそも大変
100 | * 楽になったらやろう
101 |
102 | ## Vagrant
103 |
104 | 開発にはVagrantを使用しています。VirtualBoxとVagrantをインストールしていれば、同様の環境を用意できます。
105 |
106 | ```
107 | vagrant up
108 | vagrant ssh
109 | ```
110 |
--------------------------------------------------------------------------------
/note_client/test/util/rest-api.js:
--------------------------------------------------------------------------------
1 | /*
2 | * util/rest-api.jsのテスト
3 | */
4 | import * as assert from 'power-assert'
5 | import moxios from 'moxios'
6 | import { RestApi } from 'util/rest-api'
7 |
8 | /*
9 | * APIのレスポンスをラップするモデル
10 | */
11 | class StubModel {
12 | constructor(id=null, value=null) {
13 | this.id = id
14 | this.value = value
15 | }
16 |
17 | static fromData(data) {
18 | return new StubModel(data.id, data.value)
19 | }
20 |
21 | toData() {
22 | return {
23 | value: this.value
24 | }
25 | }
26 | }
27 |
28 | describe('RestApi', () => {
29 | beforeEach(() => {
30 | moxios.install()
31 | })
32 |
33 | afterEach(() => {
34 | moxios.uninstall()
35 | })
36 |
37 | it('list', (done) => {
38 | let target = new RestApi('/foo/bar/', StubModel)
39 | target.list().then((instances) => {
40 | assert.equal(1, instances[0].id)
41 | assert.equal('test1', instances[0].value)
42 | assert.equal(2, instances[1].id)
43 | assert.equal('test2', instances[1].value)
44 | assert.equal(2, instances.length, 'データは2件')
45 | // assertでエラーが発生した場合はdoneが呼ばれないため、
46 | // テストケースがtimeoutでfailになる
47 | done()
48 | })
49 | .catch((error) => {
50 | console.log(error)
51 | reject(error)
52 | })
53 | // API呼び出しをモック
54 | moxios.wait(() => {
55 | let request = moxios.requests.mostRecent()
56 | request.respondWith({
57 | status: 200,
58 | response: [
59 | {id: 1, value: 'test1'},
60 | {id: 2, value: 'test2'}
61 | ]
62 | })
63 | })
64 | })
65 |
66 | it('create', (done) => {
67 | let requestInstance = new StubModel(null, 'test123')
68 | let target = new RestApi('/foo/bar/', StubModel)
69 | target.create(requestInstance, 'csrf-token')
70 | .then((instance) => {
71 | assert.equal(1, instance.id)
72 | assert.equal('test123', instance.value)
73 | done()
74 | })
75 | .catch((error) => {
76 | console.log(error)
77 | reject(error)
78 | })
79 | // API呼び出しをモック
80 | moxios.wait(() => {
81 | let request = moxios.requests.mostRecent()
82 | request.respondWith({
83 | status: 200,
84 | response: {id: 1, value: 'test123'}
85 | })
86 | })
87 | })
88 |
89 | it('update', (done) => {
90 | let requestInstance = new StubModel(1, 'test456')
91 | let target = new RestApi('/foo/bar/', StubModel)
92 | target.update(requestInstance, 'csrf-token')
93 | .then((instance) => {
94 | assert.equal(1, instance.id)
95 | assert.equal('test456', instance.value)
96 | done()
97 | })
98 | .catch((error) => {
99 | console.log(error)
100 | reject(error)
101 | })
102 | // API呼び出しをモック
103 | moxios.wait(() => {
104 | let request = moxios.requests.mostRecent()
105 | request.respondWith({
106 | status: 200,
107 | response: {id: 1, value: 'test456'}
108 | })
109 | })
110 | })
111 |
112 | it('destroy', (done) => {
113 | let requestInstance = new StubModel(1, 'test456')
114 | let target = new RestApi('/foo/bar/', StubModel)
115 | target.destroy(requestInstance, 'csrf-token')
116 | .then(() => {
117 | done()
118 | })
119 | .catch((error) => {
120 | console.log(error)
121 | reject(error)
122 | })
123 | // API呼び出しをモック
124 | moxios.wait(() => {
125 | let request = moxios.requests.mostRecent()
126 | request.respondWith({
127 | status: 200
128 | })
129 | })
130 | })
131 | })
132 |
--------------------------------------------------------------------------------
/note_client/src/components/Note.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
15 |
22 |
28 |
33 |
34 |
40 |
41 |
48 | {{ message }}
49 |
50 |
56 | 削除してもよろしいですか?
57 |
58 |
67 | {{ message }}
68 |
69 |
70 |
71 |
72 |
73 |
146 |
--------------------------------------------------------------------------------
/note_server/note_server/settings.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
4 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
5 |
6 | # Quick-start development settings - unsuitable for production
7 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
8 |
9 | # SECURITY WARNING: keep the secret key used in production secret!
10 | SECRET_KEY = 'm*zw1rnybebk73wphlofum@=8or25eiphgvwu196_8us3na#y8'
11 |
12 | # SECURITY WARNING: don't run with debug turned on in production!
13 | DEBUG = True
14 |
15 | ALLOWED_HOSTS = ['*']
16 | # django-debug-toolbarはINTERNAL_IPSからの通信でのみ有効になるため、VMのNATのアドレスを入れています
17 | INTERNAL_IPS = ['192.168.33.1']
18 |
19 | # Application definition
20 |
21 | INSTALLED_APPS = [
22 | 'django.contrib.admin',
23 | 'django.contrib.auth',
24 | 'django.contrib.contenttypes',
25 | 'django.contrib.sessions',
26 | 'django.contrib.messages',
27 | 'django.contrib.staticfiles',
28 | 'rest_framework',
29 | 'debug_toolbar',
30 | 'note.apps.NoteConfig',
31 | ]
32 |
33 | MIDDLEWARE = [
34 | 'django.middleware.security.SecurityMiddleware',
35 | 'django.contrib.sessions.middleware.SessionMiddleware',
36 | 'django.middleware.common.CommonMiddleware',
37 | 'django.middleware.csrf.CsrfViewMiddleware',
38 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
39 | 'django.contrib.messages.middleware.MessageMiddleware',
40 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
41 | ]
42 |
43 | # デバッグ時のみミドルウェアを有効にします
44 | if DEBUG:
45 | MIDDLEWARE.append(
46 | 'debug_toolbar.middleware.DebugToolbarMiddleware',
47 | )
48 |
49 | ROOT_URLCONF = 'note_server.urls'
50 |
51 | TEMPLATES = [
52 | {
53 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
54 | 'DIRS': [
55 | # プロジェクト直下のtemplatesディレクトリにテンプレートファイルをまとめます
56 | os.path.join(BASE_DIR, 'templates'),
57 | ],
58 | 'APP_DIRS': True,
59 | 'OPTIONS': {
60 | 'context_processors': [
61 | 'django.template.context_processors.debug',
62 | 'django.template.context_processors.request',
63 | 'django.contrib.auth.context_processors.auth',
64 | 'django.contrib.messages.context_processors.messages',
65 | ],
66 | },
67 | },
68 | ]
69 |
70 | WSGI_APPLICATION = 'note_server.wsgi.application'
71 |
72 |
73 | # Database
74 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases
75 |
76 | DATABASES = {
77 | 'default': {
78 | 'ENGINE': 'django.db.backends.sqlite3',
79 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
80 | }
81 | }
82 |
83 |
84 | # Password validation
85 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
86 |
87 | AUTH_PASSWORD_VALIDATORS = [
88 | # {
89 | # 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
90 | # },
91 | # {
92 | # 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
93 | # },
94 | # {
95 | # 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
96 | # },
97 | # {
98 | # 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
99 | # },
100 | ]
101 |
102 |
103 | # Internationalization
104 | # https://docs.djangoproject.com/en/2.0/topics/i18n/
105 |
106 | LANGUAGE_CODE = 'ja'
107 | TIME_ZONE = 'Asia/Tokyo'
108 | USE_I18N = True
109 | USE_L10N = True
110 | USE_TZ = True
111 |
112 | # Static files (CSS, JavaScript, Images)
113 | # https://docs.djangoproject.com/en/2.0/howto/static-files/
114 |
115 | STATIC_URL = '/static/'
116 |
117 | # ビルドされたクライアントアプリケーションをDjangoのstaticfilesから参照します
118 | STATICFILES_DIRS = [
119 | ('client', os.path.join(os.path.dirname(BASE_DIR), 'note_client/build')),
120 | ]
121 | # collectstaticの出力先
122 | STATIC_ROOT = os.path.join(BASE_DIR, 'static')
123 |
124 | LOGIN_URL = '/login'
125 | # ログイン後リダイレクト先
126 | LOGIN_REDIRECT_URL = 'index'
127 |
128 | # DjangoRestFramework
129 | REST_FRAMEWORK = {
130 | 'DEFAULT_AUTHENTICATION_CLASSES': (
131 | # request.userを使う
132 | 'rest_framework.authentication.SessionAuthentication',
133 | )
134 | }
135 |
--------------------------------------------------------------------------------
/note_client/test/controller/note.js:
--------------------------------------------------------------------------------
1 | /*
2 | * controller/note.jsのテスト
3 | */
4 | import * as assert from 'power-assert'
5 | import sinon from 'sinon'
6 |
7 | import { NoteController } from 'controller/note'
8 | import { Page } from 'model/page'
9 |
10 | describe('NoteController', () => {
11 | it('ready', () => {
12 | let target = new NoteController('/foo/bar/')
13 | let loadMock = sinon.spy()
14 | target.load = loadMock
15 | target.ready()
16 | assert(loadMock.called, "load()が呼ばれること")
17 | })
18 |
19 | it('load', () => {
20 | let target = new NoteController('/foo/bar/')
21 | // list()をモック
22 | target.pageApi.list = () => {
23 | return new Promise((resolve, reject) => {
24 | resolve('testLoaded')
25 | assert(target.loaded, "ロード済みになる")
26 | assert.equal('testLoaded', target.pages, "pagesに結果が保持される")
27 | })
28 | .catch((error) => {
29 | console.log(error)
30 | reject(error)
31 | })
32 | }
33 | assert(!target.loaded, "最初は未ロード")
34 | target.load()
35 | })
36 |
37 | describe('selectPage', () => {
38 | it('正常に変更できる場合', () => {
39 | let target = new NoteController('/foo/bar/')
40 | let pageCurrent = new Page(1)
41 | let pageNext = new Page(2)
42 | target.selectedPage = pageCurrent
43 | let result, message
44 | [result, message] = target.selectPage(pageNext)
45 | assert(result, 'ページを変更できる')
46 | assert.equal(2, target.selectedPage.id, 'id=2のページになる')
47 | })
48 |
49 | it('編集済み未保存の場合', () => {
50 | let target = new NoteController('/foo/bar/')
51 | let pageCurrent = new Page(1)
52 | pageCurrent.taint = true
53 | let pageNext = new Page(2)
54 | target.selectedPage = pageCurrent
55 | let result, message
56 | [result, message] = target.selectPage(pageNext)
57 | assert(!result, 'taint状態の場合はページを変更できない')
58 | assert.equal(1, target.selectedPage.id, 'id=1のページのまま')
59 | })
60 | })
61 |
62 | describe('save', () => {
63 | it('正常に保存できる場合', () => {
64 | let target = new NoteController('/foo/bar/')
65 | let pageCurrent = new Page()
66 | pageCurrent.title = "testTitle"
67 | pageCurrent.content = "testContent"
68 | target.selectedPage = pageCurrent
69 | // 保存した場合はid付きのオブジェクトがAPIから返される想定
70 | let pageSaved = new Page(1, "testTitle", "testContent")
71 | // save()をモック
72 | target.pageApi.save = (page, csrfToken) => {
73 | return new Promise((resolve, reject) => {
74 | resolve(pageSaved)
75 | assert.equal(1, target.selectedPage.id, "idが更新される")
76 | })
77 | .catch((error) => {
78 | console.log(error)
79 | reject(error)
80 | })
81 | }
82 | assert.equal(null, target.selectedPage.id, "保存前はIDがnull")
83 | let result, message, promise
84 | [result, message, promise] = target.save()
85 | assert(result, "保存の呼び出しに成功する")
86 | })
87 |
88 | it('titleが空の場合', () => {
89 | let target = new NoteController('/foo/bar/')
90 | let pageCurrent = new Page()
91 | pageCurrent.title = ""
92 | pageCurrent.content = "testContent"
93 | target.selectedPage = pageCurrent
94 | let result, message, promise
95 | [result, message, promise] = target.save()
96 | assert(!result, "保存できないこと")
97 | })
98 |
99 | it('contentが空の場合', () => {
100 | let target = new NoteController('/foo/bar/')
101 | let pageCurrent = new Page()
102 | pageCurrent.title = "testTitle"
103 | pageCurrent.content = ""
104 | target.selectedPage = pageCurrent
105 | let result, message, promise
106 | [result, message, promise] = target.save()
107 | assert(!result, "保存できないこと")
108 | })
109 | })
110 |
111 | it('revert', () => {
112 | let target = new NoteController('/foo/bar/')
113 | target.selectedPage = {}
114 | target.selectedPage.revert = sinon.spy()
115 | target.revert()
116 | assert(target.selectedPage.revert.called, 'page.revert()が呼ばれること')
117 | })
118 |
119 | describe('destroy', () => {
120 | it('既存データの場合', () => {
121 | let target = new NoteController('/foo/bar/')
122 | let pageCurrent = new Page()
123 | pageCurrent.title = "testTitle"
124 | pageCurrent.content = "testContent"
125 | target.selectedPage = pageCurrent
126 | target.pages.push(pageCurrent)
127 | // load()をモック
128 | target.load = sinon.spy()
129 | // destroy()をモック
130 | target.pageApi.destroy = (page, csrfToken) => {
131 | return new Promise((resolve, reject) => {
132 | resolve()
133 | assert.equal(null, target.selectedPage, "選択中のページが空になること")
134 | assert(target.load.called, "load()が呼ばれること")
135 | })
136 | .catch((error) => {
137 | console.log(error)
138 | reject(error)
139 | })
140 | }
141 | let result, message, promise
142 | [result, message, promise] = target.destroy()
143 | assert(result, "保存の呼び出しに成功する")
144 | })
145 |
146 | it('未保存のデータの場合', () => {
147 | let target = new NoteController('/foo/bar/')
148 | let pageCurrent = new Page()
149 | pageCurrent.title = "testTitle"
150 | pageCurrent.content = "testContent"
151 | target.selectedPage = pageCurrent
152 | target.pages.push(pageCurrent)
153 | let result, message, promise
154 | [result, message, promise] = target.destroy()
155 | assert.equal(null, target.selectedPage, "選択中のページが空になること")
156 | assert.equal(0, target.pages.length, "ページが削除されていること")
157 | })
158 | })
159 |
160 | it('create', () => {
161 | let target = new NoteController('/foo/bar/')
162 | target.create()
163 | assert(target.selectedPage, '選択中のページがあること')
164 | assert(target.selectedPage.taint, '選択中のページがtaint状態になっていること')
165 | assert.equal(1, target.pages.length, 'ページが一覧に増えていること')
166 | })
167 | })
168 |
--------------------------------------------------------------------------------