utamaro’s blog

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

DjangoでSerializerを使ったバリデーション方法について

DjangoではRestApiを作成するための便利なライブラリとしてDjango REST frameworkというものがあります。

このライブラリを使うと、簡単にapiを作成することができます。

この記事ではDjango REST frameworkで用意されているserializerを使ったバリデーション方法を紹介します。

また、Django REST frameworkでviewを表示する方法についても紹介します。

views.py

views.pyではviewsetを使って実装しています。

actionを使って、getpostといったメソッドを作らないようにしました。

actionを使った場合はurlsを少し工夫する必要がありますが、それについてはurls.pyを見てください。

from django.http import JsonResponse
from django.shortcuts import render
# Create your views here.
from rest_framework import viewsets
from rest_framework.decorators import action

from apps.xxxxxxx.serializer import SampleSerializer


class SampleViewSet(viewsets.ModelViewSet):
    serializer_class = SampleSerializer

    @action(detail=False, name='sample_post_api')
    def sample_post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        if not serializer.is_valid():
            return JsonResponse({"status": 400}, status=404)
        return JsonResponse(serializer.data, status=200)

    @action(detail=False, name='sample_get_open_view')
    def sample_get(self, request, *args, **kwargs):
        return render(request, 'home.html', status=200)

saple_getではrenderというショートカットを使用してhome.htmlを表示しています。

restframeworkのサンプルでは以下のように実装されているので、以下の方法でも良いです。

    @action(detail=True, renderer_classes=[renderers.StaticHTMLRenderer])
    def highlight(self, request, *args, **kwargs):
        snippet = self.get_object()
        return Response(snippet.highlighted)

serializer.py

countというフィールドを持っています。

countは-1か1の値を受け付けます。

IntegerFieldではmin_valuemax_valueを使えますが、0は許可してほしくありません。

そういった場合はvalidation_xxxxというようにバリデーションを追加できます。

from rest_framework import serializers

class SampleSerializer(serializers.Serializer):

    count = serializers.IntegerField(min_value=-1, max_value=1)

    def validate_count(self, count):
        if count == 0:
            raise serializers.ValidationError("count is not allowed zero.")
        return count

新しいシリアライザクラスを作成したほうが良いのかもしれません。

urls.py

views.pyで作成したapiを登録します。

restframeworkではrouterというものを使うのが一般的だと思いますが、この記事ではurlpatternsをそのまま使ってます。

routerについてはこちらを参照してください。

https://www.django-rest-framework.org/api-guide/routers/

また、検索するとよく見つかるurl()というのの代わりにpathre_pathを使用しています。

理由はurlが廃止される可能性があるからです。

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

from apps.xxxxxxxx.views import SampleViewSet

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

# sample api and view
urlpatterns += {
    re_path(r'sample/?$', SampleViewSet.as_view(actions={
        'get': 'sample_get',
    })),
    path('api/sample/post', SampleViewSet.as_view(actions={
        'post': 'sample_post'
    })),
}

.as_viewの’actionを使って登録します。

見るだけでわかると思います。

getでリクエストが来たら、sample_getメソッドへ。

postでリクエストが来たら、sample_postメソッドへ。

ちなみに、以下のように実装しないのには理由があります。

urlpatterns += {
    re_path(r'sample/?$', SampleViewSet.as_view(actions={
        'get': 'sample_get',
        'post': 'sample_post'
    })),
}

このようにすると、/sampleにリクエストした際に、restframeworkが用意している管理画面を表示できません。

postをテストしたいのに、getが最初に表示されるので、管理画面が表示されないのです。

ハロウィンだしネタアプリを作ってみた話

タイトルの通り、ネタアプリを作ってみました。

作ったアプリケーションはこちらです。

http://www.everyday.work/

「進捗どうですか?」というアプリケーションで、作成時間は1~2時間ぐらいです。

f:id:miyaji-y26:20181101082657p:plain

使用したフレームワークや環境を箇条書きにするとこんな感じです。

  • Django2.1.2
  • heroku free
  • python3.6.4
  • heroku postgresql
  • domain 1円

これまで書いてきた記事を参照しつつ短時間で作ることができました。

ブログを書き続けてよかったなと感じた瞬間です。

