Bootstrap5対応その他

という記事を書こうとWorpressダッシュボードに移動したら、いつのまにかWordpressが5.8にバージョンアップしていた。例によってDBバックアップを取れとの忠告も聞かずに、速攻アップデートボタンポチり。

特に変った様子もないが、変化点は追々チェックするとしよう。

でBootsrtap5対応だが、先月辺りまでのこのサイトの引越作業の時点でベータ版が存在していたが、あまり気にしていなかったところ、ポータルサイト構築を兼ねたWEBアプリ開発の勉強がてら色々調べていたところバージョン5のリリースを知った(今ごろですが)。機能的にはあまり差は無い(というかよく読んでいない)がJQuery依存の削除やIEなど古いブラウザサポート終了など良い感じのアップデートのようなので、まずはdjangoテストサーバー環境で導入実験を行ない、ある程度問題ないことを確認したところで主ブランチに統合して本番環境のポータルサイトでも採用することとした。

見た目はあまり変らないが、テーマのデフォルトの文字サイズなどが異なるため、若干文字が小さかったり、メニューの文字が太字だったりと細かな気にならない違いが見られた。

4.5から5への対応に関する変更点であるが、前回までのSass導入でHTML(テンプレート)にBootstrapのクラスタグを直接書くことは少なくなったので、基本的には階層構造のスタイル定義をしているSassソースファイルを修正することで対応した。

主な違いは、marginやpaddingを表現するクラスで、たとえば右マージンはmr、左マージンはmlというようにrightとleftの頭文字で表現していたが、バージョン5より、左がstart、右がendと表現するようになりそれぞれマージンとしてはms、meと表記するようになった。これはおそらく多言語対応でRTL(右から書く文化)にも対応する事により水平方向の表記は右からと左からというより、始めと終わりという意味で統一したのだろう。

あとハマったのはアコーディオン型のメニューアニメーションの件で、若干名称が異なり、

  • data-toggle → data-bs-toggle
  • data-target → data-bs-target

になっていた。当サイトはこの位しか機能を使っていないので、以上の修正で無事以前と同様に動作するようになった。

というか、せっかくBootstrap5を導入したのに、新機能を調べて(あるのか?)導入実験とかしたいのだが、他にもいろいろやりたいことありで、なかなか優先順位に困っているところ・・・
思いつき順ということもありますが・・・

VScodeの覚書

サーバー作業の関係もすっかりローカルのVScodeからリモートSSHで行う環境に慣れきってしまい、徐々にputtyとかの普通のSSH端末ソフトを使わなくなってきた。しかし、たまーにROOTで作業をしたいとかApache Userでコンテンツを直接修正したい時などはputtyなどを使用していたが、これもいっそのことVScodeをSSH端末代わりに使用してしまってはどうだろうか?との思いになり、しばらく使ってみたらいきなり躓いた。

VScodeではemacsキーバインドのプラグイン(Awesome Emacs Keymap)を使用しているが、VScodeのリモートSSHでコマンドラインを使用して、他のユーザーにsuしてエディタを使用する場合、パーミッションの関係でVScodeは使用できないのでリモートのemacs系エディタを使うことになる。そこで編集を終了して「いざエディタ終了」と「Ctrl+X、Ctrl+C」と打つと、当のリモートのemacsだけでなく、VScodeもこのキー操作を拾ってしまい、VScodeも終了してしまうという問題が起こった。

考えた挙句、しばらくは「M-x kill-emacs」と手動で打って終わらせることは可能だったが、これではどう考えても不便。。。

というか、よく考えたらキーバインドを弄って「 Ctrl+X、Ctrl+C 」を解除すれば良いじゃない?ということで、探してみてもキーマップのファイルが見つからない。

調べた結果、Windows版だと左下の歯車マークの設定から、「キーボードショートカット」を選ぶとよいということで、「Close Window」に割り当てられた「 Ctrl+X、Ctrl+C 」を解除することで、リモートemacsでもストレスなく使用できるようになった。

・・・という覚書でした。

BootstrapとかCSSの事からSASSにたどり着くまで

djangoを通じてWEBアプリの勉強中。ポータルサイトのdjangoアプリで一通り遊び尽くしたところで、今度はフロント側の見た目やスクリプトに手を付け始めた。

今まではサイトの情報をそのままコピペするようにサイトを作ってきたので、その辺のメカニズムについて勉強を始めた所。今までHTMLとかCSSは必要に応じて編集してきたが、知識は初期のHTML3?あたりで止っているので、そのあたりも昨今のHTML5まで追いつけたら良いなぁ位には考えている。

