ベスパリブ

プログラミングを主とした日記・備忘録です。ベスパ持ってないです。

Chrome拡張のContent Scriptのメモ

Content Scriptについて

  • Content ScriptはWebページに差し込まれる
  • Webページ内のidなどの要素にはアクセス可能
    • document.getElementById('title')などが可能
  • Webページ内のJavaScriptの変数にはアクセス不可能(スコープが違うので、Content Scriptの変数とバッティングすることはない。深く考える必要はない)
  • Chrome拡張のAPIの一部のみ使用可能
  • 上記の使用不可能なものを使いたいときは、Event Pageを介してデータのやり取りを行う
    • chrome.runtimeにあるSend Messageを使って、Content ScriptからEvent Pageに対してMessage Passingしてやり取りを行う

Anacondaの環境変数のパス(Windows)

$ conda update anaconda-navigator をしたらCondaHTTPErrorとかSSLErrorとかエラーだらけになった。

メインPCだとエラーにならず、ノートPCだとエラーになったので、メインPCと同じ設定にしようと思って環境変数のパスを確認したらパスが足りなかった。最終的なパスを残しておく。

C:\Users\XXXX\Anaconda3
C:\Users\XXXX\Anaconda3\Library\mingw-w64\bin
C:\Users\XXXX\Anaconda3\Library\bin
C:\Users\XXXX\Anaconda3\Library\usr\bin
C:\Users\XXXX\Anaconda3\Scripts

SSHでもBitbucketのpushやfetch時に毎回パスワード聞かれるときの対処法

ググったら似たような症状が出るわけです。

ただし上記の記事は、「HTTPSからSSHに変更したら直る」という内容です。

私の場合、SSHからHTTPSに変更したら直りました。以前まではSSH推奨であったが、今ではHTTPS推奨に変わったのでしょうか?(本当か?)推奨は時代によって変わるので、そういうことかもしれませんが確証はありません。

# リモートリポジトリのURLを表示する(git@で始まっているので、SSH接続)
$ git remote -v
origin  git@bitbucket.org:YYYY/MYPROJECT.git (fetch)
origin  git@bitbucket.org:YYYY/MYPROJECT.git (push)

# fetchすると、毎回SSHキーのパスワードを聞かれる
$ git fetch
Enter passphrase for key '/c/Users/XXXX/.ssh/bitbucket/id_ed25519':

# リモートリポジトリのURLをHTTPSに変更する
$ git remote set-url origin https://USERNAME@bitbucket.org/XXXX/MYPROJECT.git

# リモートリポジトリのURLを確認する(HTTPSになっていることを確認)
$ git remote -v
origin  https://USERNAME@bitbucket.org/YYYY/MYPROJECT.git (fetch)
origin  https://USERNAME@bitbucket.org/YYYY/MYPROJECT.git (push)

# 初回だけパスワード聞かれるが、二回目以降は聞かれなくなった
$ git fetch

ちなみにGitHubHTTPS推奨(2019年3月14日現在)というのがわかりますが、Bitbucketはそういう記事を探しても見つかりませんでした。

ただし、Change the remote URL to your repositoryの記事を読んでみると、

Update the URL for Git repositories

(中略)

If you update your URL from HTTPS to SSH, next time you push or pull from your repository, the terminal responds that it is adding the Bitbucket host to the list of known hosts. You also won't have to enter a password.

とあり、「(Gitリポジトリを)HTTPSからSSHに変更した場合は次回からパスワード聞かれない」とあります。

また、

Update the URL for Mercurial repositories

(中略)

If you update your URL from HTTPS to SSH, next time you push or pull from your repository, the terminal responds that it is adding the Bitbucket host to the list of known hosts. You also won't have to enter a password.

とあり、「(Mercurialリポジトリを)SSHからHTTPSに変更した場合は次回からパスワード聞かれない」とあります。

つまりぜんぜんわからないですが、URLを変更したらパスワードは聞かれなくなるということでしょうか。本当か?

com_error at / (-2147221008, 'CoInitialize は呼び出されていません。', None, None)

エラーの対処法

Djangoで作ったWebアプリケーションで表題のエラーが発生しました。

このWebアプリは、Web画面でファイルをアップロードし、ファイルをサーバーに渡し、サーバー側でExcelを起動しファイルを処理する。というようなものです。Excelを起動するときにエラーが発生しているようです。

Internal Server Error: /
Traceback (most recent call last):
  File "C:\Users\XXXX\AppData\Local\Continuum\anaconda3\envs\myproject\lib\site-packages\win32com\client\dynamic.py", line 89, in _GetGoodDispatch
    IDispatch = pythoncom.connect(IDispatch)
