├── .gitignore ├── README.md └── src ├── apx ├── systemd │ └── myapp.service └── testapp │ ├── app.ini │ ├── app.py │ └── wsgi.py ├── ch3 ├── board-data.txt ├── board.py ├── cookie_counter.py ├── css │ ├── a_button.html │ ├── basic_head.html │ ├── buttons.html │ ├── grid-res.html │ ├── grid.html │ ├── grids-responsive-min.css │ └── pure-min.css ├── dice.cgi ├── get_test.py ├── hello.cgi ├── hello_flask.py ├── login_app.py ├── mul.py ├── nihongo.cgi ├── session_hello.py ├── template-card-if │ ├── app_keiji.py │ ├── app_yusuke.py │ ├── static │ │ └── style.css │ └── templates │ │ └── card-age.html ├── template-card │ ├── app.py │ └── templates │ │ └── card.html ├── template-etc │ ├── app.py │ └── templates │ │ ├── layout.html │ │ └── users.html ├── uploader │ ├── app.py │ └── static │ │ └── images │ │ └── README.md ├── user.py └── webstorage.html ├── ch4 ├── bbs │ ├── app.py │ ├── bbs_data.py │ ├── bbs_data_lock.py │ ├── bbs_login.py │ ├── data │ │ └── log.json │ ├── static │ │ ├── pure-min.css │ │ └── style.css │ └── templates │ │ ├── index.html │ │ ├── login.html │ │ └── msg.html ├── fileshare │ ├── README.md │ ├── app.py │ ├── data │ │ └── README.txt │ ├── files │ │ └── README.txt │ ├── fs_data.py │ ├── static │ │ ├── pure-min.css │ │ └── style.css │ └── templates │ │ ├── admin_list.html │ │ ├── error.html │ │ ├── index.html │ │ └── info.html ├── haikusns │ ├── app.py │ ├── data │ │ └── data.json │ ├── sns_data.py │ ├── sns_user.py │ ├── static │ │ ├── pure-min.css │ │ ├── side-menu.css │ │ ├── style.css │ │ └── ui.js │ └── templates │ │ ├── index.html │ │ ├── layout.html │ │ ├── layout_login.html │ │ ├── login_form.html │ │ ├── msg.html │ │ ├── users.html │ │ └── write_form.html └── photoshare │ ├── app.py │ ├── data │ └── README.md │ ├── photo_db.py │ ├── photo_file.py │ ├── photo_sqlite.py │ ├── setup_database.py │ ├── sns_user.py │ ├── static │ ├── pure-min.css │ ├── side-menu.css │ ├── style.css │ └── ui.js │ └── templates │ ├── album.html │ ├── album_new_form.html │ ├── index.html │ ├── layout.html │ ├── layout_login.html │ ├── login_form.html │ ├── msg.html │ ├── upload_form.html │ └── user.html ├── ch5 ├── JIGYOSYO.CSV ├── KEN_ALL.CSV ├── auth.py ├── auth2.py ├── counter.json ├── exif_gps.py ├── exif_list.py ├── exif_revgeo.py ├── geoip-test.py ├── geolocation-test.html ├── geolocation.html ├── gmap.html ├── osm-current.html ├── osm-map.html ├── osm-revgeo.py ├── osm-revgeo2.py ├── page.py ├── qrcode_hello.py ├── qrcode_more.py ├── revgeo.py ├── static │ ├── README.txt │ └── pure-min.css ├── test.jpg ├── users.json ├── webapp_qrcode.py ├── wiki │ ├── app.py │ ├── data │ │ ├── FrontPage.md │ │ └── test.md │ ├── static │ │ └── pure-min.css │ ├── templates │ │ ├── edit.html │ │ ├── new.html │ │ └── show.html │ └── wikifunc.py ├── wiki2 │ ├── app.py │ ├── data │ │ ├── FrontPage.md │ │ └── a.md │ ├── static │ │ └── pure-min.css │ ├── templates │ │ ├── edit.html │ │ ├── new.html │ │ └── show.html │ └── wikifunc.py ├── zip-api.py ├── zip-csv2sqlite.py ├── zip-form.html └── zip-test.py └── ch6 ├── hoge.txt ├── tell_path.py ├── write_text.py ├── write_text_utf8.py ├── xss_no.py └── xss_test.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | GeoLite2-ASN.mmdb 3 | GeoLite2-City.mmdb 4 | __pycache__ 5 | *.pyc 6 | # JIGYOSYO.CSV 7 | # KEN_ALL.CSV 8 | zip.sqlite 9 | 10 | 11 | # Byte-compiled / optimized / DLL files 12 | __pycache__/ 13 | *.py[cod] 14 | *$py.class 15 | 16 | # C extensions 17 | *.so 18 | 19 | # Distribution / packaging 20 | .Python 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | wheels/ 33 | pip-wheel-metadata/ 34 | share/python-wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | MANIFEST 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .nox/ 54 | .coverage 55 | .coverage.* 56 | .cache 57 | nosetests.xml 58 | coverage.xml 59 | *.cover 60 | *.py,cover 61 | .hypothesis/ 62 | .pytest_cache/ 63 | 64 | # Translations 65 | *.mo 66 | *.pot 67 | 68 | # Django stuff: 69 | *.log 70 | local_settings.py 71 | db.sqlite3 72 | db.sqlite3-journal 73 | 74 | # Flask stuff: 75 | instance/ 76 | .webassets-cache 77 | 78 | # Scrapy stuff: 79 | .scrapy 80 | 81 | # Sphinx documentation 82 | docs/_build/ 83 | 84 | # PyBuilder 85 | target/ 86 | 87 | # Jupyter Notebook 88 | .ipynb_checkpoints 89 | 90 | # IPython 91 | profile_default/ 92 | ipython_config.py 93 | 94 | # pyenv 95 | .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pythonではじめる Webサービス&スマホアプリの書きかた・作りかた 2 | 3 | 本リポジトリは、以下の書籍のサンプルコードとなります。 4 | 5 | - [書籍名] Pythonではじめる Webサービス&スマホアプリの書きかた・作りかた 6 | - [出版社] ソシム 7 | - [ISBN] ISBN-10: 4802612516 / ISBN-13: 978-4802612517 8 | - [購入はこちら](https://amzn.to/3ffX4tY) 9 | 10 | # ダウンロードの仕方 11 | 12 | サンプルプログラムをダウンロードするには、ページ右上にある緑色のボタン[Code]をクリックし、続いて[Download ZIP]をクリックします。 13 | そして、ZIPファイルを解凍してご利用ください。 14 | 15 | # 実行時注意のメモ 16 | 17 | (注意) 書籍では、Flaskの対象バージョンは、1.1.1となっております。 18 | 19 | ``` 20 | pip install Flask==1.1.1 21 | ``` 22 | 23 | なお、2022年のFlask 2.0でパラメータ名に仕様変更がありました。 24 | 25 | そのため、[こちら](https://github.com/pallets/flask/issues/4753)にあるように、send_fileを使う時、`attachment_filename`を`download_name`と変更してください。 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/apx/systemd/myapp.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=uWSGI instance for testapp 3 | After=syslog.target 4 | 5 | [Service] 6 | ExecStart=/home/vagrant/.pyenv/shims/uwsgi --ini /home/vagrant/testapp/app.ini 7 | WorkingDirectory=/home/vagrant/testapp 8 | 9 | User=vagrant 10 | Group=www-data 11 | RuntimeDirectory=uwsgi 12 | Restart=always 13 | KillSignal=SIGQUIT 14 | Type=notify 15 | StandardError=syslog 16 | NotifyAccess=all 17 | 18 | [Install] 19 | WantedBy=multi-user.target 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/apx/testapp/app.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | module = wsgi:app 3 | master = true 4 | socket = /tmp/uwsgi.sock 5 | chmod-socket = 666 6 | wsgi-file=/home/vagrant/testapp/wsgi.py 7 | logto=/home/vagrant/testapp/uwsgi.log 8 | -------------------------------------------------------------------------------- /src/apx/testapp/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | app = Flask(__name__) 3 | 4 | @app.route('/') 5 | def index(): 6 | return 'Hello, Hello, Hello!' 7 | 8 | if __name__ == "__main__": 9 | app.run(host='0.0.0.0') 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/apx/testapp/wsgi.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | 3 | if __name__ == '__main__': 4 | app.run() 5 | 6 | -------------------------------------------------------------------------------- /src/ch3/board-data.txt: -------------------------------------------------------------------------------- 1 | Flask(フラスク)は、プログラミング言語Python用の、軽量なウェブアプリケーションフレームワークである。標準で提供する機能を最小限に保っているため、自身を「マイクロフレームワーク」と呼んでいる★ -------------------------------------------------------------------------------- /src/ch3/board.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, redirect 2 | import os 3 | app = Flask(__name__) 4 | 5 | # データの保存先 --- (*1) 6 | DATAFILE = './board-data.txt' 7 | 8 | # ルートにアクセスしたとき --- (*2) 9 | @app.route('/') 10 | def index(): 11 | msg = 'まだ書込はありません。' 12 | # 保存データを読む --- (*3) 13 | if os.path.exists(DATAFILE): 14 | with open(DATAFILE, 'rt') as f: 15 | msg = f.read() 16 | # メッセージボードと投稿フォーム --- (*4) 17 | return """ 18 | 19 |

メッセージボード

20 |
21 | {0}
22 |

ボードの内容を更新:

23 |
24 |
26 | 27 |
28 | 29 | """.format(msg) 30 | 31 | # POSTメソッドで/writeにアクセスしたとき --- (*5) 32 | @app.route('/write', methods=['POST']) 33 | def write(): 34 | # データファイルにメッセージを保存 --- (*6) 35 | if 'msg' in request.form: 36 | msg = str(request.form['msg']) 37 | with open(DATAFILE, 'wt') as f: 38 | f.write(msg) 39 | # ルートページにリダイレクト --- (*7) 40 | return redirect('/') 41 | 42 | if __name__ == '__main__': 43 | app.run(host='0.0.0.0') 44 | 45 | -------------------------------------------------------------------------------- /src/ch3/cookie_counter.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask import make_response,request # Cookieのため --- (*1) 3 | from datetime import datetime 4 | app = Flask(__name__) 5 | 6 | @app.route('/') 7 | def index(): 8 | # Cookieの値を取得 --- (*2) 9 | cnt_s = request.cookies.get('cnt') 10 | if cnt_s is None: 11 | cnt = 0 12 | else: 13 | cnt = int(cnt_s) 14 | # 訪問回数カウンタに1加算 --- (*3) 15 | cnt += 1 16 | response = make_response(""" 17 |

訪問回数: {}回

18 | """.format(cnt)) 19 | # Cookieに値を保存 --- (*4) 20 | max_age = 60 * 60 * 24 * 90 # 90日 21 | expires = int(datetime.now().timestamp()) + max_age 22 | response.set_cookie('cnt', value=str(cnt), 23 | max_age=max_age, 24 | expires=expires) 25 | return response 26 | 27 | if __name__ == '__main__': 28 | app.run(host='0.0.0.0', debug=True) 29 | 30 | -------------------------------------------------------------------------------- /src/ch3/css/a_button.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 |
8 | イチゴ 9 | リンゴ 10 | バナナ 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/ch3/css/basic_head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | テスト 9 | 10 | 11 |

テスト

12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/ch3/css/buttons.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 |
9 | イチゴ 11 | リンゴ 13 | バナナ 15 |
16 | 17 |
18 | 20 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /src/ch3/css/grid-res.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 13 | 14 | 15 |
16 |
17 |

あいうえお

18 |
19 |
20 |

かきくけこ

21 |
22 |
23 |

さしすせそ

24 |
25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/ch3/css/grid.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 12 | 13 | 14 |
15 |
16 |

あいうえお

17 |
18 |
19 |

かきくけこ

20 |
21 |
22 |

さしすせそ

23 |
24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/ch3/css/grids-responsive-min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | Pure v1.0.1 3 | Copyright 2013 Yahoo! 4 | Licensed under the BSD License. 5 | https://github.com/pure-css/pure/blob/master/LICENSE.md 6 | */ 7 | @media screen and (min-width:35.5em){.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-1-12,.pure-u-sm-1-2,.pure-u-sm-1-24,.pure-u-sm-1-3,.pure-u-sm-1-4,.pure-u-sm-1-5,.pure-u-sm-1-6,.pure-u-sm-1-8,.pure-u-sm-10-24,.pure-u-sm-11-12,.pure-u-sm-11-24,.pure-u-sm-12-24,.pure-u-sm-13-24,.pure-u-sm-14-24,.pure-u-sm-15-24,.pure-u-sm-16-24,.pure-u-sm-17-24,.pure-u-sm-18-24,.pure-u-sm-19-24,.pure-u-sm-2-24,.pure-u-sm-2-3,.pure-u-sm-2-5,.pure-u-sm-20-24,.pure-u-sm-21-24,.pure-u-sm-22-24,.pure-u-sm-23-24,.pure-u-sm-24-24,.pure-u-sm-3-24,.pure-u-sm-3-4,.pure-u-sm-3-5,.pure-u-sm-3-8,.pure-u-sm-4-24,.pure-u-sm-4-5,.pure-u-sm-5-12,.pure-u-sm-5-24,.pure-u-sm-5-5,.pure-u-sm-5-6,.pure-u-sm-5-8,.pure-u-sm-6-24,.pure-u-sm-7-12,.pure-u-sm-7-24,.pure-u-sm-7-8,.pure-u-sm-8-24,.pure-u-sm-9-24{display:inline-block;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-sm-1-24{width:4.1667%}.pure-u-sm-1-12,.pure-u-sm-2-24{width:8.3333%}.pure-u-sm-1-8,.pure-u-sm-3-24{width:12.5%}.pure-u-sm-1-6,.pure-u-sm-4-24{width:16.6667%}.pure-u-sm-1-5{width:20%}.pure-u-sm-5-24{width:20.8333%}.pure-u-sm-1-4,.pure-u-sm-6-24{width:25%}.pure-u-sm-7-24{width:29.1667%}.pure-u-sm-1-3,.pure-u-sm-8-24{width:33.3333%}.pure-u-sm-3-8,.pure-u-sm-9-24{width:37.5%}.pure-u-sm-2-5{width:40%}.pure-u-sm-10-24,.pure-u-sm-5-12{width:41.6667%}.pure-u-sm-11-24{width:45.8333%}.pure-u-sm-1-2,.pure-u-sm-12-24{width:50%}.pure-u-sm-13-24{width:54.1667%}.pure-u-sm-14-24,.pure-u-sm-7-12{width:58.3333%}.pure-u-sm-3-5{width:60%}.pure-u-sm-15-24,.pure-u-sm-5-8{width:62.5%}.pure-u-sm-16-24,.pure-u-sm-2-3{width:66.6667%}.pure-u-sm-17-24{width:70.8333%}.pure-u-sm-18-24,.pure-u-sm-3-4{width:75%}.pure-u-sm-19-24{width:79.1667%}.pure-u-sm-4-5{width:80%}.pure-u-sm-20-24,.pure-u-sm-5-6{width:83.3333%}.pure-u-sm-21-24,.pure-u-sm-7-8{width:87.5%}.pure-u-sm-11-12,.pure-u-sm-22-24{width:91.6667%}.pure-u-sm-23-24{width:95.8333%}.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-24-24,.pure-u-sm-5-5{width:100%}}@media screen and (min-width:48em){.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-1-12,.pure-u-md-1-2,.pure-u-md-1-24,.pure-u-md-1-3,.pure-u-md-1-4,.pure-u-md-1-5,.pure-u-md-1-6,.pure-u-md-1-8,.pure-u-md-10-24,.pure-u-md-11-12,.pure-u-md-11-24,.pure-u-md-12-24,.pure-u-md-13-24,.pure-u-md-14-24,.pure-u-md-15-24,.pure-u-md-16-24,.pure-u-md-17-24,.pure-u-md-18-24,.pure-u-md-19-24,.pure-u-md-2-24,.pure-u-md-2-3,.pure-u-md-2-5,.pure-u-md-20-24,.pure-u-md-21-24,.pure-u-md-22-24,.pure-u-md-23-24,.pure-u-md-24-24,.pure-u-md-3-24,.pure-u-md-3-4,.pure-u-md-3-5,.pure-u-md-3-8,.pure-u-md-4-24,.pure-u-md-4-5,.pure-u-md-5-12,.pure-u-md-5-24,.pure-u-md-5-5,.pure-u-md-5-6,.pure-u-md-5-8,.pure-u-md-6-24,.pure-u-md-7-12,.pure-u-md-7-24,.pure-u-md-7-8,.pure-u-md-8-24,.pure-u-md-9-24{display:inline-block;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-md-1-24{width:4.1667%}.pure-u-md-1-12,.pure-u-md-2-24{width:8.3333%}.pure-u-md-1-8,.pure-u-md-3-24{width:12.5%}.pure-u-md-1-6,.pure-u-md-4-24{width:16.6667%}.pure-u-md-1-5{width:20%}.pure-u-md-5-24{width:20.8333%}.pure-u-md-1-4,.pure-u-md-6-24{width:25%}.pure-u-md-7-24{width:29.1667%}.pure-u-md-1-3,.pure-u-md-8-24{width:33.3333%}.pure-u-md-3-8,.pure-u-md-9-24{width:37.5%}.pure-u-md-2-5{width:40%}.pure-u-md-10-24,.pure-u-md-5-12{width:41.6667%}.pure-u-md-11-24{width:45.8333%}.pure-u-md-1-2,.pure-u-md-12-24{width:50%}.pure-u-md-13-24{width:54.1667%}.pure-u-md-14-24,.pure-u-md-7-12{width:58.3333%}.pure-u-md-3-5{width:60%}.pure-u-md-15-24,.pure-u-md-5-8{width:62.5%}.pure-u-md-16-24,.pure-u-md-2-3{width:66.6667%}.pure-u-md-17-24{width:70.8333%}.pure-u-md-18-24,.pure-u-md-3-4{width:75%}.pure-u-md-19-24{width:79.1667%}.pure-u-md-4-5{width:80%}.pure-u-md-20-24,.pure-u-md-5-6{width:83.3333%}.pure-u-md-21-24,.pure-u-md-7-8{width:87.5%}.pure-u-md-11-12,.pure-u-md-22-24{width:91.6667%}.pure-u-md-23-24{width:95.8333%}.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-24-24,.pure-u-md-5-5{width:100%}}@media screen and (min-width:64em){.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-1-12,.pure-u-lg-1-2,.pure-u-lg-1-24,.pure-u-lg-1-3,.pure-u-lg-1-4,.pure-u-lg-1-5,.pure-u-lg-1-6,.pure-u-lg-1-8,.pure-u-lg-10-24,.pure-u-lg-11-12,.pure-u-lg-11-24,.pure-u-lg-12-24,.pure-u-lg-13-24,.pure-u-lg-14-24,.pure-u-lg-15-24,.pure-u-lg-16-24,.pure-u-lg-17-24,.pure-u-lg-18-24,.pure-u-lg-19-24,.pure-u-lg-2-24,.pure-u-lg-2-3,.pure-u-lg-2-5,.pure-u-lg-20-24,.pure-u-lg-21-24,.pure-u-lg-22-24,.pure-u-lg-23-24,.pure-u-lg-24-24,.pure-u-lg-3-24,.pure-u-lg-3-4,.pure-u-lg-3-5,.pure-u-lg-3-8,.pure-u-lg-4-24,.pure-u-lg-4-5,.pure-u-lg-5-12,.pure-u-lg-5-24,.pure-u-lg-5-5,.pure-u-lg-5-6,.pure-u-lg-5-8,.pure-u-lg-6-24,.pure-u-lg-7-12,.pure-u-lg-7-24,.pure-u-lg-7-8,.pure-u-lg-8-24,.pure-u-lg-9-24{display:inline-block;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-lg-1-24{width:4.1667%}.pure-u-lg-1-12,.pure-u-lg-2-24{width:8.3333%}.pure-u-lg-1-8,.pure-u-lg-3-24{width:12.5%}.pure-u-lg-1-6,.pure-u-lg-4-24{width:16.6667%}.pure-u-lg-1-5{width:20%}.pure-u-lg-5-24{width:20.8333%}.pure-u-lg-1-4,.pure-u-lg-6-24{width:25%}.pure-u-lg-7-24{width:29.1667%}.pure-u-lg-1-3,.pure-u-lg-8-24{width:33.3333%}.pure-u-lg-3-8,.pure-u-lg-9-24{width:37.5%}.pure-u-lg-2-5{width:40%}.pure-u-lg-10-24,.pure-u-lg-5-12{width:41.6667%}.pure-u-lg-11-24{width:45.8333%}.pure-u-lg-1-2,.pure-u-lg-12-24{width:50%}.pure-u-lg-13-24{width:54.1667%}.pure-u-lg-14-24,.pure-u-lg-7-12{width:58.3333%}.pure-u-lg-3-5{width:60%}.pure-u-lg-15-24,.pure-u-lg-5-8{width:62.5%}.pure-u-lg-16-24,.pure-u-lg-2-3{width:66.6667%}.pure-u-lg-17-24{width:70.8333%}.pure-u-lg-18-24,.pure-u-lg-3-4{width:75%}.pure-u-lg-19-24{width:79.1667%}.pure-u-lg-4-5{width:80%}.pure-u-lg-20-24,.pure-u-lg-5-6{width:83.3333%}.pure-u-lg-21-24,.pure-u-lg-7-8{width:87.5%}.pure-u-lg-11-12,.pure-u-lg-22-24{width:91.6667%}.pure-u-lg-23-24{width:95.8333%}.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-24-24,.pure-u-lg-5-5{width:100%}}@media screen and (min-width:80em){.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-1-12,.pure-u-xl-1-2,.pure-u-xl-1-24,.pure-u-xl-1-3,.pure-u-xl-1-4,.pure-u-xl-1-5,.pure-u-xl-1-6,.pure-u-xl-1-8,.pure-u-xl-10-24,.pure-u-xl-11-12,.pure-u-xl-11-24,.pure-u-xl-12-24,.pure-u-xl-13-24,.pure-u-xl-14-24,.pure-u-xl-15-24,.pure-u-xl-16-24,.pure-u-xl-17-24,.pure-u-xl-18-24,.pure-u-xl-19-24,.pure-u-xl-2-24,.pure-u-xl-2-3,.pure-u-xl-2-5,.pure-u-xl-20-24,.pure-u-xl-21-24,.pure-u-xl-22-24,.pure-u-xl-23-24,.pure-u-xl-24-24,.pure-u-xl-3-24,.pure-u-xl-3-4,.pure-u-xl-3-5,.pure-u-xl-3-8,.pure-u-xl-4-24,.pure-u-xl-4-5,.pure-u-xl-5-12,.pure-u-xl-5-24,.pure-u-xl-5-5,.pure-u-xl-5-6,.pure-u-xl-5-8,.pure-u-xl-6-24,.pure-u-xl-7-12,.pure-u-xl-7-24,.pure-u-xl-7-8,.pure-u-xl-8-24,.pure-u-xl-9-24{display:inline-block;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-xl-1-24{width:4.1667%}.pure-u-xl-1-12,.pure-u-xl-2-24{width:8.3333%}.pure-u-xl-1-8,.pure-u-xl-3-24{width:12.5%}.pure-u-xl-1-6,.pure-u-xl-4-24{width:16.6667%}.pure-u-xl-1-5{width:20%}.pure-u-xl-5-24{width:20.8333%}.pure-u-xl-1-4,.pure-u-xl-6-24{width:25%}.pure-u-xl-7-24{width:29.1667%}.pure-u-xl-1-3,.pure-u-xl-8-24{width:33.3333%}.pure-u-xl-3-8,.pure-u-xl-9-24{width:37.5%}.pure-u-xl-2-5{width:40%}.pure-u-xl-10-24,.pure-u-xl-5-12{width:41.6667%}.pure-u-xl-11-24{width:45.8333%}.pure-u-xl-1-2,.pure-u-xl-12-24{width:50%}.pure-u-xl-13-24{width:54.1667%}.pure-u-xl-14-24,.pure-u-xl-7-12{width:58.3333%}.pure-u-xl-3-5{width:60%}.pure-u-xl-15-24,.pure-u-xl-5-8{width:62.5%}.pure-u-xl-16-24,.pure-u-xl-2-3{width:66.6667%}.pure-u-xl-17-24{width:70.8333%}.pure-u-xl-18-24,.pure-u-xl-3-4{width:75%}.pure-u-xl-19-24{width:79.1667%}.pure-u-xl-4-5{width:80%}.pure-u-xl-20-24,.pure-u-xl-5-6{width:83.3333%}.pure-u-xl-21-24,.pure-u-xl-7-8{width:87.5%}.pure-u-xl-11-12,.pure-u-xl-22-24{width:91.6667%}.pure-u-xl-23-24{width:95.8333%}.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-24-24,.pure-u-xl-5-5{width:100%}} -------------------------------------------------------------------------------- /src/ch3/css/pure-min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | Pure v1.0.1 3 | Copyright 2013 Yahoo! 4 | Licensed under the BSD License. 5 | https://github.com/pure-css/pure/blob/master/LICENSE.md 6 | */ 7 | /*! 8 | normalize.css v^3.0 | MIT License | git.io/normalize 9 | Copyright (c) Nicolas Gallagher and Jonathan Neal 10 | */ 11 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{letter-spacing:-.31em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-align-content:flex-start;-ms-flex-line-pack:start;align-content:flex-start}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){table .pure-g{display:block}}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u{display:inline-block;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-g [class*=pure-u]{font-family:sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-12,.pure-u-1-2,.pure-u-1-24,.pure-u-1-3,.pure-u-1-4,.pure-u-1-5,.pure-u-1-6,.pure-u-1-8,.pure-u-10-24,.pure-u-11-12,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-2-24,.pure-u-2-3,.pure-u-2-5,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24,.pure-u-3-24,.pure-u-3-4,.pure-u-3-5,.pure-u-3-8,.pure-u-4-24,.pure-u-4-5,.pure-u-5-12,.pure-u-5-24,.pure-u-5-5,.pure-u-5-6,.pure-u-5-8,.pure-u-6-24,.pure-u-7-12,.pure-u-7-24,.pure-u-7-8,.pure-u-8-24,.pure-u-9-24{display:inline-block;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-10-24,.pure-u-5-12{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-14-24,.pure-u-7-12{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-15-24,.pure-u-5-8{width:62.5%}.pure-u-16-24,.pure-u-2-3{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-18-24,.pure-u-3-4{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-20-24,.pure-u-5-6{width:83.3333%}.pure-u-21-24,.pure-u-7-8{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-24-24,.pure-u-5-5{width:100%}.pure-button{display:inline-block;zoom:1;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-group{letter-spacing:-.31em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.pure-button-group{word-spacing:-.43em}.pure-button-group .pure-button{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:#444;color:rgba(0,0,0,.8);border:1px solid #999;border:none transparent;background-color:#e6e6e6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:focus,.pure-button:hover{background-image:-webkit-gradient(linear,left top,left bottom,from(transparent),color-stop(40%,rgba(0,0,0,.05)),to(rgba(0,0,0,.1)));background-image:-webkit-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000}.pure-button-disabled,.pure-button-disabled:active,.pure-button-disabled:focus,.pure-button-disabled:hover,.pure-button[disabled]{border:none;background-image:none;opacity:.4;cursor:not-allowed;-webkit-box-shadow:none;box-shadow:none;pointer-events:none}.pure-button-hidden{display:none}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-button-group .pure-button{margin:0;border-radius:0;border-right:1px solid #111;border-right:1px solid rgba(0,0,0,.2)}.pure-button-group .pure-button:first-child{border-top-left-radius:2px;border-bottom-left-radius:2px}.pure-button-group .pure-button:last-child{border-top-right-radius:2px;border-bottom-right-radius:2px;border-right:none}.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 3px #ddd;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 3px #ddd;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=color]:focus,.pure-form input[type=date]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=email]:focus,.pure-form input[type=month]:focus,.pure-form input[type=number]:focus,.pure-form input[type=password]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=text]:focus,.pure-form input[type=time]:focus,.pure-form input[type=url]:focus,.pure-form input[type=week]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129fea}.pure-form input:not([type]):focus{outline:0;border-color:#129fea}.pure-form input[type=checkbox]:focus,.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus{outline:thin solid #129fea;outline:1px auto #129fea}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=color][disabled],.pure-form input[type=date][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=email][disabled],.pure-form input[type=month][disabled],.pure-form input[type=number][disabled],.pure-form input[type=password][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=text][disabled],.pure-form input[type=time][disabled],.pure-form input[type=url][disabled],.pure-form input[type=week][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form select:focus:invalid,.pure-form textarea:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=checkbox]:focus:invalid:focus,.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=color],.pure-form-stacked input[type=date],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=email],.pure-form-stacked input[type=file],.pure-form-stacked input[type=month],.pure-form-stacked input[type=number],.pure-form-stacked input[type=password],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=text],.pure-form-stacked input[type=time],.pure-form-stacked input[type=url],.pure-form-stacked input[type=week],.pure-form-stacked label,.pure-form-stacked select,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned .pure-help-inline,.pure-form-aligned input,.pure-form-aligned select,.pure-form-aligned textarea,.pure-form-message-inline{display:inline-block;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form .pure-input-rounded,.pure-form input.pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-3-4{width:75%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form .pure-help-inline,.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=color],.pure-group input[type=date],.pure-group input[type=datetime-local],.pure-group input[type=datetime],.pure-group input[type=email],.pure-group input[type=month],.pure-group input[type=number],.pure-group input[type=password],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=text],.pure-group input[type=time],.pure-group input[type=url],.pure-group input[type=week]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0 0}.pure-form .pure-help-inline,.pure-form-message,.pure-form-message-inline{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu{-webkit-box-sizing:border-box;box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-item,.pure-menu-list{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-heading,.pure-menu-link{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-separator{display:inline-block;zoom:1;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-active>.pure-menu-children,.pure-menu-allow-hover:hover>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;-webkit-overflow-scrolling:touch;padding:.5em 0}.pure-menu-horizontal .pure-menu-children .pure-menu-separator,.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-horizontal .pure-menu-children .pure-menu-separator{display:block;width:auto}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-disabled,.pure-menu-heading,.pure-menu-link{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent}.pure-menu-active>.pure-menu-link,.pure-menu-link:focus,.pure-menu-link:hover{background-color:#eee}.pure-menu-selected>.pure-menu-link,.pure-menu-selected>.pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0} -------------------------------------------------------------------------------- /src/ch3/dice.cgi: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3.4 2 | 3 | # Content-Typeのヘッダを出力 4 | print("Content-Type: text/html; charset=UTF-8") 5 | print("") 6 | 7 | # サイコロを表示 8 | import random 9 | dice = random.randint(1, 6) 10 | # HTMLに埋め込んで表示 11 | print("

") 12 | print("Dice =", dice) 13 | print("

") 14 | 15 | -------------------------------------------------------------------------------- /src/ch3/get_test.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | app = Flask(__name__) 3 | 4 | # サーバールートへアクセスがあった時 --- (*1) 5 | @app.route('/') 6 | def index(): 7 | # フォームを表示する --- (*2) 8 | return """ 9 | 10 |
11 | 名前: 12 | 13 |
14 | 15 | """ 16 | 17 | # /hello へアクセスがあった時 --- (*3) 18 | @app.route('/hello') 19 | def hello(): 20 | # nameのパラメータを得る --- (*4) 21 | name = request.args.get('name') 22 | if name is None: name = '名無し' 23 | # 自己紹介を自動作成 24 | return """ 25 |

