utamaro’s blog

誰かの役に立つ情報を発信するブログ

Djangoのモデルに定義していないフィールドを追加する方法

こんな感じのModelを定義しているときに、追加でフィールドを入れたくなったときがありました。

class Sample(models.Model):
    uuid = models.UUIDField(unique=True)
    created_at = models.DateTimeField(auto_now_add=True)
    modified_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = 'sample'

いれたフィールドをtemplateで使用して画面に表示するといった具合で使いました。

↓のコードで対応することができました。

model.__setattr__('icon', 'icon/' + sample.author.username)

iconを表示する際にusernameの前にフォルダ名が必要だったのです。

ですが、Djangoのタグを使っていて文字列の連結ができないし、カスタムタグで拡張することもできない。(少なくとも私には見つけられませんでした。)

最初はmodelはorderdictだからmodel['icon']で対応できると思ったのですが、ダメでした。

Djangoでページングを表示するための関数

Djangoでページング処理を作成する際に作った関数を紹介します。

コードの紹介になるので、紹介はコードにコメントにて行います。

前後のページを表示するための関数

# self.limit: データ表示数
def page_data(self, count, page):
    # count: データ数
    # page: 現在のページ番号
    current_page = page
    if count == 0 or count < self.limit:
        # データ数が0の場合に対応
        prev_page = 1
        next_page = 1
        page_limit = 1
    else:
        # ページの最後尾の数値をみつける。
        page_num = int(count / self.limit)
        # 割り切れない場合は+1して、あまりを表示できるようにする
        page_limit = page_num if count % self.limit == 0 else page_num + 1

        # 前のページが1未満にならないようにする
        prev_page = 1 if (current_page - 1) < 1 else current_page - 1
        # 次のページがページ番号の限界を超えないようにする
        next_page = page_limit if (current_page + 1) > page_limit else current_page + 1
    return {
        'prev': prev_page,  # 前のページ
        'current': current_page,  # 現在のページ
        'next': next_page,  # 次のページ
        'limit': page_limit,  # ページの限界
        'next_page': 'home',  # {% url next_page %}のように使用する
    }

テンプレートのコードをなくしてしまったので紹介できません。orz

returnで返しているデータからhtmlタグを作成するとできます。

範囲でページを表示するための関数

もう少しスマートに作成する方法がありそうです。

def get_page_list(self, data_size, current_page):
    PAGE_RANGE_SIZE = 5  # ページ番号を表示する数
    if data_size % self.limit == 0:
        page_limit = int(data_size / self.limit)
    else:
        # 割り切れない場合は+1して、あまりを表示できるようにする
        page_limit = int(data_size / self.limit) + 1
    page_list = []
    # ↓ startとendを計算して、あとでループに使用する
    start = current_page - 2
    if start < 1:
        start = 1

    end = start + PAGE_RANGE_SIZE
    if end > page_limit:
        end = page_limit + 1
        # ↓ を入れないとページ番号がずれる
        start = end - PAGE_RANGE_SIZE
        if start < 1:
            start = 1

    # ページ番号を取得する
    for pi in range(start, end):
        page_list.append(pi)
    return {
        'current': current_page,  # 現在のページ
        'limit': page_limit,  # ページ番号の限界
        'nums': page_list,  # ページ番号のリスト
        'next_page': 'home',  # {% url next_page %}のように使用する
    }

テンプレートを作成する。

bulmaというcssフレームワークを使用していますが、基本は同じかと思います。

横に長くなってしまって見づらいです。どうにかしたいところです。

<ul class="pagination-list">
    {% if page.current != 1 %}
    <!-- 現在のページが先頭なら表示しない -->
    <li><a class="pagination-link" href="{% url page.next_page %}?{%query_transform request page=1%}"><<</a></li>
    {% endif %}
    {% for num in page.nums %}
    <li>
        <!-- 現在のページの場合はクラスを追加する -->
        <a class="pagination-link {% if num == page.current %} is-current {% endif %}" href="{% url page.next_page %}?{%query_transform request page=num%}">{{num}}</a>
    </li>
    {% endfor %}
    {% if page.current != page.limit %}
    <!-- 現在のページが最後尾なら表示しない -->
    <li><a class="pagination-link" href="{% url page.next_page %}?{%query_transform request page=page.limit%}">>></a></li>
    {% endif %}