pywintypes.com_error: (-2147221008, 'CoInitialize は呼び出されていません。', None, None)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\XXXX\AppData\Local\Continuum\anaconda3\envs\myproject\lib\site-packages\django\core\handlers\exception.py", line 34, in inner
    response = get_response(request)
  File "C:\Users\XXXX\AppData\Local\Continuum\anaconda3\envs\myproject\lib\site-packages\django\core\handlers\base.py", line 126, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "C:\Users\XXXX\AppData\Local\Continuum\anaconda3\envs\myproject\lib\site-packages\django\core\handlers\base.py", line 124, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "C:\Users\XXXX\workspace\myproject\csv2excel\views.py", line 73, in config_table_maker
    feat_names = handle_uploaded_file(request.FILES["file"])
  File "C:\Users\XXXX\workspace\myproject\csv2excel\views.py", line 61, in handle_uploaded_file
    feat_names = sample(file_name)
  File "C:\Users\XXXX\workspace\myproject\csv2excel\views.py", line 20, in sample
    excel = Excel()
  File "C:\Users\XXXX\workspace\myproject\src\mods\Excel.py", line 17, in __init__
    self._app = win32com.client.Dispatch("Excel.Application")
  File "C:\Users\XXXX\AppData\Local\Continuum\anaconda3\envs\myproject\lib\site-packages\win32com\client\__init__.py", line 95, in Dispatch
    dispatch, userName = dynamic._GetGoodDispatchAndUserName(dispatch,userName,clsctx)
  File "C:\Users\XXXX\AppData\Local\Continuum\anaconda3\envs\myproject\lib\site-packages\win32com\client\dynamic.py", line 114, in _GetGoodDispatchAndUserName
    return (_GetGoodDispatch(IDispatch, clsctx), userName)
  File "C:\Users\XXXX\AppData\Local\Continuum\anaconda3\envs\myproject\lib\site-packages\win32com\client\dynamic.py", line 91, in _GetGoodDispatch
    IDispatch = pythoncom.CoCreateInstance(IDispatch, None, clsctx, pythoncom.IID_IDispatch)
pywintypes.com_error: (-2147221008, 'CoInitialize は呼び出されていません。', None, None)

サーバー側でログインしてコンソールでプログラムを動かすことはできていて、Webアプリを介して(ブラウザからリクエストして)プログラムを動かそうとしたらこのエラーが発生します。

Apacheでアプリを作った時に、Apacheユーザ(www-data)にsudoersで権限を渡さないとブラウザからのリクエストでサーバ側のプログラムを走らせられないという現象に似ているので、それと同様のエラーでしょうか。

ということはDjangoに何かしらの権限を渡せば動きそうですが、そういう設定はどこで書けばいいのかちょっとわかりません。

色々調べてたら、下記の記事に同様の現象について書いてありました。

[Delphi] CoInitializeが呼び出されていません - くろねこ研究所

このため、CUI アプリケーションでこのエラーメッセージを回避するには、CoInitialize を呼び出すなどの対策が必要になる。また、CoInitialize と対で CoUninitialize を呼び出さないといけない。

この方は私とは逆に、CUIだとエラーが発生するそうです。

COM を使うとその部分は、結構長くなることが多いと思うので、クラス化して、その Create と Destroy に それぞれ CoInitialize と CoUninitialize を追加するのがオススメである。

ほかの記事も色々見たのですが、Excelを起動する前にCoInitialize()を呼び出し、Excelを終了した後にCoUninitialize()を呼び出すとOKっぽいことがわかります。クラスで書くとこんな感じでしょうか。

# -*- coding:utf-8 -*-
from win32com.client import gencache, Dispatch
import pythoncom

class Excel():

    def __init__(self, visible=False):
        """ Excel機能を提供するクラス """
        pythoncom.CoInitialize()  # Excelを起動する前にこれを呼び出す
        self._app = Dispatch("Excel.Application")
        self._app.Visible = visible

    @property
    def app(self):
        return self._app

    def quit(self):
        """ Excelを終了させる """
        self.app.Quit()
        pythoncom.CoUninitialize()  # Excelを終了した後はこれを呼び出す

で、書き換えたら動きました。ただ、このCoInitialize()が何なのか正直よくわからない。

参考URL

Coinitializeとは?

わからないままにしとくのはちょっと怖かったので、少し調べます。

[連載! とことん VC++] 第 1 回 COM 再入門 ~ COM オブジェクトの基本的利用 (COM クライアントの実装) ~ in C++

  1. CoInitialize の初期化が意味すること ~ アパートメントの準備 ~ ここで、[3] の初期化の意味をもう少しく詳しく確認しましょう。

CoInitialize 関数は COM オブジェクトの実行環境ともいえる「アパートメント」を必要に応じて 1 つ作成する役割を持ちます。この関数呼び出しによって、アパートメントを準備しなければ、COM オブジェクトを実行できません。

このアパートメントには 2 つの種類があります。それは、「STA (Single Thread Apartment)」と「MTA (Multi Thread Apartment)」です。実は、[3] の CoInitialize 関数は、STA のアパートメントを作成する作用があり、次のように CoInitializeEx 関数を使用する場合と同じです。

CoInitializeはCOMの初期化をする関数で、STAかMTAで初期化するそう。

そもそもCOMというものを明確に理解していないのでググります。

COM(Component Object Model)とは - IT用語辞典 e-Words

COMとは、マイクロソフトMicrosoft)社が提唱していた、ソフトウェアの機能を部品化して外部から呼び出して利用する仕組みを定めた技術仕様の一つ。主に同社のWindowsシリーズのOSやその上で動作するソフトウェアで利用された。

プログラムの機能を外部から呼び出して利用するための手順やデータ形式などの標準を定めており、COMの仕様に則って開発されたソフトウェア間は容易に連携して動作させることができる。特定のOSやプログラミング言語に依存しない仕様になっており、異なる言語やOSで開発されたプログラムを連携させることができる。

まあつまり、「外部アプリケーションであるExcelpythonで呼び出すにはCOMという技術を使うので、COMの初期化が必要。」というのはわかりました。でも結局、どうしてWebアプリから実行したときだけこの初期化が必要になったのかはよくわかりませんでしたね(眼鏡クイッ)。