BootstrapでレスポンシブルなWEBサイトを・・・と分かったような分かってないような事を書いてみたが、使っていくと結構便利で、あんまり考えなくても一応ちゃんとしたサイトが作ることが出来る(本サイトのポータルもdjango+Bootstrap)。しかし、いろんなサイトの情報をつまみ食いしながらゴチャゴチャやってきたら、非常にごちゃごちゃのHTMLソースが出来上がってしまった。

例えば、下記カード型サイドメニューの一部だが、クラスタグのカタマリのようになってしまう。

    <div class="container-fluid">
      <div class="row justify-content-md-center">
        <div class="col-md-2 px-1">
          <!-- 右側サイドバー -->
          <div class="card px-0 mx-0 my-2">
            <div class="card-body">
              <h4 class="card-title btn rounded-pill btn-outline-dark">Side Menu</h4>
                <ul class="list-group list-group-flush">
                   <li class="list-group-item">
   ・・・・・・

そもそも、HTMLとCSSに分かれたのって、文書と見た目の装飾を分けるのが目的で、HTMLの新しくなるにつれて、ボールドとかイタリックとか文字サイズの直接変更みたいなのは削除されてきたように思うのだが、昨日今日勉強し始めた素人的な感想だが、一応昔からHTMLには接してきてその進化も遠巻きに見てきたので、HTMLはあくまでも文書とその構造を定義するもので、見た目や表現はCSSの仕事という認識。

Bootstrapは確かに便利でいろんなパーツが揃っているが、こうもクラスタグで文書の見た目についての情報をベタベタ貼付けていると、何か本末転倒のような気がするのもあるかなーと思う。

確かに汎用性があるのは良いのだが、もっとCSSの方でスッキリ纏められないものかと、調べてみると、世の中考える人は多いもので、SASSというツールに辿りついた。これはCSSのプリプロセッサのようなもので、CSSだけでは手が届かなかった構造化したスタイルシートを定義して、CSSにコンパイルするツール。

これを使えば例えば 「card-bodyクラスのブロックに含まれるh4ヘッダはこのスタイル」と決められる。またdivを使用せずに「cardクラスのブロックに含まれるsectionというブロックはcard-bodyのスタイルを持つ」みたいな感じでdivを多用せずに意味のあるブロックで文書を構成することが出来る。

.cardclass {
  @extend .card, .px-0, .mx-0, .my-2;
  section {
    @extend .card-body;
    h4 {
      @extend .card-title, .btn, .rounded-pill, .btn-outline-dark;
    }
    ul {
      @extend .list-group, .list-group-flush;
      li {
        @extend .list-group-item;
      }
    }
  }
}

と定義出来、上のHTMLも

    <div class="container-fluid">
      <div class="row justify-content-md-center">
        <div class="col-md-2 px-1">
          <!-- 右側サイドバー -->
          <div class="cardclass">
            <section>
              <h4>Side Menu</h4>
                <ul>
                  <li>
   ・・・・・・

のように長々としたクラスタグを書かずにスッキリさせることが出来る。

久々にこの手の勉強を始めて間もないので、こういう考え方で良いのかどうか分からないが、少なくとも文書の構造を定義するHTMLの考え方はこうじゃないかなと、思う所。あとCSSファイルのサイズの肥大化でページの読み込み速度の話もあるかも知れないが、とりあえずは今はそっちは考えないとする。

と、まぁそんな感じで、自サイトを使用して色々試しているが、今日もブラウザキャッシュの罠にハマって半日ほど無駄にしてしまった。。テスト環境と本番環境で同じファイルなのに本番環境のみ一部のクラスタグの部分が反映されないとか・・・これもキャッシュだったとは。とりあえず難しいことは考えずにこまめにキャッシュクリアが大事です。
本番環境にはSassコンパイラ入れてなかったので、いろいろ大変なことになっていた。

不正アクセスログ集計ツール④

前回の記事でパイプ接続したプロセスをmapのまま返すとプロセスが終らないまま関数から帰ってきてしまうので、気持ち悪いからlist関数を使ったが、

bipset =  list(map(lambda x: ipv4adrset(x.decode('UTF-8').rstrip('\n')), proc.stdout))
 ↓
bipset =  [ipv4adrset(x.decode('UTF-8').rstrip('\n')) for x in proc.stdout]