{0}さん、こんにちは!

26 | """.format(name) 27 | 28 | if __name__ == '__main__': 29 | app.run(host='0.0.0.0') 30 | 31 | -------------------------------------------------------------------------------- /src/ch3/hello.cgi: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3.7 2 | #!/usr/local/bin/python3.4 3 | ########################## 4 | # [注意点1] 5 | # (1) Pythonのバージョンを確認して、このファイルの1行目を変更 6 | # (2) パーミッションを指定のものに変更 7 | # 8 | # ロリポップであれば以下のURLを確認してください。 9 | # 契約時期やプランによってPythonバージョンが異なります。 10 | # [URL] https://lolipop.jp/manual/hp/cgi/ 11 | # 12 | # [注意点2] 13 | # 改行コードはLFのまま変更しないようにしてください。 14 | # 15 | # [ヒント] 16 | # もし、SSHで接続可能なら、SSHでサーバに接続して 17 | # 以下のコマンドが実行可能か確認してください。 18 | # $ ./hello.cgi 19 | 20 | 21 | # Content-Typeのヘッダを出力 --- (*1) 22 | print("Content-Type: text/html; charset=UTF-8") 23 | print("") 24 | 25 | # メッセージを出力 --- (*2) 26 | print("Hello, World!") 27 | 28 | -------------------------------------------------------------------------------- /src/ch3/hello_flask.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | # Flaskのインスタンスを作成 --- (*1) 4 | app = Flask(__name__) 5 | 6 | # ルーティングの指定 --- (*2) 7 | @app.route('/') 8 | def index(): 9 | return "Hello, World!" 10 | 11 | # 実行する --- (*3) 12 | if __name__ == '__main__': 13 | app.run(host='0.0.0.0') 14 | 15 | -------------------------------------------------------------------------------- /src/ch3/login_app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, session, redirect 2 | app = Flask(__name__) 3 | app.secret_key = 'm9XE4JH5dB0QK4o4' 4 | 5 | # ログインに使うユーザー名とパスワード --- (*1) 6 | USERLIST = { 7 | 'taro': 'aaa', 8 | 'jiro': 'bbb', 9 | 'sabu': 'ccc', 10 | } 11 | 12 | @app.route('/') 13 | def index(): 14 | # ログインフォームの表示 --- (*2) 15 | return """ 16 |

ログインフォーム

17 |
18 | ユーザー名:
19 |
20 | パスワード:
21 |
22 | 23 |
24 |

→秘密のページ

25 | """ 26 | 27 | @app.route('/check_login', methods=['POST']) 28 | def check_login(): 29 | # フォームの値を取得 --- (*3) 30 | user, pw = (None, None) 31 | if 'user' in request.form: 32 | user = request.form['user'] 33 | if 'pw' in request.form: 34 | pw = request.form['pw'] 35 | if (user is None) or (pw is None): 36 | return redirect('/') 37 | # ログインチェック --- (*4) 38 | if try_login(user, pw) == False: 39 | return """ 40 |

ユーザー名かパスワードの間違い

41 |

→戻る

42 | """ 43 | # 非公開ページに飛ぶ --- (*5) 44 | return redirect('/private') 45 | 46 | @app.route('/private') 47 | def private_page(): 48 | # ログインしていなければトップへ飛ばす --- (*6) 49 | if not is_login(): 50 | return """ 51 |

ログインしてください

52 |

→ログインする

53 | """ 54 | # ログイン後のページを表示 --- (*7) 55 | return """ 56 |

ここは秘密のページ

57 |

あなたはログイン中です。

58 |

→ログアウト

59 | """ 60 | 61 | @app.route('/logout') 62 | def logout_page(): 63 | try_logout() # ログアウト処理を実行 --- (*8) 64 | return """ 65 |

ログアウトしました

66 |

→戻る

67 | """ 68 | 69 | # 以下、ログインに関する処理をまとめたもの 70 | # ログインしているかチェック --- (*9) 71 | def is_login(): 72 | if 'login' in session: 73 | return True 74 | return False 75 | 76 | # ログイン処理を行う --- (*10) 77 | def try_login(user, password): 78 | # ユーザーがリストにあるか? 79 | if not user in USERLIST: 80 | return False 81 | # パスワードがあっているか? 82 | if USERLIST[user] != password: 83 | return False 84 | # ログイン処理を実行 85 | session['login'] = user 86 | return True 87 | 88 | # ログアウトする --- (*11) 89 | def try_logout(): 90 | session.pop('login', None) 91 | return True 92 | 93 | if __name__ == '__main__': 94 | app.run(host='0.0.0.0') 95 | 96 | -------------------------------------------------------------------------------- /src/ch3/mul.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | app = Flask(__name__) 3 | 4 | @app.route('/') 5 | def index(): 6 | # URLパラメータを取得 --- (*1) 7 | a = request.args.get('a') 8 | b = request.args.get('b') 9 | # パラメータが設定されているか確認 --- (*2) 10 | if (a is None) or (b is None): 11 | return "パラメータが足りません。" 12 | # パラメータを数値に変換して計算 --- (*3) 13 | c = int(a) * int(b) 14 | # 結果を出力 --- (*4) 15 | return "

" + str(c) + "

" 16 | 17 | if __name__ == "__main__": 18 | app.run(host='0.0.0.0') 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/ch3/nihongo.cgi: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3.4 2 | 3 | # 日本語を表示するために必要 4 | import sys 5 | sys.stdin = open(sys.stdin.fileno(), 'r', 6 | encoding='UTF-8') 7 | sys.stdout = open(sys.stdout.fileno(), 'w', 8 | encoding='UTF-8') 9 | sys.stderr = open(sys.stderr.fileno(), 'w', 10 | encoding='UTF-8') 11 | 12 | # Content-Typeのヘッダを出力 13 | print("Content-Type: text/html; charset=UTF-8") 14 | print("") 15 | 16 | # 日本語を表示 17 | print("

") 18 | print("賢い子は父親を喜ばせ,愚かな子は母親を悲しませる。") 19 | print("

") 20 | 21 | -------------------------------------------------------------------------------- /src/ch3/session_hello.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, session, redirect 2 | app = Flask(__name__) 3 | app.secret_key = '9KStWezC' # 適当な値を設定 --- (*1) 4 | 5 | @app.route('/') 6 | def index(): 7 | # ユーザー名の入力フォームを出力 --- (*2) 8 | return """ 9 |

ユーザー名を入力

10 |
11 | 名前: 12 | 13 |
14 | """ 15 | 16 | @app.route('/setname') 17 | def setname(): 18 | # GETの値を取得 --- (*3) 19 | name = request.args.get('username') 20 | if not name: return redirect('/') 21 | # セッションに値を保存 --- (*4) 22 | session['name'] = name 23 | # 他のページにリダイレクト --- (*5) 24 | return redirect('/morning') 25 | 26 | def getLinks(): 27 | return """ 28 | 31 | """ 32 | 33 | @app.route('/morning') 34 | def morning(): 35 | # セッションにnameがある? --- (*6) 36 | if not ('name' in session): 37 | # nameがないのでルートに飛ばす --- (*7) 38 | return redirect('/') 39 | # セッションからユーザー名を得る --- (*8) 40 | name = session['name'] 41 | return """ 42 |

{0}さん、おはようございます!

{1} 43 | """.format(name, getLinks()) 44 | 45 | @app.route('/hello') 46 | def hello(): 47 | if not ('name' in session): 48 | return redirect('/') 49 | # セッションから名前を得てメッセージ出力 --- (*9) 50 | return """

{0}さん、こんにちは!

{1} 51 | """.format(session['name'], getLinks()) 52 | 53 | @app.route('/night') 54 | def night(): 55 | if not ('name' in session): 56 | return redirect('/') 57 | # セッションから名前を得てメッセージ出力 --- (*10) 58 | return """

{0}さん、こんばんは!

{1} 59 | """.format(session['name'], getLinks()) 60 | 61 | if __name__ == '__main__': 62 | app.run(host='0.0.0.0') 63 | 64 | -------------------------------------------------------------------------------- /src/ch3/template-card-if/app_keiji.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template 2 | app = Flask(__name__) 3 | 4 | @app.route('/') 5 | def index(): 6 | # テンプレートエンジンにデータを指定 7 | return render_template( 8 | 'card-age.html', 9 | username='ケイジ', 10 | age=19, 11 | email='keiji@example.com') 12 | 13 | if __name__ == '__main__': 14 | app.run(host='0.0.0.0') 15 | 16 | -------------------------------------------------------------------------------- /src/ch3/template-card-if/app_yusuke.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template 2 | app = Flask(__name__) 3 | 4 | @app.route('/') 5 | def index(): 6 | # テンプレートエンジンにデータを指定 7 | return render_template( 8 | 'card-age.html', 9 | username='ユウスケ', 10 | age=20, 11 | email='yusuke@example.com') 12 | 13 | if __name__ == '__main__': 14 | app.run(host='0.0.0.0') 15 | 16 | -------------------------------------------------------------------------------- /src/ch3/template-card-if/static/style.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | background-color: red; 3 | color: white; 4 | padding: 12px; 5 | } 6 | div#desc { 7 | padding-left: 3em; 8 | border: 1px solid red; 9 | } 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/ch3/template-card-if/templates/card-age.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 |

{{ username }}さんの紹介

7 |
8 | {# 年齢によって表示内容を切り替える --- (*2) #} 9 | {% if age < 20 %} 10 |

年齢は、{{ age }}才(未成年)です。

11 |

連絡先は非公開です。

12 | {% else %} 13 |

年齢は、{{ age }}才です。

14 |

メールアドレスは、{{ email }}です。

15 | {% endif %} 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /src/ch3/template-card/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route('/') 6 | def index(): 7 | # データを指定 --- (*1) 8 | username = 'ケイジ' 9 | age = 19 10 | email = 'keiji@example.com' 11 | # テンプレートエンジンにデータを指定 --- (*2) 12 | return render_template('card.html', 13 | username=username, 14 | age=age, 15 | email=email) 16 | 17 | if __name__ == '__main__': 18 | app.run(host='0.0.0.0') 19 | 20 | -------------------------------------------------------------------------------- /src/ch3/template-card/templates/card.html: -------------------------------------------------------------------------------- 1 | 2 |

{{ username }}さんの紹介

3 |
4 |

年齢は、{{ age }}才です。

5 |

メールアドレスは、{{ email }}です。

6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /src/ch3/template-etc/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template 2 | app = Flask(__name__) 3 | 4 | @app.route('/') 5 | def index(): 6 | users = [ 7 | {'name':'ケイスケ', 'age':22}, 8 | {'name':'ダイキ', 'age':25}, 9 | {'name':'セイジ', 'age':18}, 10 | ] 11 | return render_template( 12 | 'users.html', 13 | users=users) 14 | 15 | if __name__ == '__main__': 16 | app.run(debug=True, host='0.0.0.0') 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/ch3/template-etc/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 |

11 | {% block title %} 12 | 13 | {% endblock %} 14 |

15 |
16 | {% block contents %} 17 | 18 | {% endblock %} 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /src/ch3/template-etc/templates/users.html: -------------------------------------------------------------------------------- 1 | {# レイアウトを継承する --- (*1) #} 2 | {% extends "layout.html" %} 3 | 4 | {# titleのブロックを書き換える --- (*2) #} 5 | {% block title %} 6 | ユーザーリスト 7 | {% endblock %} 8 | 9 | {# contentsのブロックを書き換える --- (*3) #} 10 | {% block contents %} 11 | {% for user in users %} 12 |
13 |

{{ user.name }}

14 |

年齢は、{{ user.age }}才です。

15 |
16 | {% endfor %} 17 | {% endblock %} 18 | 19 | -------------------------------------------------------------------------------- /src/ch3/uploader/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, redirect 2 | from datetime import datetime 3 | import os 4 | 5 | # 保存先のディレクトリとURLの指定 --- (*1) 6 | IMAGES_DIR = './static/images' 7 | IMAGES_URL = '/static/images' 8 | app = Flask(__name__) 9 | 10 | @app.route('/') 11 | def index_page(): 12 | # アップロードフォーム --- (*2) 13 | return """ 14 |