com_errorのtry except

from pywintypes import com_error

try:
    # do something
except com_error:
    # do something when com_error occurred.

condaのPackagesNotFoundError

condaで何かをインストールするときにPackagesNotFoundErrorが出るときの主な原因は、

(1) タイプミス

(2) 探すリポジトリになかった

(2)の場合は探すリポジトリを追加させます。

# condaが探すリポジトリ先の確認
$conda config --get channels
# conda-forgeリポジトリの追加
$ conda config --append channels conda-forge

参考URL:

condaが探すリポジトリ先を変更する方法(conda-forgeの追加) - Qiita

Pythonのディストリビューション「Anaconda」、バージョン5.0が登場 - CIOニュース:CIO Magazine

Django Girls Tutorialをする

はじめに

Djangoの最初のプロジェクトを作る方法、毎回ググってやるのがつらいので、備忘録として残したい。 Django Girls Tutorialを通してやることで、備忘録とします。

上記のページは情報が古くなってる可能性があるので、GitHubリポジトリを確認したほうが良いかもです。

tutorial/ja at master · DjangoGirls/tutorial · GitHubgithub.com

Anacondaはインストールされていること前提。Windows環境です。

Django Girls Tutorial用の仮想環境を作る

デフォルト環境を汚したくないので、専用の仮想環境を作っておきます。

# djangogirlsという名の仮想環境を作成する
(base) > conda create -n djangogirls
# djangogirls環境に移動する
(base) > activate djangogirls
# Djangoのインストール
(djangogirls) > conda install django

これ以降、(djangogirls)は省略します。

プロジェクトを作る

tutorial/ja/django_start_project at master · DjangoGirls/tutorial · GitHub

# Django Girls Tutorial用のフォルダを作り、移動する
> mkdir djangogirls
> cd djangodirls
# プロジェクトの作成
~/djangogirls> django-admin startproject mysite .

settings.pyの設定

mysite/settings.pyの中身を次のように変更する

# タイムゾーンの設定
TIME_ZONE = 'Asia/Tokyo'
# 言語の設定
LANGUAGE_CODE = 'ja'
# 静的ファイルのパスの設定
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
# PythonAnywhereにデプロイする場合
ALLOWED_HOSTS = ['127.0.0.1', '.pythonanywhere.com']

データベースの設定

sqlite3の設定はmysite/settings.pyの以下に既に設定されている(本番環境ではsqliteは使わないこと)。

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

以下のコマンドでデータベースを作成し、サーバを起動します。

# データベースの作成
~/djangogirls> python manage.py migrate
# サーバの起動
~/djangogirls> python manage.py runserver
# ブラウザのアドレス欄に"http://127.0.0.1:8000/"を入力し、ページが表示されたら成功
(ctrl-Cでサーバの停止)

Djangoモデル

tutorial/ja/django_models at master · DjangoGirls/tutorial · GitHub

プロジェクト内にアプリケーションを作成します。

# blogという名前のアプリケーションを作成する
~/djangogirls> python manage.py startapp blog

アプリケーションを作ったら、Djangoにそれを使うように伝えます。 mysite/settings.pyファイルのINSTALLED_APPSに、'blog'という一行を追加します。

INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',
)

blog post modelの作成

blog/models.pyファイルでModelsと呼ばれる全てのオブジェクトを定義します。これがブログポストを定義する場所です。

blog/models.pyを以下のように書き換えます。

from django.db import models
from django.utils import timezone

class Post(models.Model):
    author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
    title = models.CharField(max_length=200)
    text = models.TextField()
    created_date = models.DateTimeField(
            default=timezone.now)
    published_date = models.DateTimeField(
            blank=True, null=True)

    def publish(self):
        self.published_date = timezone.now()
        self.save()

    def __str__(self):
        return self.title

models.ModelはポストがDjango Modelだという意味で、Djangoが、これはデータベースに保存すべきものだと分かるようにしています。

(クラス変数をselfでアクセスしてるけどいいのか?)

データベースにモデル用のテーブルを作る

新しいモデルをデータベースに追加します。まず、(今作った)モデルの中で少し変更があったことをDjangoに知らせる必要があります。

~/djangogirls> python manage.py makemigrations blog

Djangoがデータベースに入れる為の移行ファイルを作ってくれているので、migrateする。

~/djangogirls> python manage.py migrate blog

これでPostモデルがデータベースに入りました。

ログインページを作る

今作成したポストを追加、編集、削除するのにDjango adminを使います。

モデルをadminページで見れるようにするために、モデルをadmin.site.register(Post)で登録する必要があります。

blog/admin.pyファイルを書き換えます。

from django.contrib import admin
from .models import Post

admin.site.register(Post)

ログインして投稿する

# superuser (サイトの全てを管理するユーザー)を作る
~/djangogirls> python manage.py createsuperuser
# 忘れずにmigrateする
~/djangogirls> python manage.py migrate blog
# サーバ起動
~/djangogirls> python manage.py runserver
# http://127.0.0.1:8000/admin/ にアクセスしてログインすると、Django admin ダッシュボードへ行ける
# いくつか記事を投稿して、動作することを確認する

デプロイ

tutorial/ja/deploy at master · DjangoGirls/tutorial · GitHub

Heroku PythonAnywhereにデプロイするチュートリアル。飛ばします。

DjangoでURLはどのように機能する?

