nikkie-ftnextの日記

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

イベントレポート | #rettypy にてタイタニックハンズオンの準備に取り組みました

はじめに

明けましておめでとうございます1、nikkeです。
2019年一発目のブログはもくもく会参加レポートです。

勉強会の概要

Pythonもくもく自習室 #17 @ Rettyオフィス 2019書き初め - connpass

週末の昼下がりに、Pythonに関するやりたいことを各自持ち寄り、ゆるゆると「自習」を進めていく会です。
Python使いおよびエンジニアライフのオアシス&知見を身につける「自習室」としてゆるくご活用いただければと思います。

「技術とイイゴハン(主にランチ)を楽しむ」rettypy。
2018年9月のPyConJP前の会以来の参加となりました。
前回:イベントレポート | Pythonもくもく自習室 #13 あやめの分類器をAPIに組み込み、Dockerイメージ作成 #rettypy - nikkie-ftnextの日記

取り組んだこと

1/30に開催するKaggleタイタニックの生存予測ハンズオンの準備を進めました。

先週のpyhack合宿にて壁打ちし、100件超えのコメントをいただきました。(レビューをいただき誠にありがとうございます!)

コメントに優先度をつけた上で、

の2点を進めました。

達成したこと

時間は限られていますが、少しずつアップデートしていきます。

知ったこと

PyDataのオーガナイザの方から、PyDataでのタイタニックハンズオンの経験(分析が大事)をご共有いただき、ありがたかったです。

他の方の取り組みから

Djangoの静的ファイル関連

Django Girls Tutorialで追加したCSSファイルが効かないという質問がありました。2
CSSでカワイくしよう · Django Girls Tutorial

shinyorkeさんの回答から私の静的ファイルの理解が浅いと気づき、ドキュメントを確認しました。
静的ファイル (画像、JavaScript、CSS など) を管理する | Django documentation | Django
PythonAnywhereでcollectstaticする意味がうっすらとわかった気がします。

  • debugがTrueなら自動で静的ファイル配信(runserver)
  • 本番サーバはcollectstaticで静的ファイル配信ディレクトリに収集

runserverと本番環境とで違うことでハマりやすくなっているように感じます。
Dockerで環境用意して本番環境の設定で開発してみようと思います。

なお本番サーバでの静的ファイル配信方法も数パターンあるようです。
静的ファイルのデプロイ | Django documentation | Django

  • アプリと同じサーバから配信するケース(Apacheの設定例あり)
  • 専用サーバから配信するケース
  • クラウドサービスやCDNから配信するケース(S3の例あり)

感想

「ハンズオンのTAやってもいいですよ」と立候補してくださった方がいらっしゃって、大変嬉しかったです。
Django関係でDjango Girls Tutorialを紹介いただき、参加した方のもくもくがはかどったのでしたら、翻訳に参加した身としてこんなに嬉しいことはありません。

1日ありがとうございました。


  1. 勉強会登壇という大きなアウトプットが控えており、その準備を優先していました。だいぶ間が空いてしまったと反省しており、アウトプットの敷居を下げて習慣を取り戻そうと思います。

  2. runserverで動かす際、テンプレートやCSSを追加したときにそれが見つからない場合があります。Ctrl+Cで一度止め、runserverを再度実行することで解決します。質問者の方は再実行で解決したそうです。

Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 3/3) コメント機能 <後編>

※1本の記事として書き上げましたが、記事が長くなりすぎたため、前編・後編に分けています。

ブログからコメントを作成できるようにする(承前)

コメント入力画面へ遷移するように設定(承前)

AttributeErrorへの対応として、blog/views.pyadd_comment_to_post関数を用意します。

from django.shortcuts import render, get_object_or_404
from django.utils import timezone
from .models import Post
# importの行は以下の1行のみ変更
from .forms import PostForm, CommentForm # CommentFormのimportを追加

# これまでに作成した関数は変更なし