あと、webサービスを作りたい人や、プログラミングの勉強をしている人は今回私が作ったような小さなアプリケーションから始めると良いと思います。

小さなアプリケーションでも、大きなアプリケーションでもやることは変わらないからです。

設計して、開発して、テストして、デプロイして、、あれこれと、リリースまでにやることが体験できます。

最初は時間がかかりますが、何度かやっているとやり方が分かるので、3回ぐらいやるとパパっと作れるようになります。

そして、慣れたら大きなサービスを作ってみましょう。

きっとうまくいきます

はみ出す文字列を三点リーダーにする方法

領域をはみ出したときに三点リーダー(…)にする方法を紹介します。

既出かと思いますが、詳しい内容とかあまり見つからない(こうやればできるというのは見つかる)ので、私なりに調べた内容を載せます。

領域をはみ出したときに三点リーダー(…)にする場合は以下のように設定します。

span.ellipsis {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

spanタグのclassにellipsisを設定した場合、はみ出した部分を三点リーダーにします。

spanタグが対象なのは特に意味はないです。強いて言うならellipsisの使い方をわかりやすくするためです。

※ このスタイルでは複数行の対応はできません。

overflow:hiddenについて

領域をはみ出した際に、はみ出したものを表示しないようにします。

scrollとか、autoにするとスクロールバーが出るやつです。

余談ですが、最近はscrollとかautoは使わなくてperfect-scrollbar.jsというのを使ってます。

white-space:nowrapについて

空白文字の扱いを決めています。

nowrapを指定すると、一行で表示されます。

preでもできると思いますが、改行やタブが残るのでうまくいかない場合があると思います。

こちらのページがわかりやすいと思います。

https://developer.mozilla.org/ja/docs/Web/CSS/white-space

text-overflow: ellipsisについて

はみ出した部分を三点リーダーにします。

おまけ

youtubeのサイドバーを見てみると、同じようなことをやってるのがわかります。

さらに、右側に24pxの余白をいれて、見やすくするというテクニックを発見できます。

24pxというのは1.5rem(1rem=16pxのとき)なので何か意味があるのだと思います。

なんの意味があるのかはわからないです。

margin-right: 24px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 1.4rem;
font-weight: 400;
line-height: 2.1rem;
-ms-flex: 1 1 0.000000001px;
-webkit-flex: 1;
flex: 1;
-webkit-flex-basis: 0.000000001px;
flex-basis: 0.000000001px;

lxmlを使ったxmlのパース方法

lxmlというライブラリを使ってxmlをパースしたときのメモを記事にしています。

lxmlはBeautifulSoupというスクレイピングのライブラリでも使われたりしなかったりします。

xmlファイルを取得する

pythonでファイルを読み込む方法はいろいろありますが、再帰的にファイルを取得しつつ、指定したディレクトリ以下のxmlファイルをすべて取得します。

from lxml import etree
from pathlib import Path

def get_xml_files(target):
    files = list(Path(target).glob('**/*.xml'))
    return files

get_xml_filesを実行すると、指定したディレクトリ以下のxmlファイルのパスがすべて取得できます。

このパスをlxmlで読み込んで、利用します。

lxmlでxmlをパースする

get_xml_filesを使って、ファイルまでのパスを取得後にパース処理をします。

xml_files = get_xml_files(xml_target)
for xml_file in xml_files:
    xml_file_name = str(xml_file)
    tree = etree.parse(xml_file_name)

xml_filePosixPothクラスのインスタンスなので、そのままetree.parseに渡せません。

一度strで文字列にしてからparseします。

コメントの削除方法

xmlのコメントが残っていると、forでループする際にcommentが引っかかります。

これを解消するために、removeしようと考えたのですが、remove系の関数がありませんでした。

理想ではtree.rm_comments()とかetree.rm_comments()があったらよかったです。

というわけで、対処方法が以下のプログラムです。

def remove_comments(tree):
    comments = tree.xpath('//comment()')
    for comment in comments:
        parent = comment.getparent()
        parent.remove(comment)

tagを判定する

以下のプログラムは、mybatisというjavaのormで使うxmlをパースする際に使った関数です。

xmlの中身はこのようなものです。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE sqlMap PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN" "http://ibatis.apache.org/dtd/sql-map-2.dtd">

<sqlMap namespace="jp.hoge.com">
    <select id="selectId" resultClass="Sample" parameterClass="string">
        SELECT
            id
        FROM
            sample
        WHERE
            id = 1
    </select>
</sqlMap>

このxmlをパースしてステートメントごとに配列にしています。

def get_statements(tree):
    statement_elements = []
    for element in tree.iter():
        if element.tag in ['select', 'insert', 'update', 'delete', 'procedure']:
            statement_elements.append(element)
    return statement_elements

属性を取得する際はelement.get('id')のようにします。

tree.iter()でelementを取得できますが、少し癖があります。

深さ優先探索のようにタグを取得していくので、書くのが難しかったです。

一度に欲しい情報を取得するのではなく、個別に取得する方法を採用しました。

例えば、以下のようなxmlがあった場合、sqlをdictに格納して、selectをパースしている最中にincludeを見つけたらdictを参照して、中身を置き換えます。

<sqlMap namespace="jp.hoge.com">
    <sql id="sampleInclude">
        where
            id = 2
    </sql>
    <select id="selectId" resultClass="Sample" parameterClass="string">
        SELECT
            id
        FROM
            sample
        <include refid="sampleInclude" />
    </select>
</sqlMap>

bulmaのpanelのヘッダーにボタンを付ける

bulmaというcssフレームワークでpanelというものがあります。

このpanelではpanel-headingというのがあるのですが、このヘッダーに編集ボタンは用意されていません。

この記事ではbulmaのpanelのヘッダーに編集ボタンを付ける方法を紹介します。

panelについてのドキュメントはこちらです。

https://bulma.io/documentation/components/panel/

最終的にこのようなものができます。

f:id:miyaji-y26:20181030075329p:plain

html

panelのテンプレートに編集を追加しています。

<nav class="panel">
    <p class="panel-heading is-size-6 sp-panel-heading">
        管理
        <a class="button is-small sp-right" href="edit">
            <span>編集</span>
            <span class="icon is-small">
                <i class="fas fa-cog"></i>
            </span>
        </a>
    </p>
    <a class="panel-block" href="">
        <span class="panel-icon">
            <i class="fas fa-flag" aria-hidden="true"></i>
        </span>
        <span class="is-size-7">お気に入り</span>
    </a>
</nav>

追加している部分はこの部分です。

<a class="button is-small sp-right" href="edit">
    <span>編集</span>
    <span class="icon is-small">
        <i class="fas fa-cog"></i>
    </span>
</a>

css

cssは殆どをbulmaで補っているので、2つ追加するだけです。

.sp-panel-heading {
    display: flex;
    align-items: center;
}

.sp-panel-heading .sp-right {
    margin-left: auto;
}

bulmaのbutton is-smallでも、高さが合わずに管理という文字がずれます。

そのため、flexの指定とalign-items: centerで調整しています。

ボタンを右寄せするためにmargin-left: auto;をいれています。

注意

margin-left: auto;を入れていますが、ヘッダーに入れる文字によってはレイアウトが崩れる恐れがあります。

その場合はflex: 1;などで調整するのが良いでしょう。

画像の上に文字列を表示する方法

htmlとcssを使って、画像にマウスを置くと文字が出てくる部品の作り方を解説します。

今回javascriptは使用しません。また、scssも使いません。もちろんwebpackとかビルドは必要ないです。

最終的に↓のようなものを作れます。 f:id:miyaji-y26:20181027202015g:plain

htmlのコード

htmlのコードはこのようになっています。

<div>
    <div class="sp-image-wrap">
        <img src="https://bulma.io/images/bulma-banner.png" alt="">
        <span class="sp-description">Bulma is an open source CSS framework based on Flexbox and built with Sass. It's 100% responsive, fully modular, and available for free.</span>
    </div>
    <span>Bulma: a modern CSS framework based on Flexbox</span>
</div>

imgのsrcにはサンプルとしてbulmaというcssフレームワークの画像を使っています。

今回の実装には関係ないですが、とても使いやすく、便利なコンポーネントが揃っています。

htmlでイメージ部分と、画像の下に出す文字列を分けています。

sp-descriptionをimgの上に表示されているように見せます。

cssのコード

cssの書き方はたくさんあると思いますが、ひとまずコードを載せます。

.sp-image-wrap {
    position: relative;
}

.sp-image-wrap:before {
    position: absolute;
    content: '';
    background-color: #c3c3c3a3;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    visibility: hidden;
    z-index: -1;
}

.sp-image-wrap .sp-description {
    position: absolute;
    top: 0;
    bottom: 0;
    height: 100%;
    width: 100%;
    padding: 16px;
    overflow-y: scroll;
    visibility: hidden;
    z-index: -1;
    transform: translateY(-10px);
    transition: transform .3s ease;
}

.sp-image-wrap:hover img {
    filter: blur(5px);
}
.sp-image-wrap:hover:before {
    visibility: visible;
    z-index: 10;
}
.sp-image-wrap:hover .sp-description {
    visibility: visible;
    z-index: 11;
    transform: translateY(0px);
}

機能を実装する上で工夫した点です。

  • sp-image-wraprelativeにして、その子要素のsp-descriptionabsoluteにした
  • hover時に上から降りてくるようにtransformtransitionを設定した
  • sp-image-wrapbeforeにオーバーレイを設定し、.sp-image-wrap:hover:beforeとすることでhover時にbeforeを表示するようにした
  • そのままオーバーレイを表示すると、背景画像が残ってしまって見えづらいためfilter:blurを使ってモザイク処理した
  • display:noneを使うのではなく、アニメーションすることも考えてvisibilityを仕様した

stackoverflowのapiをpythonで使ってみた

stackoverflowのoauthを試してみたので、そのときにやったことを紹介します。

基本的なステップについてはすべてドキュメントにかかれていたので、試す際に書いたコードを載せます。

ドキュメントはこちらです。

https://api.stackexchange.com/docs/authentication#scope

import requests
import json

client_id = '12345'
client_secret = 'xxxxxxxxxxxxxxx'

scope = 'read_inbox'
redirect_uri = 'http://localhost'

def step1():
    step1 = requests.get(
        'https://stackoverflow.com/oauth',
        {
            'client_id': client_id,
            'scope': scope,
            'redirect_uri': redirect_uri,
        }
    )
    print(step1)
    print(step1.url)

def step2(code):
    step2 = requests.post(
        'https://stackoverflow.com/oauth/access_token/json',
        {
            'client_id': client_id,
            'client_secret': client_secret,
            'code': code,
            'redirect_uri': redirect_uri,
        }
    )
    print(step2)
    obj = json.loads(step2.text)
    access_token = obj.get('access_token')
    expires = obj.get('expires')
    print(access_token, expires)
    '{"access_token":"xxxxxxxxxxxxxxx","expires":86400}'

if __name__ == '__main__':
    step1()
    # step2()

step1について

step1ではcodeというものを取得します。

requests.getで取得した文字列の中に、↓のようなurlが含まれているので、そのurlをブラウザで開きます。

https://stackoverflow.com/users/login?xxxxxxxxxxxxxxxxxx

Django等で作成している場合は、urlにリダイレクトする感じで良いはずです。

ブラウザで開くと、stackoverflowのログイン画面が開きます。

ユーザーがアプリケーションのアクセスを許可すると、リダイレクトurlへリクエストが来ます。

サンプルのコードは、サーバーサイドの実装はしていないので、ブラウザのurlを見ます。

ここで取得したcodeをコピーして、step2を実行します。

step2

step2は簡単です。

step1で取得したcodeをpostで送ると、access_tokenとexpireを取得できます。

このaccess_tokenを使って、ユーザーが持っている情報を取得できます。

DjangoではSocialAuth用のテーブルにaccess_tokenを保存していたので、DBに保存しても良いでしょう。

そして、有効期限が切れたらもう一度tokenを発行してもらう形で対応できると思います。

おまけ

認証が必要ないapiもあります。

stackoverflowでログインが不要な処理はできます。

ドキュメントはこちらです。

https://api.stackexchange.com/docs

例えば、回答一覧を取得するなら↓で実行できます。

def get_answers():
    url = 'https://api.stackexchange.com/2.2/answers?order=desc&sort=activity&site=stackoverflow'
    r = requests.get(url)
    print(r.text)