tutorial/ja/django_urls at master · DjangoGirls/tutorial · GitHub

mysite/urls.py内は次のようになっている。

from django.conf.urls import include, url
from django.contrib import admin

urlpatterns = [
    # Examples:
    # url(r'^$', 'mysite.views.home', name='home'),
    # url(r'^blog/', include('blog.urls')),
    # admin/で始まる全てのURLについて、Djangoが返すべきviewをこの行で指定しています。
    url(r'^admin/', include(admin.site.urls)),
]

最初のURLを作る

http://127.0.0.1:8000/ はブログの入口ページなので、投稿したブログポストのリストを表示するようにします。

mysite/urls.py ファイルは簡潔なままにしておきたいので、mysite/urls.pyではblogアプリからURLをインポートするだけにしましょう。

from django.urls import path, include
from django.contrib import admin

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('blog.urls')),
]

これでDjangoは 'http://127.0.0.1:8000/' に来たリクエストはblog.urlsへリダイレクトするようになり、それ以降はそちらを参照するようになります。

blogのURL

blog.urls.pyを作ります。

from django.urls import path
# blogアプリケーション内の全てのviewをインポートする
from . import views

# URLパターンの定義
# post_list という名前の ビュー をルートURLに割り当てる
urlpatterns = [
    path('', views.post_list, name='post_list'),
]