# 以下の行を追加
def add_comment_to_post(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if request.method == "POST":
        form = CommentForm(request.POST)
        if form.is_valid():
            comment = form.save(commit=False)
            comment.post = post
            comment.save()
            return redirect('post_detail', pk=post.pk)
    else:
        form = CommentForm()
    return render(request, 'blog/add_comment_to_post.html', {'form': form})

Djangoフォーム · Django Girls Tutorial でやったように、comment = form.save(commit=False)で、コメント用フォームに結びつけられたデータからデータベースに保存するためのコメントのオブジェクトを生成しています。1
ForeignKeyのpostについて、comment.post = postとして、コメントが紐づく記事を設定しています。2
(記事のidを渡すのでなく、記事そのものを渡すと、Django側で処理してくれるようです。)

add_comment_to_post関数が追加されたので、記事詳細画面が表示されるようになります。(URL例:http://localhost:8000/post/1/
f:id:nikkie-ftnext:20181229182441p:plain

コメント入力フォーム追加

ところが、追加した「Add comment」ボタンをクリックすると、TemplateDoesNotExistというエラーが表示されます。
f:id:nikkie-ftnext:20181229182510p:plain

「Add comment」ボタンをクリックすると、先ほど追加したadd_comment_to_post関数が呼び出されます。
関数の中で、render(request, 'blog/add_comment_to_post.html', {'form': form})と、blog/add_comment_to_post.htmlテンプレートを表示するように設定しているのですが、そのテンプレートが見つからずエラーとなっています。
blog/add_comment_to_post.htmlテンプレートを作りましょう。

blog/templates/blog/add_comment_to_post.html

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

{% block content %}
  <h1>New comment</h1>
  <form method="POST" class="post-form">{% csrf_token %}
    {{ form.as_p }}
    <button type="submit" class="save btn btn-default">Send</button>
  </form>
{% endblock %}

add_comment_to_post.htmlpost_edit.htmlテンプレートと非常に似ています。
テンプレートが追加されたので、「Add comment」ボタンをクリックした後のエラーが消え、コメントが書けるようになりました。

▼ログインしていなくてもコメントが書けます f:id:nikkie-ftnext:20181229182522p:plain

▼追加されました f:id:nikkie-ftnext:20181229182533p:plain

コメントの承認機能と削除機能をつける

コメント承認機能・コメント削除機能追加

ここまででブログの記事に誰もがコメントを書け、そのコメントは誰でも見られるようになりました。
ここでは、ブログにログインしているユーザが承認したコメントに限って、誰でも見られるように修正します。

記事詳細画面の変更内容

  • ログインしているユーザは、すべてのコメントが見られる:(1)
  • ログインしているユーザは、コメントの承認または削除ができる3:(2)
  • ログインしていないユーザは、承認されたコメントのみを見られる:(3)

(1)〜(3)ができるように、記事詳細画面のテンプレートを編集します。

blog/templates/blog/post_detail.html

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

{% block content %}
  <div class="post">
    <!-- 省略 -->
  </div>
  <hr>
  <a class="btn btn-default" href="{% url 'add_comment_to_post' pk=post.pk %}">Add comment</a>
  {% for comment in post.comments.all %}
    {% if user.is_authenticated or comment.approved_comment %} <!-- 追加 (1)と(3)に対応 -->
    <div class="comment">
      <!-- div class="date" を変更。(2)に対応 -->
      <div class="date">
        {{ comment.created_date }}
        {% if not comment.approved_comment %}
          <a class="btn btn-default" href="{% url 'comment_remove' pk=comment.pk %}"><span class="glyphicon glyphicon-remove"></span></a>
          <a class="btn btn-default" href="{% url 'comment_approve' pk=comment.pk %}"><span class="glyphicon glyphicon-ok"></span></a>
        {% endif %}
      </div>
      <!-- div class="date" の変更 終わり -->
      <strong>{{ comment.author }}</strong>
      <p>{{ comment.text|linebreaks }}</p>
    </div>
    {% endif %} <!-- 追加 -->
  {% empty %}
    <p>No comments here yet :(</p>
  {% endfor %}
{% endblock %}

ログインしているユーザの場合、user.is_authenticatedTrueなので、すべてのコメントが表示されます。
そのうち、承認されていない各コメントには削除ボタンと承認ボタンが表示されます。
ログインしていないユーザの場合、comment.approved_commentがTrueになる(すなわち、承認済みの)コメントが表示されます。
承認済みなので、コメントに削除ボタンと承認ボタンは表示されません。4

記事詳細画面を表示すると、NoReverseMatchエラーです。 f:id:nikkie-ftnext:20181229182552p:plain
comment_remove(とcomment_approve)というURLが設定されていないために発生しているので、blog/urls.pyを編集します。

from django.urls import path
from . import views

urlpatterns = [
    # これまで作成したpath()関数に変更はなし
    # リストの末尾の要素として以下の2行を追加する
    path('comment/<int:pk>/approve/', views.comment_approve, name='comment_approve'),
    path('comment/<int:pk>/remove/', views.comment_remove, name='comment_remove'),
]

AttributeErrorにより、サーバが起動しなくなりました。
f:id:nikkie-ftnext:20181229182610p:plain

blog/views.pycomment_approve関数(とcomment_remove関数)が見つからないために発生しているので、これらの関数を追加します。

from django.shortcuts import render, get_object_or_404
from django.utils import timezone
# importの行は以下の1行のみ変更
from .models import Post, Comment # Commentのimportを追加
from .forms import PostForm, CommentForm

# これまでに作成した関数は変更なし

# 以下の行を追加
@login_required
def comment_approve(request, pk):
    comment = get_object_or_404(Comment, pk=pk)
    comment.approve()
    return redirect('post_detail', pk=comment.post.pk)

@login_required
def comment_remove(request, pk):
    comment = get_object_or_404(Comment, pk=pk)
    comment.delete()
    return redirect('post_detail', pk=comment.post.pk)

comment.post.pkでコメントが紐づく記事のpk(ID)を取得できます。
Djangoシェルでの確認

>>> from blog.models import Comment
>>> comment = Comment.objects.get(id=1)
>>> comment.post
<Post: 初投稿:dockerで環境構築>
>>> comment.post.pk
1

これでエラーは解決し、ログインするとコメントの承認/削除を行うことができます。
f:id:nikkie-ftnext:20181229182627p:plain

ログインしていないユーザには、承認したコメントだけが表示されます。
f:id:nikkie-ftnext:20181229182640p:plain

記事一覧に表示されるコメント数を承認されたコメント数に変更する

最後に、記事一覧のコメント数を承認されたコメントの数に変更します。
(ログインの有無にかかわらず、承認されたコメント数の表示となります)

変更するのはテンプレートとモデルの2箇所です。
まず、blog/templates/blog/post_list.htmlを変更します。

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

{% block content %}
  {% for post in posts %}
    <div class="post">
      <div class="date">
        <p>published: {{ post.published_date }}</p>
      </div>
      <h1><a href="{% url 'post_detail' pk=post.pk %}">{{ post.title }}</a></h1>
      <p>{{ post.text|linebreaksbr }}</p>
      <!-- 以下の1行だけを変更しています。変更前:<a href="{% url 'post_detail' pk=post.pk %}">Comments: {{ post.comments.count }}</a> -->
      <a href="{% url 'post_detail' pk=post.pk %}">Comments: {{ post.approved_comments.count }}</a>
    </div>
  {% endfor %}
{% endblock %}

コメントの数はPostモデルのapproved_commentsメソッドで取得した数としています。
しかし、approved_commentsはまだ実装されていないため、この時点ではコメント数には何も表示されません。
そこで、blog/models.pyを変更します。
Postモデルにapproved_commentsメソッドを追加します。

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


class Post(models.Model):
    # プロパティには変更がないため省略

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

    # 以下のメソッドを追加するだけ
    def approved_comments(self):
        return self.comments.filter(approved_comment=True)
    
    def __str__(self):
        return self.title


class Comment(models.Model):
    # 変更がないため省略

approved_commentsメソッドでは、前編冒頭のCommentモデルのForeignKey設定で、post.commentsと書けるように設定したことを利用しています。
追加したメソッドをDjangoシェルで試してみます。

>>> from blog.models import Post
>>> post = Post.objects.get(id=1)
>>> post.approved_comments()
<QuerySet [<Comment: フォームからコメントを作りました!>, <Comment: 承認のテストです>]>
>>> post.approved_comments().count()
2

記事一覧画面で承認されたコメントの数が表示されるようになりました!
f:id:nikkie-ftnext:20181229182657p:plain

終わりに

コメント機能、手を動かす部分は長かったですが、やっていることはDjango Girls Tutorialでやってきた機能追加手順と変わらないと思います。
Djangoで機能追加するときの一連の手順の練習という印象です。

nikkie流Django機能追加手順5

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

ExtensionsのDjango2系の動作確認はこれで完了です。
PyConJPのスプリントで協力いただいた翻訳があるので、そのレビューを進めていきます。
最後までお読みいただき、ありがとうございました。

脚注


  1. https://docs.djangoproject.com/ja/2.1/topics/forms/modelforms/#the-save-method

  2. ForeignKeyの同様の書き方として post.author = request.user がありました。ref: https://tutorial.djangogirls.org/ja/django_forms/

  3. ログインしているユーザであっても、一度承認されたコメントは削除できません。(ただし、Django Adminからは削除可能です)

  4. 一度承認されたコメントを削除できるようにする場合、この箇所を修正することになりそうです。(ログインしているユーザに限って、削除ボタンと承認ボタンを表示する)

  5. Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 1/3) #モグモグDjango - nikkie-ftnextの日記Djangoの機能追加手順」より

Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 3/3) コメント機能 <前編>

※1本の記事として書き上げましたが、記事が長くなりすぎたため、前編・後編に分けています。

はじめに

いつも心は虹色に! nikkieです。
Django Girls Tutorial ExtensionsのDjango2系動作確認の第3弾(最終回)です。
第1弾はこちら:Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 1/3) #モグモグDjango - nikkie-ftnextの日記
第2弾はこちら:Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 2/3) セキュリティ編 - nikkie-ftnextの日記
(#pyhack でもくもくして、このブログを作成しました)

コメント機能を実装したブログはこのような感じです。
f:id:nikkie-ftnext:20181229182225p:plain

背景

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

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

今回はコメント機能のチュートリアルを進める中で気づいたことを書きます。
Extensionsに取り組まれる方の参考になれば幸いです。

前提

  • macOS 10.13.6
  • Python 3.6.6
  • Django 2.0.9
  • venvで仮想環境を作って動かしています(スクショ中に「Dockerで動かしています」とありますが、今回は当てはまりません)

コメント機能追加の流れ

大きく分けると3ステップです。

  1. コメントを管理画面から入力できるようにし、画面に表示する
    • コメントのモデルを追加
    • コメントのモデル用のマイグレーション実施
    • 管理画面から操作できるように設定
    • ブログアプリにコメント表示(記事詳細画面、記事一覧画面)
  2. ブログからコメントを作成できるようにする
    • コメント用のフォーム追加
    • 記事詳細画面からコメント入力画面へ遷移するように設定
      • 記事詳細画面のテンプレートにボタンを追加
      • コメント入力画面のURL設定
      • コメント入力画面のビュー関数作成
    • コメント入力フォーム追加
  3. コメントの承認機能と削除機能をつける
    • 記事詳細画面のテンプレートにコメント承認ボタン・削除ボタンを追加
    • コメント承認機能、コメント削除機能用のURL設定
    • コメント承認用ビュー関数、コメント削除用ビュー関数作成
    • 記事一覧に表示されるコメント数を承認されたコメント数に変更する

それでは順に見ていきましょう。

コメントを管理画面から入力できるようにし、画面に表示する

コメント用のクラス追加

blog/models.pyCommentクラスを追加します。

# importの行とPostモデルの定義は省略

class Comment(models.Model):
    post = models.ForeignKey('blog.Post', on_delete=models.CASCADE, related_name='comments')
    author = models.CharField(max_length=200)
    text = models.TextField()
    created_date = models.DateTimeField(default=timezone.now)
    approved_comment = models.BooleanField(default=False)

    def approve(self):
        self.approved_comment = True
        self.save()

    def __str__(self):
        return self.text

Post(記事)とComment(コメント)の関係は

  • 1つのコメントは1つの記事につく (A)
  • 1つの記事は複数のコメントを持つ (B)

です。
commentをCommentモデルのインスタンスとすると、comment.postとしてコメントの紐づく記事にアクセスできます。(A)
postをPostモデルのインスタンスとすると、post.comment_set1として記事に紐づくコメント全体にアクセスできます。2 (B)
(B)のケースで、ForeignKeyrelated_name引数を設定することで、post.<related_name引数の値>というアクセスが可能になります。
(今回のコードではpost.commentsと書けるということです)

管理画面からコメントを作成できるようにする

新しくコメントモデルを追加した後は、マイグレーションを実施します。

$ python manage.py makemigrations blog
$ python manage.py migrate blog

管理画面からコメントが作成できるように、blog/admin.pyを編集します。

from django.contrib import admin
from .models import Post, Comment # Commentを追加

admin.site.register(Post)
admin.site.register(Comment) # 追加

f:id:nikkie-ftnext:20181229182250p:plain

コメントを表示する

管理画面から作成したコメントを表示します。
記事詳細画面、記事一覧画面の順で表示するコードを追加していきます。

blog/templates/blog/post_detail.html
(ブログを書いている途中、詳細画面に<hr>タグ(記事本文とコメントを分けるための水平線を表示)を書き忘れていることに気づきました。
以下のスクショの詳細画面には水平線が表示されていませんが、本来は表示されるはずです。)

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

{% block content %}
  <div class="post">
    <!-- 省略 -->
  </div>
  <!-- 追加 -->
  <hr>
  {% for comment in post.comments.all %}
    <div class="comment">
      <div class="date">{{ comment.created_date }}</div>
      <strong>{{ comment.author }}</strong>
      <p>{{ comment.text|linebreaks }}</p>
    </div>
  {% empty %}
    <p>No comments here yet :(</p>
  {% endfor %}
  <!-- 追加 終わり -->
{% endblock %}

{% empty %}というタグが使われていますが、これは{% for %}タグで「ループさせようとした配列が空、または存在しなかった場合に表示する文字列を指定」するものだそうです。3

コメントの本文の表示にlinebreaksというフィルタが使われています。
記事の本文の表示ではlinebreaksbrでした。<p>{{ post.text|linebreaksbr }}</p> 4
2つの挙動の違いは以下の通りです。

  • linebreaksbrは、改行を<br>タグに置き換えます5
    • Joel\nis a\n\nslugを渡すと、Joel<br>is a<br><br>slugとなりました
  • linebreaksタグは空行が続く改行を<p>タグに置き換え、改行を<br>タグに置き換えます6
    • Joel\nis a\n\nslugを渡すと、<p>Joel<br>is a</p><p>slug</p>となりました

記事詳細画面にコメントが表示されるようになりました!
f:id:nikkie-ftnext:20181229182308p:plain

コメントにスタイルを設定します。

blog/static/css/blog.css

/* これまでのスタイル設定の下に追加 */
.comment {
  margin: 20px 0px 20px 20px;
}

スタイルを設定するとコメントが右に寄ります。
f:id:nikkie-ftnext:20181229182337p:plain

続いて、記事一覧画面に、各記事に紐づくコメントの数を表示します。

blog/templates/blog/post_list.html

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

{% block content %}
  {% for post in posts %}
    <div class="post">
      <div class="date">
        <p>published: {{ post.published_date }}</p>
      </div>
      <h1><a href="{% url 'post_detail' pk=post.pk %}">{{ post.title }}</a></h1>
      <p>{{ post.text|linebreaksbr }}</p>
      <!-- 以下の1行を追加するだけです -->
      <a href="{% url 'post_detail' pk=post.pk %}">Comments: {{ post.comments.count }}</a>
    </div>
  {% endfor %}
{% endblock %}

post.comments.countで各記事のコメント件数が取得されます。
Djangoシェルで試してみました。

>>> from blog.models import Post
>>> post = Post.objects.get(pk=1)
>>> post.comments.count()
2

f:id:nikkie-ftnext:20181229182405p:plain

ブログからコメントを作成できるようにする

コメント用のフォームを追加

blog/forms.pyにコメント用のフォーム(CommentForm)を追加します。

from django import forms

from .models import Post, Comment # Commentのimportを追加

class PostForm(forms.ModelForm):
    # 変更はないため省略

# 以下を追加する
class CommentForm(forms.ModelForm):

    class Meta:
        model = Comment
        fields = ('author', 'text',)

コメント入力画面へ遷移するように設定

記事詳細画面からコメント入力画面へ遷移するように設定していきます。
Django URL · Django Girls Tutorial 以降で経験したように、エラーを出しながら1つ1つ解決していきます。

記事詳細画面のテンプレートに、コメント入力画面へ遷移するボタン(aタグ)を追加します。
blog/templates/blog/post_detail.html

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

{% block content %}
  <div class="post">
    <!-- 省略 -->
  </div>
  <hr>
  <!-- 追加 -->
  <a class="btn btn-default" href="{% url 'add_comment_to_post' pk=post.pk %}">Add comment</a>
  <!-- 追加 終わり -->
  {% for comment in post.comments.all %}
    <div class="comment">
      <div class="date">{{ comment.created_date }}</div>
      <strong>{{ comment.author }}</strong>
      <p>{{ comment.text|linebreaks }}</p>
    </div>
  {% empty %}
    <p>No comments here yet :(</p>
  {% endfor %}
{% endblock %}

記事詳細画面にアクセスすると(URL例:http://localhost:8000/post/1/)、NoReverseMatchというエラーが出ます。(図はExtensions参照)
これは、追加した箇所のhref="{% url 'add_comment_to_post' pk=post.pk %}が原因です。
add_comment_to_postというURLが未設定のために発生しています。
では、blog/urls.pyで設定しましょう。

from django.urls import path
from . import views

urlpatterns = [
    # これまで作成したpath()関数に変更はなし
    # リストの末尾の要素として以下の1行を追加する
    path('post/<int:pk>/comment/', views.add_comment_to_post, name='add_comment_to_post'),
]

すると、別のエラーAttributeErrorが現れ、runserverに失敗します。(NoReverseMatchというエラーを解決し、先に進んだと考えましょう)

f:id:nikkie-ftnext:20181229182426p:plain

blog/urls.pyに追加した行で、http://localhost:8000/post/1/comment/のようなURLに対して、blog/views.pyadd_comment_to_post関数を実行するように設定しています。
ところが、現時点ではblog/views.pyadd_comment_to_post関数が見つからないため、エラーとなっています。
では、blog/views.pyadd_comment_to_post関数を用意しましょう。

この解決は後編の記事に続きます。

脚注


  1. 「ForeignKey 定義内の related_name パラメータをセットして FOO_set の名前をオーバーライドできます」ref: https://docs.djangoproject.com/ja/2.1/topics/db/queries/#backwards-related-objects

  2. Djangoモデル の章でPostモデルにauthor = models.ForeignKey('auth.User', on_delete=models.CASCADE)と定義しています。これにより、me = User.objects.get(username='ftnext')がauthorとなる記事はme.post_set.all()で取得できます。(Djangoシェルで確認する場合は、まずfrom django.contrib.auth.models import Userを実行してください)

  3. https://docs.djangoproject.com/ja/2.1/ref/templates/builtins/#for-empty

  4. https://tutorial.djangogirls.org/ja/template_extending/

  5. https://docs.djangoproject.com/ja/2.1/ref/templates/builtins/#linebreaksbr

  6. https://docs.djangoproject.com/ja/2.1/ref/templates/builtins/#linebreaks

イベントレポート | #imas_hack にてPythonでアイドルのシルエットをアスキーアートっぽくしました

はじめに

いつも心は虹色に! nikkieです。
忙しい年の瀬ではありますが、アイマスハッカソンに参加してきました。
作りたいプロダクトのアイデアを一通り試すことができ、他の方の取り組みからエンジニアという副業をアイマスでハックするという視点に気づくことができました。

イベントの概要

【増枠】アイマスハッカソン2018 - connpass

プロデューサーの皆様のあふれんばかり愛を表現する場として今回のハッカソンを企画しました。
皆様のアイドルへの愛を表現する場として、 また同僚のアイドルへの愛を感じる場として、是非お楽しみください!

2016年の初回から存在は知っていたアイマスハッカソン
バリバリ活動されている方の中に入ることに参加前は不安も感じていました。
プロデューサーとして愛はあれども副業メインすぎるためです。
参加してみたところ、不安は杞憂で、1日楽しめました。

プロデューサーとしてのnikkie

取り組んだこと

ニコニコ動画で見た、アイドルのシルエットのアスキーアートをプログラムから自動生成することに取り組みました。

▼こんなアスキーアートを作りたい f:id:nikkie-ftnext:20181223003240j:plain

シルエットの切り出しについては、前日に触っています:イベントレポート | 第94回 #pyhack にてDjango Girls Tutorial Extensionで手を動かし、Background Removal APIを触りました - nikkie-ftnextの日記
シルエットが切り出された前提で、アスキーアートのように画像を加工することに取り組みました。

  • 色付きの四角と白い四角を並べて表現することにする(まずMVPを作る)
  • 色付きの四角はアイドルのイメージカラーとする2
  • 四角に置き換える際は、以前取り組んだモザイクアートのプログラムが流用できそう
  • まずシルエットを2色に減色する。四角で置き換えるため格子状に区切り、どちらの色が多いかで、色付きの四角で置き換えるか、白い四角で置き換えるか判断する

成果物

f:id:nikkie-ftnext:20181224232020p:plain

ソースコードはこちら:
20181223 #imas_hack · GitHub

開発環境

  • Python 3.6.6
  • jupyter==1.0.0
  • matplotlib==3.0.2
  • numpy==1.15.4
  • Pillow==5.3.0
  • scikit-learn==0.20.2

notebookのポイント

画像の原色処理には以下を使います。

Pillowを使う理由は、キャラクターを抜き出した画像の仕様によります。
remove.bg で抜き出しているのですが、背景の部分の画素はA(alpha 透過度)が0に設定されているだけで、RGBの色情報を持ちます。
次のQiita記事を試したところ、背景込みで2色に原色した画像ができました:【機械学習】scikit learnでの画像減色プログラム - Qiita
(なお、KMeansでのクラスタリングでRGBの値のみを使うのは妥当と考えています)
元の画像のAの情報を保持する必要があるようなので、opencvから使い慣れたPillowにスイッチしました。
減色後に元の画像と同様のAの値を設定することで背景を消すことができました。

減色処理に使う画素の範囲についても2通り考えられます。
一つは背景込みで全部の画素を使う方法、もう一つはA=0となっている背景の画素を除く方法です。
現時点ではどちらを使うかは画像によるという結論です。
ソースコード箱崎星梨花のケースでは全部の画素を使ったところイマイチだったので、背景の画素を除きました)

numpyでの画像の扱いについて

  1. Pillowで画像を読み込み、RGBAのタプルからなるリストを作る(画像の縦と横の情報を消し、一列に並べた状態):getdata
  2. 1のリストの各要素からRGBの値のみ取得して新たなリストを作る(このRGBのデータをクラスタリングに使う。ここで背景の画素を除く場合もある)
  3. 2のRGBのリストを行列に変換(2で画素を除いていない場合、サイズ:(画像の縦 × 横, 3チャネルRGB))
  4. 3の行列をKMeansに渡してクラスタリング。2色を見つける
  5. 3の行列について各画素がどちらの色になるか判定する。このとき1で読み込んだ画素のAの情報を追加する(2で画素を除いていない場合、サイズ:(画像の縦 × 横, 4チャネルRGBA)という行列ができる)
  6. 画像を表す状態に行列のサイズを変換する:reshape
  7. 行列からRGBAの画像に変換する:fromarray

5ではnp.uint8という型を指定する必要がありました。(これを指定しないと画像が表示されません)
また5でnp.append()を使って一次元配列の末尾に要素を追加するのにハマりました。
参考:配列末尾へ要素を追加するNumPyのappendの使い方 - DeepAge

第三引数を指定しないと第一、第二引数で指定された配列のshapeとは関係なく一次元配列が生成されます。

2色に減色した画像を色付きの四角と白い四角で作成する処理は、過去に作ったモザイクアートのプログラムをベースにしています。

  • 画像を5×5の格子に分割(余りが出ないようにするためにサイズが5の倍数になるようにcropしておく)
  • 減色処理のうちどちらの色が多いか数える。ここで多い方の色で塗ると減色した2色からなるモザイク調になる
  • 塗り方の指定をアイドルのイメージカラーと白色に変更(ここは人の目で見て対応させています)

改良点はいくつかありますが、アイデアが実現できたのでハッカソンでの進捗には満足しています。

  • ノートブックを複製しているので、パッケージ化する
  • 減色する色を3−4色に増やし、この色はイメージカラーの四角、この色は白い四角というように人の目で判断する
  • 四角以外の図形を導入してみる

あにべんLT再演しました

副業メインでも愛を叫びたかったので、5分LTしました。
「もう時間がないんです」を差し込んでもらえたり、春香カラーのサイリウム降っていただけたり、発表してよかったです。

他の方の取り組みから

今回参加して気づいたのはエンジニアという副業をアイマスでハックする視点です。
具体的には、

  • 新しい技術を触るときにアイマスへのコントリビュートを兼ねた開発をする(例:Vue.jsを触ってアイマスの〇〇を作る)
  • 開発体験をアイマスでハックする(テストデータにアイドルの名前を使うことでノッて開発できる)

の2つで、「個人開発の中に取り入れたいな」と思っています。

皆さんの取り組みは非常に興味深くて、

  • 曲同士の似た部分を判定
  • Dockerコンテナの名前をアイマスっぽくする

などなど、成果物が気になる取り組みがたくさんありました。
また、コードが書けることはコントリビュートに必須ではないことも分かりました。
Microsoft Flow」などノンコーディングなツールでカバーできるため、大事なのはコントリビュートしたい強い気持ちと言えそうです。

知った中で気になったものを挙げておきます。

感想

技術で真剣に遊ぶのはやっぱりいいですね。
もくもく会で少しずつでも積み上げてきたから、今回やりたいことが一通り実現できたのだと考えています。
全力で遊んで少しはできるようになった減色処理やnumpyでの画像の扱いが、エンジニアとしての幅を広げることにもなると考えています。

参加者は文脈を共有しているので、LTはめちゃくちゃ盛り上がりました。
LT中にアップルパイつまみ食いしちゃったり4、マイニング始めたりと、字面だけ見たらカオスすぎですが、楽しませていただきました。
LTを盛り上げられるのも、歴戦のプロデューサーだからこそだと思います。

会場スポンサーのSpeeeさん、オフィスの蔵書量がすごかったです。
図書館になっていて、同じ背表紙の色のオライリー本で各段が埋まっていて圧巻でした。
同じスペースでOSS Daysというイベントを定期的に開催されているそうで、またお邪魔してみたいです。

参加者、運営者、スポンサーの皆さま、1日どうもありがとうございました。
本当に楽しかったので、今後も参加していきたいです。


  1. いわしまんさんのブログ記事 【イベント】WEBエンジニア勉強会#10にお邪魔してきた話 - Rのつく財団入り口 より。ハッカソンに飛び込むのにこの言葉に背中を押していただきました。ありがとうございます。(次回お会いしたら、直接お礼をお伝えしよう)

  2. アイドルのイメージカラー - アイマスDB(情報まとめ)

  3. https://kotobank.jp/word/RDF-49

  4. 「アップルパイ・プリンセス」でアップルパイを作る - #つくりおき

イベントレポート | 第94回 #pyhack にてDjango Girls Tutorial Extensionで手を動かし、Background Removal APIを触りました

はじめに

いつも心は虹色に! nikkieです。
2018年のもくもく会納めに #pyhack に参加してきました。

勉強会の概要

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

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

今回は、常連さんから初参加の方まで、満員御礼といった感じでした。

取り組んだこと

  • Django Girls Tutorial ExtensionのDjango2系での動作確認
  • 画像の背景を除いてくれるAPIを試す(Background Removal API

ExtensionのDjango2系での動作確認

Django Girls Tutorialの拡張版(翻訳進行中)のコメントの章Django 2.x系に書き換えたらどうなるかを検証しました。

この章に取り組むと、コメントの表示機能や入力機能、管理者が承認/削除する機能が追加されます。
Django 2.x系向けの変更点は、urls.pyをpath関数で書き換えるくらいでした。(url関数でも動くので必須ではありません)

■記事一覧画面(承認されたコメントの数が表示される)
f:id:nikkie-ftnext:20181223003117p:plain

■記事詳細画面(ログインユーザはコメントを管理できる。ログインしていないユーザもコメントが可能)
f:id:nikkie-ftnext:20181223003200p:plain

検証が終わったので、原文のコードについてPRを出しました。
今後は日本語翻訳のレビューを進めていきます。
検証時に気づいたことをブログにまとめているところです。(長くかかりそうなので次の作業へスイッチしました)

画像の背景を除いてくれるAPI

人物が写った画像(写真)の背景を消し、人物だけを取り出すAPIを最近知りました。

知ったときに思ったのは、「アニメ画像からアニメキャラクターを取り出せるのでは?」でした。

切り出したキャラクターを加工して、以下のアスキーアートをプログラムから作ろうとしています。
色付きの四角と枠線だけの四角から構成されるシルエット画像です。
(2018年のニコニコ動画でのアイドルマスターシリーズ一挙放送で見かけたアスキーアートです)
f:id:nikkie-ftnext:20181223003240j:plain

手元にモザイクアートのプログラム(Python製)があるので、これをベースに実現できると考えています。
背景を除くAPIで切り出したキャラクターだけの画像について、格子状に分割し、各領域を色で塗りつぶすのか否かを判定するというロジックで実装予定です。
(23日の某ハッカソンで試します)

このAPI(Webアプリ)で 遊んで どんな画像なら切り出せるかを検証していました。

  • キービジュアルのような大きめのアニメイラストは切り出せる(響け!ユーフォニアム、君の名は)
  • ARキャラクターは切り出せる(デレステや舞台めぐりのAR写真)
  • 私のツイッターアイコンのような顔のアップの画像からは切り出せない
    • 画像の領域を広げてみたが、今のところ成功していない

API Beta版のテストユーザを募集していたので、申込んでみました。(想定していない使い方だと思いますが、通ることを祈ってます)

他の方の取り組みから

自分のメモとしてツイートを一元化。

感想

本編から懇親会と非常に楽しい時間でした。
#pyhack を機に、ほぼ毎週のようにPythonもくもく会に参加し始めて1年が経ち、振り返ると感慨深いものがありました。
次回は冬山合宿、せっかくのまとまった開発期間なので、取り組むことを決めて参加しようと思います。
皆さま、1日ありがとうございました!

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日目は空いているようです(投稿時点)ので、アウトプットのネタがある方はぜひどうぞ!

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