</ul>

query_transformというのは独自に作ったtagです。

これを使うと、クエリストリングをスマートに作成できます。

@register.simple_tag
def query_transform(request, **kwargs):
    updated = request.GET.copy()
    for k, v in kwargs.items():
        updated[k] = v

    return updated.urlencode()

これはどこかの記事で紹介していた気がします。

stackoverflowで載っていたものを使っています。

pythonでjwtを使う際に作ったutilについて

作成したjwtのutilを紹介します。

作成したutilですが、まだまだ修正途中のものです。使用する場合は注意してください。

jwtについての詳細についてはこちらを参照してください。

https://jwt.io/introduction/

コードと解説の距離が離れると読みづらいと思いますので、できる限りコメントを書きました。

そちらを見てください。

import datetime
from datetime import datetime, timedelta

import jwt
import pytz


class JwtContent(object):

    JWT_EXPIRE_DAY = 1

    JWT_ALGORITHM = "HS256"

    JWT_SECRET_KEY = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

    JWT_COOKIE_NAME = "Authentication"

    COOKIE_EXPIRE_FORMAT = "%a, %d-%b-%Y %H:%M:%S GMT"


class JwtUtil(object):

    @staticmethod
    def create_token(payload, is_revoke=False):
        """
        payloadを使用してjwtトークンを発行する
        :param is_revoke: Trueにするとtokenを有効期限切れにする
        :param payload: dict object
        :return: jwtでエンコードされたトークン
        """
        timezone = pytz.timezone('Asia/Tokyo')
        now = datetime.now(tz=timezone)
        if is_revoke:
            delta = timedelta(days=-JwtContent.JWT_EXPIRE_DAY)
        else:
            delta = timedelta(days=+JwtContent.JWT_EXPIRE_DAY)
        result = now + delta
        payload["exp"] = result
        encode_data = jwt.encode(
            payload,
            JwtContent.JWT_SECRET_KEY,
            algorithm=JwtContent.JWT_ALGORITHM
        )
        return encode_data.decode('utf-8')

    @staticmethod
    def is_expired_token(jwt_token):
        if jwt_token is None:
            return True, None, None
        try:
            decode_data = jwt.decode(
                jwt_token,
                JwtContent.JWT_SECRET_KEY,
                algorithms=[JwtContent.JWT_ALGORITHM]
            )
            return False, decode_data, None
        except jwt.ExpiredSignatureError as e:
            # 有効期限切れ
            return True, None, e
        except jwt.InvalidTokenError as e:
            # decodeが実行できなかった
            return True, None, e
        except Exception as e:
            return True, None, e

    @staticmethod
    def is_expired_session_token(request, cookie_name):
        """
        requestからトークンのクッキーを取り出してそのクッキーの有効期限を判定する
        :param cookie_name:
        :param request:
        :return: 成功時デコードされたデータ(dict object). 失敗時True
        """
        jwt_token = request.COOKIES.get(cookie_name)
        return JwtUtil.is_expired_token(jwt_token)

    @staticmethod
    def set_jwt_cookie(response, jwt_cookie_data, cookie_name):
        """
        responseオブジェクトにjwtトークンキーをセットする
        jwtのexpireはcommon.config.JWT_EXPIRE_DAYによって変わる
        また、タイムフォーマットはAsia/Tokyoのゾーンとしている
        セキュリティのため、httponlyをtrueにしている
        :param response:
        :param jwt_cookie_data:
        :return:
        """
        timezone = pytz.timezone('Asia/Tokyo')
        expire_datetime = datetime.now(tz=timezone) + timedelta(days=+JwtContent.JWT_EXPIRE_DAY)
        expire_datetime_str = expire_datetime.strftime(
            JwtContent.COOKIE_EXPIRE_FORMAT
        )
        response.set_cookie(
            cookie_name,
            jwt_cookie_data,
            expires=expire_datetime_str, httponly=True
        )
        return response

    @staticmethod
    def delete_jwt_cookie(response, cookie_name):
        response.delete_cookie(cookie_name)
        return response