このURLパターンは空の文字列に一致し、Djangoはビューを見つけるとき、URLのフルパスの前半にくっつくドメイン名(つまり、http://127.0.0.1:8000/ の部分)を無視します。 このパターンは誰かがあなたのWebサイトの 'http://127.0.0.1:8000/' というアドレスにアクセスしてきたら views.post_list が正しい行き先だということをDjangoに伝えます。

最後の name='post_list' は、ビューを識別するために使われるURL の名前です。 これはビューと同じ名前にすることもできますが、全然別の名前にすることもできます。 プロジェクトでは名前づけされたURLを後で使うことになるので、アプリのそれぞれのURLに名前をつけておくのは重要です。また、URLの名前はユニークで覚えやすいものにしておきましょう。

ビューって何?

tutorial/ja/django_views at master · DjangoGirls/tutorial · GitHub

ビュー はアプリのロジックを書いていくところです。 ビューは、以前あなたが作った モデル に情報を要求し、それを テンプレート に渡します。 テンプレートは、次の章で作ります。 ビューはただのPythonの関数です。Python入門の章で書いたものよりもちょっと複雑なだけですよ。

ビューは、views.py に記述します。私たちの場合 blog/views.py に書くことになります。

ビューを作る

blog/views.pyを作ります。

from django.shortcuts import render


def post_list(request):
    return render(request, 'blog/post_list.html', {})

これは request を引数に取り、blog/post_list.htmlテンプレートを表示する (組み立てる) render 関数を return しています。

テンプレートって何?

tutorial/ja/html at master · DjangoGirls/tutorial · GitHub

テンプレートはhtmlのこと(たぶん)。

テンプレートは、blog/templates/blogディレクトリに保存されています。 それでは、最初に、自分のblogディレクトリの中にtemplatesという名前のディレクトリを作成してください。 次に、自分のtemplatesディレクトリの中にblogという名前のディレクトリを作ります。

blog
└───templates
    └───blog

(なぜ、両方ともblogという名前の付いたディレクトリを2つ作成する必要があるのか不思議に思う人もいるかもしれません。あとで分かると思いますが、簡単に言うと、これは、もっと複雑なことをやろうとした時に、それが楽にできるようにしてくれる便利な命名法なのです。)

テンプレートを作る

blog/templates/blog/post_list.htmlを作ります。

<html>
    <head>
        <title>Django Girls blog</title>
    </head>
    <body>
        <p>Hello!</p>
        <p>It works!</p>
    </body>
</html>

ここまで来たら、サーバを起動して動作確認します。

~/djangogirls> python manage.py runserver

http://127.0.0.1:8000/ にアクセスして、ページが表示されたら成功です。

クエリセット

データベースへの接続方法と、データストアについて。

tutorial/ja/django_orm at master · DjangoGirls/tutorial · GitHub

クエリセットが何かと言うと、モデルが提供しているオブジェクトのリストのことです。クエリセットは、データベースからデータを読み込んだり、抽出したり、言われた通りにやってくれます。

# djangoのコンソールを開く
$ python manage.py shell
>>> from blog.models import Post
# Post(投稿)データをすべて取り出す
>>> Post.objects.all() 
<QuerySet [<Post: Alice is god.>, <Post: Bob is good man.>, <Post: Carol is super man.>]>
# 新しい投稿を作成する
>>> from django.contrib.auth.models import User
>>> User.objects.all()  # どんなユーザが登録されているか確認する
<QuerySet [<User: USERNAME>]>
>>> me = User.objects.get(username='USERNAME')
>>> Post.objects.create(author = me, title = 'Sample title', text = 'Test')
# 新しい投稿が作られたか確認する
>>> Post.objects.all()
<QuerySet [<Post: Alice is god.>, <Post: Bob is good man.>, <Post: Carol is super man.>, <Post: Sample title>]>
# 投稿者でフィルタリングする
>>> Post.objects.filter(author=me)
# タイトルに"title"が含まれているもののフィルタリング
>>> Post.objects.filter(title__contains='title')
# 未公開の投稿を公開する
>>> post = Post.objects.get(id=1)  # ID1の投稿を取得
>>> post.publish()
# 公開済みの投稿のみを取りだす
>>> from django.utils import timezone
>>>Post.objects.filter(published_date__lte=timezone.now())
# created_date フィールドでソートする
>>> Post.objects.order_by('created_date')  # 昇順
>>> Post.objects.order_by('-created_date')  # 降順
# 公開済みの投稿をpublished_dateフィールドでソートする
>>> Post.objects.filter(published_date__lte=timezone.now()).order_by('published_date')
# 終了
exit()

クエリセット2

tutorial/ja/dynamic_data_in_templates at master · DjangoGirls/tutorial · GitHub

・投稿内容を保存するためのPostモデルは、models.pyに定義した ・投稿の一覧を表示するpost_listはviews.pyにあり、そこにテンプレートも追加した。 ・投稿をどうやってHTMLファイルに出力すればよいか?

大まかなイメージとしては、データベースに保存された記事を取り出して、テンプレートのHTMLファイルの中に行儀よく並べるだけのことですけど。 正確には、 ビュー が モデルとテンプレートの橋渡しをしてくれます。私達が作業している post_list ビュー の場合、表示したいデータを取り出して、テンプレートファイルに渡すことになります。基本的に、どのモデルのデータを、どのテンプレートに表示させるかは、 ビューに 記述します。

blog/views.pyを開き、編集します。

from django.shortcuts import render
from django.utils import timezone
from .models import Post

def post_list(request):
    """ 投稿をリスト表示する """
    # 公開した投稿をpublished_dateでソートする
    _posts = Post.objects.filter(published_date__lte=timezone.now()).order_by('published_date')

    # render()関数について
    # 第1引数:リクエスト(ユーザから受け取ったすべての情報が詰まっている)
    # 第2引数:テンプレートファイル
    # 第3引数:指定した情報を、テンプレートファイルに渡す
    return render(request, 'blog/post_list.html', {'posts': _posts})

クエリセットのAPIリファレンス:QuerySet API reference | Django ドキュメント | Django

テンプレートタグとは?

tutorial/ja/django_templates at master · DjangoGirls/tutorial · GitHub

テンプレートタグとは、HTMLにPyhtonのようなコードを埋め込むためのもの。

HTML内に{{ posts }}のように書くと、render()関数の第3引数で指定したJSONのpostsキーのデータを表示できる

blog/templates/blog/post_list.htmlを次のように編集します。

<html>
    <head>
        <title>Django Girls blog</title>
    </head>
    <body>
        <div>
            <h1><a href="/">Django Girls Blog</a></h1>
        </div>
        
        {% for post in posts %}
            <div>
                <p>published: {{ post.published_date }}</p>
                <h1><a href="">{{ post.title }}</a></h1>
                <p>{{ post.text|linebreaksbr }}</p>
            </div>
        {% endfor %}
    </body>
</html>

CSSでカワイくしよう

tutorial/ja/css at master · DjangoGirls/tutorial · GitHub

DjangoCSSファイルはどこに置けば(どう書けば)よいのか?

Bootstrapを使う

Bootstrap は美しいWebサイトを開発するためのHTMLとCSSフレームワーク

Bootstrapのインストール

blog/templates/blog/post_list.html<head> タグに以下を追加します。

<!-- 3.2.0のバージョン番号は好きに指定してね:) バージョンによって廃止になったアイコンあるので注意:( -->
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">

これは、あなたのプロジェクトにファイルを追加しているわけではありません。インターネット上にあるファイルを指しているだけです。

jQueryと使い方は同じですね。

Djangoの静的ファイル

静的ファイルとは、CSSファイルや画像ファイルといった、動的な変更が発生しないファイルのことです。

静的ファイルはプロジェクトのどこに置けばいいの?

知りたいのはこれだよこれ。

Djangoは、ビルトインの "admin" アプリにより、静的ファイルをどこで探せばいいのかわかっています。私たちがやることは、blog アプリのための静的ファイルを追加することだけです。

そのために、blogアプリの中に static というフォルダを作ります。

djangogirls
├── blog
│   ├── migrations
│   ├── static
│   └── templates
└── mysite

Djangoは、全てのアプリのフォルダ内の "static" と名づけられた全てのフォルダを自動的に探して、その中身を静的ファイルとして使えるようにします。

すご。

この"static"と名付けられたすべてのフォルダを自動的に探す機能は、mysite/setting.pyの以下の箇所でしているようです。

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.1/howto/static-files/

STATIC_URL = '/static/'
# 後述のCSSがうまく反映されなかったら、以下を追加する
STATIC_ROOT = os.path.join(BASE_DIR, '/')
STATICFILES_DIRS = (
    os.path.join(BASE_DIR, "static"),
)

最初のCSSファイル!

以下のようにファイルを作成します。

djangogirls
└─── blog
     └─── static
          └─── css
               └─── blog.css

blog/static/css/blog.cssは適当に次のようにします。

h1 a {
    color: #FCA205;
}

・HTMLファイルの先頭に{% load static %}と書くことで、テンプレートに静的ファイルを読み込むことができる。

CSSファイルの読み込みは、<link rel="stylesheet" href="{% static 'css/blog.css' %}">のように書く

blog/templates/blog/post_list.htmlは次のようにします。

{% load static %}
<html>
    <head>
        <title>Django Girls blog</title>
        <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
        <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">
        <link rel="stylesheet" href="{% static 'css/blog.css' %}">
    </head>
(以下略)

フォント

<link href="//fonts.googleapis.com/css?family=Lobster&subset=latin,latin-ext" rel="stylesheet" type="text/css">

・ Lobster というフォントをGoogle Fonts (https://www.google.com/fonts) から読み込める

font-family: 'Lobster';CSSに追加することでフォントが適用できる

最終的に次のようになります。

/blog/static/css/blog.css

.page-header {
    background-color: #ff9400;
    margin-top: 0;
    padding: 20px 20px 20px 40px;
}

.page-header h1, .page-header h1 a, .page-header h1 a:visited, .page-header h1 a:active {
    color: #ffffff;
    font-size: 36pt;
    text-decoration: none;
}

.content {
    margin-left: 40px;
}

h1, h2, h3, h4 {
    font-family: 'Lobster', cursive;
}

.date {
    color: #828282;
}

.save {
    float: right;
}

.post-form textarea, .post-form input {
    width: 100%;
}

.top-menu, .top-menu:hover, .top-menu:visited {
    color: #ffffff;
    float: right;
    font-size: 26pt;
    margin-right: 20px;
}

.post {
    margin-bottom: 70px;
}

.post h1 a, .post h1 a:visited {
    color: #000000;
}

/blog/templates/blog/post_list.html

{% load static %}
<html>
    <head>
        <title>Django Girls blog</title>
        <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/4.3.0/css/bootstrap.min.css">
        <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/4.3.0/css/bootstrap-theme.min.css">
        <link href="http://fonts.googleapis.com/css?family=Lobster&subset=latin,latin-ext" rel="stylesheet" type="text/css">
        <link rel="stylesheet" href="{% static 'css/blog.css' %}">
    </head>
    <body>
        <div class="page-header">
            <h1><a href="/">Django Girls Blog</a></h1>
        </div>
        
        <div class="content container">
            <div class="row">
                <div class="col-md-8">
                    {% for post in posts %}
                        <div class="post">
                            <div class="date">
                                <p>published: {{ post.published_date }}</p>
                            </div>
                            <h1><a href="">{{ post.title }}</a></h1>
                            <p>{{ post.text|linebreaksbr }}</p>
                        </div>
                    {% endfor %}
                </div>
            </div>
        </div>
    </body>
</html>

テンプレートを拡張する

テンプレートを拡張しよう · Django Girls Tutorial

Djangoのまた別の素敵なところはテンプレート拡張です。これは何を意味するのでしょうか?それはHTMLの共通部分をウェブサイトの異なるページで使えるということです。

基本テンプレートを作成する

・基本テンプレートは各ページを拡張するための最も基本的なテンプレート。

blog/templates/blog/以下にbase.htmlファイルを作成します。

blog
└───templates
    └───blog
            base.html
            post_list.html

base.htmlを基本的なテンプレート(HTML)

post_list.htmlを「投稿を一覧表示する部分」の拡張テンプレート

として作成し直します。

まず、post_list.htmlの内容を丸々コピペしてbase.htmlに貼り付け、次のように「投稿を一覧表示する部分」を削除して、代わりにblock タグで囲みます。

{% load static %}
<html>
    <head>
        <title>Django Girls blog</title>
        <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/4.3.0/css/bootstrap.min.css">
        <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/4.3.0/css/bootstrap-theme.min.css">
        <link href="http://fonts.googleapis.com/css?family=Lobster&subset=latin,latin-ext" rel="stylesheet" type="text/css">
        <link rel="stylesheet" href="{% static 'css/blog.css' %}">
    </head>

    <body>
        <div class="page-header">
            <h1><a href="/">Django Girls Blog</a></h1>
        </div>
        <div class="content container">
            <div class="row">
                <div class="col-md-8">
                {% block content %}
                {% endblock %}
                </div>
            </div>
        </div>
    </body>
</html>

最後に、post_list.htmlを以下のように書き換えます。

{% extends 'blog/base.html' %}  <!-- blog/base.htmlを拡張する -->

<!-- block content タグの中身を定義する -->
{% block content %}
    {% for post in posts %}
        <div class="post">
            <div class="date">
                <p>published: {{ post.published_date }}</p>
            </div>
            <h1><a href="">{{ post.title }}</a></h1>
            <p>{{ post.text|linebreaksbr }}</p>
        </div>
    {% endfor %}
{% endblock %}

アプリケーションを拡張する

アプリケーションを拡張しよう · Django Girls Tutorial

もう、ウェブサイトを作るのに必要な全ての章は終わりました。どのようにモデル、URL、ビュー、テンプレートを書いたら良いかわかっています(中略) さあ練習しましょう!

投稿の詳細へのテンプレートリンクを作成する

投稿リストの投稿のタイトルから投稿の詳細ページへのリンクを作るように、post_list.htmlにテンプレートリンクを追加します。

・変更前

<h1><a href="">{{ post.title }}</a></h1>

・変更後

<h1><a href="{% url 'post_detail' pk=post.pk %}">{{ post.title }}</a></h1>

・{% %}という表記はDjangoのテンプレートタグを使用していることを意味している

・post_detailの部分は、Djangoblog/urls.pyに書かれた name=post_detail のURLを待ち受ける

・pk=post.pkについては、pkはプライマリキーの略で、データベースの各レコードのユニークな名前

・post.pkと書くことによって、Postインスタンスのプライマリキーにアクセスする

投稿の詳細へのURLを作成する

post_detail ビュー用にurls.pyにURLを作成します。

最初の投稿の詳細がこのURLで表示されるようにします:http://127.0.0.1:8000/post/1/

blog/urls.pyのURLパターンを次のようにします。

urlpatterns = [
    path('', views.post_list, name='post_list'),
    path('post/<int:pk>/', views.post_detail, name='post_detail'),
]
  • post/ はURLが post に続けて / で始まることを意味する
  • <int:pk>は整数の値を期待し、その値がpkという名前の変数でビューに渡されることを意味する
  • / – それからURLの最後に再び / が必要

つまり、ブラウザにhttp://127.0.0.1:8000/post/5/を入力すると、Djangoはpost_detailというビューを探していると理解します。そしてpkが5という情報をそのビューに転送します。

投稿の詳細ビューを追加する

ビューに追加のパラメータpkが与えられるようにしたので、ビューにそれを受け取る処理を追加します。

その処理する関数をdef post_detail(request, pk):として定義します。 urls.pyで指定した名前(pk)と同名にする必要があることに注意します。

blog/views.pyに次を追加します。

def post_detail(request, pk):
    from django.shortcuts import render, get_object_or_404
    post = get_object_or_404(Post, pk=pk)
    return render(request, 'blog/post_detail.html', {'post': post})

投稿の詳細へのテンプレートリンクを作成する

blog/templates/blog/post_detail.htmlを作成します。

{% extends 'blog/base.html' %}

{% block content %}
    <div class="post">
        {% if post.published_date %}
            <div class="date">
                {{ post.published_date }}
            </div>
        {% endif %}
        <h1>{{ post.title }}</h1>
        <p>{{ post.text|linebreaksbr }}</p>
    </div>
{% endblock %}
  • {% if ... %} ... {%endif%}では、公開日(published_date)が空でないかを確認している

Djangoフォーム

Djangoフォーム · Django Girls Tutorial

Django adminを使わず、記事を追加したり編集したりできるようにします。Djangoフォームを使えば、考え得る大抵の入力フォームは作れるらしいです。

Djangoフォームは、フォームをゼロから定義できたり、フォームの結果をモデルに保存できるModelFormを作れたりします。

そのためのblog/forms.pyを作成します。

blog
   └── forms.py
from django import forms
from .models import Post

class PostForm(forms.ModelForm):

    class Meta:
        model = Post
        fields = ('title', 'text',)

フォームにおけるページへのリンク

・blog/templates/blog/base.htmlをpage-headerと名付けたdiv中に次のリンク<a href="{% url 'post_new' %}" class="top-menu"><span class="glyphicon glyphicon-plus"></span></a>を追加します

        <div class="page-header">
            <a href="{% url 'post_new' %}" class="top-menu"><span class="glyphicon glyphicon-plus"></span></a>
            <h1><a href="/">Django Girls Blog</a></h1>
        </div>

URL

blog/urls.pyに、URLパターンを追加します。

urlpatterns = [
    path('', views.post_list, name='post_list'),
    path('post/<int:pk>/', views.post_detail, name='post_detail'),
    path('post/new/', views.post_new, name='post_new'),
]

post_new ビュー

blog/views.pyにpost_newビューを追加します。

from .forms import PostForm

def post_new(request):
    form = PostForm()
    return render(request, 'blog/post_edit.html', {'form': form})

テンプレート

blog/templates/blog/post_edit.htmlファイルを作ります。フォームを動かすにはいくつかやることがあります。

  • フォームを表示する必要があります。 私たちは(例えば){{ form.as_p }} でこれを行うことができます。
  • 上記の行は HTMLのformタグでラップする必要があります:
    ...
  • Save ボタンが必要です。これをHTMLのbuttonタグで行います:
  • 最後に
    タグの開始直後に、 {% csrf_token %}を追加する必要があります。 フォームをセキュアにするためこれは非常に重要です! これを忘れると、Djangoはフォームを保存しようとすると文句を言うでしょう

フォームを保存する

blog/views.pyは今以下のようになっている。

def post_new(request):
    form = PostForm()
    return render(request, 'blog/post_edit.html', {'form': form})
  • フォームを送信したとき、request.POST にデータが追加されている(のすべてのフィールドは今 request.POST にある)

このビュー では、扱わなくてはならない2つの別々のシチュエーションがあります。

  • 1つ目は、最初にページにアクセスしてきた時で空白のフォームが必要な場合
  • 2つ目はすべてのフォームデータが入力された状態でビューに戻ってくる場合です。 したがって条件分岐を追加する必要があります(そのためにifを使います)
if request.method == "POST":
    [...]
else:
    form = PostForm()

CSRF verification failed. Request aborted.エラーが表示されるようなら、settings.pyに以下を追加します。

MIDDLEWARE_CLASSES = (
    'django.middleware.csrf.CsrfViewMiddleware',
)

フォームのバリデーション(検証)

ブログのポストは title と text のフィールドが必要です。 Post モデルではこれらのフィールドがなくてもよいとは書いておらず (published_date とは対照的に)、Djangoはその場合、それらのフィールドには何らかの値が設定されることを期待します。title と text を入力せずに保存してみましょう。何が起こるでしょうか?

「このフィールドを入力してください」と表示されます。

フォームの編集

既存のデータを編集するためのページを作成する

編集ボタンを追加するため、blog/templates/blog/post_detail.html<div class="post">部分を次のようにします。

    <div class="post">
        {% if post.published_date %}
            <div class="date">
                {{ post.published_date }}
            </div>
        {% endif %}
        <a class="btn btn-default" href="{% url 'post_edit' pk=post.pk %}">編集<span class="glyphicon glyphicon-pencil"></span></a>
        <h1>{{ post.title }}</h1>
        <p>{{ post.text|linebreaksbr }}</p>
    </div>

URLパターンを登録するため、blog/urls.pyurlpatternsに次を追加します。

path('post/<int:pk>/edit/', views.post_edit, name='post_edit'),

テンプレートは blog/templates/blog/post_edit.html を再利用するのでそのままです。

ビューは、blog/views.pyに次を追加します。

def post_edit(request, pk):
    """ 投稿を編集する """
    # 編集したいPostモデルを get_object_or_404(Post, pk=pk) で取得する
    post = get_object_or_404(Post, pk=pk)
    if request.method == "POST":
        # フォームを作るときは既存の投稿をインスタンスとして渡す
        form = PostForm(request.POST, instance=post)
        if form.is_valid():
            post = form.save(commit=False)
            post.author = request.user
            post.published_date = timezone.now()
            post.save()
            return redirect('post_detail', pk=post.pk)
    else:
        form = PostForm(instance=post)
    return render(request, 'blog/post_edit.html', {'form': form})

これでOK。

セキュリティ

誰でも新しい投稿を作成することができてしまう状態なので、管理者でログインしているユーザのみ新しい投稿ボタンを表示させるようにします。

blog/templates/blog/base.htmlのボタン部分を、次のように編集します。

{% if user.is_authenticated %}
    <a href="{% url 'post_new' %}" class="top-menu"><span class="glyphicon glyphicon-plus"></span></a>
{% endif %}

この{% if %}は、ページをリクエストしているユーザーがログインしている場合にのみ、リンクがブラウザに送信されるようにします。 これは新しい投稿の作成を完全に保護するものではありませんが、それは良い第一歩です。 私たちは拡張レッスンでより多くのセキュリティをカバーします。

(完全に保護するものではないのか……セキュリティ的に何が問題あるんだろう。)

編集ボタンも同様にします。

blog/templates/blog/post_detail.htmlの編集ボタン部分を、次のように編集します。

{% if user.is_authenticated %}
     <a class="btn btn-default" href="{% url 'post_edit' pk=post.pk %}"><span class="glyphicon glyphicon-pencil"></span></a>
{% endif %}

これでOK。

シークレットウィンドウでページを読み込むと、リンクが表示されず、アイコンも表示されなくなります。

チュートリアルは以上です。

次のステップは?

次のステップは? · Django Girls Tutorial

感想

Djangoのモデル、ビュー、テンプレート、フォームの概念の説明がわかりやすかった。

AtCoderSort

オブジェクト群を何かの値でソートしたいとき、それはともかくAtCoderのレートで先にソートするソート手法。

コード

# coding: utf-8
class Person():
    
    def __init__(self, rate, height, age, name):
        self.rate = rate      # レート
        self.height = height  # 身長
        self.age = age        # 年齢
        self.name = name      # 名前


def AtCoderSort(p, key, key_reverse=False):
    """ 
    (1) ソートしたい属性(key)を指定する
    (2) レートの高い順にソートする
    (3) レートが等しい場合、(1)で指定した属性でソートする
    """
    from operator import attrgetter
    p = sorted(p, key=attrgetter(key), reverse=key_reverse)
    return sorted(p, key=lambda person: person.rate, reverse=True)


def solve():
    N = 5
    p = [None] * N
    p[0] = Person(1700, 170, 17, "Alice")
    p[1] = Person(1000, 170, 18, "Bob")
    p[2] = Person(2000, 150, 17, "Carol")
    p[3] = Person(2800, 160, 17, "Eve")
    p[4] = Person(1000, 150, 17, "Frank")

    print("--ソート前--")
    for i in range(N):
        print("{}, {}, {}, {}".format(p[i].rate, p[i].height, p[i].age, p[i].name))
    
    print("--背の小さい順でAtCoderSortする--")
    sorted_p = AtCoderSort(p, "height")
    for i in range(N):
        print("{}, {}, {}, {}".format(sorted_p[i].rate, sorted_p[i].height, sorted_p[i].age, sorted_p[i].name))

    print("--年齢が高い順でAtCoderSortする--")
    sorted_p = AtCoderSort(p, "age", key_reverse=True)
    for i in range(N):
        print("{}, {}, {}, {}".format(sorted_p[i].rate, sorted_p[i].height, sorted_p[i].age, sorted_p[i].name))
    

if __name__ == "__main__":
    solve()

結果

--ソート前--
1700, 170, 17, Alice
1000, 170, 18, Bob
2000, 150, 17, Carol
2800, 160, 17, Eve
1000, 150, 17, Frank
--背の小さい順でAtCoderSortする--
2800, 160, 17, Eve
2000, 150, 17, Carol
1700, 170, 17, Alice
1000, 150, 17, Frank
1000, 170, 18, Bob
--年齢が高い順でAtCoderSortする--
2800, 160, 17, Eve
2000, 150, 17, Carol
1700, 170, 17, Alice
1000, 170, 18, Bob
1000, 150, 17, Frank

参考URL

B: AtCoderでじゃんけんを - AtCoder Regular Contest 048 | AtCoder