アップロード

15 |
18 | 19 | 20 |
21 | 22 | """ 23 | 24 | @app.route('/upload', methods=['POST']) 25 | def upload(): 26 | # アップされていなければトップへ飛ばす --- (*3) 27 | if not ('upfile' in request.files): 28 | return redirect('/') 29 | # アップしたファイルのオブジェクトを得る --- (*4) 30 | temp_file = request.files['upfile'] 31 | # JPEGファイル以外は却下する --- (*5) 32 | if temp_file.filename == '': 33 | return redirect('/') 34 | if not is_jpegfile(temp_file.stream): 35 | return '

JPEG以外アップできません

' 36 | # 保存先のファイル名を決める --- (*6) 37 | time_s = datetime.now().strftime('%Y%m%d%H%M%S') 38 | fname = time_s + '.jpeg' 39 | # 一時ファイルを保存先ディレクトリへ保存 --- (*7) 40 | temp_file.save(IMAGES_DIR + '/' + fname) 41 | # 画像の表示ページへ飛ぶ 42 | return redirect('/photo/' + fname) 43 | 44 | @app.route('/photo/') 45 | def photo_page(fname): 46 | # 画像ファイルがあるか確認する --- (*8) 47 | if fname is None: return redirect('/') 48 | image_path = IMAGES_DIR + '/' + fname 49 | image_url = IMAGES_URL + '/' + fname 50 | if not os.path.exists(image_path): 51 | return '

画像がありません

' 52 | # 画像を表示するHTMLを出力する --- (*9) 53 | return """ 54 |

画像がアップロードされています

55 |

URL: {0}
File: {1}