データが存在しない場合にデータを追加するSQL

テーブル内にデータが存在しない場合に限って、データを追加する方法を紹介します。

クエリについては日本語で解説をするよにも、まずはコードを見たほうが早いと思うので、先に載せます。

こちらが、データを追加するクエリです。

INSERT INTO tag (
    name,
    created_at,
    modified_at
)
SELECT
    'test',
    now(),
    now()
WHERE
    NOT EXISTS (
        SELECT
            1
        FROM
            tag
        WHERE
            name = 'test'
    )

上記のクエリは、tagを追加する際に、すでにタグが存在していたら追加しないというクエリです。

これは、select insert文というものです。そして、not existを使って、存在しない場合を条件で追加しています。

where 1で真になるので、データがある場合でinsertが実行されます。

おまけ

一般的なinsert文はこのようなクエリだと思います。

insert into tag (
    xxxx,
    yyyy,
    zzzz
) values (
    1,
    2,
    3
)

じゃあ、このクエリにwhereをつければよいのでは?と考えるかもしれませんが、それは構文エラーになります。

一応クエリを載せておきます。

INSERT INTO tag (
    name,
    created_at,
    modified_at
) values (
    'test',
    now(),
    now()
)
WHERE
    NOT EXISTS (
        SELECT
            1
        FROM
            tag
        WHERE
            name = 'test'
    )

pythonを使ってcsvのファイルを生成する方法

pythonを使ったcsvのファイルを生成する方法を紹介します。

この記事では、csvの読み込みは扱いません。csvファイルを作成する方法を紹介しています。

csvを扱うための便利なライブラリをインストールします。

pip install pandas

csvを読み書きするためにpandasをインストールします。

pandasをインストールするとnumpyもインストールされます。

csvファイルを生成する関数を作成します。

output_csv_root = 'csv/files'
def gen_csv(dataset, file_name):
    df = pd.DataFrame(dataset)
    file_path = os.path.join(output_csv_root, file_name + '.csv')
    df.to_csv(file_path)

gen_csvを実行すると、{output_csv_root}/{filename}.csvが作成されます。

datasetに指定する値は、二次元の配列を指定します。

[[1,2,3], [4,5,6]]

これがこうなります。

#, 1, 2, 3
1, 1, 2, 3
2, 4, 5, 6

一行目の#, 1, 2, 3は見出しです。

この見出しは、自分で指定することもできます。

指定する場合は、dataFrameを作成する際にcolumnsを指定します。

df = pd.DataFrame([rows], columns=['A', 'B', 'C'])

また、行番号が必要ない場合は、このようにindexをFalseにします。

df.to_csv(file_path, index=False)

mecabを使って解析する方法

mecabのインストール方法等はいろいろなサイトに書かれているので、pythonのプログラミング部分を紹介します。

意外と実装について書かれてる記事が少なくてびっくりしました。なので、help()を使って使い方を試しました。

以下の環境で作業しています。

mac osX
brewでmecabとmecab-ipadicをインストール
mecab-python3
mecab-ipadic-neologd

mecabインスタンスにはparseToNode(self, *args)parseToString(self, *args)があります。

このうち、parseToString(self, *args)は文字列にパースされるので少し使いづらいです。

なので、parseToNode(self, *args)を使用します。

mecab = MeCab.Tagger('-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd')
novel_data = novel_list.get(0)
node = mecab.parseToNode('Google Analytics(グーグルアナリティクス)は、Googleが無料で提供するWebページのアクセス解析サービス。')
while node:
    surface = node.surface
    feature = node.feature
    print(surface, feature)
    node = node.next

このようなデータを取得できます。

 BOS/EOS,*,*,*,*,*,*,*,*