の様に、わざわざmap使うより、下のようにリスト内包表記で直接リストを返すような仕組みを用いた方がスマートな気がした。とくに今回のようにmap直後にlistするような使い方の場合特に。

mapやfilterはイテレータなので直後にforなどのループ処理を行なう場合に適しており、リストそのものが欲しいときにはmapを使わずに[リスト内包表記]をするのが良いきがする。リスト内包表記やlist式のリスト生成では各項目の変換式(関数)が一度に使用され結果のリストが返されるが、イテレータでは生成段階では変換式(関数)は実行されず、for文など繰返し処理が行なわれる時に一つずつ実行されることになる。

たとえばファイルを読み込み関数があったとして、オープンしたファイルハンドルに対して読み出して検索や加工をする処理の結果をイテレータとして返してしまうと、関数からリターンした段階でファイルを閉じると、そのイテレータからは読み出せないという結果になる。

ということでその場合は、リストとして返す関数が正解となる。

あと、イテレータからリストに変換するときに一度繰返し処理が行なわれるという考え方からすると、for文に使用する前にリストに変換するのは効率が悪く。動作速度的にも不利になるようなので、そういう観点で使い分けていく目安になるのではないかと思う。

次に、ログファイルの解析部分。これも前回記事で、djangoの認証バックエンドをチョットいじって、ログイン失敗のログを残せるようにしたので、ここではそれを検出してリストとして返すこととする。ログファイルはApacheユーザーに読取り権限があるので、そのまま読見込めばいい。ただし、/var/logに保存している場合など、logrotateの処理が行なわれる場合それも追跡して行ないたい。また古いログはrotate時にgzip圧縮されるのでこれにも対応したい。

from blocklib import ipv4adrset
import os, glob, gzip
logger = getLogger(__name__)
import re, json

# djangoのアクセスログ(json形式)を読み込に認証履歴のレポートを返す
# 認証履歴のログはdjango認証バックエンドを継承して認証時のログ出力機能を追加したもの
# [引数]djlog : 解析するアクセスログ(json形式)をフルパスで与える(gzipもOK)
#              Noneの場合は、デフォルト django_log_json を使用(logorotateされたものも含み全てを読み込む)
#       st : レポート対象の開始日時(logrotateされたファイルはこの日時より新しいものを読み込み対象とする)
#       ed : レポート対象の終了日時
#       address : レポート対象のIPアドレスオブジェクト(Noneの場合は全て)サブネット検索対応
def get_django_loginfails(djlog, st, ed, address=None):
    if djlog is not None:
        djlogs = [djlog]
    else:
        djlogs = [x for x in glob.glob(django_log_json + '*') if re.match('.*(log|gz|[0-9])',x) and os.path.getmtime(x) > st.timestamp()]
        djlogs.sort(key=lambda x: os.path.getmtime(x))
    retlist = []
    for jlog in djlogs:
        logger.debug(jlog)
        with open(jlog, 'r') if not re.match('.*\.gz$', jlog) else gzip.open(jlog, 'rt') as jf:
            #jl = json.load(jf)
            decoder = json.JSONDecoder()
            jlines = map(lambda x: decoder.raw_decode(x), jf)
                #aa = decoder.raw_decode(line)
            for jl in map(lambda x: x[0], jlines):
                # fromisoformatで読み込むために末尾のZが邪魔なので削除
                # UTCのタイムゾーンを設定してJST時刻に変換する
                ts = datetime.fromisoformat(jl['timestamp'].rstrip('Z')).replace(tzinfo=timezone.utc).astimezone(tz_jst)
                ts = ts.replace(microsecond=0)
                if ts > st and ts < ed:
                    (dat, event) = (jl['ip'], jl['event'])
                    res0 = re.match('user logged in : (.+)$', event)
                    res1 = re.match('login faild.*: (.+)$', event)
                    event = False if res1 else True if res0 else None
                    if event is None:
                        continue
                    name = res1[1] if res1 else res0[1] if res0 else ''
                    if address is not None and address != ipv4adrset(dat):
                        continue
                    logger.debug('%s : %s - %s(%s)' % (str(ts), dat, name, event))
                    retlist.append(
                        {
                            'name':name,
                            'datetime':ts,
                            'address':ipv4adrset(dat),
                            'status':event,
                            'source':'django',
                        }
                    )
                    #print('%s : %s : %s' % (str(ts), dat, event))
    return retlist

