├── 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 | 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 | 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 |
8 |
9 |
10 | {% csrf_token %} 11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 | 20 |
21 |
22 |
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 | [![Build Status](https://travis-ci.org/tokibito/note-app-django-vue-javascript.svg?branch=master)](https://travis-ci.org/tokibito/note-app-django-vue-javascript) 4 | 5 | ![ノート](note-app.png "ノート") 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 | 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 | --------------------------------------------------------------------------------