Google Analytics 名詞,固有名詞,一般,*,*,*,Google Analytics,グーグルアナリティクス,グーグルアナリティクス
( 記号,括弧開,*,*,*,*,(,(,(
グーグルアナリティクス 名詞,固有名詞,一般,*,*,*,Google Analytics,グーグルアナリティクス,グーグルアナリティクス
) 記号,括弧閉,*,*,*,*,),),)
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
、 記号,読点,*,*,*,*,、,、,、
Google 名詞,固有名詞,一般,*,*,*,Google,グーグル,グーグル
が 助詞,格助詞,一般,*,*,*,が,ガ,ガ
無料 名詞,一般,*,*,*,*,無料,ムリョウ,ムリョー
で 助詞,格助詞,一般,*,*,*,で,デ,デ
提供 名詞,サ変接続,*,*,*,*,提供,テイキョウ,テイキョー
する 動詞,自立,*,*,サ変・スル,基本形,する,スル,スル
Webページ 名詞,固有名詞,一般,*,*,*,Webページ,ウェブページ,ウェブページ
の 助詞,連体化,*,*,*,*,の,ノ,ノ
アクセス解析 名詞,固有名詞,一般,*,*,*,アクセス解析,アクセスカイセキ,アクセスカイセキ
サービス 名詞,サ変接続,*,*,*,*,サービス,サービス,サービス
。 記号,句点,*,*,*,*,。,。,。
 BOS/EOS,*,*,*,*,*,*,*,*

surfaceは単語を取得でき、featureではその解析結果が取得できます。

結果を見るとわかるかと思いますが、BOS/EOS,*,*,*,*,*,*,*,*というのが出てきています。

これを除きつつ、データを使いやすいように取得します。

def main():
    mecab = MeCab.Tagger('-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd')
    node = mecab.parseToNode('Google Analytics(グーグルアナリティクス)は、Googleが無料で提供するWebページのアクセス解析サービス。')

    node_list = []
    while node:
        surface = node.surface
        feature = node.feature
        c = feature.split(',')[0]
        if not c == 'BOS/EOS':
            node_list.append({
                'surface': surface,
                'feature': feature,
            })
        node = node.next
    for n in node_list:
        print(n)

if __name__ == '__main__':
    main()

実行結果がこちらです。

{'surface': '', 'feature': '名詞,固有名詞,一般,*,*,*,Google Analytics,グーグルアナリティクス,グーグルアナリティクス'}
{'surface': '\t', 'feature': '記号,括弧開,*,*,*,*,(,(,('}
{'surface': 'グーグルアナリティクス', 'feature': '名詞,固有名詞,一般,*,*,*,Google Analytics,グーグルアナリティクス,グーグルアナリティクス'}
{'surface': ')', 'feature': '記号,括弧閉,*,*,*,*,),),)'}
{'surface': 'は', 'feature': '助詞,係助詞,*,*,*,*,は,ハ,ワ'}
{'surface': '、', 'feature': '記号,読点,*,*,*,*,、,、,、'}
{'surface': 'Google', 'feature': '名詞,固有名詞,一般,*,*,*,Google,グーグル,グーグル'}
{'surface': 'が', 'feature': '助詞,格助詞,一般,*,*,*,が,ガ,ガ'}
{'surface': '無料', 'feature': '名詞,一般,*,*,*,*,無料,ムリョウ,ムリョー'}
{'surface': 'で', 'feature': '助詞,格助詞,一般,*,*,*,で,デ,デ'}
{'surface': '提供', 'feature': '名詞,サ変接続,*,*,*,*,提供,テイキョウ,テイキョー'}
{'surface': 'する', 'feature': '動詞,自立,*,*,サ変・スル,基本形,する,スル,スル'}
{'surface': 'Webページ', 'feature': '名詞,固有名詞,一般,*,*,*,Webページ,ウェブページ,ウェブページ'}
{'surface': 'の', 'feature': '助詞,連体化,*,*,*,*,の,ノ,ノ'}
{'surface': 'アクセス解析', 'feature': '名詞,固有名詞,一般,*,*,*,アクセス解析,アクセスカイセキ,アクセスカイセキ'}
{'surface': 'サービス', 'feature': '名詞,サ変接続,*,*,*,*,サービス,サービス,サービス'}
{'surface': '。', 'feature': '記号,句点,*,*,*,*,。,。,。'}