ログはdjaongo.structlogでjson形式で出力したものを対象としている

  • 17行目globクラスを使用して指定したファイル名で始まるlogrotateファイルのリストを作成する(検索開始日付stよりも新しいタイムスタンプのファイルという条件も含める)
  • それぞれに対して、ファイルオープンし(拡張子gzだったらgzipオープンに変更)1行ずつ読み出し、対象パターンを見つけたら辞書形式のデータの配列として返す

ソースプログラムを貼付けてるけど、実際はそのままでは動かない一部の公開なので誰の役にも立たないただの覚え書きなのであった。
json形式だからさっくり読めるだろうとして始めたら何かエラーになってサッパリ読めない。。何でだろうと思って先人様たちの記事をたどって調べたら、こんなもんだと上記のようにデコーダを作って1行ずつ読み込んでいくようだ。
ぇぇぇーーー。同じくpython使ってjson形式のログを掃き出してるのになんで直接読めないん??ってなった。こんど時間があったら調べてみよう

djangoセキュリティログ追加

チュートリアルを使用したdjangoの勉強と並行して、djangoで構成している本サイトのポータルページもある程度のセキュリティー対策が急がれる。djangoを素で使用しているサイトは少ないようでwordpressのようにBOTによる攻撃はまだ見られない。何らかのCMSを使用しているのを見越して適当なURLをぶっ込んでくるBOTは404で弾けるのであまり問題にならないが、ごくたまーに、しつこくadminでログインしようとする輩が現れるようだ。

このWordpressのサイトでも結構多かったので(普通に見に来てくれる人は殆ど居ないのにwww)、User Login Historyというプラグインを入れている。ここもたまに100回以上のadminログイン試行をしてくる輩がいるので、このログを利用してIPを特定しblockset.pyで手動だけどお引き取り願うことにしている。

同じような仕組みをdjangoのポータルページにも仕掛けようと勉強がてら色々と調べたところ、ログイン用の認証バックエンドという仕組みが利用できそうだということが分かった。デフォルトの<project>/setting.pyには明示的に定義はされていないが(django.conf.global_settings にデフォルト定義)、AUTHENTICATION_BACKENDSという配列がありここのカスタムや追加の認証バックエンドを追加出来る(<project>/settings にて再定義)

デフォルトでは’django.contorib.auth.backends.ModelBackend’が一つ定義されているので、こちらを継承カスタマイズして認証結果をログ出力できるようにする方針とする。

django.contrib.auth.backends.ModelBackend 抜粋
class ModelBackend(BaseBackend):
    """
    Authenticates against settings.AUTH_USER_MODEL.
    """

    def authenticate(self, request, username=None, password=None, **kwargs):
        if username is None:
            username = kwargs.get(UserModel.USERNAME_FIELD)
        if username is None or password is None:
            return
        try:
            user = UserModel._default_manager.get_by_natural_key(username)
        except UserModel.DoesNotExist:
            # Run the default password hasher once to reduce the timing
            # difference between an existing and a nonexistent user (#20760).
            UserModel().set_password(password)
        else:
            if user.check_password(password) and self.user_can_authenticate(user):
                return user
       ・・・・・・

認証判定にはメソッドauthenticateが機能しており、認証OKであればusernameを、NGであればNoneを返す関数であるため、クラス継承により下記のようにメソッドをオーバーライドして再定義する

<project>/backends.py
from django.contrib.auth.backends import ModelBackend
from logging import getLogger # using logging module

class ModelLogBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        logger = getLogger('loginInfo') # example
        res = super().authenticate(request, username, password, **kwargs)
        if res is None:
            logger.info('login faild, bad user: %s' % username)
        else:
            logger.info('user logged in : %s' % username)
        return res

super()で親クラスのメゾッドに丸投げして、結果に応じてログを吐き出す機構を追加。継承したクラスを新たなバックエンドとして<project>/settings.pyに定義する。

# Authntication backend
AUTHENTICATION_BACKENDS = [
    '<project>.backends.ModelLogBackend'
]

<project>はdjangoのプロジェクト名に置き換える。

ちなみにloggerの設定は別の記事を参照して完成しておくこと。当サイトの場合は他のアクセスログの都合でstructlogを使用している(設定さえしておけばloggingと同等に使用可能)。

もともと、ポータルとしてのdjangoサイトは出来上がっていたので、この機能を付けたくてdjangoの本格勉強を始めたという流れでした。蓋を開けてみると思ったより簡単で、同時にカスタマイズ性の高さに今更ながら感動です。次はdjabngo+bootstrap(japascript)による動的サイトの実装実験を目指したいな。