56 | 57 | """.format(image_url, image_path) 58 | 59 | # JPEGファイルかどうかを確認する --- (*10) 60 | def is_jpegfile(fp): 61 | byte = fp.read(2) # 先頭2バイトを読む 62 | fp.seek(0) # ポインタを先頭に戻す 63 | return byte[:2] == b'\xFF\xD8' 64 | 65 | if __name__ == '__main__': 66 | app.run(host='0.0.0.0') 67 | 68 | -------------------------------------------------------------------------------- /src/ch3/uploader/static/images/README.md: -------------------------------------------------------------------------------- 1 | ここに画像ファイルが保存されます 2 | -------------------------------------------------------------------------------- /src/ch3/user.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | app = Flask(__name__) 3 | 4 | @app.route('/') 5 | def index(): 6 | return "test" 7 | 8 | @app.route('/users/') 9 | def users(user_id): 10 | return "ユーザー {0} のページ".format(user_id) 11 | 12 | if __name__ == '__main__': 13 | app.run(host='0.0.0.0') 14 | 15 | -------------------------------------------------------------------------------- /src/ch3/webstorage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/ch4/bbs/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect, url_for, session 2 | from flask import render_template, request 3 | import os, json, datetime 4 | import bbs_login # ログイン管理モジュール --- (*1) 5 | import bbs_data # データ入出力用モジュール --- (*2) 6 | 7 | # Flaskインスタンスと暗号化キーの指定 8 | app = Flask(__name__) 9 | app.secret_key = 'U1sNMeUkZSuuX2Zn' 10 | 11 | # 掲示板のメイン画面 --- (*3) 12 | @app.route('/') 13 | def index(): 14 | # ログインが必要 --- (*4) 15 | if not bbs_login.is_login(): 16 | return redirect('/login') 17 | # ログ一覧を表示 --- (*5) 18 | return render_template('index.html', 19 | user=bbs_login.get_user(), 20 | data=bbs_data.load_data()) 21 | 22 | # ログイン画面を表示 --- (*6) 23 | @app.route('/login') 24 | def login(): 25 | return render_template('login.html') 26 | 27 | # ログイン処理 --- (*7) 28 | @app.route('/try_login', methods=['POST']) 29 | def try_login(): 30 | user = request.form.get('user', '') 31 | pw = request.form.get('pw', '') 32 | # ログインに成功したらルートページへ飛ぶ 33 | if bbs_login.try_login(user, pw): 34 | return redirect('/') 35 | # 失敗した時はメッセージを表示 36 | return show_msg('ログインに失敗しました') 37 | 38 | # ログアウト処理 --- (*8) 39 | @app.route('/logout') 40 | def logout(): 41 | bbs_login.try_logout() 42 | return show_msg('ログアウトしました') 43 | 44 | # 書き込み処理 --- (*9) 45 | @app.route('/write', methods=['POST']) 46 | def write(): 47 | # ログインが必要 --- (*10) 48 | if not bbs_login.is_login(): 49 | return redirect('/login') 50 | # フォームのテキストを取得 --- (*11) 51 | ta = request.form.get('ta', '') 52 | if ta == '': return show_msg('書込が空でした。') 53 | # データに追記保存 --- (*12) 54 | bbs_data.save_data_append( 55 | user=bbs_login.get_user(), 56 | text=ta) 57 | return redirect('/') 58 | 59 | # テンプレートを利用してメッセージを出力 --- (*13) 60 | def show_msg(msg): 61 | return render_template('msg.html', msg=msg) 62 | 63 | if __name__ == '__main__': 64 | app.run(debug=True, host='0.0.0.0') 65 | 66 | -------------------------------------------------------------------------------- /src/ch4/bbs/bbs_data.py: -------------------------------------------------------------------------------- 1 | import os, json, datetime 2 | 3 | # 保存先のファイルを指定 --- (*1) 4 | BASE_DIR = os.path.dirname(__file__) 5 | SAVE_FILE = BASE_DIR + '/data/log.json' 6 | 7 | # ログファイル(JSON形式)を読み出す --- (*2) 8 | def load_data(): 9 | if not os.path.exists(SAVE_FILE): 10 | return [] 11 | with open(SAVE_FILE, 'rt', encoding='utf-8') as f: 12 | return json.load(f) 13 | 14 | # ログファイルへ書き出す --- (*3) 15 | def save_data(data_list): 16 | with open(SAVE_FILE, 'wt', encoding='utf-8') as f: 17 | json.dump(data_list, f) 18 | 19 | # ログを追記保存 --- (*4) 20 | def save_data_append(user, text): 21 | # レコードを用意 22 | tm = get_datetime_now() 23 | data = {'name': user, 'text': text, 'date': tm} 24 | # 先頭にレコードを追記して保存 --- (*5) 25 | data_list = load_data() 26 | data_list.insert(0, data) 27 | save_data(data_list) 28 | 29 | # 日時を文字列で得る 30 | def get_datetime_now(): 31 | now = datetime.datetime.now() 32 | return "{0:%Y/%m/%d %H:%M}".format(now) 33 | 34 | -------------------------------------------------------------------------------- /src/ch4/bbs/bbs_data_lock.py: -------------------------------------------------------------------------------- 1 | import os, json, datetime 2 | import fcntl 3 | import time 4 | 5 | # 保存先のファイルを指定 --- (*1) 6 | BASE_DIR = os.path.dirname(__file__) 7 | SAVE_FILE = BASE_DIR + '/data/log.json' 8 | 9 | # ログファイル(JSON形式)を読み出す --- (*2) 10 | def load_data(): 11 | if not os.path.exists(SAVE_FILE): 12 | return [] 13 | for i in range(100): 14 | f = open(SAVE_FILE, 'r+', encoding='utf-8') 15 | try: 16 | fcntl.flock(f, fcntl.LOCK_EX) 17 | data = json.load(f) 18 | fcntl.flock(f, fcntl.LOCK_UN) 19 | return data 20 | except IOError: 21 | print('IOError') 22 | time.sleep(0.1) 23 | finally: 24 | f.close() 25 | 26 | # ログファイルへ書き出す --- (*3) 27 | def save_data(data_list): 28 | for i in range(100): 29 | f = open(SAVE_FILE, 'r+', encoding='utf-8') 30 | try: 31 | fcntl.flock(f, fcntl.LOCK_EX) 32 | json.dump(data_list, f) 33 | f.truncate() 34 | fcntl.flock(f, fcntl.LOCK_UN) 35 | except IOError: 36 | print('IOError') 37 | time.sleep(0.1) 38 | finally: 39 | f.close() 40 | 41 | # ログを追記保存 --- (*4) 42 | def save_data_append(user, text): 43 | # レコードを用意 44 | tm = get_datetime_now() 45 | data = {'name': user, 'text': text, 'date': tm} 46 | # 先頭にレコードを追記して保存 --- (*5) 47 | data_list = load_data() 48 | data_list.insert(0, data) 49 | save_data(data_list) 50 | 51 | # 日時を文字列で得る 52 | def get_datetime_now(): 53 | now = datetime.datetime.now() 54 | return "{0:%Y/%m/%d %H:%M}".format(now) 55 | 56 | -------------------------------------------------------------------------------- /src/ch4/bbs/bbs_login.py: -------------------------------------------------------------------------------- 1 | from flask import session, redirect 2 | 3 | # ログイン用ユーザーの一覧を定義 --- (*1) 4 | USERLIST = { 5 | 'taro': 'aaa', 6 | 'jiro': 'bbb', 7 | 'sabu': 'ccc', 8 | } 9 | 10 | # ログインしているか調べる --- (*2) 11 | def is_login(): 12 | return 'login' in session 13 | 14 | # ログイン処理 --- (*3) 15 | def try_login(user, password): 16 | # 該当ユーザーがいるか? 17 | if user not in USERLIST: return False 18 | # パスワードが合っているか? 19 | if USERLIST[user] != password: return False 20 | # ログイン処理 --- (*4) 21 | session['login'] = user 22 | return True 23 | 24 | # ログアウト処理 --- (*5) 25 | def try_logout(): 26 | session.pop('login', None) 27 | return True 28 | 29 | # セッションからユーザー名を得る --- (*6) 30 | def get_user(): 31 | if is_login(): return session['login'] 32 | return 'not login' 33 | -------------------------------------------------------------------------------- /src/ch4/bbs/data/log.json: -------------------------------------------------------------------------------- 1 | [{"name": "taro", "text": "\u53cd\u5bfe\u306e\u53cd\u5bfe\u306f\u540c\u610f\u3002", "date": "2020/05/11 22:43"}, {"name": "taro", "text": "\u3069\u3046\u3057\u3088\u3046\u304b\u306a\uff1f", "date": "2020/05/11 22:43"}, {"name": "taro", "text": "\u4f55\u3092\u3057\u3088\u3046\u304b\u306a\uff1f", "date": "2020/05/11 22:43"}, {"name": "taro", "text": "\u4eca\u65e5\u4f55\u3057\u3066\u305f\u304b\u306a\uff1f", "date": "2020/05/11 22:43"}, {"name": "taro", "text": "\u30c6\u30b9\u30c8", "date": "2020/05/11 20:00"}, {"name": "taro", "text": "\u3053\u3061\u3089\u3053\u305d\u3001\u697d\u3057\u304b\u3063\u305f\u3067\u3059\u3002", "date": "2020/02/06 16:39"}, {"name": "sabu", "text": "\u4eca\u65e5\u3001\u30e9\u30f3\u30c1\u3042\u308a\u304c\u3068\u3046\u3054\u3056\u3044\u307e\u3057\u305f\u3002\u3044\u308d\u3044\u308d\u306a\u304a\u8a71\u304c\u805e\u3051\u3066\u697d\u3057\u304b\u3063\u305f\u3067\u3059\u3002", "date": "2020/02/06 16:38"}, {"name": "taro", "text": "\u826f\u3044\u3067\u3059\u306d\u30fc\uff01\u3044\u3064\u3082\u306e\u30a4\u30f3\u30c9\u30ab\u30ec\u30fc\u306e\u5e97\u306b\u884c\u304d\u307e\u3057\u3087\u3046\u3002\u305d\u308c\u3067\u306f\u3001\u304a\u663c12\u6642\u306b\u96c6\u5408\u3067\uff01", "date": "2020/02/05 16:47"}, {"name": "jiro", "text": "\u8ab0\u304b\u3001\u304a\u663c\u4e00\u7dd2\u306b\u884c\u304d\u307e\u305b\u3093\u304b\uff1f\u4eca\u65e5\u306f\u3001\u30ab\u30ec\u30fc\u304c\u98df\u3079\u305f\u3044\u306a\u3041\u30fc\u3002", "date": "2020/02/05 16:46"}] -------------------------------------------------------------------------------- /src/ch4/bbs/static/pure-min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | Pure v1.0.1 3 | Copyright 2013 Yahoo! 4 | Licensed under the BSD License. 5 | https://github.com/pure-css/pure/blob/master/LICENSE.md 6 | */ 7 | /*! 8 | normalize.css v^3.0 | MIT License | git.io/normalize 9 | Copyright (c) Nicolas Gallagher and Jonathan Neal 10 | */ 11 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{letter-spacing:-.31em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-align-content:flex-start;-ms-flex-line-pack:start;align-content:flex-start}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){table .pure-g{display:block}}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u{display:inline-block;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-g [class*=pure-u]{font-family:sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-12,.pure-u-1-2,.pure-u-1-24,.pure-u-1-3,.pure-u-1-4,.pure-u-1-5,.pure-u-1-6,.pure-u-1-8,.pure-u-10-24,.pure-u-11-12,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-2-24,.pure-u-2-3,.pure-u-2-5,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24,.pure-u-3-24,.pure-u-3-4,.pure-u-3-5,.pure-u-3-8,.pure-u-4-24,.pure-u-4-5,.pure-u-5-12,.pure-u-5-24,.pure-u-5-5,.pure-u-5-6,.pure-u-5-8,.pure-u-6-24,.pure-u-7-12,.pure-u-7-24,.pure-u-7-8,.pure-u-8-24,.pure-u-9-24{display:inline-block;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-10-24,.pure-u-5-12{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-14-24,.pure-u-7-12{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-15-24,.pure-u-5-8{width:62.5%}.pure-u-16-24,.pure-u-2-3{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-18-24,.pure-u-3-4{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-20-24,.pure-u-5-6{width:83.3333%}.pure-u-21-24,.pure-u-7-8{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-24-24,.pure-u-5-5{width:100%}.pure-button{display:inline-block;zoom:1;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-group{letter-spacing:-.31em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.pure-button-group{word-spacing:-.43em}.pure-button-group .pure-button{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:#444;color:rgba(0,0,0,.8);border:1px solid #999;border:none transparent;background-color:#e6e6e6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:focus,.pure-button:hover{background-image:-webkit-gradient(linear,left top,left bottom,from(transparent),color-stop(40%,rgba(0,0,0,.05)),to(rgba(0,0,0,.1)));background-image:-webkit-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000}.pure-button-disabled,.pure-button-disabled:active,.pure-button-disabled:focus,.pure-button-disabled:hover,.pure-button[disabled]{border:none;background-image:none;opacity:.4;cursor:not-allowed;-webkit-box-shadow:none;box-shadow:none;pointer-events:none}.pure-button-hidden{display:none}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-button-group .pure-button{margin:0;border-radius:0;border-right:1px solid #111;border-right:1px solid rgba(0,0,0,.2)}.pure-button-group .pure-button:first-child{border-top-left-radius:2px;border-bottom-left-radius:2px}.pure-button-group .pure-button:last-child{border-top-right-radius:2px;border-bottom-right-radius:2px;border-right:none}.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 3px #ddd;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 3px #ddd;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=color]:focus,.pure-form input[type=date]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=email]:focus,.pure-form input[type=month]:focus,.pure-form input[type=number]:focus,.pure-form input[type=password]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=text]:focus,.pure-form input[type=time]:focus,.pure-form input[type=url]:focus,.pure-form input[type=week]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129fea}.pure-form input:not([type]):focus{outline:0;border-color:#129fea}.pure-form input[type=checkbox]:focus,.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus{outline:thin solid #129fea;outline:1px auto #129fea}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=color][disabled],.pure-form input[type=date][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=email][disabled],.pure-form input[type=month][disabled],.pure-form input[type=number][disabled],.pure-form input[type=password][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=text][disabled],.pure-form input[type=time][disabled],.pure-form input[type=url][disabled],.pure-form input[type=week][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form select:focus:invalid,.pure-form textarea:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=checkbox]:focus:invalid:focus,.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=color],.pure-form-stacked input[type=date],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=email],.pure-form-stacked input[type=file],.pure-form-stacked input[type=month],.pure-form-stacked input[type=number],.pure-form-stacked input[type=password],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=text],.pure-form-stacked input[type=time],.pure-form-stacked input[type=url],.pure-form-stacked input[type=week],.pure-form-stacked label,.pure-form-stacked select,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned .pure-help-inline,.pure-form-aligned input,.pure-form-aligned select,.pure-form-aligned textarea,.pure-form-message-inline{display:inline-block;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form .pure-input-rounded,.pure-form input.pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-3-4{width:75%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form .pure-help-inline,.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=color],.pure-group input[type=date],.pure-group input[type=datetime-local],.pure-group input[type=datetime],.pure-group input[type=email],.pure-group input[type=month],.pure-group input[type=number],.pure-group input[type=password],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=text],.pure-group input[type=time],.pure-group input[type=url],.pure-group input[type=week]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0 0}.pure-form .pure-help-inline,.pure-form-message,.pure-form-message-inline{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu{-webkit-box-sizing:border-box;box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-item,.pure-menu-list{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-heading,.pure-menu-link{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-separator{display:inline-block;zoom:1;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-active>.pure-menu-children,.pure-menu-allow-hover:hover>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;-webkit-overflow-scrolling:touch;padding:.5em 0}.pure-menu-horizontal .pure-menu-children .pure-menu-separator,.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-horizontal .pure-menu-children .pure-menu-separator{display:block;width:auto}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-disabled,.pure-menu-heading,.pure-menu-link{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent}.pure-menu-active>.pure-menu-link,.pure-menu-link:focus,.pure-menu-link:hover{background-color:#eee}.pure-menu-selected>.pure-menu-link,.pure-menu-selected>.pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0} -------------------------------------------------------------------------------- /src/ch4/bbs/static/style.css: -------------------------------------------------------------------------------- 1 | .content { 2 | margin-left: auto; 3 | margin-right: auto; 4 | max-width: 768px; 5 | } 6 | h1 { 7 | background-color: #0078e7; 8 | color: white; 9 | padding: 10px; margin:0; 10 | } 11 | form { padding: 8px; } 12 | textarea { width: 99%; } 13 | .box { 14 | border-left: 12px solid #0078e7; 15 | border-bottom: 1px solid #c0c0f0; 16 | border-top: 1px solid #f0f0f0; 17 | margin: 8px; padding: 8px; 18 | } 19 | .box_h { 20 | margin:4px; padding: 4px; 21 | } 22 | #menu-switch { 23 | text-decoration: none; 24 | color: white; 25 | font-size: 40px; 26 | width: 40px; 27 | } 28 | #menu { 29 | display: none; 30 | padding: 12px; 31 | border:1px solid silver; 32 | } 33 | -------------------------------------------------------------------------------- /src/ch4/bbs/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 会員制の掲示板 6 | 7 | 8 |
9 | 10 |

11 | 会員制の掲示板 - {{ user }}

12 | 13 | 20 |
22 | 23 | 26 |
27 | 28 | {% for i in data %} 29 |
30 |

{{ i.name }} - {{ i.date }}

31 |

{{ i.text }}

32 |
33 | {% endfor %} 34 | 35 | 47 |
48 | 49 | -------------------------------------------------------------------------------- /src/ch4/bbs/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | ログイン 6 | 7 | 8 |
9 |

会員制の掲示板

10 |
12 | 😃 ログインが必要です 13 |
14 | 15 | 16 | 17 | 18 | 21 |
22 |
23 |
24 | 25 | -------------------------------------------------------------------------------- /src/ch4/bbs/templates/msg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | メッセージ 6 | 7 | 8 | 9 |
10 |

{{ msg }}

11 |
→トップページへ
12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/ch4/fileshare/README.md: -------------------------------------------------------------------------------- 1 | # ファイル転送サービス 2 | 3 | (注意) Flaskはバージョン1.1.1をご利用ください。 4 | 5 | ``` 6 | pip install Flask==1.1.1 7 | ``` 8 | 9 | 2022年のFlask 2.0でパラメータ名に仕様変更がありました。 10 | 11 | そのため、[こちら](https://github.com/pallets/flask/issues/4753)にあるように、send_fileを使う時、`attachment_filename`を`download_name`と変更してください。 12 | 13 | -------------------------------------------------------------------------------- /src/ch4/fileshare/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect, request 2 | from flask import render_template, send_file 3 | import os, json, time 4 | import fs_data # ファイルやデータを管理するモジュール --- (*1) 5 | 6 | app = Flask(__name__) 7 | MASTER_PW = 'abcd' # 管理用パスワード --- (*2) 8 | 9 | @app.route('/') 10 | def index(): 11 | # ファイルのアップロードフォームを表示 --- (*3) 12 | return render_template('index.html') 13 | 14 | @app.route('/upload', methods=['POST']) 15 | def upload(): 16 | # アップロードしたファイルのオブジェクト --- (*4) 17 | upfile = request.files.get('upfile', None) 18 | if upfile is None: return msg('アップロード失敗') 19 | if upfile.filename == '': return msg('アップロード失敗') 20 | # メタ情報を取得 --- (*5) 21 | meta = { 22 | 'name': request.form.get('name', '名無し'), 23 | 'memo': request.form.get('memo', 'なし'), 24 | 'pw': request.form.get('pw', ''), 25 | 'limit': int(request.form.get('limit', '1')), 26 | 'count': int(request.form.get('count', '0')), 27 | 'filename': upfile.filename 28 | } 29 | if (meta['limit'] == 0) or (meta['pw'] == ''): 30 | return msg('パラメータが不正です。') 31 | # ファイルを保存 --- (*6) 32 | fs_data.save_file(upfile, meta) 33 | # ダウンロード先の表示 --- (*7) 34 | return render_template('info.html', 35 | meta=meta, mode='upload', 36 | url=request.host_url + 'download/' + meta['id']) 37 | 38 | @app.route('/download/') 39 | def download(id): 40 | # URLが正しいか判定 --- (*8) 41 | meta = fs_data.get_data(id) 42 | if meta is None: return msg('パラメータが不正です') 43 | # ダウンロードページを表示 --- (*9) 44 | return render_template('info.html', 45 | meta=meta, mode='download', 46 | url=request.host_url + 'download_go/' + id) 47 | 48 | @app.route('/download_go/', methods=['POST']) 49 | def download_go(id): 50 | # URLが正しいか再び判定 --- (*10) 51 | meta = fs_data.get_data(id) 52 | if meta is None: return msg('パラメータが不正です') 53 | # パスワードの確認 --- (*11) 54 | pw = request.form.get('pw', '') 55 | if pw != meta['pw']: return msg('パスワードが違います') 56 | # ダウンロード回数の確認 --- (*12) 57 | meta['count'] = meta['count'] - 1 58 | if meta['count'] < 0: 59 | return msg('ダウンロード回数を超えました。') 60 | fs_data.set_data(id, meta) 61 | # ダウンロード期限の確認 --- (*13) 62 | if meta['time_limit'] < time.time(): 63 | return msg('ダウンロードの期限が過ぎています') 64 | # ダウンロードできるようにファイルを送信 --- (*14) 65 | return send_file(meta['path'], 66 | as_attachment=True, 67 | attachment_filename=meta['filename']) 68 | 69 | @app.route('/admin/list') 70 | def admin_list(): 71 | # マスターパスワードの確認 --- (*15) 72 | if request.args.get('pw', '') != MASTER_PW: 73 | return msg('マスターパスワードが違います') 74 | # 全データをデータベースから取り出して表示 --- (*16) 75 | return render_template('admin_list.html', 76 | files=fs_data.get_all(), pw=MASTER_PW) 77 | 78 | @app.route('/admin/remove/') 79 | def admin_remove(id): 80 | # マスターパスワードを確認してファイルとデータを削除 --- (*17) 81 | if request.args.get('pw', '') != MASTER_PW: 82 | return msg('マスターパスワードが違います') 83 | fs_data.remove_data(id) 84 | return msg('削除しました') 85 | 86 | def msg(s): # テンプレートを使ってエラー画面を表示 87 | return render_template('error.html', message=s) 88 | 89 | # 日時フォーマットを簡易表示するフィルタ設定 --- (*18) 90 | def filter_datetime(tm): 91 | return time.strftime( 92 | '%Y/%m/%d %H:%M:%S', 93 | time.localtime(tm)) 94 | # フィルタをテンプレートエンジンに登録 95 | app.jinja_env.filters['datetime'] = filter_datetime 96 | 97 | if __name__ == '__main__': 98 | app.run(debug=True, host='0.0.0.0') 99 | 100 | -------------------------------------------------------------------------------- /src/ch4/fileshare/data/README.txt: -------------------------------------------------------------------------------- 1 | データファイルが保存されます 2 | -------------------------------------------------------------------------------- /src/ch4/fileshare/files/README.txt: -------------------------------------------------------------------------------- 1 | ここにファイルが保存されます 2 | -------------------------------------------------------------------------------- /src/ch4/fileshare/fs_data.py: -------------------------------------------------------------------------------- 1 | from tinydb import TinyDB, where 2 | import uuid, time, os 3 | 4 | # パスの指定 --- (*1) 5 | BASE_DIR = os.path.dirname(__file__) 6 | FILES_DIR = BASE_DIR + '/files' 7 | DATA_FILE = BASE_DIR + '/data/data.json' 8 | 9 | # アップロードされたファイルとメタ情報の保存 10 | def save_file(upfile, meta): 11 | # UUIDの生成 --- (*2) 12 | id = 'FS_' + uuid.uuid4().hex 13 | # アップロードされたファイルを保存 --- (*3) 14 | upfile.save(FILES_DIR + '/' + id) 15 | # メタデータをDBに保存 --- (*4) 16 | db = TinyDB(DATA_FILE) 17 | meta['id'] = id 18 | # 期限を計算 --- (*5) 19 | term = meta['limit'] * 60 * 60 * 24 20 | meta['time_limit'] = time.time() + term 21 | # 情報をデータベースに挿入 --- (*6) 22 | db.insert(meta) 23 | return id 24 | 25 | # データベースから任意のIDのデータを取り出す --- (*7) 26 | def get_data(id): 27 | db = TinyDB(DATA_FILE) 28 | f = db.get(where('id') == id) 29 | if f is not None: 30 | f['path'] = FILES_DIR + '/' + id 31 | return f 32 | 33 | # データを更新する --- (*8) 34 | def set_data(id, meta): 35 | db = TinyDB(DATA_FILE) 36 | db.update(meta, where('id') == id) 37 | 38 | # 全てのデータを取得する --- (*9) 39 | def get_all(): 40 | db = TinyDB(DATA_FILE) 41 | return db.all() 42 | 43 | # アップロードされたファイルとメタ情報の削除 --- (*10) 44 | def remove_data(id): 45 | # ファイルを削除 --- (*11) 46 | path = FILES_DIR + '/' + id 47 | os.remove(path) 48 | # メタデータを削除 --- (*12) 49 | db = TinyDB(DATA_FILE) 50 | db.remove(where('id') == id) 51 | -------------------------------------------------------------------------------- /src/ch4/fileshare/static/style.css: -------------------------------------------------------------------------------- 1 | .content { 2 | margin-left: auto; 3 | margin-right: auto; 4 | max-width: 768px; 5 | } 6 | h1 { 7 | background-color: #0078e7; 8 | color: white; 9 | padding: 10px; margin:0; 10 | } 11 | #info { 12 | margin: 16px; padding: 8px; 13 | } 14 | select { 15 | width:13em; 16 | } 17 | .fblock { 18 | padding: 0em 0em 1em 2em; 19 | } 20 | .box { 21 | border:1px solid silver; 22 | padding: 8px; margin: 8px; 23 | } 24 | .box-noborder { 25 | padding: 8px; margin: 8px; 26 | } 27 | th { text-align: right; } 28 | -------------------------------------------------------------------------------- /src/ch4/fileshare/templates/admin_list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 |
8 |

管理ページ

9 | {% for f in files %} 10 |
13 |

{{ f.name }}さん - {{ f.memo }} - 14 | {{ f.time_limit | datetime }} - {{ f.count }}回
15 | 削除 16 | - ダウンロード

17 |
18 | {% endfor %} 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /src/ch4/fileshare/templates/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 |
8 |

{{ message }}

9 |

→他のファイルをアップロード

10 |
11 | 12 | -------------------------------------------------------------------------------- /src/ch4/fileshare/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 |
9 |

ファイル転送サービス

10 |
11 |
14 |
15 | 最初にファイルを選んでください。 16 |
17 | 18 | 19 |
20 | 下記の必要項目を記述してください。 21 |
22 | 23 | 25 | 26 | 28 | 29 | 31 | 32 | 37 | 38 | 44 |
45 | 48 |
49 |
50 |
51 |
52 | 53 | -------------------------------------------------------------------------------- /src/ch4/fileshare/templates/info.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 |
8 |

9 | 10 | {% if mode == 'upload' %} 11 | アップロード完了 12 | {% else %} 13 | ダウンロードできます 14 | {% endif %} 15 |

16 |
17 |

ファイルの情報:

18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
所有者{{ meta.name }}
ファイルの説明{{ meta.memo }}
ファイル名{{ meta.filename }}
ダウンロード回数{{ meta.count }}
保存期限{{ meta.time_limit | datetime }}
40 |
41 | {% if mode == 'upload' %} 42 |
43 |

ダウンロード先の情報:

44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
URL
パスワード{{ meta.pw }}
54 |
55 | {% else %} 56 |
57 |
59 | 60 | 61 | 64 |
65 |
66 | {% endif %} 67 |

→他のファイルをアップロード

68 |
69 | 70 | -------------------------------------------------------------------------------- /src/ch4/haikusns/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect, render_template 2 | from flask import request, Markup 3 | import os, time 4 | import sns_user as user, sns_data as data 5 | 6 | # Flaskインスタンスと暗号化キーの指定 7 | app = Flask(__name__) 8 | app.secret_key = 'TIIDe5TUMtPUHpyu' 9 | 10 | # --- URLのルーティング --- (*1) 11 | @app.route('/') # --- (*2) 12 | @user.login_required 13 | def index(): 14 | me = user.get_id() 15 | return render_template('index.html', id=me, 16 | users=user.get_allusers(), 17 | fav_users=data.get_fav_list(me), 18 | timelines=data.get_timelines(me)) 19 | 20 | @app.route('/login') # --- (*3) 21 | def login(): 22 | return render_template('login_form.html') 23 | 24 | @app.route('/login/try', methods=['POST']) # --- (*4) 25 | def login_try(): 26 | ok = user.try_login(request.form) 27 | if not ok: return msg('ログインに失敗しました') 28 | return redirect('/') 29 | 30 | @app.route('/logout') # --- (*5) 31 | def logout(): 32 | user.try_logout() 33 | return msg('ログアウトしました') 34 | 35 | @app.route('/users/') # --- (*6) 36 | @user.login_required 37 | def users(user_id): 38 | if user_id not in user.USER_LOGIN_LIST: # --- (*7) 39 | return msg('ユーザーが存在しません') 40 | me = user.get_id() 41 | return render_template('users.html', 42 | user_id=user_id, id=me, 43 | is_fav=data.is_fav(me, user_id), 44 | text_list=data.get_text(user_id)) 45 | 46 | @app.route('/fav/add/') # --- (*8) 47 | @user.login_required 48 | def fav_add(user_id): 49 | data.add_fav(user.get_id(), user_id) 50 | return redirect('/users/' + user_id) 51 | 52 | @app.route('/fav/remove/') # --- (*9) 53 | @user.login_required 54 | def remove_fav(user_id): 55 | data.remove_fav(user.get_id(), user_id) 56 | return redirect('/users/' + user_id) 57 | 58 | @app.route('/write') # --- (*10) 59 | @user.login_required 60 | def write(): 61 | return render_template('write_form.html', 62 | id=user.get_id()) 63 | 64 | @app.route('/write/try', methods=['POST']) # --- (*11) 65 | @user.login_required 66 | def try_write(): 67 | text = request.form.get('text', '') 68 | if text == '': return msg('テキストが空です。') 69 | data.write_text(user.get_id(), text) 70 | return redirect('/') 71 | 72 | def msg(msg): 73 | return render_template('msg.html', msg=msg) 74 | 75 | # --- テンプレートのフィルタなど拡張機能の指定 --- (*12) 76 | # CSSなど静的ファイルの後ろにバージョンを自動追記 --- (*13) 77 | @app.context_processor 78 | def add_staticfile(): 79 | return dict(staticfile=staticfile_cp) 80 | def staticfile_cp(fname): 81 | path = os.path.join(app.root_path, 'static', fname) 82 | mtime = str(int(os.stat(path).st_mtime)) 83 | return '/static/' + fname + '?v=' + str(mtime) 84 | 85 | # 改行を有効にするフィルタを追加 --- (*14) 86 | @app.template_filter('linebreak') 87 | def linebreak_fiter(s): 88 | s = s.replace('&', '&').replace('<', '<') \ 89 | .replace('>', '>').replace('\n', '
') 90 | return Markup(s) 91 | 92 | # 日付をフォーマットするフィルタを追加 --- (*15) 93 | @app.template_filter('datestr') 94 | def datestr_fiter(s): 95 | return time.strftime('%Y年%m月%d日', 96 | time.localtime(s)) 97 | 98 | if __name__ == '__main__': 99 | app.run(debug=True, host='0.0.0.0') 100 | -------------------------------------------------------------------------------- /src/ch4/haikusns/data/data.json: -------------------------------------------------------------------------------- 1 | {"_default": {}, "fav": {"15": {"id": "jiro", "fav_id": "taro"}, "16": {"id": "jiro", "fav_id": "jiro"}, "22": {"id": "sabu", "fav_id": "siro"}, "38": {"id": "sabu", "fav_id": "taro"}, "39": {"id": "sabu", "fav_id": "goro"}, "42": {"id": "taro", "fav_id": "jiro"}, "43": {"id": "taro", "fav_id": "sabu"}, "44": {"id": "taro", "fav_id": "siro"}, "45": {"id": "taro", "fav_id": "muro"}, "46": {"id": "taro", "fav_id": "taro"}, "47": {"id": "taro", "fav_id": "goro"}}, "text": {"1": {"id": "jiro", "text": "\u6625\u304c\u6765\u305f\r\n\u304b\u3048\u308b\u3082\u6ce3\u304f\u3088\r\n\u6c60\u306e\u5074", "time": 1581091698.7167811}, "2": {"id": "jiro", "text": "\u304b\u3048\u308b\u3055\u3093\r\n\u6c60\u306b\u3066\u4eca\u65e5\u3082\r\n\u306a\u304f\u65e5\u3005\u3088", "time": 1581092682.0981839}, "3": {"id": "jiro", "text": "aaa", "time": 1581092743.669108}, "4": {"id": "jiro", "text": "\u304a\u30fc\u3044\r\n\u3042\u3042\u3042\u3042\r\n\u3044\u308d\u3044\u308d\u3060", "time": 1581128261.924575}, "5": {"id": "taro", "text": "\u51ac\u304c\u6765\u305f\r\n\u3000\u3084\u3070\u3044\u305e\u5bd2\u3044\u3068\r\n\u732b\u3082\u306a\u304f", "time": 1581130500.896428}, "6": {"id": "taro", "text": "\u6d77\u306b\u6765\u305f\r\n\u3000\u6ce2\u6253\u3061\u969b\u3067\r\n\u3000\u3000\u663c\u5bdd\u3057\u305f", "time": 1581130534.2869408}, "7": {"id": "sabu", "text": "\u9ce5\u306e\u58f0\r\n\u3000\u6a39\u306e\u4e0b\u6dbc\u3080\r\n\u3000\u3000\u590f\u6563\u6b69", "time": 1581131330.9644969}, "8": {"id": "taro", "text": "\u5f85\u3061\u4eba\u3082\r\n\u3000\u5c11\u3057\u9045\u308c\u308b\r\n\u3000\u3000\u6625\u306e\u7a7a", "time": 1581137657.264885}, "9": {"id": "sabu", "text": "\u65b0\u7dd1\u3067\r\n\u3000\u3042\u3081\u7389\u98df\u3079\u3066\r\n\u3000\u3000\u5bdd\u8ee2\u3093\u3060", "time": 1581137778.825313}, "10": {"id": "goro", "text": "\u3000\u3000\u6d74\u8863\u6765\u3066\r\n\u3000\u82b1\u706b\u306b\u898b\u5165\u308b\r\n\u590f\u306e\u591c\r\n", "time": 1581159755.760719}, "11": {"id": "sabu", "text": "\u6625\u7720\u3082\r\n\u3000\u3000\u7de0\u3081\u5207\u308a\u8fd1\u3057\r\n\u3000\u7b46\u7f6e\u304b\u305a", "time": 1581181860.3861802}, "12": {"id": "jiro", "text": "\u685c\u54b2\u304d\r\n\u3000\u88cf\u5ead\u306e\u5bb4\r\n\u3000\u3000\u8cd1\u3084\u304b\u306a", "time": 1581186265.9742599}, "13": {"id": "sabu", "text": "\u96ea\u7a4d\u3082\u308b\r\n\u3000\u99c5\u524d\u30d3\u30eb\u304c\r\n\u3000\u3000\u767d\u304f\u306a\u308b", "time": 1582248452.298648}, "14": {"id": "taro", "text": "\u590f\u3059\u304e\u3066\r\n\u3000\u6cf3\u3050\u30d7\u30fc\u30eb\u306f\r\n\u3000\u3000\u808c\u5bd2\u3044", "time": 1582270703.023259}}} -------------------------------------------------------------------------------- /src/ch4/haikusns/sns_data.py: -------------------------------------------------------------------------------- 1 | from tinydb import TinyDB, Query 2 | import time, os 3 | 4 | # パスの指定 --- (*1) 5 | BASE_DIR = os.path.dirname(__file__) 6 | DATA_FILE = BASE_DIR + '/data/data.json' 7 | 8 | # データベースを開く --- (*2) 9 | db = TinyDB(DATA_FILE) 10 | 11 | # お気に入り登録用のfavテーブルのオブジェクトを返す --- (*3) 12 | def get_fav_table(): 13 | return db.table('fav'), Query() 14 | 15 | def add_fav(id, fav_id): # --- (*4) 16 | table, q = get_fav_table() 17 | a = table.search( 18 | (q.id == id) & (q.fav_id == fav_id)) 19 | if len(a) == 0: 20 | table.insert({'id': id, 'fav_id': fav_id}) 21 | 22 | def is_fav(id, fav_id): # --- (*5) 23 | table, q = get_fav_table() 24 | a = table.get( 25 | (q.id == id) & (q.fav_id == fav_id)) 26 | return a is not None 27 | 28 | def remove_fav(id, fav_id): # --- (*6) 29 | table, q = get_fav_table() 30 | table.remove( 31 | (q.id == id) & (q.fav_id == fav_id)) 32 | 33 | def get_fav_list(id): # --- (*7) 34 | table, q = get_fav_table() 35 | a = table.search(q.id == id) 36 | return [row['fav_id'] for row in a] 37 | 38 | # 俳句保存用のtextテーブルのオブジェクトを返す --- (*8) 39 | def get_text_table(): 40 | return db.table('text'), Query() 41 | 42 | def write_text(id, text): # --- (*9) 43 | table, q = get_text_table() 44 | table.insert({ 45 | 'id': id, 46 | 'text': text, 47 | 'time': time.time()}) 48 | 49 | def get_text(id): # --- (*10) 50 | table, q = get_text_table() 51 | return table.search(q.id == id) 52 | 53 | # タイムラインに表示するデータを取得する --- (*11) 54 | def get_timelines(id): 55 | # お気に入りユーザーの一覧を取得 --- (*12) 56 | table, q = get_text_table() 57 | favs = get_fav_list(id) 58 | favs.append(id) # 自身も検索対象に入れる 59 | # 期間を指定して作品一覧を取得 --- (*13) 60 | tm = time.time() - (24*60*60) * 30 # 30日分 61 | a = table.search( 62 | q.id.one_of(favs) & (q.time > tm)) 63 | return sorted(a, 64 | key=lambda v:v['time'], 65 | reverse=True) # --- (*14) 66 | 67 | -------------------------------------------------------------------------------- /src/ch4/haikusns/sns_user.py: -------------------------------------------------------------------------------- 1 | # ログインなどユーザーに関する処理をまとめた 2 | from flask import Flask, session, redirect 3 | from functools import wraps 4 | 5 | # ユーザー名とパスワードの一覧 --- (*1) 6 | USER_LOGIN_LIST = { 7 | 'taro': 'aaa', 8 | 'jiro': 'bbb', 9 | 'sabu': 'ccc', 10 | 'siro': 'ddd', 11 | 'goro': 'eee', 12 | 'muro': 'fff' } 13 | 14 | # ログインしているかの確認 --- (*2) 15 | def is_login(): 16 | return 'login' in session 17 | 18 | # ログインを試行する --- (*3) 19 | def try_login(form): 20 | user = form.get('user', '') 21 | password = form.get('pw', '') 22 | # パスワードチェック 23 | if user not in USER_LOGIN_LIST: return False 24 | if USER_LOGIN_LIST[user] != password: 25 | return False 26 | session['login'] = user 27 | return True 28 | 29 | # ユーザー名を得る --- (*4) 30 | def get_id(): 31 | return session['login'] if is_login() else '未ログイン' 32 | 33 | # 全ユーザーの情報を得る --- (*5) 34 | def get_allusers(): 35 | return [ u for u in USER_LOGIN_LIST ] 36 | 37 | # ログアウトする --- (*6) 38 | def try_logout(): 39 | session.pop('login', None) 40 | 41 | # ログイン必須を処理するデコレーターを定義 --- (*7) 42 | def login_required(func): 43 | @wraps(func) 44 | def wrapper(*args, **kwargs): 45 | if not is_login(): 46 | return redirect('/login') 47 | return func(*args, **kwargs) 48 | return wrapper 49 | -------------------------------------------------------------------------------- /src/ch4/haikusns/static/side-menu.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #777; 3 | } 4 | 5 | .pure-img-responsive { 6 | max-width: 100%; 7 | height: auto; 8 | } 9 | 10 | /* 11 | Add transition to containers so they can push in and out. 12 | */ 13 | #layout, 14 | #menu, 15 | .menu-link { 16 | -webkit-transition: all 0.2s ease-out; 17 | -moz-transition: all 0.2s ease-out; 18 | -ms-transition: all 0.2s ease-out; 19 | -o-transition: all 0.2s ease-out; 20 | transition: all 0.2s ease-out; 21 | } 22 | 23 | /* 24 | This is the parent `
` that contains the menu and the content area. 25 | */ 26 | #layout { 27 | position: relative; 28 | left: 0; 29 | padding-left: 0; 30 | } 31 | #layout.active #menu { 32 | left: 150px; 33 | width: 150px; 34 | } 35 | 36 | #layout.active .menu-link { 37 | left: 150px; 38 | } 39 | /* 40 | The content `
` is where all your content goes. 41 | */ 42 | .content { 43 | margin: 0 auto; 44 | padding: 0 2em; 45 | max-width: 800px; 46 | margin-bottom: 50px; 47 | line-height: 1.6em; 48 | } 49 | 50 | .header { 51 | margin: 0; 52 | color: #333; 53 | text-align: center; 54 | padding: 2.5em 2em 0; 55 | border-bottom: 1px solid #eee; 56 | } 57 | .header h1 { 58 | margin: 0.2em 0; 59 | font-size: 3em; 60 | font-weight: 300; 61 | } 62 | .header h2 { 63 | font-weight: 300; 64 | color: #ccc; 65 | padding: 0; 66 | margin-top: 0; 67 | } 68 | 69 | .content-subhead { 70 | margin: 50px 0 20px 0; 71 | font-weight: 300; 72 | color: #888; 73 | } 74 | 75 | 76 | 77 | /* 78 | The `#menu` `
` is the parent `
` that contains the `.pure-menu` that 79 | appears on the left side of the page. 80 | */ 81 | 82 | #menu { 83 | margin-left: -150px; /* "#menu" width */ 84 | width: 150px; 85 | position: fixed; 86 | top: 0; 87 | left: 0; 88 | bottom: 0; 89 | z-index: 1000; /* so the menu or its navicon stays above all content */ 90 | background: #191818; 91 | overflow-y: auto; 92 | -webkit-overflow-scrolling: touch; 93 | } 94 | /* 95 | All anchors inside the menu should be styled like this. 96 | */ 97 | #menu a { 98 | color: #999; 99 | border: none; 100 | padding: 0.6em 0 0.6em 0.6em; 101 | } 102 | 103 | /* 104 | Remove all background/borders, since we are applying them to #menu. 105 | */ 106 | #menu .pure-menu, 107 | #menu .pure-menu ul { 108 | border: none; 109 | background: transparent; 110 | } 111 | 112 | /* 113 | Add that light border to separate items into groups. 114 | */ 115 | #menu .pure-menu ul, 116 | #menu .pure-menu .menu-item-divided { 117 | border-top: 1px solid #333; 118 | } 119 | /* 120 | Change color of the anchor links on hover/focus. 121 | */ 122 | #menu .pure-menu li a:hover, 123 | #menu .pure-menu li a:focus { 124 | background: #333; 125 | } 126 | 127 | /* 128 | This styles the selected menu item `
  • `. 129 | */ 130 | #menu .pure-menu-selected, 131 | #menu .pure-menu-heading { 132 | background: #1f8dd6; 133 | } 134 | /* 135 | This styles a link within a selected menu item `
  • `. 136 | */ 137 | #menu .pure-menu-selected a { 138 | color: #fff; 139 | } 140 | 141 | /* 142 | This styles the menu heading. 143 | */ 144 | #menu .pure-menu-heading { 145 | font-size: 110%; 146 | color: #fff; 147 | margin: 0; 148 | } 149 | 150 | /* -- Dynamic Button For Responsive Menu -------------------------------------*/ 151 | 152 | /* 153 | The button to open/close the Menu is custom-made and not part of Pure. Here's 154 | how it works: 155 | */ 156 | 157 | /* 158 | `.menu-link` represents the responsive menu toggle that shows/hides on 159 | small screens. 160 | */ 161 | .menu-link { 162 | position: fixed; 163 | display: block; /* show this only on small screens */ 164 | top: 0; 165 | left: 0; /* "#menu width" */ 166 | background: #000; 167 | background: rgba(0,0,0,0.7); 168 | font-size: 10px; /* change this value to increase/decrease button size */ 169 | z-index: 10; 170 | width: 2em; 171 | height: auto; 172 | padding: 2.1em 1.6em; 173 | } 174 | 175 | .menu-link:hover, 176 | .menu-link:focus { 177 | background: #000; 178 | } 179 | 180 | .menu-link span { 181 | position: relative; 182 | display: block; 183 | } 184 | 185 | .menu-link span, 186 | .menu-link span:before, 187 | .menu-link span:after { 188 | background-color: #fff; 189 | width: 100%; 190 | height: 0.2em; 191 | } 192 | 193 | .menu-link span:before, 194 | .menu-link span:after { 195 | position: absolute; 196 | margin-top: -0.6em; 197 | content: " "; 198 | } 199 | 200 | .menu-link span:after { 201 | margin-top: 0.6em; 202 | } 203 | 204 | 205 | /* -- Responsive Styles (Media Queries) ------------------------------------- */ 206 | 207 | /* 208 | Hides the menu at `48em`, but modify this based on your app's needs. 209 | */ 210 | @media (min-width: 48em) { 211 | 212 | .header, 213 | .content { 214 | padding-left: 2em; 215 | padding-right: 2em; 216 | } 217 | 218 | #layout { 219 | padding-left: 150px; /* left col width "#menu" */ 220 | left: 0; 221 | } 222 | #menu { 223 | left: 150px; 224 | } 225 | 226 | .menu-link { 227 | position: fixed; 228 | left: 150px; 229 | display: none; 230 | } 231 | 232 | #layout.active .menu-link { 233 | left: 150px; 234 | } 235 | } 236 | 237 | @media (max-width: 48em) { 238 | /* Only apply this when the window is small. Otherwise, the following 239 | case results in extra padding on the left: 240 | * Make the window small. 241 | * Tap the menu to trigger the active state. 242 | * Make the window large again. 243 | */ 244 | #layout.active { 245 | position: relative; 246 | left: 150px; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/ch4/haikusns/static/style.css: -------------------------------------------------------------------------------- 1 | .page { 2 | margin-right: auto; 3 | margin-left: auto; 4 | max-width: 796px; 5 | } 6 | h1 { 7 | background-color: white; 8 | color:brown; 9 | padding: 8px 8px 8px 24px; 10 | margin: 8px; 11 | } 12 | h2 { 13 | padding: 4px; margin: 4px; 14 | font-size: 1em; 15 | border-bottom: 1px dotted silver; 16 | } 17 | h3 { 18 | text-align: center; 19 | } 20 | .box { 21 | padding: 8px; margin: 4px; 22 | border-bottom: 1px solid #f0f0f0; 23 | } 24 | .box_c { 25 | padding: 8px; margin: 4px; 26 | text-align:center; 27 | border-bottom: 1px dotted #e0e0e0; 28 | } 29 | .haiku-list { 30 | margin-right: auto; 31 | margin-left: auto; 32 | max-width: 400px; 33 | } 34 | .haiku { 35 | border: 1px solid silver; 36 | background-color: #f9f9fe; 37 | line-height: 40px; font-size: 16px; 38 | padding: 14px 20px 30px 20px; 39 | margin: 14px 20px 20px 20px; 40 | border-radius: 7px; 41 | color: brown; 42 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.16), 43 | 0 2px 5px rgba(0, 0, 0, 0.23); 44 | } 45 | .info { 46 | padding: 6px; margin: 4px; 47 | border-top: 1px dotted silver; 48 | color: silver; 49 | font-size: 10px; 50 | text-align: right; 51 | } 52 | #msg { 53 | font-size: 1.5em; font-weight: bold; 54 | padding: 20px; margin: 8px; 55 | border: 1px solid silver; 56 | color: red; 57 | } 58 | #loginform { 59 | padding: 4px 8px 4px 12px; 60 | } 61 | textarea { 62 | width: 99%; 63 | } 64 | .footer { 65 | text-align: center; 66 | } 67 | 68 | -------------------------------------------------------------------------------- /src/ch4/haikusns/static/ui.js: -------------------------------------------------------------------------------- 1 | (function (window, document) { 2 | 3 | var layout = document.getElementById('layout'), 4 | menu = document.getElementById('menu'), 5 | menuLink = document.getElementById('menuLink'), 6 | content = document.getElementById('main'); 7 | 8 | function toggleClass(element, className) { 9 | var classes = element.className.split(/\s+/), 10 | length = classes.length, 11 | i = 0; 12 | 13 | for(; i < length; i++) { 14 | if (classes[i] === className) { 15 | classes.splice(i, 1); 16 | break; 17 | } 18 | } 19 | // The className is not found 20 | if (length === classes.length) { 21 | classes.push(className); 22 | } 23 | 24 | element.className = classes.join(' '); 25 | } 26 | 27 | function toggleAll(e) { 28 | var active = 'active'; 29 | 30 | e.preventDefault(); 31 | toggleClass(layout, active); 32 | toggleClass(menu, active); 33 | toggleClass(menuLink, active); 34 | } 35 | 36 | menuLink.onclick = function (e) { 37 | toggleAll(e); 38 | }; 39 | 40 | content.onclick = function(e) { 41 | if (menu.className.indexOf('active') !== -1) { 42 | toggleAll(e); 43 | } 44 | }; 45 | 46 | }(this, this.document)); 47 | -------------------------------------------------------------------------------- /src/ch4/haikusns/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout_login.html" %} 2 | 3 | {% block contents %} 4 | 5 | {# お気に入り登録しているユーザーの一覧を表示 --- (*1) #} 6 |
    7 | お気に入りの作家: 8 | {% for u in fav_users %} 9 | {{ u }} 10 | {% endfor %} 11 |
    12 | 13 | {# 全てのユーザーの一覧を表示 --- (*2) #} 14 |
    15 | すべての作家: 16 | {% for u in users %} 17 | {{ u }} 18 | {% endfor %} 19 |
    20 | 21 | 22 | {# タイムラインを表示 --- (*3) #} 23 |

    {{ id }}のタイムライン

    24 |
    25 | {% if timelines | length == 0 %} 26 |
    タイムラインに作品がありません。 27 | 俳句を書くか他のユーザーをお気に入りにしてください。
    28 | {% endif %} 29 | {% for i in timelines %} 30 |
    31 | {{ i.text | linebreak }} 32 |

    33 | {{ i.time | datestr }} 34 | 作: {{ i.id }}

    35 |
    36 | {% endfor %} 37 |
    38 | 39 | 40 | {% endblock %} 41 | 42 | -------------------------------------------------------------------------------- /src/ch4/haikusns/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 俳句SNS 10 | 11 |
    12 | {% block header %} 13 | 14 | {% endblock %} 15 |
    16 |
    17 |

    18 | {% block title %} 19 | 俳句SNS 20 | {% endblock %} 21 |

    22 |
    23 | 24 |
    25 | {% block contents %} 26 | 27 | {% endblock %} 28 |
    29 | 30 | {% block footer %} 31 | 33 | {% endblock %} 34 |
    35 |
    36 | 37 | {% block footer_script %}{% endblock %} 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/ch4/haikusns/templates/layout_login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block header %} 4 | 5 | 6 | 7 | 8 | 9 | 25 | {% endblock %} 26 | 27 | {% block footer_script %} 28 | 29 | {% endblock %} 30 | 31 | -------------------------------------------------------------------------------- /src/ch4/haikusns/templates/login_form.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block contents %} 4 |
    5 |
    7 | ログインしてください。 8 |
    9 |
    10 | 11 | 13 |
    14 |
    15 | 16 | 18 |
    19 |
    20 | 23 |
    24 |
    25 |
    26 |
    27 | {% endblock %} 28 | 29 | -------------------------------------------------------------------------------- /src/ch4/haikusns/templates/msg.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block contents %} 4 |
    {{ msg }}
    5 | {% endblock %} 6 | 7 | -------------------------------------------------------------------------------- /src/ch4/haikusns/templates/users.html: -------------------------------------------------------------------------------- 1 | {% extends "layout_login.html" %} 2 | 3 | {# ユーザー個別ページなのでタイトルも変更 --- (*1) #} 4 | {% block title %} 5 | {{ user_id }}さんの俳句ページ 6 | {% endblock %} 7 | 8 | {% block contents %} 9 | 10 | {# お気に入り登録有無と登録解除リンク --- (*2) #} 11 |
    12 | {% if is_fav %} 13 | 👍 15 | お気に入りを外す 16 | {% else %} 17 | お気に入りにする 19 | {% endif %} 20 |
    21 | 22 |

    作品

    23 | 24 | {# 作品一覧を表示 --- (*3) #} 25 | {% if text_list | length == 0 %} 26 | 作品はまだありません。 27 | {% endif %} 28 |
    29 | {% for t in text_list %} 30 |
    31 | {{ t.text | linebreak }} 32 |

    {{ t.time | datestr }}

    33 |
    34 | {% endfor %} 35 |
    36 | 37 | {% endblock %} 38 | 39 | -------------------------------------------------------------------------------- /src/ch4/haikusns/templates/write_form.html: -------------------------------------------------------------------------------- 1 | {% extends "layout_login.html" %} 2 | {% block contents %} 3 |

    俳句の書き込み

    4 |
    5 |
    7 | 8 |

    9 | 11 |
    12 |
    13 |


    14 | {% endblock %} 15 | 16 | -------------------------------------------------------------------------------- /src/ch4/photoshare/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect, request 2 | from flask import render_template, send_file 3 | import photo_db, sns_user as user # 自作モジュールを取り込む 4 | 5 | app = Flask(__name__) 6 | app.secret_key = 'dpwvgAxaY2iWHMb2' 7 | 8 | # ログイン処理を実現する --- (*1) 9 | @app.route('/login') 10 | def login(): 11 | return render_template('login_form.html') 12 | 13 | @app.route('/login/try', methods=['POST']) 14 | def login_try(): 15 | ok = user.try_login(request.form) 16 | if not ok: return msg('ログイン失敗') 17 | return redirect('/') 18 | 19 | @app.route('/logout') 20 | def logout(): 21 | user.try_logout() 22 | return msg('ログアウトしました') 23 | 24 | 25 | # メイン画面 - メンバーの最新写真を全部表示する --- (*2) 26 | @app.route('/') 27 | @user.login_required 28 | def index(): 29 | return render_template('index.html', 30 | id=user.get_id(), 31 | photos=photo_db.get_files()) 32 | 33 | # アルバムに入っている画像一覧を表示 --- (*3) 34 | @app.route('/album/') 35 | @user.login_required 36 | def album_show(album_id): 37 | album = photo_db.get_album(album_id) 38 | return render_template('album.html', 39 | album=album, 40 | photos=photo_db.get_album_files(album_id)) 41 | 42 | # ユーザーがアップした画像の一覧を表示 --- (*4) 43 | @app.route('/user/') 44 | @user.login_required 45 | def user_page(user_id): 46 | return render_template('user.html', id=user_id, 47 | photos=photo_db.get_user_files(user_id)) 48 | 49 | # 画像ファイルのアップロードに関する機能を実現する --- (*5) 50 | @app.route('/upload') 51 | @user.login_required 52 | def upload(): 53 | return render_template('upload_form.html', 54 | albums=photo_db.get_albums(user.get_id())) 55 | 56 | @app.route('/upload/try', methods=['POST']) 57 | @user.login_required 58 | def upload_try(): 59 | # アップロードされたファイルを確認 --- (*6) 60 | upfile = request.files.get('upfile', None) 61 | if upfile is None: return msg('アップロード失敗') 62 | if upfile.filename == '': return msg('アップロード失敗') 63 | # どのアルバムに所属させるかをフォームから値を得る --- (*7) 64 | album_id = int(request.form.get('album', '0')) 65 | # ファイルの保存とデータベースへの登録を行う --- (*8) 66 | photo_id = photo_db.save_file(user.get_id(), upfile, album_id) 67 | if photo_id == 0: return msg('データベースのエラー') 68 | return redirect('/user/' + str(user.get_id())) 69 | 70 | # アルバムの作成機能 --- (*9) 71 | @app.route('/album/new') 72 | @user.login_required 73 | def album_new(): 74 | return render_template('album_new_form.html') 75 | 76 | @app.route('/album/new/try') 77 | @user.login_required 78 | def album_new_try(): 79 | id = photo_db.album_new(user.get_id(), request.args) 80 | if id == 0: return msg('新規アルバム作成に失敗') 81 | return redirect('/upload') 82 | 83 | 84 | # 画像ファイルを送信する機能 --- (*10) 85 | @app.route('/photo/') 86 | @user.login_required 87 | def photo(file_id): 88 | ptype = request.args.get('t', '') 89 | photo = photo_db.get_file(file_id, ptype) 90 | if photo is None: return msg('ファイルがありません') 91 | return send_file(photo['path']) 92 | 93 | 94 | def msg(s): 95 | return render_template('msg.html', msg=s) 96 | 97 | # CSSなど静的ファイルの後ろにバージョンを自動追記 98 | @app.context_processor 99 | def add_staticfile(): 100 | return dict(staticfile=staticfile_cp) 101 | def staticfile_cp(fname): 102 | import os 103 | path = os.path.join(app.root_path, 'static', fname) 104 | mtime = str(int(os.stat(path).st_mtime)) 105 | return '/static/' + fname + '?v=' + str(mtime) 106 | 107 | if __name__ == '__main__': 108 | app.run(debug=True, host='0.0.0.0') 109 | 110 | -------------------------------------------------------------------------------- /src/ch4/photoshare/data/README.md: -------------------------------------------------------------------------------- 1 | このフォルダに photos.sqlite3 が保存されます 2 | 3 | -------------------------------------------------------------------------------- /src/ch4/photoshare/photo_db.py: -------------------------------------------------------------------------------- 1 | import re, photo_file, photo_sqlite 2 | from photo_sqlite import exec, select 3 | 4 | # 新規アルバムを作成 --- (*1) 5 | def album_new(user_id, args): 6 | name = args.get('name', '') 7 | if name == '': return 0 8 | album_id = exec( 9 | 'INSERT INTO albums (name, user_id) VALUES (?,?)', 10 | name, user_id) 11 | return album_id 12 | 13 | # 特定ユーザーのアルバム一覧を得る --- (*2) 14 | def get_albums(user_id): 15 | return select( 16 | 'SELECT * FROM albums WHERE user_id=?', 17 | user_id) 18 | 19 | # 特定のアルバム情報を得る --- (*3) 20 | def get_album(album_id): 21 | a = select('SELECT * FROM albums WHERE album_id=?', album_id) 22 | if len(a) == 0: return None 23 | return a[0] 24 | 25 | # アルバム名を得る --- (*4) 26 | def get_album_name(album_id): 27 | a = get_album(album_id) 28 | if a == None: return '未分類' 29 | return a['name'] 30 | 31 | # アップロードされたファイルを保存 --- (*5) 32 | def save_file(user_id, upfile, album_id): 33 | # JPEGファイルだけを許可 34 | if not re.search(r'\.(jpg|jpeg)$', upfile.filename): 35 | print('JPEGではない:', upfile.filename) 36 | return 0 37 | # アルバム未指定の場合、未分類アルバムを自動的に作る --- (*6) 38 | if album_id == 0: 39 | a = select('SELECT * FROM albums ' + 40 | 'WHERE user_id=? AND name=?', 41 | user_id, '未分類') 42 | if len(a) == 0: 43 | album_id = exec('INSERT INTO albums '+ 44 | '(user_id, name) VALUES (?,?)', 45 | user_id, '未分類') 46 | else: 47 | album_id = a[0]['album_id'] 48 | # ファイル情報を保存 --- (*7) 49 | file_id = exec(''' 50 | INSERT INTO files (user_id, filename, album_id) 51 | VALUES (?, ?, ?)''', 52 | user_id, upfile.filename, album_id) 53 | # ファイルを保存 --- (*8) 54 | upfile.save(photo_file.get_path(file_id)) 55 | return file_id 56 | 57 | # ファイルに関する情報を得る --- (*9) 58 | def get_file(file_id, ptype): 59 | # データベースから基本情報を得る 60 | a = select('SELECT * FROM files WHERE file_id=?', file_id) 61 | if len(a) == 0: return None 62 | p = a[0] 63 | p['path'] = photo_file.get_path(file_id) 64 | # サムネイル画像の指定であれば作成する --- (*10) 65 | if ptype == 'thumb': 66 | p['path'] = photo_file.make_thumbnail(file_id, 300) 67 | return p 68 | 69 | # ファイルの一覧を得る --- (*11) 70 | def get_files(): 71 | a = select('SELECT * FROM files ' + 72 | 'ORDER BY file_id DESC LIMIT 50') 73 | for i in a: 74 | i['name'] = get_album_name(i['album_id']) 75 | return a 76 | 77 | # アルバムに入っているファイルの一覧を得る --- (*12) 78 | def get_album_files(album_id): 79 | return select(''' 80 | SELECT * FROM files WHERE album_id=? 81 | ORDER BY file_id DESC''', album_id) 82 | 83 | # ユーザーのファイルの一覧を得る --- (*13) 84 | def get_user_files(user_id): 85 | a = select(''' 86 | SELECT * FROM files WHERE user_id=? 87 | ORDER BY file_id DESC LIMIT 50''', user_id) 88 | for i in a: 89 | i['name'] = get_album_name(i['album_id']) 90 | return a 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/ch4/photoshare/photo_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PIL import Image 3 | 4 | # パスの指定 --- (*1) 5 | BASE_DIR = os.path.dirname(__file__) 6 | DATA_FILE = BASE_DIR + '/data/photos.sqlite3' 7 | FILES_DIR = BASE_DIR + '/files' 8 | 9 | # 画像ファイルの保存パスを返す --- (*2) 10 | def get_path(file_id, ptype = ''): 11 | return FILES_DIR + '/' + str(file_id) + ptype + '.jpg' 12 | 13 | # サムネイルを作成する --- (*3) 14 | def make_thumbnail(file_id, size): 15 | src = get_path(file_id) 16 | des = get_path(file_id, '-thumb') 17 | # 既にサムネイルが作成されているなら作らない --- (*4) 18 | if os.path.exists(des): return des 19 | # 正方形に切り取る --- (*5) 20 | img = Image.open(src) 21 | msize = img.width if img.width < img.height else img.height 22 | img_crop = image_crop_center(img, msize) 23 | # 指定サイズにリサイズ --- (*6) 24 | img_resize = img_crop.resize((size, size)) 25 | img_resize.save(des, quality=95) 26 | return des 27 | 28 | # 画像の中心を正方形に切り取る --- (*7) 29 | def image_crop_center(img, size): 30 | cx = int(img.width / 2) 31 | cy = int(img.height / 2) 32 | img_crop = img.crop(( 33 | cx - size / 2, cy - size / 2, 34 | cx + size / 2, cy + size / 2)) 35 | return img_crop 36 | 37 | -------------------------------------------------------------------------------- /src/ch4/photoshare/photo_sqlite.py: -------------------------------------------------------------------------------- 1 | import re, sqlite3, photo_file 2 | 3 | # データベースを開く --- (*1) 4 | def open_db(): 5 | conn = sqlite3.connect(photo_file.DATA_FILE) 6 | conn.row_factory = dict_factory 7 | return conn 8 | 9 | # SELECT句の結果を辞書型で得られるようにする --- (*2) 10 | def dict_factory(cursor, row): 11 | d = {} 12 | for idx, col in enumerate(cursor.description): 13 | d[col[0]] = row[idx] 14 | return d 15 | 16 | # SQLを実行する --- (*3) 17 | def exec(sql, *args): 18 | db = open_db() 19 | c = db.cursor() 20 | c.execute(sql, args) 21 | db.commit() 22 | return c.lastrowid 23 | 24 | # SQLを実行して結果を得る --- (*4) 25 | def select(sql, *args): 26 | db = open_db() 27 | c = db.cursor() 28 | c.execute(sql, args) 29 | return c.fetchall() 30 | 31 | -------------------------------------------------------------------------------- /src/ch4/photoshare/setup_database.py: -------------------------------------------------------------------------------- 1 | from photo_sqlite import exec 2 | 3 | exec(''' 4 | /* ファイル情報 */ 5 | CREATE TABLE files ( 6 | file_id INTEGER PRIMARY KEY AUTOINCREMENT, 7 | user_id TEXT, 8 | filename TEXT, 9 | album_id INTEGER DEFAULT 0, /* なし */ 10 | created_at TIMESTAMP DEFAULT (DATETIME('now', 'localtime')) 11 | ) 12 | ''') 13 | 14 | exec(''' 15 | /* アルバム情報 */ 16 | CREATE TABLE albums ( 17 | album_id INTEGER PRIMARY KEY AUTOINCREMENT, 18 | name TEXT, 19 | user_id TEXT, 20 | created_at TIMESTAMP DEFAULT (DATETIME('now', 'localtime')) 21 | ) 22 | ''') 23 | 24 | print('ok') 25 | 26 | -------------------------------------------------------------------------------- /src/ch4/photoshare/sns_user.py: -------------------------------------------------------------------------------- 1 | # ログインなどユーザーに関する処理をまとめた 2 | from flask import Flask, session, redirect 3 | from functools import wraps 4 | 5 | # ユーザー名とパスワードの一覧 --- (*1) 6 | USER_LOGIN_LIST = { 7 | 'taro': 'aaa', 8 | 'jiro': 'bbb', 9 | 'sabu': 'ccc', 10 | 'siro': 'ddd', 11 | 'goro': 'eee' } 12 | 13 | # ログインしているかの確認 --- (*2) 14 | def is_login(): 15 | return 'login' in session 16 | 17 | # ログインを試行する --- (*3) 18 | def try_login(form): 19 | user = form.get('user', '') 20 | password = form.get('pw', '') 21 | # パスワードチェック 22 | if user not in USER_LOGIN_LIST: return False 23 | if USER_LOGIN_LIST[user] != password: 24 | return False 25 | session['login'] = user 26 | return True 27 | 28 | # ユーザー名を得る --- (*4) 29 | def get_id(): 30 | return session['login'] if is_login() else '未ログイン' 31 | 32 | # 全ユーザーの情報を得る --- (*5) 33 | def get_allusers(): 34 | return [ u for u in USER_LOGIN_LIST ] 35 | 36 | # ログアウトする --- (*6) 37 | def try_logout(): 38 | session.pop('login', None) 39 | 40 | # ログイン必須を処理するデコレーターを定義 --- (*7) 41 | def login_required(func): 42 | @wraps(func) 43 | def wrapper(*args, **kwargs): 44 | if not is_login(): 45 | return redirect('/login') 46 | return func(*args, **kwargs) 47 | return wrapper 48 | -------------------------------------------------------------------------------- /src/ch4/photoshare/static/side-menu.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #777; 3 | } 4 | 5 | .pure-img-responsive { 6 | max-width: 100%; 7 | height: auto; 8 | } 9 | 10 | /* 11 | Add transition to containers so they can push in and out. 12 | */ 13 | #layout, 14 | #menu, 15 | .menu-link { 16 | -webkit-transition: all 0.2s ease-out; 17 | -moz-transition: all 0.2s ease-out; 18 | -ms-transition: all 0.2s ease-out; 19 | -o-transition: all 0.2s ease-out; 20 | transition: all 0.2s ease-out; 21 | } 22 | 23 | /* 24 | This is the parent `
    ` that contains the menu and the content area. 25 | */ 26 | #layout { 27 | position: relative; 28 | left: 0; 29 | padding-left: 0; 30 | } 31 | #layout.active #menu { 32 | left: 150px; 33 | width: 150px; 34 | } 35 | 36 | #layout.active .menu-link { 37 | left: 150px; 38 | } 39 | /* 40 | The content `
    ` is where all your content goes. 41 | */ 42 | .content { 43 | margin: 0 auto; 44 | padding: 0 2em; 45 | max-width: 800px; 46 | margin-bottom: 50px; 47 | line-height: 1.6em; 48 | } 49 | 50 | .header { 51 | margin: 0; 52 | color: #333; 53 | text-align: center; 54 | padding: 2.5em 2em 0; 55 | border-bottom: 1px solid #eee; 56 | } 57 | .header h1 { 58 | margin: 0.2em 0; 59 | font-size: 3em; 60 | font-weight: 300; 61 | } 62 | .header h2 { 63 | font-weight: 300; 64 | color: #ccc; 65 | padding: 0; 66 | margin-top: 0; 67 | } 68 | 69 | .content-subhead { 70 | margin: 50px 0 20px 0; 71 | font-weight: 300; 72 | color: #888; 73 | } 74 | 75 | 76 | 77 | /* 78 | The `#menu` `
    ` is the parent `
    ` that contains the `.pure-menu` that 79 | appears on the left side of the page. 80 | */ 81 | 82 | #menu { 83 | margin-left: -150px; /* "#menu" width */ 84 | width: 150px; 85 | position: fixed; 86 | top: 0; 87 | left: 0; 88 | bottom: 0; 89 | z-index: 1000; /* so the menu or its navicon stays above all content */ 90 | background: #191818; 91 | overflow-y: auto; 92 | -webkit-overflow-scrolling: touch; 93 | } 94 | /* 95 | All anchors inside the menu should be styled like this. 96 | */ 97 | #menu a { 98 | color: #999; 99 | border: none; 100 | padding: 0.6em 0 0.6em 0.6em; 101 | } 102 | 103 | /* 104 | Remove all background/borders, since we are applying them to #menu. 105 | */ 106 | #menu .pure-menu, 107 | #menu .pure-menu ul { 108 | border: none; 109 | background: transparent; 110 | } 111 | 112 | /* 113 | Add that light border to separate items into groups. 114 | */ 115 | #menu .pure-menu ul, 116 | #menu .pure-menu .menu-item-divided { 117 | border-top: 1px solid #333; 118 | } 119 | /* 120 | Change color of the anchor links on hover/focus. 121 | */ 122 | #menu .pure-menu li a:hover, 123 | #menu .pure-menu li a:focus { 124 | background: #333; 125 | } 126 | 127 | /* 128 | This styles the selected menu item `
  • `. 129 | */ 130 | #menu .pure-menu-selected, 131 | #menu .pure-menu-heading { 132 | background: #1f8dd6; 133 | } 134 | /* 135 | This styles a link within a selected menu item `
  • `. 136 | */ 137 | #menu .pure-menu-selected a { 138 | color: #fff; 139 | } 140 | 141 | /* 142 | This styles the menu heading. 143 | */ 144 | #menu .pure-menu-heading { 145 | font-size: 110%; 146 | color: #fff; 147 | margin: 0; 148 | } 149 | 150 | /* -- Dynamic Button For Responsive Menu -------------------------------------*/ 151 | 152 | /* 153 | The button to open/close the Menu is custom-made and not part of Pure. Here's 154 | how it works: 155 | */ 156 | 157 | /* 158 | `.menu-link` represents the responsive menu toggle that shows/hides on 159 | small screens. 160 | */ 161 | .menu-link { 162 | position: fixed; 163 | display: block; /* show this only on small screens */ 164 | top: 0; 165 | left: 0; /* "#menu width" */ 166 | background: #000; 167 | background: rgba(0,0,0,0.7); 168 | font-size: 10px; /* change this value to increase/decrease button size */ 169 | z-index: 10; 170 | width: 2em; 171 | height: auto; 172 | padding: 2.1em 1.6em; 173 | } 174 | 175 | .menu-link:hover, 176 | .menu-link:focus { 177 | background: #000; 178 | } 179 | 180 | .menu-link span { 181 | position: relative; 182 | display: block; 183 | } 184 | 185 | .menu-link span, 186 | .menu-link span:before, 187 | .menu-link span:after { 188 | background-color: #fff; 189 | width: 100%; 190 | height: 0.2em; 191 | } 192 | 193 | .menu-link span:before, 194 | .menu-link span:after { 195 | position: absolute; 196 | margin-top: -0.6em; 197 | content: " "; 198 | } 199 | 200 | .menu-link span:after { 201 | margin-top: 0.6em; 202 | } 203 | 204 | 205 | /* -- Responsive Styles (Media Queries) ------------------------------------- */ 206 | 207 | /* 208 | Hides the menu at `48em`, but modify this based on your app's needs. 209 | */ 210 | @media (min-width: 48em) { 211 | 212 | .header, 213 | .content { 214 | padding-left: 2em; 215 | padding-right: 2em; 216 | } 217 | 218 | #layout { 219 | padding-left: 150px; /* left col width "#menu" */ 220 | left: 0; 221 | } 222 | #menu { 223 | left: 150px; 224 | } 225 | 226 | .menu-link { 227 | position: fixed; 228 | left: 150px; 229 | display: none; 230 | } 231 | 232 | #layout.active .menu-link { 233 | left: 150px; 234 | } 235 | } 236 | 237 | @media (max-width: 48em) { 238 | /* Only apply this when the window is small. Otherwise, the following 239 | case results in extra padding on the left: 240 | * Make the window small. 241 | * Tap the menu to trigger the active state. 242 | * Make the window large again. 243 | */ 244 | #layout.active { 245 | position: relative; 246 | left: 150px; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/ch4/photoshare/static/style.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color:black; 3 | padding: 8px; margin: 4px; 4 | border-top: 2px solid #909090; 5 | border-bottom: 2px solid #909090; 6 | } 7 | h2 { 8 | padding: 8px; margin: 4px; 9 | } 10 | h3 { 11 | text-align: center; 12 | } 13 | .menu { 14 | font-size: 0.8em; 15 | background-color: #f0f0ff; 16 | padding: 8px; margin: 4px; 17 | border: 1px solid silver; 18 | } 19 | .box { 20 | padding: 8px; 21 | margin: 4px; 22 | border-top: 1px solid #909090; 23 | } 24 | #msg { 25 | padding: 24px; margin: 32px; 26 | background-color: #f0f0f0; 27 | font-size: 1.4em; 28 | color: black; 29 | } 30 | .datetime { 31 | font-size: 0.7em; 32 | color: gray; 33 | } 34 | .photo_list { 35 | text-align: center; 36 | margin-left: auto; 37 | margin-right: auto; 38 | max-width: 302px; 39 | } 40 | .photo_border { 41 | border: 1px solid silver; 42 | padding: 2px; 43 | margin:0; 44 | width: 302px; 45 | background-color: black; 46 | } 47 | .photo_info { 48 | padding: 8px; 49 | text-align: center; 50 | } 51 | .photo { 52 | border-top: 1px dotted silver; 53 | padding: 8px; margin: 4px; 54 | } 55 | .info_box { 56 | text-align: center; 57 | } 58 | .form_block { 59 | max-width: 300px; 60 | margin-left: auto; 61 | margin-right: auto; 62 | margin-bottom: 100px; 63 | } 64 | .footer { 65 | border-top: 1px dotted silver; 66 | margin-top: 12px; 67 | padding: 12px; 68 | text-align: center; 69 | } 70 | -------------------------------------------------------------------------------- /src/ch4/photoshare/static/ui.js: -------------------------------------------------------------------------------- 1 | (function (window, document) { 2 | 3 | var layout = document.getElementById('layout'), 4 | menu = document.getElementById('menu'), 5 | menuLink = document.getElementById('menuLink'), 6 | content = document.getElementById('main'); 7 | 8 | function toggleClass(element, className) { 9 | var classes = element.className.split(/\s+/), 10 | length = classes.length, 11 | i = 0; 12 | 13 | for(; i < length; i++) { 14 | if (classes[i] === className) { 15 | classes.splice(i, 1); 16 | break; 17 | } 18 | } 19 | // The className is not found 20 | if (length === classes.length) { 21 | classes.push(className); 22 | } 23 | 24 | element.className = classes.join(' '); 25 | } 26 | 27 | function toggleAll(e) { 28 | var active = 'active'; 29 | 30 | e.preventDefault(); 31 | toggleClass(layout, active); 32 | toggleClass(menu, active); 33 | toggleClass(menuLink, active); 34 | } 35 | 36 | menuLink.onclick = function (e) { 37 | toggleAll(e); 38 | }; 39 | 40 | content.onclick = function(e) { 41 | if (menu.className.indexOf('active') !== -1) { 42 | toggleAll(e); 43 | } 44 | }; 45 | 46 | }(this, this.document)); 47 | -------------------------------------------------------------------------------- /src/ch4/photoshare/templates/album.html: -------------------------------------------------------------------------------- 1 | {% extends "layout_login.html" %} 2 | 3 | {% block title %}{{ album.name }}{% endblock %} 4 | 5 | {% block contents %} 6 |

    {{ album.user_id }}さんのアルバム

    7 |
    8 | {% for i in photos %} 9 |
    10 |
    11 | 12 | 14 |
    15 | {{ i.created_at }} 16 |

    17 |
    18 | {% endfor %} 19 |
    20 | 21 | 26 | {% endblock %} 27 | 28 | -------------------------------------------------------------------------------- /src/ch4/photoshare/templates/album_new_form.html: -------------------------------------------------------------------------------- 1 | {% extends "layout_login.html" %} 2 | 3 | {% block title %} 4 | 写真共有 >
    新規アルバムの作成 5 | {% endblock %} 6 | 7 | {% block contents %} 8 |
    9 |
    11 | 12 | 14 | 16 |
    17 |
    18 | {% endblock %} 19 | 20 | -------------------------------------------------------------------------------- /src/ch4/photoshare/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout_login.html" %} 2 | 3 | {% block contents %} 4 |

    写真一覧

    5 | {% if photos | length == 0 %} 6 |
    まだ写真はありません。
    7 | {% endif %} 8 |
    9 | {% for i in photos %} 10 |
    11 |
    12 | 14 | {{ i.created_at }} 15 |
    16 | 22 |
    23 | {% endfor %} 24 |
    25 | {% endblock %} 26 | 27 | -------------------------------------------------------------------------------- /src/ch4/photoshare/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 写真共有 10 | 11 |
    12 | {% block header %} 13 | 14 | {% endblock %} 15 | 16 |
    17 |
    18 |

    {% block title %}写真共有{% endblock %}

    19 |
    20 | 21 | {% block contents %} 22 | 23 | {% endblock %} 24 | 25 | {% block footer %} 26 | 29 | {% endblock %} 30 |
    31 |
    32 | 33 | {% block footer_script %}{% endblock %} 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/ch4/photoshare/templates/layout_login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block header %} 3 | 4 | 5 | 6 | 7 | 8 | 24 | {% endblock %} 25 | 26 | {% block footer_script %} 27 | 28 | {% endblock %} 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/ch4/photoshare/templates/login_form.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block contents %} 4 |

    ログインしてください

    5 |
    6 |
    8 | 9 | 11 | 12 | 14 | 16 |
    17 |
    18 | {% endblock %} 19 | 20 | -------------------------------------------------------------------------------- /src/ch4/photoshare/templates/msg.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block contents %} 4 |
    {{ msg }}
    5 | {% endblock %} 6 | 7 | -------------------------------------------------------------------------------- /src/ch4/photoshare/templates/upload_form.html: -------------------------------------------------------------------------------- 1 | {% extends "layout_login.html" %} 2 | 3 | {% block title %} 4 | 写真共有 >
    アップロード 5 | {% endblock %} 6 | 7 | {% block contents %} 8 |
    9 |
    12 | 写真ファイルを選んでください。 13 |
    14 | 15 | 16 |
    17 |
    18 | 19 | 26 |
    27 |
    28 | 30 |
    31 |
    32 |
    33 | 34 |
    35 |

    以下よりアルバムを作成できます。

    36 | →新規アルバムを作成 38 |
    39 |
    40 | {% endblock %} 41 | 42 | -------------------------------------------------------------------------------- /src/ch4/photoshare/templates/user.html: -------------------------------------------------------------------------------- 1 | {% extends "layout_login.html" %} 2 | 3 | {% block title %} 4 | {{ id }}さん
    の作品集 5 | {% endblock %} 6 | 7 | {% block contents %} 8 |
    9 | {% for i in photos %} 10 |
    11 |
    12 | {{ i.name }}より - 13 | {{ i.created_at }} 14 |
    15 | {% endfor %} 16 |
    17 |
    18 | {% endblock %} 19 | 20 | -------------------------------------------------------------------------------- /src/ch5/JIGYOSYO.CSV: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kujirahand/book-webservice-python/9cc0616bcb987cd49ca78f0590152323c4416588/src/ch5/JIGYOSYO.CSV -------------------------------------------------------------------------------- /src/ch5/KEN_ALL.CSV: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kujirahand/book-webservice-python/9cc0616bcb987cd49ca78f0590152323c4416588/src/ch5/KEN_ALL.CSV -------------------------------------------------------------------------------- /src/ch5/auth.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | import os, json, hashlib, base64 3 | 4 | app = Flask(__name__) 5 | APP_DIR = os.path.dirname(__file__) 6 | DATA_FILE = APP_DIR + '/users.json' 7 | 8 | # ハッシュ化のためのSALT --- (*1) 9 | HASH_SALT = 'uN6yjW:qqU#6X_dGaqK!LG0Fi_eK_OA3' 10 | 11 | # パスワードをハッシュ化する --- (*2) 12 | def password_hash(password): 13 | key = password + HASH_SALT 14 | key_b = key.encode('utf-8') # バイナリ化 15 | return hashlib.sha256(key_b).hexdigest() 16 | 17 | # パスワードが正しいかを検証する --- (*2a) 18 | def password_verify(password, hash): 19 | hash_v = password_hash(password) 20 | return (hash_v == hash) 21 | 22 | # ファイルからログイン情報を読む --- (*3) 23 | def load_users(): 24 | if os.path.exists(DATA_FILE): 25 | with open(DATA_FILE, 'rt') as fp: 26 | return json.load(fp) 27 | return {} 28 | 29 | # ファイルへログイン情報を保存 --- (*4) 30 | def save_users(users): 31 | with open(DATA_FILE, 'wt', encoding='utf-8') as fp: 32 | json.dump(users, fp) 33 | 34 | # 新規ユーザーを追加 --- (*5) 35 | def add_user(id, password): 36 | users = load_users() 37 | if id in users: 38 | return False 39 | users[id] = password_hash(password) 40 | save_users(users) 41 | return True 42 | 43 | # ログインできるか確認 --- (*6) 44 | def check_login(id, password): 45 | users = load_users() 46 | if id not in users: 47 | return False 48 | return password_verify(password, users[id]) 49 | 50 | # ブラウザのメイン画面 --- (*7) 51 | @app.route('/') 52 | def index(): 53 | return ''' 54 | 55 |

    ユーザー登録

    {0}
    56 |

    ユーザーログイン

    {1} 57 | 58 | '''.format( 59 | get_form('/register', '登録'), 60 | get_form('/login', 'ログイン')) 61 | 62 | def get_form(action, caption): 63 | return ''' 64 |
    65 | ID:
    66 |
    67 | パスワード:
    68 |
    69 | 70 |
    71 | '''.format(action, caption) 72 | 73 | # ユーザー登録 --- (*8) 74 | @app.route('/register', methods=['POST']) 75 | def register(): 76 | id = request.form.get('id') 77 | pw = request.form.get('pw') 78 | if id == '': 79 | return '

    失敗:IDが空です。

    ' 80 | # ユーザーを追加 81 | if add_user(id, pw): 82 | return '

    登録に成功

    戻る' 83 | else: 84 | return '

    登録に失敗

    ' 85 | 86 | # ユーザー認証 --- (*9) 87 | @app.route('/login', methods=['POST']) 88 | def login(): 89 | id = request.form.get('id') 90 | pw = request.form.get('pw') 91 | if id == '': 92 | return '

    失敗:IDが空です。

    ' 93 | # パスワードを照合 94 | if check_login(id, pw): 95 | return '

    ログインに成功

    ' 96 | else: 97 | return '

    失敗

    ' 98 | 99 | if __name__ == '__main__': 100 | app.run(debug=True, host='0.0.0.0') 101 | -------------------------------------------------------------------------------- /src/ch5/auth2.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | import os, json, hashlib, base64 3 | 4 | app = Flask(__name__) 5 | APP_DIR = os.path.dirname(__file__) 6 | DATA_FILE = APP_DIR + '/users.json' 7 | 8 | # パスワードからハッシュを生成する --- (*1) 9 | def password_hash(password): 10 | salt = os.urandom(16) 11 | digest = hashlib.pbkdf2_hmac('sha256', 12 | password.encode('utf-8'), salt, 10000) 13 | return base64.b64encode(salt + digest).decode('ascii') 14 | 15 | # パスワードが正しいかを検証する --- (*2) 16 | def password_verify(password, hash): 17 | b = base64.b64decode(hash) 18 | salt, digest_v = b[:16], b[16:] 19 | digest_n = hashlib.pbkdf2_hmac('sha256', 20 | password.encode('utf-8'), salt, 10000) 21 | return digest_n == digest_v 22 | 23 | # ファイルからログイン情報を読む 24 | def load_users(): 25 | if os.path.exists(DATA_FILE): 26 | with open(DATA_FILE, 'rt') as fp: 27 | return json.load(fp) 28 | return {} 29 | 30 | # ファイルへログイン情報を保存 31 | def save_users(users): 32 | with open(DATA_FILE, 'wt', encoding='utf-8') as fp: 33 | json.dump(users, fp) 34 | 35 | # 新規ユーザーを追加 36 | def add_user(id, password): 37 | users = load_users() 38 | if id in users: 39 | return False 40 | users[id] = password_hash(password) 41 | save_users(users) 42 | return True 43 | 44 | # ログインできるか確認 45 | def check_login(id, password): 46 | users = load_users() 47 | if id not in users: 48 | return False 49 | return password_verify(password, users[id]) 50 | 51 | # ブラウザのメイン画面 52 | @app.route('/') 53 | def index(): 54 | return ''' 55 | 56 |

    ユーザー登録

    {0}
    57 |

    ユーザーログイン

    {1} 58 | 59 | '''.format( 60 | get_form('/register', '登録'), 61 | get_form('/login', 'ログイン')) 62 | 63 | def get_form(action, caption): 64 | return ''' 65 |
    66 | ID:
    67 |
    68 | パスワード:
    69 |
    70 | 71 |
    72 | '''.format(action, caption) 73 | 74 | # ユーザー登録 75 | @app.route('/register', methods=['POST']) 76 | def register(): 77 | id = request.form.get('id') 78 | pw = request.form.get('pw') 79 | if id == '': 80 | return '

    失敗:IDが空です。

    ' 81 | # ユーザーを追加 82 | if add_user(id, pw): 83 | return '

    登録に成功

    戻る' 84 | else: 85 | return '

    登録に失敗

    ' 86 | 87 | # ユーザー認証 88 | @app.route('/login', methods=['POST']) 89 | def login(): 90 | id = request.form.get('id') 91 | pw = request.form.get('pw') 92 | if id == '': 93 | return '

    失敗:IDが空です。

    ' 94 | # パスワードを照合 95 | if check_login(id, pw): 96 | return '

    ログインに成功

    ' 97 | else: 98 | return '

    失敗

    ' 99 | 100 | if __name__ == '__main__': 101 | app.run(debug=True, host='0.0.0.0') 102 | -------------------------------------------------------------------------------- /src/ch5/counter.json: -------------------------------------------------------------------------------- 1 | {"_default": {}, "count_visitor": {"1": {"v": 5}}} -------------------------------------------------------------------------------- /src/ch5/exif_gps.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import PIL.ExifTags as ExifTags 3 | 4 | # Exif一覧を取得する関数 --- (*1) 5 | def get_exif(fname): 6 | # 画像を読み込む 7 | img = Image.open(fname) 8 | # Exif情報を扱いやすく辞書型に変換 9 | exif = {} 10 | for id, value in img._getexif().items(): 11 | if id in ExifTags.TAGS: 12 | tag = ExifTags.TAGS[id] 13 | exif[tag] = value 14 | return exif 15 | 16 | # GPS情報を取り出す関数 --- (*2) 17 | def get_gps(fname): 18 | exif = get_exif(fname) 19 | if not ('GPSInfo' in exif): 20 | return None, None 21 | # GPSタグを取り出す 22 | gps_tags = exif['GPSInfo'] 23 | gps = {} 24 | for t in gps_tags: 25 | tag = ExifTags.GPSTAGS.get(t, t) 26 | if tag: 27 | gps[tag] = gps_tags[t] 28 | lat = conv_deg(gps['GPSLatitude']) 29 | lat_ref = gps["GPSLatitudeRef"] 30 | if lat_ref != 'N': lat = 0 - lat 31 | lng = conv_deg(gps['GPSLongitude']) 32 | lng_ref = gps['GPSLongitudeRef'] 33 | if lng_ref != 'E': lng = 0 - lng 34 | return lat, lng 35 | 36 | # 緯度経度を計算する関数 --- (*3) 37 | def conv_deg(v): 38 | # 分数を度に変換 39 | d = float(v[0][0]) / float(v[0][1]) 40 | m = float(v[1][0]) / float(v[1][1]) 41 | s = float(v[2][0]) / float(v[2][1]) 42 | return d + (m / 60.0) + (s / 3600.0) 43 | 44 | # 画像から位置情報を取り出す --- (*4) 45 | if __name__ == '__main__': 46 | lat, lng = get_gps('test.jpg') 47 | print(lat, lng) 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/ch5/exif_list.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import PIL.ExifTags as ExifTags 3 | 4 | # 画像ファイルを読み込む --- (*1) 5 | img = Image.open('test.jpg') 6 | # Exif情報を得る --- (*2) 7 | exif = img._getexif() 8 | # Exif情報を列挙する --- (*3) 9 | for id, value in exif.items(): 10 | tag = ExifTags.TAGS[id] 11 | print(tag + ":", value) 12 | 13 | -------------------------------------------------------------------------------- /src/ch5/exif_revgeo.py: -------------------------------------------------------------------------------- 1 | import exif_gps 2 | import geocoder 3 | 4 | # 写真から位置情報を取得 5 | lat, lng = exif_gps.get_gps('test.jpg') 6 | if lat is None: 7 | print('位置情報はありません。') 8 | quit() 9 | 10 | # 逆ジオコーディング(OpenStreetMap APIを利用) 11 | g = geocoder.osm((lat, lng), method='reverse') 12 | print('写真の住所:') 13 | print(g.address) 14 | 15 | -------------------------------------------------------------------------------- /src/ch5/geoip-test.py: -------------------------------------------------------------------------------- 1 | # ライブラリの取り込み 2 | import geoip2.database 3 | 4 | # 確認したいIPアドレスの指定 5 | check_ip = '157.7.44.174' 6 | 7 | # データベースを読み込む 8 | reader = geoip2.database.Reader('GeoLite2-City.mmdb') 9 | # DBを検索 10 | rec = reader.city(check_ip) 11 | # 検索結果を表示 12 | print('IP:', check_ip) 13 | print('Country:', rec.country.name) 14 | print('City:', rec.city.name) 15 | print('Latitude:', rec.location.latitude) 16 | print('Longitude:', rec.location.longitude) 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/ch5/geolocation-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 | 5 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/ch5/geolocation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/ch5/gmap.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 |
    9 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/ch5/osm-current.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | 11 | 12 |
    13 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/ch5/osm-map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | 11 | 12 |
    13 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/ch5/osm-revgeo.py: -------------------------------------------------------------------------------- 1 | import geocoder 2 | 3 | # 緯度経度を指定 4 | pos = (35.659025, 139.745025) 5 | # OpenStreetMapを使って逆ジオコーディング 6 | g = geocoder.osm(pos, method='reverse') 7 | 8 | print('Country:', g.country) 9 | print('State:', g.state) 10 | print('City:', g.city) 11 | print('Street:', g.street) 12 | 13 | -------------------------------------------------------------------------------- /src/ch5/osm-revgeo2.py: -------------------------------------------------------------------------------- 1 | import geocoder 2 | import pprint 3 | 4 | pos = (35.659025, 139.745025) 5 | g = geocoder.osm(pos, method='reverse') 6 | pprint.pprint(g.json) 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/ch5/page.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | import math 3 | # 1ページに表示するデータ数 4 | limit = 3 5 | # サンプルデータ --- (*1) 6 | data = [ 7 | {"name": "リンゴ", "price": 370}, 8 | {"name": "イチゴ", "price": 660}, 9 | {"name": "バナナ", "price": 180}, 10 | {"name": "マンゴ", "price": 450}, 11 | {"name": "トマト", "price": 250}, 12 | {"name": "セロリ", "price": 180}, 13 | {"name": "パセリ", "price": 220}, 14 | {"name": "ミカン", "price": 530}, 15 | {"name": "エノキ", "price": 340}] 16 | 17 | app = Flask(__name__) 18 | 19 | # ブラウザのメイン画面 --- (*2) 20 | @app.route('/') 21 | def index(): 22 | # ページ番号を得る --- (*3) 23 | page_s = request.args.get('page', '0') 24 | page = int(page_s) 25 | # 表示データの先頭を計算 --- (*4) 26 | index = page * limit 27 | # 表示データを取り出す --- (*5) 28 | s = '
    ' 29 | for i in data[index : index+limit]: 30 | s += '
    ' 31 | s += '品名: ' + i['name'] + '
    ' 32 | s += '値段: ' + str(i['price']) + '円' 33 | s += '
    ' 34 | s += '
    ' 35 | # ページャーを作る --- (*6) 36 | s += make_pager(page, len(data), limit) 37 | return ''' 38 | 39 | 41 | 42 | 45 |

    商品

    46 | ''' + s + '' 47 | 48 | def make_button(href, label): 49 | klass = 'pure-button' 50 | if href == '#': klass += ' pure-button-disabled' 51 | return ''' 52 | {2} 53 | '''.format(href, klass, label) 54 | 55 | def make_pager(page, total, per_page): 56 | # ページ数を計算 --- (*7) 57 | page_count = math.ceil(total / per_page) 58 | s = '
    ' 59 | # 前へボタン --- (*8) 60 | prev_link = '?page=' + str(page - 1) 61 | if page <= 0: prev_link = '#' 62 | s += make_button(prev_link, '←前へ') 63 | # ページ番号 --- (*9) 64 | s += '{0}/{1}'.format(page+1, page_count) 65 | # 次へボタン --- (*10) 66 | next_link = '?page=' + str(page + 1) 67 | if page >= page_count - 1: next_link = '#' 68 | s += make_button(next_link, '次へ→') 69 | s += '
    ' 70 | return s 71 | 72 | if __name__ == '__main__': 73 | app.run(debug=True, host='0.0.0.0') 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/ch5/qrcode_hello.py: -------------------------------------------------------------------------------- 1 | import qrcode 2 | 3 | img = qrcode.make('https://kujirahand.com') 4 | img.save('qrcode.png') 5 | print('ok') 6 | 7 | -------------------------------------------------------------------------------- /src/ch5/qrcode_more.py: -------------------------------------------------------------------------------- 1 | import qrcode 2 | 3 | # QRコードの生成で細かい設定を行う場合 --- (*1) 4 | qr = qrcode.QRCode( 5 | box_size=4, 6 | border=8, 7 | version=12, 8 | error_correction=qrcode.constants.ERROR_CORRECT_Q) 9 | # 描画するデータを指定する --- (*2) 10 | qr.add_data('https://kujirahand.com/') 11 | # QRコードの元データを作る --- (*3) 12 | qr.make() 13 | # データをImageオブジェクトとして取得 --- (*4) 14 | img = qr.make_image() 15 | # Imageをファイルに保存 --- (*5) 16 | img.save('qrcode2.png') 17 | print('ok') 18 | 19 | -------------------------------------------------------------------------------- /src/ch5/revgeo.py: -------------------------------------------------------------------------------- 1 | import reverse_geocode 2 | 3 | # 調べたい緯度経度を配列で指定 4 | coords = [(35.659025, 139.74505)] 5 | # 逆ジオコーディング 6 | areas = reverse_geocode.search(coords) 7 | # 結果を表示 8 | print('Coord:', coords[0]) 9 | print('Country:', areas[0]['country']) 10 | print('City:', areas[0]['city']) 11 | 12 | -------------------------------------------------------------------------------- /src/ch5/static/README.txt: -------------------------------------------------------------------------------- 1 | staticディレクトリはFlaskの静的ファイルの配置ディレクトリです。 2 | Flaskを利用したWebアプリで利用されます。 3 | -------------------------------------------------------------------------------- /src/ch5/static/pure-min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | Pure v1.0.1 3 | Copyright 2013 Yahoo! 4 | Licensed under the BSD License. 5 | https://github.com/pure-css/pure/blob/master/LICENSE.md 6 | */ 7 | /*! 8 | normalize.css v^3.0 | MIT License | git.io/normalize 9 | Copyright (c) Nicolas Gallagher and Jonathan Neal 10 | */ 11 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{letter-spacing:-.31em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-align-content:flex-start;-ms-flex-line-pack:start;align-content:flex-start}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){table .pure-g{display:block}}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u{display:inline-block;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-g [class*=pure-u]{font-family:sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-12,.pure-u-1-2,.pure-u-1-24,.pure-u-1-3,.pure-u-1-4,.pure-u-1-5,.pure-u-1-6,.pure-u-1-8,.pure-u-10-24,.pure-u-11-12,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-2-24,.pure-u-2-3,.pure-u-2-5,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24,.pure-u-3-24,.pure-u-3-4,.pure-u-3-5,.pure-u-3-8,.pure-u-4-24,.pure-u-4-5,.pure-u-5-12,.pure-u-5-24,.pure-u-5-5,.pure-u-5-6,.pure-u-5-8,.pure-u-6-24,.pure-u-7-12,.pure-u-7-24,.pure-u-7-8,.pure-u-8-24,.pure-u-9-24{display:inline-block;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-10-24,.pure-u-5-12{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-14-24,.pure-u-7-12{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-15-24,.pure-u-5-8{width:62.5%}.pure-u-16-24,.pure-u-2-3{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-18-24,.pure-u-3-4{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-20-24,.pure-u-5-6{width:83.3333%}.pure-u-21-24,.pure-u-7-8{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-24-24,.pure-u-5-5{width:100%}.pure-button{display:inline-block;zoom:1;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-group{letter-spacing:-.31em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.pure-button-group{word-spacing:-.43em}.pure-button-group .pure-button{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:#444;color:rgba(0,0,0,.8);border:1px solid #999;border:none transparent;background-color:#e6e6e6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:focus,.pure-button:hover{background-image:-webkit-gradient(linear,left top,left bottom,from(transparent),color-stop(40%,rgba(0,0,0,.05)),to(rgba(0,0,0,.1)));background-image:-webkit-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000}.pure-button-disabled,.pure-button-disabled:active,.pure-button-disabled:focus,.pure-button-disabled:hover,.pure-button[disabled]{border:none;background-image:none;opacity:.4;cursor:not-allowed;-webkit-box-shadow:none;box-shadow:none;pointer-events:none}.pure-button-hidden{display:none}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-button-group .pure-button{margin:0;border-radius:0;border-right:1px solid #111;border-right:1px solid rgba(0,0,0,.2)}.pure-button-group .pure-button:first-child{border-top-left-radius:2px;border-bottom-left-radius:2px}.pure-button-group .pure-button:last-child{border-top-right-radius:2px;border-bottom-right-radius:2px;border-right:none}.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 3px #ddd;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 3px #ddd;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=color]:focus,.pure-form input[type=date]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=email]:focus,.pure-form input[type=month]:focus,.pure-form input[type=number]:focus,.pure-form input[type=password]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=text]:focus,.pure-form input[type=time]:focus,.pure-form input[type=url]:focus,.pure-form input[type=week]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129fea}.pure-form input:not([type]):focus{outline:0;border-color:#129fea}.pure-form input[type=checkbox]:focus,.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus{outline:thin solid #129fea;outline:1px auto #129fea}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=color][disabled],.pure-form input[type=date][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=email][disabled],.pure-form input[type=month][disabled],.pure-form input[type=number][disabled],.pure-form input[type=password][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=text][disabled],.pure-form input[type=time][disabled],.pure-form input[type=url][disabled],.pure-form input[type=week][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form select:focus:invalid,.pure-form textarea:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=checkbox]:focus:invalid:focus,.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=color],.pure-form-stacked input[type=date],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=email],.pure-form-stacked input[type=file],.pure-form-stacked input[type=month],.pure-form-stacked input[type=number],.pure-form-stacked input[type=password],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=text],.pure-form-stacked input[type=time],.pure-form-stacked input[type=url],.pure-form-stacked input[type=week],.pure-form-stacked label,.pure-form-stacked select,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned .pure-help-inline,.pure-form-aligned input,.pure-form-aligned select,.pure-form-aligned textarea,.pure-form-message-inline{display:inline-block;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form .pure-input-rounded,.pure-form input.pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-3-4{width:75%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form .pure-help-inline,.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=color],.pure-group input[type=date],.pure-group input[type=datetime-local],.pure-group input[type=datetime],.pure-group input[type=email],.pure-group input[type=month],.pure-group input[type=number],.pure-group input[type=password],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=text],.pure-group input[type=time],.pure-group input[type=url],.pure-group input[type=week]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0 0}.pure-form .pure-help-inline,.pure-form-message,.pure-form-message-inline{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu{-webkit-box-sizing:border-box;box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-item,.pure-menu-list{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-heading,.pure-menu-link{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-separator{display:inline-block;zoom:1;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-active>.pure-menu-children,.pure-menu-allow-hover:hover>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;-webkit-overflow-scrolling:touch;padding:.5em 0}.pure-menu-horizontal .pure-menu-children .pure-menu-separator,.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-horizontal .pure-menu-children .pure-menu-separator{display:block;width:auto}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-disabled,.pure-menu-heading,.pure-menu-link{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent}.pure-menu-active>.pure-menu-link,.pure-menu-link:focus,.pure-menu-link:hover{background-color:#eee}.pure-menu-selected>.pure-menu-link,.pure-menu-selected>.pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0} -------------------------------------------------------------------------------- /src/ch5/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kujirahand/book-webservice-python/9cc0616bcb987cd49ca78f0590152323c4416588/src/ch5/test.jpg -------------------------------------------------------------------------------- /src/ch5/users.json: -------------------------------------------------------------------------------- 1 | {"taro": "14a4dc90b4940e3bedb9f05f9aa6cff1669e3d3e97c755ad4ba126d548facf6d", "jiro": "90a928c472f1ec2a9b2a007372af5301ca760431789173e735fda29c4287d554", "sabu": "f3e92e23e05829091173848ecf1475dc32146033d1b8debfd6949afe090d5eca"} -------------------------------------------------------------------------------- /src/ch5/webapp_qrcode.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, redirect 2 | from tinydb import TinyDB 3 | import qrcode 4 | 5 | # アクセスをカウントした後にジャンプするURL --- (*1) 6 | JUMP_URL = 'https://kujirahand.com/' 7 | FILE_COUNTER = './counter.json' 8 | 9 | # Flaskを生成 --- (*2) 10 | app = Flask(__name__) 11 | # TinyDBを開く --- (*3) 12 | db = TinyDB(FILE_COUNTER) 13 | 14 | @app.route('/') 15 | def index(): 16 | # 訪問用QRコードを生成 --- (*4) 17 | url = request.host_url + 'jump' 18 | img = qrcode.make(url) 19 | img.save('./static/qrcode_jump.png') 20 | # 画面にQRコードを表示 --- (*5) 21 | counter = get_counter() 22 | return ''' 23 |

    以下のQRコードを名刺に印刷

    24 |
    25 | {0}
    26 | 現在の訪問者は、{1}人です。 27 | '''.format(url, counter) 28 | 29 | @app.route('/jump') 30 | def jump(): 31 | # アクセスをカウントアップ --- (*6) 32 | v = get_counter() 33 | table = db.table('count_visitor') 34 | table.update({'v': v + 1}) 35 | # 任意のURLにリダイレクト --- (*7) 36 | return redirect(JUMP_URL) 37 | 38 | def get_counter(): 39 | # アクセスを数える --- (*8) 40 | table = db.table('count_visitor') 41 | a = table.all() 42 | if len(a) == 0: 43 | # もし最初なら値0を挿入する --- (*9) 44 | table.insert({'v': 0}) 45 | return 0 46 | return a[0]['v'] 47 | 48 | if __name__ == '__main__': 49 | app.run(host='0.0.0.0', port=5001) 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/ch5/wiki/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect 2 | from flask import render_template, request 3 | import wikifunc 4 | 5 | # Flaskインスタンス生成 6 | app = Flask(__name__) 7 | 8 | # Wikiのメイン画面 --- (*1) 9 | @app.route('/') 10 | def index(): 11 | return show('FrontPage') 12 | 13 | # 新規作成画面 --- (*2) 14 | @app.route('/new') 15 | def new_page(): 16 | page_name = request.args.get("page_name") 17 | if page_name is None: 18 | return render_template('new.html') 19 | else: 20 | return redirect('/edit/' + page_name) 21 | 22 | # Wikiの編集画面 --- (*3) 23 | @app.route('/edit/') 24 | def edit(page_name): 25 | return render_template('edit.html', 26 | page_name=page_name, 27 | body=wikifunc.read_file(page_name)) 28 | 29 | # 編集内容を保存する --- (*4) 30 | @app.route('/edit_save/', methods=["POST"]) 31 | def edit_save(page_name): 32 | body = request.form.get("body") 33 | wikifunc.write_file(page_name, body) 34 | return redirect('/' + page_name) 35 | 36 | # Wikiの表示 --- (*5) 37 | @app.route('/') 38 | def show(page_name): 39 | print(page_name) 40 | return render_template('show.html', 41 | page_name=page_name, 42 | body=wikifunc.read_file(page_name, html=True)) 43 | 44 | if __name__ == '__main__': 45 | app.run(debug=True, host='0.0.0.0') 46 | 47 | -------------------------------------------------------------------------------- /src/ch5/wiki/data/FrontPage.md: -------------------------------------------------------------------------------- 1 | # 🟡▲の設定更新手順 2 | 3 | - xxx サービスを止める 4 | - confファイルを編集 5 | - サービスの起動 6 | - Nサーバーから動作確認 7 | 8 | -------------------------------------------------------------------------------- /src/ch5/wiki/data/test.md: -------------------------------------------------------------------------------- 1 | #aaaaa 2 | 3 | fffff ``fff`` bbb 4 | ggggg 5 | 6 | | aa | bbb | ccc | 7 | |---|---|---| 8 | | dd | ee | ff | 9 | | dd | ee | ff | 10 | -------------------------------------------------------------------------------- /src/ch5/wiki/static/pure-min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | Pure v1.0.1 3 | Copyright 2013 Yahoo! 4 | Licensed under the BSD License. 5 | https://github.com/pure-css/pure/blob/master/LICENSE.md 6 | */ 7 | /*! 8 | normalize.css v^3.0 | MIT License | git.io/normalize 9 | Copyright (c) Nicolas Gallagher and Jonathan Neal 10 | */ 11 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{letter-spacing:-.31em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-align-content:flex-start;-ms-flex-line-pack:start;align-content:flex-start}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){table .pure-g{display:block}}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u{display:inline-block;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-g [class*=pure-u]{font-family:sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-12,.pure-u-1-2,.pure-u-1-24,.pure-u-1-3,.pure-u-1-4,.pure-u-1-5,.pure-u-1-6,.pure-u-1-8,.pure-u-10-24,.pure-u-11-12,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-2-24,.pure-u-2-3,.pure-u-2-5,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24,.pure-u-3-24,.pure-u-3-4,.pure-u-3-5,.pure-u-3-8,.pure-u-4-24,.pure-u-4-5,.pure-u-5-12,.pure-u-5-24,.pure-u-5-5,.pure-u-5-6,.pure-u-5-8,.pure-u-6-24,.pure-u-7-12,.pure-u-7-24,.pure-u-7-8,.pure-u-8-24,.pure-u-9-24{display:inline-block;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-10-24,.pure-u-5-12{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-14-24,.pure-u-7-12{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-15-24,.pure-u-5-8{width:62.5%}.pure-u-16-24,.pure-u-2-3{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-18-24,.pure-u-3-4{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-20-24,.pure-u-5-6{width:83.3333%}.pure-u-21-24,.pure-u-7-8{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-24-24,.pure-u-5-5{width:100%}.pure-button{display:inline-block;zoom:1;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-group{letter-spacing:-.31em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.pure-button-group{word-spacing:-.43em}.pure-button-group .pure-button{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:#444;color:rgba(0,0,0,.8);border:1px solid #999;border:none transparent;background-color:#e6e6e6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:focus,.pure-button:hover{background-image:-webkit-gradient(linear,left top,left bottom,from(transparent),color-stop(40%,rgba(0,0,0,.05)),to(rgba(0,0,0,.1)));background-image:-webkit-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000}.pure-button-disabled,.pure-button-disabled:active,.pure-button-disabled:focus,.pure-button-disabled:hover,.pure-button[disabled]{border:none;background-image:none;opacity:.4;cursor:not-allowed;-webkit-box-shadow:none;box-shadow:none;pointer-events:none}.pure-button-hidden{display:none}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-button-group .pure-button{margin:0;border-radius:0;border-right:1px solid #111;border-right:1px solid rgba(0,0,0,.2)}.pure-button-group .pure-button:first-child{border-top-left-radius:2px;border-bottom-left-radius:2px}.pure-button-group .pure-button:last-child{border-top-right-radius:2px;border-bottom-right-radius:2px;border-right:none}.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 3px #ddd;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 3px #ddd;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=color]:focus,.pure-form input[type=date]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=email]:focus,.pure-form input[type=month]:focus,.pure-form input[type=number]:focus,.pure-form input[type=password]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=text]:focus,.pure-form input[type=time]:focus,.pure-form input[type=url]:focus,.pure-form input[type=week]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129fea}.pure-form input:not([type]):focus{outline:0;border-color:#129fea}.pure-form input[type=checkbox]:focus,.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus{outline:thin solid #129fea;outline:1px auto #129fea}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=color][disabled],.pure-form input[type=date][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=email][disabled],.pure-form input[type=month][disabled],.pure-form input[type=number][disabled],.pure-form input[type=password][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=text][disabled],.pure-form input[type=time][disabled],.pure-form input[type=url][disabled],.pure-form input[type=week][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form select:focus:invalid,.pure-form textarea:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=checkbox]:focus:invalid:focus,.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=color],.pure-form-stacked input[type=date],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=email],.pure-form-stacked input[type=file],.pure-form-stacked input[type=month],.pure-form-stacked input[type=number],.pure-form-stacked input[type=password],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=text],.pure-form-stacked input[type=time],.pure-form-stacked input[type=url],.pure-form-stacked input[type=week],.pure-form-stacked label,.pure-form-stacked select,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned .pure-help-inline,.pure-form-aligned input,.pure-form-aligned select,.pure-form-aligned textarea,.pure-form-message-inline{display:inline-block;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form .pure-input-rounded,.pure-form input.pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-3-4{width:75%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form .pure-help-inline,.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=color],.pure-group input[type=date],.pure-group input[type=datetime-local],.pure-group input[type=datetime],.pure-group input[type=email],.pure-group input[type=month],.pure-group input[type=number],.pure-group input[type=password],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=text],.pure-group input[type=time],.pure-group input[type=url],.pure-group input[type=week]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0 0}.pure-form .pure-help-inline,.pure-form-message,.pure-form-message-inline{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu{-webkit-box-sizing:border-box;box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-item,.pure-menu-list{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-heading,.pure-menu-link{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-separator{display:inline-block;zoom:1;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-active>.pure-menu-children,.pure-menu-allow-hover:hover>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;-webkit-overflow-scrolling:touch;padding:.5em 0}.pure-menu-horizontal .pure-menu-children .pure-menu-separator,.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-horizontal .pure-menu-children .pure-menu-separator{display:block;width:auto}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-disabled,.pure-menu-heading,.pure-menu-link{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent}.pure-menu-active>.pure-menu-link,.pure-menu-link:focus,.pure-menu-link:hover{background-color:#eee}.pure-menu-selected>.pure-menu-link,.pure-menu-selected>.pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0} -------------------------------------------------------------------------------- /src/ch5/wiki/templates/edit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 |
    7 |

    編集: {{ page_name }}

    8 | 9 |
    11 |
    12 | 14 | 17 |
    18 |
    19 |
    20 | 21 | -------------------------------------------------------------------------------- /src/ch5/wiki/templates/new.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 |
    8 |

    新規作成

    9 |
    10 |
    11 | 12 | 15 |
    16 |
    17 |
    18 | 19 | -------------------------------------------------------------------------------- /src/ch5/wiki/templates/show.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 14 | 15 |
    16 | 17 |

    {{ page_name }}

    18 | 19 |
    {{ body | safe }}
    20 |
    21 |
    22 | 23 | 編集 25 | 新規 27 |
    28 |
    29 | 30 | -------------------------------------------------------------------------------- /src/ch5/wiki/wikifunc.py: -------------------------------------------------------------------------------- 1 | import os, markdown 2 | 3 | # データ保存先ディレクトリ --- (*1) 4 | DIR_DATA = os.path.dirname(__file__) + '/data' 5 | 6 | # マークダウン変換用オブジェクト --- (*2) 7 | md = markdown.Markdown(extensions=['tables']) 8 | 9 | # Wikiページ名から実際のファイルパスへ変換 --- (*3) 10 | def get_filename(page_name): 11 | return DIR_DATA + "/" + page_name + ".md" 12 | 13 | # ファイルを読みHTMLに変換して返す --- (*4) 14 | def read_file(page_name, html=False): 15 | path = get_filename(page_name) 16 | if os.path.exists(path): 17 | with open (path, "rt", encoding="utf-8") as f: 18 | s = f.read() 19 | if html: s = md.convert(s) # --- (*5) 20 | return s 21 | return "" 22 | 23 | # ファイルへ書き込む --- (*6) 24 | def write_file(page_name, body): 25 | path = get_filename(page_name) 26 | with open (path, "wt", encoding="utf-8") as f: 27 | f.write(body) 28 | 29 | -------------------------------------------------------------------------------- /src/ch5/wiki2/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect 2 | from flask import render_template, request 3 | import wikifunc 4 | 5 | # Flaskインスタンス生成 6 | app = Flask(__name__) 7 | 8 | # Wikiのメイン画面 9 | @app.route('/') 10 | def index(): 11 | return show('FrontPage') 12 | 13 | # 新規作成画面 14 | @app.route('/new') 15 | def new_page(): 16 | page_name = request.args.get("page_name") 17 | if page_name is None: 18 | return render_template('new.html') 19 | else: 20 | return redirect('/edit/' + page_name) 21 | 22 | # Wikiの編集画面 --- (*1) 23 | @app.route('/edit/') 24 | def edit(page_name): 25 | body, hash = wikifunc.read_file(page_name) 26 | return render_template('edit.html', 27 | page_name=page_name, 28 | body=body, 29 | hash=hash, 30 | warn='') 31 | 32 | # 編集内容を保存する --- (*2) 33 | @app.route('/edit_save/', methods=["POST"]) 34 | def edit_save(page_name): 35 | body2 = request.form.get("body") 36 | hash2 = request.form.get("hash") 37 | # 編集開始時点より別の編集があったか確認 38 | body1, hash1 = wikifunc.read_file(page_name) 39 | if hash1 != hash2: # 編集の競合があった 40 | # 差分を調査 41 | print("diff=", hash1, hash2) 42 | res = wikifunc.get_diff(page_name, body2, hash2) 43 | return render_template('edit.html', 44 | page_name=page_name, 45 | body=res, 46 | hash=hash1, 47 | warn='編集に競合がありました。') 48 | # 競合がなければ保存 --- (*3) 49 | wikifunc.write_file(page_name, body2) 50 | return redirect('/' + page_name) 51 | 52 | # Wikiの表示 53 | @app.route('/') 54 | def show(page_name): 55 | body, _ = wikifunc.read_file(page_name, html=True) 56 | return render_template('show.html', 57 | page_name=page_name, 58 | body=body) 59 | 60 | if __name__ == '__main__': 61 | app.run(debug=True, host='0.0.0.0') 62 | 63 | -------------------------------------------------------------------------------- /src/ch5/wiki2/data/FrontPage.md: -------------------------------------------------------------------------------- 1 | # 🟡▲の設定更新手順 2 | - xxx サービスを止める 3 | - confファイルを編集 4 | - サービスの起動 5 | - Nサーバーから動作確認する -------------------------------------------------------------------------------- /src/ch5/wiki2/data/a.md: -------------------------------------------------------------------------------- 1 | あああああああああ 2 | いいいいいいいいい 3 | ううううううう 4 | -------------------------------------------------------------------------------- /src/ch5/wiki2/templates/edit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 |
    7 |

    編集: {{ page_name }} 8 | {{ warn }} 9 |

    10 | 11 |
    13 |
    14 | 15 | 17 | 20 |
    21 |
    22 |
    23 | 24 | -------------------------------------------------------------------------------- /src/ch5/wiki2/templates/new.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 |
    8 |

    新規作成

    9 |
    10 |
    11 | 12 | 15 |
    16 |
    17 |
    18 | 19 | -------------------------------------------------------------------------------- /src/ch5/wiki2/templates/show.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 14 | 15 |
    16 | 17 |

    {{ page_name }}

    18 | 19 |
    {{ body | safe }}
    20 |
    21 |
    22 | 23 | 編集 25 | 新規 27 |
    28 |
    29 | 30 | -------------------------------------------------------------------------------- /src/ch5/wiki2/wikifunc.py: -------------------------------------------------------------------------------- 1 | import os, hashlib, re, subprocess, markdown 2 | 3 | # 定数の指定 --- (*1) 4 | DIR_DATA = os.path.dirname(__file__) + '/data' 5 | DIR_BACKUP = DIR_DATA + '/backup' 6 | DIFF3 = 'diff3' 7 | 8 | # マークダウン変換用オブジェクト 9 | md = markdown.Markdown(extensions=['tables']) 10 | 11 | # Wikiページ名から実際のファイルパスへ変換 12 | def get_filename(page_name): 13 | return DIR_DATA + "/" + page_name + ".md" 14 | 15 | # バックアップファイルの保存先のパスを取得 --- (*2) 16 | def get_backup(page_name, hash): 17 | if not os.path.exists(DIR_BACKUP): 18 | os.makedirs(DIR_BACKUP) 19 | return DIR_BACKUP + '/' + page_name + "." + hash 20 | 21 | # ファイルを読みHTMLに変換して返す --- (*3) 22 | def read_file(page_name, html=False): 23 | text = read_f(get_filename(page_name)) 24 | hash = get_hash(text) 25 | print("read:", hash) 26 | if html: text = md.convert(text) 27 | return text, hash 28 | 29 | # ファイルへ書き込む --- (*4) 30 | def write_file(page_name, body): 31 | body = re.sub(r'\r\n|\r|\n', "\n", body) 32 | # メインファイル 33 | write_f(get_filename(page_name), body) 34 | # バックアップファイルを書き込む 35 | hash = get_hash(body) 36 | write_f(get_backup(page_name, hash), body) 37 | 38 | # 差分を求める --- (*5) 39 | def get_diff(page_name, text, hash): 40 | newfile = DIR_DATA + '/__投稿__' 41 | write_f(newfile, text) 42 | orgfile = DIR_DATA + '/__編集前__' 43 | backupfile = get_backup(page_name, hash) 44 | write_f(orgfile, read_f(backupfile)) 45 | curfile = DIR_DATA + '/__更新__' 46 | write_f(curfile, read_f(get_filename(page_name))) 47 | cp = subprocess.run([ 48 | DIFF3, '-a', '-m', 49 | newfile, orgfile, curfile], 50 | encoding='utf-8', stdout=subprocess.PIPE) 51 | print(cp) 52 | res = cp.stdout 53 | res = res.replace(DIR_DATA + '/', '') 54 | return res 55 | 56 | # ファイルを読むだけ 57 | def read_f(path): 58 | text = "" 59 | if os.path.exists(path): 60 | with open (path, "rt", encoding="utf-8") as f: 61 | text = f.read() 62 | return text 63 | 64 | # ファイルを書くだけ 65 | def write_f(path, text): 66 | with open (path, "wt", encoding="utf-8") as f: 67 | f.write(text) 68 | 69 | # ハッシュ値を求める 70 | def get_hash(s): 71 | return hashlib.sha256(s.encode("utf-8")).hexdigest() 72 | -------------------------------------------------------------------------------- /src/ch5/zip-api.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | import sqlite3, os, json 3 | 4 | # 郵便番号のデータベースのパスを特定 --- (*1) 5 | base_path = os.path.dirname(os.path.abspath(__file__)) 6 | db_path = base_path + '/zip.sqlite' 7 | form_path = base_path + '/zip-form.html' 8 | 9 | # Flaskを取り込む --- (*2) 10 | app = Flask(__name__) 11 | 12 | # ルートにアクセスがあったとき --- (*3) 13 | @app.route('/') 14 | def index(): 15 | with open(form_path) as f: 16 | return f.read() 17 | 18 | # APIにアクセスがあったとき --- (*4) 19 | @app.route('/api') 20 | def api(): 21 | # パラメータを取得 --- (*5) 22 | q = request.args.get("q", "") 23 | # データベースから値を取得 --- (*6) 24 | conn = sqlite3.connect(db_path) 25 | c = conn.cursor() 26 | c.execute( 27 | 'SELECT ken,shi,cho FROM zip WHERE code=?', 28 | [q]) 29 | items = c.fetchall() 30 | conn.close() 31 | # 結果をJSONで出力 --- (*7) 32 | res = [] 33 | for i, r in enumerate(items): 34 | ken,shi,cho = (r[0], r[1], r[2]) 35 | res.append(ken + shi + cho) 36 | print(q, ":", ken + shi + cho) 37 | return json.dumps(res) 38 | 39 | # Flaskを開始する --- (*8) 40 | if __name__ == '__main__': 41 | app.run(host='0.0.0.0') 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/ch5/zip-csv2sqlite.py: -------------------------------------------------------------------------------- 1 | import sqlite3, csv 2 | 3 | # 保存先の指定 4 | FILE_SQLITE = 'zip.sqlite' 5 | 6 | # DBに接続してテーブルを作成 --- (*1) 7 | conn = sqlite3.connect(FILE_SQLITE) 8 | conn.execute(''' 9 | CREATE TABLE IF NOT EXISTS zip ( 10 | zip_id INTEGER PRIMARY KEY, 11 | code TEXT, 12 | ken TEXT, 13 | shi TEXT, 14 | cho TEXT 15 | ) 16 | ''') 17 | 18 | # 過去に入っているデータがあれば削除 --- (*2) 19 | conn.execute('DELETE FROM zip') 20 | 21 | # CSVを読んでDBに入れる関数 --- (*3) 22 | def read_csv(fname): 23 | c = conn.cursor() 24 | f = open(fname, encoding='cp932') 25 | reader = csv.reader(f) 26 | for row in reader: 27 | code = row[2] 28 | ken = row[6] 29 | shi = row[7] 30 | cho = row[8] 31 | if cho == '以下に掲載がない場合': 32 | cho = '' 33 | print(code, ken, shi, cho) 34 | c.execute( 35 | 'INSERT INTO zip (code,ken,shi,cho) ' + 36 | 'VALUES (?,?,?,?)', 37 | [code,ken,shi,cho]) 38 | f.close() 39 | conn.commit() 40 | 41 | # 事業所用のデータをDBに入れる関数 --- (*4) 42 | def read_jigyosyo_csv(fname): 43 | c = conn.cursor() 44 | f = open(fname, encoding='cp932') 45 | reader = csv.reader(f) 46 | for row in reader: 47 | code = row[7] 48 | ken = row[3] 49 | shi = row[4] 50 | cho = row[5] + row[6] + ' ' + row[2] 51 | print(code, ken, shi, cho) 52 | conn.execute( 53 | 'INSERT INTO zip (code,ken,shi,cho) ' + 54 | 'VALUES (?,?,?,?)', 55 | [code,ken,shi,cho]) 56 | f.close() 57 | conn.commit() 58 | 59 | # CSVファイルを読む --- (*5) 60 | read_csv('KEN_ALL.CSV') 61 | read_jigyosyo_csv('JIGYOSYO.CSV') 62 | conn.close() 63 | print('ok') 64 | 65 | -------------------------------------------------------------------------------- /src/ch5/zip-form.html: -------------------------------------------------------------------------------- 1 | 2 |

    住所入力

    3 | 4 |
    5 | 郵便番号:
    6 | 7 |
    8 | 住所:
    9 |
    10 | 11 |
    12 | 13 | 14 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/ch5/zip-test.py: -------------------------------------------------------------------------------- 1 | # テスト用の郵便番号 2 | code = '1510065' 3 | 4 | # データベースに接続 5 | import sqlite3 6 | conn = sqlite3.connect('zip.sqlite') 7 | 8 | # 郵便番号を検索 9 | c = conn.cursor() 10 | res = c.execute('SELECT * FROM zip WHERE code=?', [code]) 11 | for row in res: 12 | print(row) 13 | -------------------------------------------------------------------------------- /src/ch6/hoge.txt: -------------------------------------------------------------------------------- 1 | いろはにほへと -------------------------------------------------------------------------------- /src/ch6/tell_path.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # スクリプトのファイルパス 4 | print("script path:", __file__) 5 | # スクリプトを配置しているディレクトリのパス 6 | print("script dir: ", os.path.dirname(__file__)) 7 | 8 | # スクリプトの絶対パス 9 | apath = os.path.abspath(__file__) 10 | print("script path abs:", apath) 11 | print("script dir abs:", os.path.dirname(apath)) 12 | 13 | -------------------------------------------------------------------------------- /src/ch6/write_text.py: -------------------------------------------------------------------------------- 1 | with open("hoge.txt", "wt") as f: 2 | f.write("いろはにほへと") 3 | 4 | -------------------------------------------------------------------------------- /src/ch6/write_text_utf8.py: -------------------------------------------------------------------------------- 1 | with open("hoge.txt", "wt", encoding="utf-8") as f: 2 | f.write("いろはにほへと") 3 | 4 | -------------------------------------------------------------------------------- /src/ch6/xss_no.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route('/') 6 | def index(): 7 | # 入力フォームを表示 8 | return ''' 9 | 10 |
    11 | 名前: 12 | 13 |
    14 | ''' 15 | 16 | @app.route('/kakunin') 17 | def kakunin(): 18 | import html 19 | # フォームの値を取得 20 | name = request.args.get('name', '') 21 | # エスケープ処理を施す 22 | name_html = html.escape(name) 23 | # フォームに name を埋め込んで表示 24 | return ''' 25 | 26 |

    名前は、{0}さんです。

    27 | 28 | '''.format(name_html) 29 | 30 | if __name__ == '__main__': 31 | app.run(debug=True, host='0.0.0.0') 32 | -------------------------------------------------------------------------------- /src/ch6/xss_test.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route('/') 6 | def index(): 7 | # 入力フォームを表示 8 | return ''' 9 | 10 |
    11 | 名前: 12 | 13 |
    14 | ''' 15 | 16 | @app.route('/kakunin') 17 | def kakunin(): 18 | # 確認画面を表示 19 | # フォームの値を取得 20 | name = request.args.get('name', '') 21 | # フォームに name を埋め込んで表示 22 | return ''' 23 | 24 |

    名前は、{0}さんです。

    25 | 26 | '''.format(name) 27 | 28 | if __name__ == '__main__': 29 | app.run(debug=True, host='0.0.0.0') 30 | --------------------------------------------------------------------------------