nikkie-ftnextの日記

イベントレポートや読書メモを発信

Django Girls Tutorial修了者が投票アプリチュートリアルでDjangoの理解を深めました(その3, 4編)

この記事は、Django Advent Calendar 2018 5日目の記事です。

Django公式チュートリアル(投票アプリ作成)その3・その4に取り組んで得た気づきをアウトプットします。

はじめに

いつも心は虹色に! nikkieです。
Django公式ドキュメントのチュートリアル(投票アプリ)で手を動かした後のアウトプットが眠っていたので、アドベントカレンダーを機に消化することにしました。
その3(ビューとテンプレート)とその4(汎用ビューに書き直し)について扱います。
(この記事は、その5以降を進めるための足掛かりにする目的もあります。)

nikkieについて

アドベントカレンダーのリンクから来られた方向けに簡単に自己紹介します。

  • Pythonは始めて1年過ぎたくらい(執筆時点)
  • 趣味の開発や業務でWeb周りを触ってきた(Flask, Django
  • 2018年5月〜Django Girls Tutorialの翻訳・レビュー、2018年10月〜コーチ

チュートリアルの概要

はじめての Django アプリ作成、その 1 | Django documentation | Django から始まる7回構成のチュートリアルです。
作るアプリの名前をとって「投票アプリチュートリアル」と呼ぶことにします。
なお、私のDjangoレベルは「Django Girls Tutorial修了」です。
Django Girls Tutorialを終えてから投票アプリチュートリアルに取り組んだことで、Djangoへの理解が深まったと感じています。
また、このアウトプットでDjango Girls Tutorialと投票アプリチュートリアルを少しでも繋ぐことができれば嬉しいです。

その1・2についてのアウトプットはこちらです:
前回同様に今回も長くなってしまったので、拾い読みも推奨します。

動作環境

その3:ビューとテンプレート

はじめての Django アプリ作成、その 3 | Django documentation | Django

その3ではURLconf、テンプレート、HttpResponse(とショートカット)について、理解が深まりました。

URLconf

URLパターンは、URLを単に一般化したものです。
(中略)URLconf はURLパターンをビューにマッピングします。

プロジェクト(mysite)のsettings.pyには

ROOT_URLCONF = 'mysite.urls'

と指定されています。
これにより、mysite/urls.pyがロードされるのだと気づきました。

テンプレート

ビューにページのデザインがハードコード1されている問題を解決するために、テンプレートが導入されました。2

プロジェクト(mysite)のsettings.pyには

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True, # ここがポイント
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

と指定されています。
この指定により、INSTALLED_APPSのそれぞれのtemplatesディレクトリがテンプレートの検索対象になるそうです。

polls/templates/index.htmlではなく、polls/templates/polls/index.htmlと配置する理由は、テンプレートに名前空間を与えるためと知り、なるほどと思いました。

HttpResponse

Django Girls Tutorialでは意識してこなかったのですが、ビューはHttpResponseが肝と認識しました。

Django にとって必要なのは HttpResponse か、あるいは例外です。

先のハードコードの例で、テンプレートなしでもHttpResponseを返せばいいことが分かります。
テンプレートを返す場合は、以下の2つが必要です。

  1. loader.get_template()でテンプレートを取得する
  2. テンプレートに渡す変数を辞書で定義する(コンテキストと呼ばれる)

テンプレートの返却はHttpResponse(template.render(context, request))のようになります。

これはrender()を使って簡潔に書くことができます。
Django Girls Tutorialで見かけたrender()3が裏でやっていることはHttpResponseを返すことなのですね。

▼render()を使った質問一覧画面:
f:id:nikkie-ftnext:20181203234416p:plain:w400

モデルの取得についても複数手順からなるので、ショートカットget_object_or_404()が用意されています。
裏でやっていることは、

  1. getでオブジェクトを取得してみる
  2. オブジェクトが存在しない場合、Http404エラーを上げる
  3. オブジェクトが存在する場合、取得したオブジェクトをテンプレートに渡して返却(render()関数)

です。

▼質問詳細画面:
f:id:nikkie-ftnext:20181203234552p:plain:w400
▼質問詳細画面で404エラーが返るケース:
f:id:nikkie-ftnext:20181203234606p:plain

(再度)テンプレート

question.question_textのようなドットでつなげた構文にも検索順序があることが分かりました。

  1. 辞書のキーとして検索
  2. 属性として検索
  3. リストインデックスとして検索

今度はリンクがハードコードされています。
これは{% url %}を使って解決します。
このタグで使われるのは、path()のname引数です。4
f:id:nikkie-ftnext:20181204213336p:plain:w400

index.html{% url 'detail' question.id %}を使うことで、polls/urls.pypath('specifics/<int:question_id>/', views.detail, name='detail'),と変更しても、テンプレートは変更不要となりました5
f:id:nikkie-ftnext:20181204213248p:plain:w400

(再度)URLconf

テンプレートに名前空間がありましたが、URLconfにも名前空間が登場します。
それがpolls/urls.pyapp_nameです。 (app_name = 'polls'
名前空間の指定後は、index.htmlのテンプレートタグは{% url 'polls:detail' question.id %}となります。

その3についての疑問点

Djangoが採用している「カップリング」という概念は何を意味するのか?

Django の最も大きな目標の一つは、ルーズカップリングの維持にあります。

その4:汎用ビューに書き直し

はじめての Django アプリ作成、その 4 | Django documentation | Django

その4ではフォームを扱った後に、その3以降で追加した部分を汎用ビューで書き直します。
コードの内容が非常に簡単になって、「処理を全然書いてないけど、いいの?」と衝撃を受けました。

フォーム

残っている投票機能を実装します。
フォームを使った処理のポイント(Djangoに限らない)を学びました。

  • サーバのデータを更新するフォームには、 method="post"を使う
  • 自サイト内をURLに指定したフォームは、クロスサイトリクエストフォージェリ対策をする(Djangoでは{% csrf_token %}
  • POSTされたデータの扱いが成功した後は常に HttpResponseRedirect を返す(戻るボタン押下で再度データが送られることを防ぐため)

polls/views.pyのコードについて

  • try except else 参考:https://docs.python.jp/3/tutorial/errors.html#handling-exceptions

    else 節を設ける場合、全ての except 節よりも後ろに置かなければなりません。 else 節は try 節で全く例外が送出されなかったときに実行されるコードを書くのに役立ちます。

  • selected_choiceにget_object_or_404を使わないのは、エラー時にHttp404ではなくquestionを使ってrenderをしているからと考えた

  • URLをreverseで構築している
  • vote{{ choice.votes|pluralize }}でchoice.votesの値に応じて複数形のsの出し分けをしている

ここまでで投票と結果表示が実装されました。
▼投票画面:
f:id:nikkie-ftnext:20181204213558p:plain:w400
▼結果表示画面:
f:id:nikkie-ftnext:20181204213622p:plain:w400
▼選択肢を選んでいないときのエラー表示:
f:id:nikkie-ftnext:20181204213651p:plain:w400

汎用ビュー

一通り実装したコードを汎用ビューで書き直しました。6

汎用ビューとは、よくあるパターンを抽象化して、 Python コードすら書かずにアプリケーションを書き上げられる状態にしたものです。

手順

  1. URLconfのアップデート(汎用ビューを使う上での命名規則を適用)
  2. 古いビューの削除(index, detail, results)
  3. 汎用ビューで実装(ListViewとDetailView)

polls/views.pyの変更が劇的すぎます!

polls/views.py(Before)

from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.urls import reverse

from .models import Choice
from .models import Question


def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    context = {'latest_question_list': latest_question_list}
    return render(request, 'polls/index.html', context)

def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})

def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/results.html', {'question': question})

def vote(request, question_id):
    # 変更がないため省略

polls/views.py(After)

from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.urls import reverse
from django.views import generic # 追加

from .models import Choice
from .models import Question


class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        return Question.objects.order_by('-pub_date')[:5]


class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'


class ResultsView(generic.DetailView):
    model = Question
    template_name = 'polls/results.html'


def vote(request, question_id):
    # 変更がないため省略

汎用ビューにはmodeltemplate_nameを指定しているだけです。
質問一覧画面については、コンテキスト変数の指定や、質問の取得方法の指定もしています。

終わりに

今回はここまでです。
次回、その5(テスト)からのアウトプットでお会いしましょう。
最後までお読みいただき、ありがとうございました。


  1. PyConJP 2018 の懇親会でSwall0wTech さんから聞いたハードウェアの話を思い出しました。ハードコードしたソフトウェアはハードウェアだそうです(ソースはClean Architectureと記憶)

  2. この部分、リスト内包表記をさらっと使って、output = ', '.join([q.question_text for q in latest_question_list])となっています。このあたりDjango Girls Tutorialとのギャップを感じるところです。

  3. render()Djangoビュー · Django Girls Tutorial に登場します。

  4. 次の箇所の理解が深まりました:https://docs.djangoproject.com/ja/2.1/intro/tutorial01/#path-argument-name

  5. リンク先が<a href="/polls/specifics/1/">What's up?</a>のように自動で生成されています

  6. 汎用ビューもショートカットと呼ばれるようです。render()と同じ呼ばれ方ですね。ショートカットは便利ですが、「使わずに書くとどう動くか」を知っていると、より使いこなせるんじゃないかと思います。

イベントレポート&LT報告 | #pyladiestokyo 4周年パーティ(2018/10開催)

この記事は、PyLadies Advent Calendar 2018 4日目の記事です。

PyLadiesとのあれこれを共有しましょうー
PyLadies参加したことないよっていう方も性別問わずウェルカムです(´∇`)ノ

ということで、2018年10月に参加したPyLadies Tokyo - 4周年記念パーティについて、眠っていたメモをもとにレポートします。

はじめに

いつも心は虹色に! nikkieです。
10月のPyLadies Tokyo 4周年記念パーティについてレポートします。
レポートは2つの内容で構成されます。

  • 印象的だったLTの紹介
  • 自分のLTについて

nikkieについて

アドベントカレンダーのリンクから来られた方向けに簡単に自己紹介します。

  • stapy 4代目LT王子(男性です)
  • Pythonは始めて1年過ぎたくらい(執筆時点)
  • PyLadiesとはうっすらとしたつながり(2018/10~ Django Girls Workshopのコーチ)

イベントの概要

PyLadies Tokyo - 4周年記念パーティ - connpass

この10月でPyLadies Tokyoは最初のMeetup開催から丸4年を迎えます。
設立4周年をみんなでお祝いするために、持ち寄り形式 & LT形式で4周年記念パーティーを開催します!
(略)
本イベントは男性も参加できます!

お祝い&アウトプットの機会ということで、LTに手を挙げました。

当日の様子は、以下のリンクからツイートで確認できます。(2018/10/08の #pyladiestokyo ツイートを検索)
https://twitter.com/search?f=tweets&vertical=default&q=%23pyladiestokyo%20since%3A2018-10-08%20until%3A2018-10-09&src=typd&lang=ja

印象的だったLT

私の主観でLTを紹介していきます。

Data Classesをつかってみよう(どりらんさん)

https://nbviewer.jupyter.org/format/slides/github/drillan/PyLadiesTokyo04LT/blob/master/slides.ipynb#/

データクラスを詳しく解説されています。(毎回情報が詰まっていて、素晴らしいと思っています)
スライドを元に手を動かせば、データクラスは一通り理解できそうです。

Pythonと数学と多面体とペーパークラフト(はむかずさん)

衝撃を受けたのが、数学の理論を取り入れたハサミ!

PDF生成ライブラリは初めて知りました。
PDF生成 (ReportLab) | Python-izm

はむかずさんの関連エントリ
ペーパークラフト準正多面体 – はむかず!

Seleniumクローラーを作っていて詰まったこと(あおいさん)

スクレイピングツールSeleniumやSplashの話を聞けて勉強になりました。
(BeautifulSoupだとJSで描画する要素が取得できなくて、Seleniumを知ったところでした)
Splash参考:[Python]ヘッドレスブラウザSplashと共にスクレイピングをしたメモ - Qiita

LTリンク

LT: 翻訳は人のためならず

Django Girls Tutorialは、2018年夏にDjango2.0.x系に対応しました!
翻訳に参加することは他の人のためだけでなく自分のためになることを学んだので、学んだこと3点を共有しました。

  1. Djangoの使い方
    • 何度も手を動かしたことで機能追加する流れが見えてきました。
    • テンプレート(既存の変更) →URL→モデル→ビュー→テンプレート(新規追加)
  2. Django Girls Tutorialに精通
  3. コミュニティの力
    • Django2.0.x系対応を成し遂げたのは、一人ひとりが少しずつ力を出し合った結果と考えています

LTの補足

交流タイムに聞かれた内容を元に補足します。

Django Girls Tutorialには、Django1系に対応した日本語版があります。
2018年5月のDjango Congressにて2.0.x版Tutorialの日本語翻訳プロジェクトが立ち上がりました。

翻訳では、Django1系対応の日本語版チュートリアルをベースにしています。(新規に一から翻訳する必要はなかったわけです)
はじめてプログラミングをする方向けに書かれているので、「英語→原文に沿った日本語→わかりやすい日本語」と翻訳+言い回しの推敲を行いました。

翻訳に参加しましたが、私はDjangoの上級者というわけではなく、Django Girls Tutorialを1回やった初学者です。
また、ドキュメントなどで英語を読むのには慣れていますが、聞く・話すはあまり得意ではありません。
それでも翻訳はできる(しかも自分の勉強にもなる!)ので、「やってみたいな」という方は一歩踏み出していただけたらと思います。

感想

LTと交流タイムが交互にあり、美味しい軽食をつまみながら交流という、非常に楽しい時間でした。
(例年パーティの持ち寄りには皆さん気合が入るらしいです)
N周年パーティが気になるという男性の皆さん、来年はぜひ会場でお会いしましょう 笑
また、「Django Girls Workshopに参加された方と5周年パーティーで会えたら素晴らしいな」と思います。
4周年パーティの運営者、登壇者、参加者の皆さま、どうもありがとうございました。

なお、PyLadiesパーティは技術書典5と重なっていました。
朝から技術書典に並び、パーティに移動、パーティ後は有志で技術書典巡りと満喫した1日でした。
(kaizumakiさん、kumappp27さん、技術書典までありがとうございました!)

おわりに

4周年パーティでPyLadiesのイベントに初めて参加しましたが、「実はもっと前のテストコードの回からつながりがあったんだな」とレポートを書いてみて気づきました。
今後もイベント資料の公開を楽しみにしています^ ^

今後は各地を巡るPyLadies Caravanが企画されており、「Django Girlsとタイアップできたら素敵だな」と思っています。
初回は福岡です:PyLadies Caravan in 福岡 - connpass

PyLadies Advent Calendar 2018 5日目は空いているようです(投稿時点)ので、アウトプットのネタがある方はぜひどうぞ!

最後までお読みくださり、ありがとうございました。

Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 2/3) セキュリティ編

はじめに

いつも心は虹色に! nikkieです。
Django Girls Tutorial ExtensionsのDjango2系動作確認の第2弾です。
第1弾はこちら:Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 1/3) #モグモグDjango - nikkie-ftnextの日記
先日の #モグモグDjango で確認したことをブログにまとめました。

背景

Django Girls Tutorial Extensionsでは、Django Girls Tutorialで作ったブログアプリに機能追加していきます。
ExtensionsのコードはDjango 1系で書かれており、Django Girls Tutorialの翻訳メンバーの間では「Django 2系に書き換えたいね」という話が出ていました。

Django 2系に書き換える対象は、3ページあります。

今回はセキュリティのチュートリアルを進める中で気づいたことを書きます。

ブログアプリで何が問題か

Django Girls Tutorialの中で、ログインしているユーザだけに投稿作成ボタンや編集ボタンが見えるようにしました。
参照:Djangoフォーム · Django Girls Tutorial の「セキュリティ」

これは新しい投稿の作成を完全に保護するものではありませんが、それは良い第一歩です。 私たちは拡張レッスンでより多くのセキュリティをカバーします。

確かに画面上にボタンが見えなくなったのですが、URLを推測すると記事が作れてしまうという問題があります。1
ログインしていないユーザでhttp://127.0.0.1:8000/post/new/にアクセスすると、記事の作成画面が表示されます。
f:id:nikkie-ftnext:20181110200323p:plain

セキュリティ対応

ログインが求められるように修正

URLを推測されても記事が作成できないようにする対応はそれほど大変ではありませんでした。
ビューの関数に@login_requiredを設定すると、ログインが求められるように変更できます。

blog/views.py

from django.contrib.auth.decorators import login_required # 追加
from django.shortcuts import redirect
# 省略

@login_required # 追加
def post_new(request):
    # 省略
    
@login_required # 追加
def post_edit(request, pk):
    # 省略

@login_required # 追加
def post_draft_list(request):
    # 省略

@login_required # 追加
def post_publish(request, pk):
    # 省略

@login_required # 追加
def post_remove(request, pk):
    # 省略

この時点ではログイン画面のテンプレートが用意されていないため、ログインしていないユーザで、http://127.0.0.1:8000/post/new/(記事作成のURL)にアクセスするとエラーとなります。
f:id:nikkie-ftnext:20181110200403p:plain
ですが、誰でも記事が作れる状態ではひとまず修正されました。

ログイン画面を用意する

ログイン画面用のURLの設定は、プロジェクト側で(mysite/urls.pyで)行います。

# 省略
from django.contrib.auth import views # 追加

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/login/', views.login, name='login'), # 追加
    path('', include('blog.urls')),
]

ログイン画面のテンプレートを追加しましょう。 blog/templatesの下にregistrationというフォルダを作り、login.htmlを置きます。
ログイン機能はDjangoに用意されたものを使うために、命名規則を守る必要があると理解しました。

blog/templates/registration/login.html

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

{% block content %}
  {% if form.errors %}
    <p>Your username and password didn't match. Please try again.</p>
  {% endif %}

  <form method="POST" action="{% url 'login' %}">
    {% csrf_token %}
    <table>
      <tr>
        <td>{{ form.username.label_tag }}</td>
        <td>{{ form.username }}</td>
      </tr>
      <tr>
        <td>{{ form.password.label_tag }}</td>
        <td>{{ form.password }}</td>
      </tr>
    </table>

    <input type="submit" value="login">
    <input type="hidden" name="next" value="{{ next }}">
  </form>
{% endblock %}

テンプレートで使われているform.username.label_tagは、HTMLのlabel要素を作る関数だそうです。
https://docs.djangoproject.com/en/2.0/topics/forms/#rendering-fields-manually

ログインしていないユーザで、http://127.0.0.1:8000/post/new/(記事作成のURL)にアクセスすると、以下のようなログイン画面が表示されます!
(ただし、画像はログイン画面に直接アクセスしたときのものです。)
f:id:nikkie-ftnext:20181110200336p:plain

ログイン画面周りでは、あと少しだけコードを修正する必要があります。
ログイン画面のURL(http://127.0.0.1:8000/accounts/login/)を直接叩いてログインした場合、ログインしたあとにページが見つからずにエラーとなります。
f:id:nikkie-ftnext:20181124162627p:plain

この対応はmysite/settings.pyで行います。
末尾に以下の1行を追加しました。

LOGIN_REDIRECT_URL = '/'

この設定により、ログイン画面のURLを直接叩いた場合、ログインに成功後、記事一覧(http://127.0.0.1:8000/)が表示されます。

ログイン・ログアウトのためのリンクを追加

ログイン機能を使うためのリンクを追加します。

blog/templates/blog/base.html

    {% if user.is_authenticated %}
      <a href="{% url 'post_new' %}" class="top-menu"><span class="glyphicon glyphicon-plus"></span></a>
      <a href="{% url 'post_draft_list' %}" class="top-menu"><span class="glyphicon glyphicon-edit"></span></a>
    {% else %} <!-- 追加 -->
      <a href="{% url 'login' %}" class="top-menu"><span class="glyphicon glyphicon-lock"></span></a> <!-- 追加 -->
    {% endif %}

ログインしていない場合、ヘッダーにログイン画面に遷移するリンク(鍵のアイコン)が表示されます。 f:id:nikkie-ftnext:20181110200452p:plain

ログアウトの機能も実装します。

mysite/urls.py

# 省略

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/login/', views.login, name='login'),
    path('accounts/logout/', views.logout, name='logout', kwargs={'next_page': '/'}), # 追加
    path('', include('blog.urls')),
]

blog/templates/blog/base.html

    {% if user.is_authenticated %}
      <a href="{% url 'post_new' %}" class="top-menu"><span class="glyphicon glyphicon-plus"></span></a>
      <a href="{% url 'post_draft_list' %}" class="top-menu"><span class="glyphicon glyphicon-edit"></span></a>
      <p class="top-menu">Hello {{ user.username }} <small>(<a href="{% url 'logout' %}">Log out</a>)</small></p> <!-- 追加 -->
    {% else %}
      <a href="{% url 'login' %}" class="top-menu"><span class="glyphicon glyphicon-lock"></span></a>
    {% endif %}

ログインユーザの情報とログアウトのリンクがヘッダーに表示されるようになりました。
f:id:nikkie-ftnext:20181110195444p:plain

書き換えなくてもDjango2.0.9で動きました

ここまででDjango2系向けに書き換えたのは、urlpatternsのurl関数です(path関数にしました)。 Django2系でもurl関数は使えるので、Extensionsのコードのままでも動きました。
参考:Django 1.11 と 2.0 の違い (「現場で使える 基礎 Django」本の補講その1) - akiyoko blog

django.conf.urls.url() を使った書き方は、Django 2.0 でもそのまま使えるように互換性が保たれていますし、(後略)

mysite/urls.py

from django.conf.urls import url # 追加
from django.contrib import admin
from django.urls import path, include

from django.contrib.auth import views # 追加


urlpatterns = [
    path('admin/', admin.site.urls),
    url(r'^accounts/login/$', views.login, name='login'), # 追加
    url(r'^accounts/logout/$', views.logout, name='logout', kwargs={'next_page': '/'}), # 追加
    path('', include('blog.urls')),
]

mysite/urls.pyをDjango2系向けにアップデート

この記事を書く中で記述の食い違いに気づきました。

  • 原文(Django 1系):django.contrib.auth.views.login
  • セキュリティのバートで参照されているドキュメント2の書き方:django.contrib.auth.login

結論を言うと、mysite/urls.pyを以下のように書き換えた方がよいことがわかりました。

# 省略
urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/login/', views.LoginView.as_view(), name='login'), # 変更
    path('accounts/logout/', views.LogoutView.as_view(next_page='/'), name='logout'), # 変更
    path('', include('blog.urls')),
]

これはDjango 2系で動く書き方です。
LoginView()を使う書き換えをしないと、Django 2.1.xで動かないと考えています。3
django.contrib.auth.views.loginを使った書き方は、Django 2.0.xまでで動く書き方で、2.1.xでは動かないという認識です)

LogoutView.as_view()では、引数にログアウト後に表示されるページ(記事一覧)のURLを渡しています。4
mysite/settings.pyにLOGOUT_REDIRECT_URL = '/'を指定することでも同じ動きが実現できましたが、Django Adminからログアウトした場合も記事一覧に遷移するという副作用がありました。

なお、django.contrib.auth.loginを使う書き方はビュー向けであるようで、importしてからpath('accounts/login/', login, name='login')のように使うとエラーとなりました。(TypeError: login() missing 1 required positional argument: 'user'

終わりに

Extensionsでログイン機能を触ったことで、Djangoでアプリケーションが作れそうな気がしてきました。

そして、残るはコメント機能。
Django2系の動作確認最終回でまたお会いしましょう。
最後までお読みいただき、ありがとうございました。

脚注


  1. PythonAnywhereで公開されているDjango Girls Tutorialのブログアプリは第三者が記事を作れる状態なので、なるべくExtensionsのセキュリティのパートまで進めていただくのを個人的には推奨します。

  2. ドキュメントのURLのうち、Djangoのバージョンだけ2.0に変えて参照しました:https://docs.djangoproject.com/en/2.0/topics/auth/default/#how-to-log-a-user-out

  3. django.contrib.auth.views.loginのソースを見たところ、'The login() view is superseded by the class-based LoginView().'RemovedInDjango21Warning)と記載されていました。Django 2.1.xではLoginView()を使う書き方に置き換わるようです。→リリースノートを確認したら削除されていました:https://docs.djangoproject.com/ja/2.1/releases/2.1/#features-removed-in-2-1

  4. コード例:Django 1.9 から 2.1 に上げる時の備忘録 - Qiita

「The macOS installation couldn't be completed」にOS再インストールで対応しました

はじめに

いつも心は虹色に! nikkieです。
先日MacBook Proに問題が発生し、なんとか解決したので、備忘録がてら残します。

発生した問題

Macを再起動したら「The macOS installation couldn't be completed」というエラーメッセージが表示されました。

このエラーメッセージから抜け出せずに弱りました。。

  • Shutdown:シャットダウン後、再度起動すると同様のエラーメッセージが表示される
  • Restart:再起動が完了すると同様のエラーメッセージが表示される

MacBook Proについて

  • macOS 10.13.x(High Sierra1
  • FileVaultを有効にしている

解決策

いったんMacをシャットダウンした後、リカバリーモード(Cmd+R押し)で起動して、Macintosh HDにOSを再インストールしました。

注意:この方法はデータが消えないとは言い切れないので、リスキーです。自己責任で実施してください。

試したこと:セーフモード起動

OS再インストールの前にセーフモード(Shift押し)起動を試しました。
先ほどのエラーメッセージは表示されませんでしたが、プログレスバーが100%に達した後、画面が遷移しない事象が発生。。
やむなく(リスクを取って)電源ボタン長押しで強制終了しています。

Appleサポート曰く

Appleサポートの推奨する対応は、外付けハードディスクを用意することだそうです。

  1. 外付けハードディスクにOS再インストール
  2. 外付けハードディスクのOSで起動し、Macintosh HDのバックアップを取得する(必要なファイルのコピーをとる)
  3. Macintosh HDにOS再インストール

Macintosh HDにOSを再インストールすると、理論上はOSの部分だけが置き換わるはずですが、ユーザのデータに影響がないとは言い切れないため、外付けハードディスクの用意を推奨しているそうです。

外付けハードディスクについて

教訓:APFSでフォーマットできるものを選びましょう。

Mac向け・フォーマット不要」と謳った外付けハードディスクを用意したところ、APFS2でのフォーマットに失敗しました。
どうやらHigh SIerra以降のAPFSに対応していなかったようです。
「もういいや。まっさらでも仕事できないよりマシ」と割り切り、リスクを取って再インストールする決断ができました。

OS再インストールの副作用

Xcodeコマンドラインツールが消えていたので入れました。
xcode-select --install
現時点ではそのくらいで、ほとんどのデータは消えていないようです。

終わりに

OS再インストールで、データが消えずに解決して本当によかったです。
PCはいつ動かなくなるかわからないと痛感したので、クラウド上にデータを置くことを習慣化します。(日々の忙しさで後回しにしてしまいがちだったので要反省)
PCにグレートリセットがかかっても、問題ないと言えるようにしないといけませんね。

今回Appleサポートの方には非常に素晴らしい対応をしていただきました。
本当にありがとうございました。

OS再インストールにて「復旧サーバに接続できない」と表示される場合の対応(12/2 追記)

  1. Wi-Fi接続の確認
  2. PCの時刻の確認(リカバリーモードでもターミナルが起動できる。dateコマンドで確認する。再設定もdateコマンドを使う)

参考:macOSの再インストールで「復旧サーバに接続できませんでした。」とエラーが出たときの対処方法 - creativi.tea

参考情報


  1. おそらく10.13.6です(参考情報(1)によるとこの事象は10.13.4〜6で発生するらしいです)

  2. 詳細には立ち入りませんが、High Sierra以降のファイルフォーマットです

Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 1/3) #モグモグDjango

はじめに

いつも心は虹色に! nikkieです。
2018/11/10に参加した #モグモグDjango で、Django Girls Tutorial Extensions がDjango2系で動作するように確認しました。
ExtensionsをDjango 2系で動かす中で気づいたことをアウトプットします。

勉強会の概要

akiyokoさんの『現場で使えるDjangoの教科書』頒布をきっかけに始まった、Djangoもくもく会です。
第3回 モグモグDjango - connpass

Djangoもくもく会 モグモグDjango を開催します. Django初心者の方から,上級者の方までどなたでもご参加下さい(^^)

この会は,しーーっんとしているもくもく会ではなく,質問や分からないところがあったら,その場で聞くことができる環境にしたい!と思っています.

取り組んだこと

Django Gitls Tutorial(Djangoでブログアプリを作るチュートリアル)にはExtensionsという拡張版があります。
Introduction · Django Girls Tutorial: Extensions
9月のPyConJP Sprintにて有志で翻訳したのですが、ExtensionsはコードがDjango 1系ということが発覚していました。1
GitHub - DjangoGirlsJapan/tutorial-extensions: Additional tasks for tutorial (翻訳はWIP、また未レビューです)
Extensions翻訳を完了させるために、コードをDjango 2系に置き換えたいとメンバー間で考えており、今回のもくもく会で着手しました。

達成したこと

Django 2系に書き換える対象は、3ページあります。

このうち、機能追加とセキュリティについてDjango2系で動作確認できました

以下では、機能追加のパートを進める中で気づいたことを書いていきます。(セキュリティについては記事を分けます)

ブログアプリに機能追加

このパートで追加する機能は、大きく分けて3つです。

  • ドラフト(下書き)の一覧機能
  • ドラフトを公開する機能
  • 記事を削除する機能

前提

以下の環境で動かしています。

なお、機能追加のチュートリアルは、9月から11月までの間にコードがDjango2系に更新されていました。(確認しやすかったです。ありがとうございます!)

Djangoの機能追加手順

これまで手を動かしてきた経験を踏まえると、機能追加するには以下の順がよさそうです。2

  1. 新規ページへ遷移するリンクを追加
  2. 新規ページのURL設定
  3. 新規ページのビュー作成
  4. 新規ページのテンプレート作成

Djangoは機能追加する際に触る箇所が多いので、「機能追加手順のチェックリストを持つと、使いやすく感じられるかな」と考えています。

ドラフトの一覧機能

まず、ドラフト一覧画面に遷移するアイコンをヘッダーに追加します。
(記事が作成できるのはログインしているユーザのみとしていたので、ドラフトが見えるのもログインしているユーザに限定しています)

blog/templates/blog/base.html

<!-- 省略 -->
  <div class="page-header">
    {% if user.is_authenticated %}
      <a href="{% url 'post_new' %}" class="top-menu"><span class="glyphicon glyphicon-plus"></span></a>
      <a href="{% url 'post_draft_list' %}" class="top-menu"><span class="glyphicon glyphicon-edit"></span></a> <!-- 追加 -->
    {% endif %}
    <h1><a href="/">Django Girls Blog</a></h1>
  </div>
<!-- 省略 -->

次に、ドラフト一覧画面のURLを追加します。

blog/urls.py

urlpatterns = [
    # 省略
    path('drafts/', views.post_draft_list, name='post_draft_list'), # 追加
]

ビューの編集では2つのことを行います。

1つ目は、記事を作成・編集したときにpublished_dateを設定しないようにすること。
これにより、作成した記事が即時公開されなくなり、ドラフト扱いとすることができます。

  • 新規作成した場合、published_dateは空(ドラフト状態)
  • ドラフトの記事を編集した場合、published_dateは空のまま(ドラフトのまま)
  • 既に公開した記事を編集した場合、published_dateの値は変わらない(公開されたまま)

2つ目は、ドラフトの記事の一覧を表示するための関数を追加すること。
ドラフトの記事はpublished_datenullです。

blog/views.py

def post_new(request):
    if request.method == "POST":
        form = PostForm(request.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()
    return render(request, 'blog/post_edit.html', {'form': form})

def post_edit(request, 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})

# 以下を追加
def post_draft_list(request):
    posts = Post.objects.filter(published_date__isnull=True).order_by('created_date')
    return render(request, 'blog/post_draft_list.html', {'posts': posts})

最後に、ドラフト一覧のテンプレートを作成します。

blog/templates/blog/post_draft_list.html

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

{% block content %}
  {% for post in posts %}
    <div class="post">
      <p class="date">created: {{ post.created_date|date:'d-m-Y' }}</p>
      <h1><a href="{% url 'post_detail' pk=post.pk %}">{{ post.title }}</a></h1>
      <p>{{ post.text|truncatechars:200 }}</p>
    </div>
  {% endfor %}
{% endblock %}

このテンプレートには2つのフィルタが登場します。

  • date
    • 指定したフォーマットで日付を表示するフィルタ
    • d-m-Y:日(2桁)-月(2桁)-年(4桁)
  • truncatechar
    • 文字列の後ろを切り捨てて、指定した長さ(ここでは200文字)とするフィルタ
    • 切り捨てた文字列は...と3文字で表されるので、post.textの先頭から197文字が取り出される
    • ドキュメント記載の例 {{ "Joel is a slug"|truncatechars:9 }}Joel i...Joel iの6文字+...の3文字で合計9文字)

ここまでで、ドラフトボタンからドラフト一覧画面に遷移できます。
f:id:nikkie-ftnext:20181110195137p:plain

ドラフトを公開する機能

まず、記事の詳細画面にドラフトを公開するボタンを作ります。
(公開されていない記事=ドラフトの場合のみ、公開ボタンが見えるようにしています3

blog/templates/blog/post_detail.html

    {% if post.published_date %}
      <div class="date">
        {{ post.published_date }}
      </div>
    {% else %} <!-- 追加 -->
      <a class="btn btn-default" href="{% url 'post_publish' pk=post.pk %}">Publish</a> <!-- 追加 -->
    {% endif %}

次に、ドラフト公開に使うURLを追加します。
Extensionsの原文のまま('post/<pk>/publish/')でもビューのget_object_or_404は動きましたが、チュートリアル本編のURLの設定に合わせて'post/<int:pk>/publish/'としています。4

blog/urls.py

urlpatterns = [
    # 省略
    path('drafts/', views.post_draft_list, name='post_draft_list'),
    path('post/<int:pk>/publish/', views.post_publish, name='post_publish'),  # 追加
]

最後にビューにpost_publish関数を追加します。

blog/views.py

def post_publish(request, pk):
    post = get_object_or_404(Post, pk=pk)
    post.publish()
    return redirect('post_detail', pk=pk)

追加した関数で使われているpublishメソッドですが、これは Djangoモデル · Django Girls Tutorial で定義したものです。
Extensionsにて、伏線がようやく回収されました!

記事を公開した後は記事詳細にリダイレクトするので、新規で作成するテンプレートはありません。

以上で、ドラフト公開ボタンが付きました!
f:id:nikkie-ftnext:20181110195249p:plain

記事を削除する機能

記事の削除機能の追加は、ドラフト公開機能と同様の流れです。

記事の削除ボタンが見えるのは、ログインしているユーザのみです。

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>
      <a class="btn btn-default" href="{% url 'post_remove' pk=post.pk %}"><span class="glyphicon glyphicon-remove"></span></a> <!-- 追加 -->
    {% endif %}

削除機能の場合も、URLの設定はチュートリアル本編の記載に合わせています。

blog/urls.py

urlpatterns = [
    # 省略
    path('drafts/', views.post_draft_list, name='post_draft_list'),
    path('post/<int:pk>/publish/', views.post_publish, name='post_publish'),
    path('post/<int:pk>/remove/', views.post_remove, name='post_remove'),  # 追加
]

blog/views.py

def post_remove(request, pk):
    post = get_object_or_404(Post, pk=pk)
    post.delete()
    return redirect('post_list')

deleteDjangoモデルが持っているメソッドだそうです。

こうして、記事が削除できるようになりました!
f:id:nikkie-ftnext:20181110195316p:plain

感想

Django Girls Tutorialの懇切丁寧な解説から方向転換し、Extensionsは「気になるところは自分で調べてね」という感じです。(独り立ちを意識しているのかもしれません)
ファイルの変更箇所を明確にする意図でこの記事は詳しく書いていますが、実際に手を入れる箇所はそんなに多くはありませんでした。
下書き機能もついて、ブログアプリの機能が充実してきて嬉しい限りです^ ^

教えていただいたもの

今回アウトプットが遅くなってしまいましたが、モグモグDjango参加者、運営者の皆さま、どうもありがとうございました!
Extensionsはperfectじゃなくてもいいので、なる早でdoneに持っていく所存です。

脚注


  1. path関数ではなくurl関数が使われていたため、発覚しました。

  2. 参照 週末ログ | Django Girls Tutorial翻訳レビュー 100%到達! - nikkie-ftnextの日記 なお、機能追加にモデルが必要な場合は作成が必要です。(ビューの前に作ることになると思います)

  3. ブログを書いていて気づいたのですが、Extensionsのとおりに進めると、ログインしていないユーザでも公開できてしまうみたいですね。セキュリティのパートを終えると、ログインしているユーザだけが公開できるようになりました

  4. 原文のまま('post/<pk>/publish/')だとstrのpk(例:'1')でget_object_or_404が動きます。'post/<int:pk>/publish/'とすると、intのpk(例:1)がget_object_or_404に渡ります。

Event report | Python Web Day (additional track in #ploneconf2018 )

はじめに

いつも心は虹色に! nikkieです。
手伝っていたPlone Conf 2018併設イベント「Python Web Day」は全プログラム終了しました。
英語も織り交ぜてレポートブログに挑戦します。

Hello, Everyone! This is nikkie.
"Python Web Day" has finished all programs.
I try to write the report in Japanese and English.

前提(About nikkie)

  • ソフトウェアエンジニア3年目 (3rd year software engineer)
  • Pythonでは半年程度のWeb開発の経験あり (web development with Python about half of a year)
  • Ploneは未経験(先日vagrantで環境構築してみました)(novice about Plone. I tried creating an environment with vagrant)

  • 英語は読むことが多く、話そうとすると言葉に詰まりがちです。(I often read English. I do not speak and write much)

多くの方の力を借りて、併設イベントの手伝いをやりきれました。
深くお礼申し上げます。
With the help of many people, I was able to run Python Web Day.
Thank you very much, especially @terapyon, @takanory, @mamix1116, @kaizumaki.

イベントの概要 (About Python Web Day)

Python製のCMS Ploneについての国際カンファレンスです。
海外からの参加者に日本のエンジニアの活動を知ってもらい、交流するという狙いで併設イベントも開催されました。
Python Web Day」はWebについて話す併設イベントです。 @mamix1116さん(Django Girlsのオーガナイザー)のサポートとして、10月から手伝っていました。

#ploneconf2018 is an international conference about Plone.
We have additional tracks so participants from overseas can know activities of engineers in Japan and interact with each other.
Python Web Day is an additional Track on 7 Nov.
I was helping in the operation from October as a support of @mamix1116, the organizer of Django Girls in Japan,

オープニング

「どこから来ましたか」で盛り上がりました。
多くの方がヨーロッパから来られていて驚き!
また、多くの方が4回以上参加されているそうです。

It got excited at the topic "Where did you come from?"
So many people came from Europe!
And many people have participated more than 3 times!

Keynote: THE STATE OF PLONE

Zopeゴジラ、「Difficult to Kill」で会場大爆笑でした。
A huge laugh happened at the topic Zope = Godzilla, "Difficult to Kill".

バトンタッチしたPaulさん(Plone FoundationのChair)からPython Web Dayモデレータとして紹介いただくという素敵なサプライズもありました!
There was also a nice surprise that Paul introduced @mamix1116 and me as moderators of Python Web Day!

カンファレンス本編のトークは後ほどYouTubeに上がるそうです。
The conference talks will be uploaded to YouTube later.

以下、Python Web Dayのトークを紹介します。
I introduce talks of Python Web Day.

Build a RESTful API with the Serverless Framework

@masahitoさんによるトークです。

  • AWSではAWS API GatewayAWS LambdaでAPIを作れる(We can create a API in AWS with AWS API Gateway and AWS Lambda)
  • Serverless Frameworkを導入すると1コマンドでデプロイできる(We can deploy with 1 command using Serverless Framework)
  • プラグインを使うとデプロイしやすくなる(Plugins of Serverless Framework make deploy easier)

Azure Functionsで使ってみたいと思いました。
I want to try Serverless Framework with Azure Functions.

Building a REST API with Django and Django REST Framework

@checkpointさんによるトークです。

  • REST APIを一から作るのは大変なので、フレームワークを使う(It is hard to create a REST API from scratch, so we use a framework)
  • Django REST Frameworkを使いこなすにはSerializerが肝」と感じた(It seems for me that Serializer is important to master Django REST Framework)

Powerful geographic web framework GeoDjango

@omegaさんによるトークです。

GeoDjangoは聞いたことはありますが、触ったことはありません。
確かに地理情報を扱いやすそうです。
I have heard of GeoDjango, but I have never touched it.
It certainly seems easy to handle geographic information.

@mamix1116さんからも話を聞くことが多いので、この機に触ってみようと思います。
I will try the tutorial that I taught. (@mamix1116 loves GeoDjango)
GeoDjango Tutorial | Django documentation | Django

Sphinx customization for OGP support

@shimizukawaさんによるトークです。

Sphinx ExtensionでのOGPタグの生成ロジックを聞いて、少しOGPの理解が深まったと感じています。
I feel that my understanding of OGP has improved a bit, listening to the generation logic of OGP tags in Sphinx Extension.

Sphinxつながりで、LTにて紹介された:linenos:なども試してみたいです。
I'd like to try :linenos: introduced in LT about Sphinx.

How to make the fastest Router in Python

GitHub - kwatch/router-sample: sample Router classes in Python

@makotokuwataさんによるトークです。

  • Linear search is slow
  • Regex search is fast(ただし工夫は必要 But need some ingenuity)
  • State Machine approach is the fastest

ルータを早くするために、計算コストが小さい処理で実装するという観点は勉強になりました。
I was glad to hear about implementation with processing with a small calculation cost (for fastest router).

Micro Service Architecture with Machine Learning Application

GitHub - Swall0w/microservice-ml-demo: Micro service architecture using rest API

  • モデルをデプロイしやすくために、Flaskアプリケーションから切り出す(Separate a model from a Flask application to make deploy easier)
  • クライアントとFlaskアプリケーション間はREST API (REST API is used between a client and the Flask application)
  • FlaskアプリケーションとML Model間がgRPC (gRPC is used between the Flask application and the model)

終わりに

最後までお読みいただきありがとうございました。
今後トークのメモのリンクを追加します。(イベント準備総括ブログも書く予定です)
英語の表現に不自然なところがあったら教えていただけると嬉しいです。

Thank you for reading.
I will add a talk note link later. (I'm also planning to write an event preparation summary blog.)
I would be happy if you could tell me if there are unnatural English expressions.

イベントレポート | 第93回 #pyhack にて #ploneconf 併設イベント準備を進めました

はじめに

いつも心は虹色に! nikkieです。
定期的に参加している #pyhack で、Plone Conf 併設イベント Python Web Day の準備に取り組みました。

勉強会の概要

(第93回)Python mini Hack-a-thon - connpass

スプリントのゆるい版みたいな感じで各自自分でやりたいことを持ってきて、勝手に開発を進めています。参加費は無料です。 初めての方も常連さんもぜひご参加ください。

今回はいつもより人数が少なめでこぢんまりとした雰囲気でした。
常連が少ないのと対照的に初参加の方が多かったです。

なお、裏番組としてオープンソースカンファレンスがありました。(Plone Confのアピールも行われたそうです)
オープンソースカンファレンス2018 Tokyo/Fall - オープンソースの文化祭!

Plone Conf 情報まとめ

2018/10/14からのアップデートをまとめておきます。

カンファレンスの併設イベントは

  • 11/7:Python Web Day
  • 11/8:Database Day
  • 11/9:Frontend Day

です。
「ちょっと興味あるかも」という方、国内開催のまたとない機会です!
ぜひご来場ください。

11/5のイベントのトピックの一つ、ヘッドレス(頭がない)からGuillotina(ギロチン)だそうです。(一本取られた)

取り組んだこと

併設イベントについて、海外からの参加者に周知するためのスライドのドラフトを作成しました。
(pyhackで作成したものを別リポジトリに切り分けました。10/29-週でアップデートしていきます)

キーノートの後に少し時間をいただいて併設イベント「Python Web Day」についてお知らせします。
内容は

  • 併設イベントがあることを紹介(11/7はPython Web Day)
  • Python Web Dayの狙いを共有
  • Python Web Dayのトークラインナップを紹介

です。
私自身英語でのトークはほとんど経験がないのですが、可能な限り準備して臨みます。

移動時間に部屋の前で呼び込みに使うスライドや、タイムキープに使うスライドのドラフトも準備しました。

他の方の取り組みから

感想

今回も非常に楽しい時間でした。
今回は懇親会まで参加したのですが、笑い話から考えさせられる話まで盛りだくさんであっという間でした。
転職して感じていることには別の見方があるということを、懇親会という思いもよらないタイミングで気づいたのが一番の収穫です。
1日ありがとうございました!