utamaro’s blog

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

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

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

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

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

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)

Djangoのテンプレート内でcloudinaryのディレクトリを指定して画像を表示する

Djangoを使って、cloudinaryで保存している画像を表示する場合、{% cloudinary %}を使用します。

その際に、ファイル名を指定することができるのですが、ファイル名のみだとディレクトリ分けしていた場合表示されません。

そこでなんとかディレクトリを指定して、画像を表示したいと思いました。

そこで取った方法が以下の書き方です。

{% cloudinary public_id crop="fill/dir" class="is-rounded" %}

これで出力されるurlは以下のようになります。

http://res.cloudinary.com/hqarhdoek/image/upload/c_fill/dir/image_name

ポイントはcrop部分です。

もともとはcropの値はfillだけで、その場合のurlは以下のようになります。

http://res.cloudinary.com/hqarhdoek/image/upload/c_fill/imange_name

ちなみに、folder="dir"としてもだめでした。

問題点

widthやheightを指定する場合、以下のようなurlになります。

'https://res.cloudinary.com/demo/image/upload/c_fill,h_150,w_100/sample.jpg' 

すると、fill/dirのようになってもうまく動きません。

あくまでもファイル名の前の区切りがフォルダとしなければなりません。

その場合は、おとなしくサーバーサイドでファイル名をつくりましょう。

herokuに上げて動作を確認してると、imageが404になったりしたので、この方法は間違っているのかもしれません。

その検証はしてなくて、他のバグを直してるときについでに解決していたので未検証です。

ただ、ローカルでは動いていたのでいまいち原因がわかってないです。

Djangoでログイン後に特定の処理を挟む方法

表題の通りですが、Djangoでログイン後に一回だけ処理を入れたいときの方法を考えて見ました。

一つしか思いつかなかったのですが、紹介したいと思います。

やることをは簡単で、ログイン後のリダイレクトに一つだけviewを挟む形です。

まずは、settings.pyに以下の項目を追加します。

LOGIN_REDIRECT_URL = 'before-home'

これを追加すると、ログイン後にbefore-homeにリダイレクトされます。

before-homeというのはurls.pyにnameで設定した名前を入れます。

次にview.pyを修正します。

class GoToHomeView(RedirectView):
    pattern_name = 'home'

    def get_redirect_url(self, *args, **kwargs):
        request = self.request
        if request.user.is_authenticated:
            # ログインしている場合は、なにか処理する
            # is_authenticatedを入れなくても良いですが、ログインしてるとは限らない
            self.execute(request.user)
        return super().get_redirect_url(*args, **kwargs)

    def execute(self, user):
        # なにか処理

RedirectViewについては説明は↓に記載されています。

https://docs.djangoproject.com/ja/2.1/ref/class-based-views/base/#redirectview

コードの例では、pattern_nameにhomeを指定していて、リダイレクトするとhomeが呼ばれます。

get_redirect_urlをオーバーライドしており、ログイン後に処理が実行されるようになります。

if request.user.is_authenticated:でセーフコードを入れる理由としては、ログインしなくてもbefore-homeが実行される可能性があるからです。

@require_loginを入れても同じことができると考えたのですが、以下のコードでつまづきました。

    def decorator(view_func):
        @wraps(view_func)
        def _wrapped_view(request, *args, **kwargs):
            if test_func(request.user):

このコードはfrom django.contrib.auth.decorators内のコードで、login_requiredが実行されたときに呼ばれます。

その際にRedirectViewではuser値が取得できないのでエラーになります。

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

エラーの内容

うまい解決方法が見つかったら、別の記事に書きたいと思います。