djangoチュートリアルメモ

基本的にはdjangoサイトのチュートリアルをそのままトレース。やった事だけをメモしていこうかと。。。

  • 仮想環境への移行 source bin/activate
  • djangoインストール : pip install Django
  • バージョン確認:python -m django –version → 3.2.4
  • プロジェクト作成:django-admin startproject mysite
  • とりあえずテストサーバー起動:python manage.py runserver 0:8000
    外部からアクセスするため(ただしファイヤーウォールで接続制限をかけること)
  • 「Invalid HTTP_HOST header」エラーが出るので、setting.pyを修正
    ALLOWED_HOSTS = [‘*’]
    デフォルトでは空になっているので、自サイト以外からはアクセスできないようだ
  • チュートリアルに沿ってアプリケーション作成
    python manage.py startapp polls
  • ここまで作った状態で、プロジェクト下とライブラリのdjango下をそれぞれgitの管理化に入れて、pythonの勉強を兼ねて、動作の仕組みを解析する予定。

Git小技

gitignore

djangoプロジェクトをcommitするときに、コンパイル済みの*.pycなどあとから「このファイルは対象外にしたいなー」てなったとき。

  • .gitignoreに無視したいファイルを追加
  • すでにコミットしているファイルを対象外(リポジトリから削除)とする
.gitignore:
*.pyc

git管理のトップディレクトリに移動して、git rmで対象削除する

find . -name "*.pyc" -exec git rm -f "{}" \;

Tree表示

git log をきれいにみる方法

ここのサイトを参考に

git log --graph --pretty=format:'%x09%C(auto) %h %Cgreen %ar %Creset%x09by"%C(cyan ul)%an%Creset" %x09%C(auto)%s %d'

下記コマンドにてエイリアス登録

git config --global alias.tree 'log --graph --pretty=format:"%x09%C(auto) %h %Cgreen %ar %Creset%x09by"%C(cyan ul)%an%Creset" %x09%C(auto)%s %d"'

各ユーザーごとホームディレクトリの.gitconfigに追加される。

あとエディタも使い慣れたのにしたいので、

$ git config --global core.editor emacs

loggerの件(pythonモジュール)

プログラムのデバッグや各種情報を出力するのにprintを使用したり、エラー出力のためにsys.stderr.writeを使った板が、今後djangoなどWEBアプリ開発を念頭に入れて、loggingモジュールを使用できるよう勉強していきたい。

ここのページを参考にすると、なるほどいつもはモジュールをまんまimportしているが、それだとインスタンスを生成せずに使用してしまう可能性もある。なるべく使用するのみをimportするような習慣をつけておいたほうがよさそう。

×import logging
×logger = logging.getLogger(__name__)

〇from logging import getLogger, StreamHandler, DEBUG, INFO
〇logger = getLogger(?__name__)

あとはおまじないの様に、下記コードを追加して出力先とレベルを設定するとよいそうだ

logger = getLogger(__name__)
handler = StreamHandler()
handler.setLevel(DEBEG)
logger.setLevel(DEBUG)
logger.addHandler(handler)
logger.propagate = False

#
#
logger.info('inform')
logger.error('error occerd')

setLevelは2か所あるので、引数をDEBUGなどを変数に格納して、一括で変えられるようにしてもいいかもしれない。

勉強がてら、上記修正をblockset.pyに適用して、sys.stderr.write()の部分をlogger.info()に変更。動作自体は特に変化はないが、ちゃんと作ってる感じが良いかも・・・
日に日に、情報コピペの覚書投稿になりつつある・・・・

Python覚書き

今更ながら流行の人工知能に触れておこうと、言語オタク的にとりあえずPythonから入る事にしました。それが結構面白くて、かつてRubyにハマったのを思い出しました。何となく似ているような、少し方向性が違うような違った楽しさあり。

昔からあった言語だけど、改めて学んでみると、コーディング体裁による制御構造とか色々特徴的だったり、クラス、イテレータなど一通りの機能はあるので、今までのRubyプログラムとかも簡単に移植できそう。


# -*- coding: utf-8 -*-
# おまじない
import sys
def main()
        .........
if __name__ == "__main__"
        main()

上の例は、単品として実行可能なモジュールで、import ライブラリとして利用する場合は main()は実行されないが、単独で起動された場合はmain()が実行されるようです。