いい感じです。

使うときはforでループして、dictからデータを取得すると良いでしょう。

もしくはこのようにデータを追加して、tupleで使用してもよいと思います。

node_list.append(
    (surface, feature,)
)

使用するときはこんな感じです。

for s, f in node_list:
    print(s, f)

DjangoでCSRFを含んだリクエストをtagを使って実行する方法

CSRFトークンを含んだAPIの実行方法で、ドキュメントに載っているような方法ではなく、tagを使った方法を紹介します。

tagを使った方法というのは、{% cookie 'csrftoken' %}でhtml内にtokenを入れて、その値を使う方法です。

修正が必要な箇所は以下の3箇所です。

Djangoの対応

まずは、テンプレートから使用できるようにtagを作成します。

作成する場所は、プロジェクトディレクトリがある場所にtemplate_tags/tags.pyというファイルを作成しました。

@register.simple_tag(takes_context=True)
def cookie(context, cookie_name):  # could feed in additional argument to use as default value
    request = context['request']
    result = request.COOKIES.get(cookie_name, '')  # I use blank as default value
    return result

次にsettings.pyを編集します。

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR,  'templates')],
        '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',
            ],
            'libraries': {  # ←これを追加
                'tags': 'template_tags.tags',
            }
        },
    },
]

tagのファイルを読み込むようにしています。

これでDjango側の対応は終わりです。

html(template)の対応

Djangoで使用するtemplate html内に以下のように書きます。

<meta name="csrf" content="{% cookie 'csrftoken' %}">

最終的にhtmlのmetaタグにcsrfを追加して、jsで読み込んで使用します。

<meta name="csrf" content="W3ni7NdZeaPWYgfoRShxFVNLHCimZWxpZsL8QMJ4VtlBguv9lo7NoYGO7Gl0eD0D">

javascriptの対応

javascriptの対応も簡単です。

function call(callback) {
    $.ajax({
        url:'/api/hoge',
        type:'POST',
        beforeSend: function(request) {
            let csrftoken = $("meta[name='csrf']").attr('content')
            request.setRequestHeader("X-CSRFToken", csrftoken);
        },
        data:{
            'limit': 100,
        }
    })
    .done(function(data) {
        console.log(data);
        callback(data);
    })
    .fail(function(data) {
        console.log(data);
    })
}

この部分を追加すると対応が可能です。

beforeSend: function(request) {
    let csrftoken = $("meta[name='csrf']").attr('content')
    request.setRequestHeader("X-CSRFToken", csrftoken);
},

また、状況に合わせて以下のコードを入れると良いとおもいます。

csrfが必要なリクエストと、不要なリクエストを分けて、必要ならcsrftokenありでリクエストするものです。

function csrfSafeMethod(method) {
    // these HTTP methods do not require CSRF protection
    return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
function ajaxInit() {
    $.ajaxSetup({
        beforeSend: function(xhr, settings) {
            let csrftoken = getCookie('csrftoken');
            if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
                xhr.setRequestHeader("X-CSRFToken", csrftoken);
            }
        }
    });
}

tagを使う方法はstackoverflowで見つけました。

python - Django read cookie in template tag - Stack Overflow

失敗パターン

csrf_tokenで対応しようとして失敗

Djangoではformでリクエストを実行する際に{% csrf_token %}というものを使えます。

これを使えばtoken値を上記みたいなコードを使わずに簡単に実装できるのではと考えました。

metaタグにcsrf_tokenを入れれると思ったのですが…

<meta name="csrf" content="{% csrf_token %}">

{% csrf_token %}が展開されると<input>タグが作成されるのでダメでした。

画面上に/>という文字列が表示されていたり、domがめちゃくちゃになったり(body直下にheadがあった)しました。

あえなく断念しました。