lazy_segtreeをちょっと強くした

BeatsやMergeSortTreeも作れるように。

ACLのlazy_segtreeに以下のメソッドを追加する。

template <class H, bool (*h)(S&, H&)>
    void query(int l, int r, H &res, bool upd=true) {
        assert(0 <= l && l<=r && r<= _n);
        if (l==r) return ;

        l += size;
        r += size;

        if(upd){
            for (int i = log; i >= 1; i--) {
                if (((l >> i) << i) != l) push(l >> i);
                if (((r >> i) << i) != r) push((r - 1) >> i);
            }
        }

        while (l < r) {
            if (l & 1) _query<H,h>(l++, res, upd);
            if (r & 1) _query<H,h>(--r, res, upd);
            l >>= 1;
            r >>= 1;
        }

        return;
    }
    template <class H, bool (*h)(S&, H&)>
    void _query(int k, H &res, bool upd) {
        if(h(d[k], res)){
            rep(i,2) _query<H,h>(k*2+i, res, upd);
        }
        if(upd && k<_n)update(k);
        return ;
    }

    template <bool (*h)(S, F)>
    void apply2(int l, int r, F f){
        assert(0 <= l && l <= r && r <= _n);
        if (l == r) return;

        l += size;
        r += size;

        for (int i = log; i >= 1; i--) {
            if (((l >> i) << i) != l) push(l >> i);
            if (((r >> i) << i) != r) push((r - 1) >> i);
        }

        {
            int l2 = l, r2 = r;
            while (l < r) {
                if (l & 1) all_apply2<h>(l++, f);
                if (r & 1) all_apply2<h>(--r, f);
                l >>= 1;
                r >>= 1;
            }
            l = l2;
            r = r2;
        }

        for (int i = 1; i <= log; i++) {
            if (((l >> i) << i) != l) update(l >> i);
            if (((r >> i) << i) != r) update((r - 1) >> i);
        }
    }
    template <bool (*h)(S, F)>
    void all_apply2(int k, F f) {
        if(h(d[k], f)){
            push(k);
            rep(i,2) all_apply2<h>(k*2+i, f);
            update(k);
            return ;
        }
        d[k] = mapping(f, d[k]);
        if (k < size) lz[k] = composition(f, lz[k]);
        return ;
    }

使い方

  • void query<H,h>(int l, int r, H &res, bool upd=true)
    • prodより強いやつ
    • H: 戻り値の型
    • h: 集計関数かつ戻り値でスルー判定。
    • upd: 合間で通ったノードを更新させるか。
  • `apply2(int l, int r, F f)
    • h: スルー判定

MergeSortTree

注意点

  • 初期化はコンストラクタにvector<S>として渡す。
  • upd=falseにする
  • 一点更新はできない。

G - Smaller Sumの例

struct S{
    vector<ll> a;
    vector<ll> sum;
};
S op(S a, S b){
    S res;
    res.a.resize(a.a.size()+b.a.size());
    merge(a.a.begin(), a.a.end(), b.a.begin(), b.a.end(), res.a.begin());
    res.sum={0};
    for(ll e: res.a) res.sum.push_back(res.sum.back()+e);    
    return res;
}
S e(){
    return S{};
}
using F=ll;
S mapping(F f, S s){
    return s;
}
F composition(F f, F g){
    return f;
}
F id(){
    return 0ll;
}

ll x;
bool g(S &s, ll &h){
    int i = upper_bound(s.a.begin(),s.a.end(), x)-s.a.begin();
    h += s.sum[i];
    return false;
}

using LST = lazy_segtree<S,op,e,F,mapping,composition,id>;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int N;
    cin>>N;
    vector<int> A(N);
    rep(i,N)cin>>A[i];
    vector<S> init(N);
    rep(i,N) init[i] = S{{A[i]}, {0, A[i]}};
    LST mstree(init);
    int Q;
    cin>>Q;
    ll B=0;
    while(Q--){
        ll a,b,c;
        cin>>a>>b>>c;
        ll l = a^B;
        ll r = b^B;
        x = c^B;
        l--;
        ll ans=0;
        mstree.query<ll, g>(l, r, ans, false);
        cout << ans << "\n";
        B=ans;
    }
}

queryの1つ目のtp引数は戻り値の型。今回の場合x以下の値の総和なのでlong longを渡す。
2つ目のtp引数は計算用の関数を渡す。もしノードをさらに深堀りたいなら、何もせずにtrueを返す。そうでないなら計算してfalseを返す。葉に到達したときは必ずfalseを返すようにしないとバグるよ。

SegmentTreeBeats

注意点

  • 計算量は保証されないので考えて判定関数を設計してね。

M - 等しい数の例
すべて等しいノードに到達するまで深掘ることで、計算を簡単にしている。
extra nodeはクエリ全体で高々O((N+Q)logN)個になるので、全体計算量はO(N+(N+Q)logN)
区間をsetで管理する解法より速いのかは不明。

vector<int> mc;
ll ans;

struct S{
    int mi, ma, sz;
};
S op(S a, S b){
    S res;
    res.mi = min(a.mi, b.mi);
    res.ma = max(a.ma, b.ma);
    res.sz = a.sz+b.sz;
    return res;
}
S e(){
    return S{(int)2e9, (int)-2e9, 0};
}
using F = ll;
S mapping(F f, S s){
    if(f!=-1){
        s.mi=f;
        s.ma=f;
    }
    return s;
}
F composition(F f, F g){
    return (f!=-1?f:g);
}
F id(){
    return -1;
}

bool h(S s, F f){
    if(s.mi!=s.ma) return true;
    ans -= 1ll*s.sz*(mc[s.mi]-s.sz);
    mc[s.mi] -= s.sz;

    ans += 1ll*s.sz*mc[f];
    mc[f] += s.sz;
    return false;
}

using LST = lazy_segtree<S,op,e,F,mapping,composition,id>;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    map<int,int> zap;
    int N;
    cin>>N;
    vector<int> A(N);
    rep(i,N)cin>>A[i], zap[A[i]]=0;
    int Q;
    cin>>Q;
    vector<array<int,3>> qu(Q);
    rep(i,Q){
        int l,r,x;
        cin>>l>>r>>x;
        l--;
        zap[x]=0;
        qu[i] = {l,r,x};
    }
    {
        int i = 0;
        for(auto& [f,s]:zap) s=i++;
    }
    rep(i,N) A[i] = zap[A[i]];
    rep(i,Q) qu[i][2] = zap[qu[i][2]];
    mc.assign(zap.size(),0);
    rep(i,N){
        ans += mc[A[i]];
        mc[A[i]]++;
    }
    vector<S> init(N);
    rep(i,N) init[i] = S{A[i],A[i],1};
    LST seg(init);
    rep(i,Q){
        auto [l,r,x] = qu[i];
        seg.apply2<h>(l,r,x);
        cout<<ans<<"\n";
    }
}

h関数でtrueを返せばスルーして子のノードに任せるよ。

PCK2024予選雑解説(7,8以外)

なぜ7,8が抜けているかというと、わからないからです。

1問目 もらったキャンディ

4036 < PCK Prelim < Challenges | Aizu Online Judge

C-10を出力してください

#include <bits/stdc++.h>
#define rep(i,n) for(int i=0;i<(n);++i)
using namespace std;
using ll = long long;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int c;
    cin>>c;
    cout<<c-10<<endl;
}

2問目 神輿(みこし)の担ぎ手

4037 < PCK Prelim < Challenges | Aizu Online Judge
w/cの切り上げを計算して、それがN以下かどうか。

a/bの切り上げは(a+b-1)/bで計算できる。

#include <bits/stdc++.h>
#define rep(i,n) for(int i=0;i<(n);++i)
using namespace std;
using ll = long long;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n,c,w;
    cin>>n>>c>>w;
    int x=(w+c-1)/c;
    if(x>n)cout<<"No"<<endl;
    else{
        cout<<"Yes"<<endl;
        cout<<x<<endl;
    }
}

3問目 ラッキーナンバー

PCK Prelim < Challenges | Aizu Online Judge

問題文のとおりに数えるだけです。
nは文字列として受け取ると楽です。

#include <bits/stdc++.h>
#define rep(i,n) for(int i=0;i<(n);++i)
using namespace std;
using ll = long long;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int d;
    string n;
    cin>>d>>n;
    int cnt=0;
    for(char c:n){
        cnt+=(c-'0'==d);
    }
    cout<<cnt<<endl;
}

4問目 魔法のポケット

4039 < PCK Prelim < Challenges | Aizu Online Judge
このあたりから難しくなってきます。

もし、C,Dが最小の2でも40回くらい叩けばビスケットの数は10^12を超えます。T<=10^11なので右のポケット、左のポケットを叩く回数は、大体40以下まで考えれば良い。

全探索。オーバーフローに気をつけます。

#include <bits/stdc++.h>
#define rep(i,n) for(int i=0;i<(n);++i)
using namespace std;
using ll = long long;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    ll A,B,C,D,T;
    cin>>A>>B>>C>>D>>T;
    int ans = 1e9;
    ll x = A;
    rep(a,40){
        ll y = B;
        rep(b,40){
            if(x+y==T) ans = min(ans, a+b);    
            y*=D;
            if(y>T) break;
        }
        x*=C;
        if(x>T) break;
    }
    if(ans==1e9) cout<<"No"<<endl;
    else {
        cout<<"Yes"<<endl;
        cout<<ans<<endl;
    }
}

5問目 データセンター

404 Error | Aizu Online Judge

自由に移動できるので最初のファイルの位置は考える必要はありません。大事なのはファイルの総数です。
できるだけ多くのサーバーを余らせるには、たくさんのファイルが持てるサーバーに優先的に押し付けるべき。

貪欲法

#include <bits/stdc++.h>
#define rep(i,n) for(int i=0;i<(n);++i)
using namespace std;
using ll = long long;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int n,m;
    cin>>n>>m;
    vector<int> c(n),a(n);
    rep(i,n){
        cin>>c[i]>>a[i];
    }
    int sum = 0;
    rep(i,n) sum+=a[i];
    sort(c.begin(),c.end(),greater<int>());
    int use = 0;
    rep(i,n){
        if(sum>0){
            sum -= c[i];
            use++;
        }
    }
    if(n-use>=m) cout<<"Yes"<<endl;
    else cout<<"No"<<endl;
}

6問目 湖の調査

Aizu Online Judge

.の連結成分の個数を数える問題です。
周りに隣接している.の集合をカウントしてはいけないので、予め取り除いておきます。

unionfindでも良いですが、本番ではライブラリぺた~できないのでdfsでやると楽です。

#include <bits/stdc++.h>
#define rep(i,n) for(int i=0;i<(n);++i)
using namespace std;
using ll = long long;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int h,w;
    cin>>h>>w;
    vector<string> s(h);
    rep(i,h) cin>>s[i];

    vector<vector<int>> used(h,vector<int>(w,0));
    auto dfs = [&](auto dfs, int y, int x) -> void {
        if(y<0 || h<=y || x<0 || w<=x) return;
        if(s[y][x]=='#') return;
        if(used[y][x]) return;
        used[y][x]=1;
        vector<int> dd = {0,1,0,-1,0};
        rep(i,4){
            dfs(dfs, y+dd[i], x+dd[i+1]);
        }
    };

    rep(i,h){
        dfs(dfs, i, 0);
        dfs(dfs, i, w-1);
    }
    rep(j,w){
        dfs(dfs, 0, j);
        dfs(dfs, h-1, j);
    }

    int cnt = 0;
    rep(i,h){
        rep(j,w){
            if(s[i][j]=='#') continue;
            if(used[i][j]) continue;
            dfs(dfs, i, j);
            cnt++;
        }
    }
    cout<<cnt<<endl;
}

9問目 壁のリフォーム

404 Error | Aizu Online Judge

「隣り合う列の高さが同じ」は考えにくい。
図を書いてみるとわかるが、すべてのiについてh[i]-d[i]の値を一致させることと同じ。

「差の総和を最小化するときは中央値を使う」という典型がある。
C - Linear Approximation

#include <bits/stdc++.h>
#define rep(i,n) for(int i=0;i<(n);++i)
using namespace std;
using ll = long long;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int n;
    cin>>n;
    vector<ll> h(n),d(n);
    rep(i,n)cin>>h[i];
    rep(i,n)cin>>d[i];

    vector<ll> sa(n);
    rep(i,n) sa[i] = h[i]-d[i];

    sort(sa.begin(),sa.end());
    ll md = sa[n/2];
    
    ll cnt = 0;
    rep(i,n) cnt += abs(sa[i]-md);
    cout<<cnt<<endl;
}

10問目 宇宙戦艦イヅア

404 Error | Aizu Online Judge
各クエリの入力がめっちゃおおいですが、各操作の最大しか考慮しなくて良いです。

直感的には
dp[i][j]i番目のクエリまでみて、現在のポンプの強さがjのときのスコアの最大値
というdpをやりたいですが、ポンプの強さが超大きいので間に合いません。
N<=2000であることに注目します。

クエリ1(ポンプ増強)は、以降クエリ2を行う回数が分かれば全体のスコアに影響する量が求まるのでその時に即加算すれば、現在のポンプの強さをdpに持たせなくて良い。
dp[i][j]= i個目のクエリまで見て、以降にクエリ2をj回行うと決めたときのスコアの最大値

答えはdp[N][0]

#include <bits/stdc++.h>
#define rep(i,n) for(int i=0;i<(n);++i)
using namespace std;
using ll = long long;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int N;
    cin>>N;
    vector<vector<ll>> dp(N+1,vector<ll>(N+1,-1e18));
    rep(i,N+1)dp[0][i] = 0;
    rep(i,N){
        int L;
        cin>>L;
        int l1 = 0;
        int l2 = 0;
        rep(j,L){
            int t,a;
            cin>>t>>a;
            if(t==1) l1 = max(l1, a);
            else l2 = max(l2, a);
        }
        rep(j,N+1){
            if(l1>0 && j>0) dp[i+1][j-1] = max(dp[i+1][j-1], dp[i][j]+l1);
            if(l2>0) dp[i+1][j] = max(dp[i+1][j], dp[i][j]+l2*j);
        }
    }
    cout<<dp[N][0]<<endl;
}

11問目 デイリーミッション

Aizu Online Judge

指定された順で部屋を順番に訪れていく。敵の部屋に訪れる回数を最小化したうえでの、部屋の移動回数を最小値を求めてください、
各小鬼の場所への移動について、始点と終点は決まっているので、独立に考えて最後に足し合わせれば良いです。
子鬼の数が超多くて、部屋の数は300と小さめ。

ワーシャルフロイドです。

ただし普通のワーシャルフロイドとは少しだけ違うところがあって、

  • 辺ではなく頂点に重みがついている。
    • これはその頂点へ向かっている辺に重みがあると考えて良いです。
  • 最小化したい値が2つある。
    • 普通にpairで持てばいいです。
    • 毒の部屋を最小化したうえで移動回数を最小化したいので、pair<毒部屋訪数, 移動回数>の順で持たせる
      • もしくは、毒部屋へ訪れると1e9ペナルティ、移動すると1ペナルティとして、mod1e9を出力しても良い。
#include <bits/stdc++.h>
#define rep(i,n) for(int i=0;i<(n);++i)
using namespace std;
using ll = long long;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int N,M;
    cin>>N>>M;
    vector<vector<ll>> G(N,vector<ll>(N,1e18));
    rep(i,N) G[i][i]=0;
    rep(i,M){
        int u,v;
        cin>>u>>v;
        u--;v--;
        G[u][v]=1;
        G[v][u]=1;
    }
    int K;
    cin>>K;
    vector<int> a(K);
    rep(i,K) cin>>a[i],a[i]--;

    int Q;
    cin>>Q;
    rep(i,Q){
        int b;
        cin>>b;
        b--;
        rep(j,N){
            G[j][b]+=1e9;
        }
    }
    rep(k,N){
        rep(i,N){
            rep(j,N){
                G[i][j] = min(G[i][j], G[i][k]+G[k][j]);
            }
        }
    }
    ll ans = 0;
    int mae = 0;
    rep(i,K){
        ans += G[mae][a[i]];
        mae=a[i];
    }
    cout<<ans%(int)1e9<<endl;
    
}

12問目 ログインボーナス

404 Error | Aizu Online Judge
遅延セグ木とは言っていますが、計算がかなり複雑。

g(l,i) = Ai*Si + Bi*f(l,i)

Ai*Siはやるだけ。
Bi*f(l,i)が面倒です。
f(l,i)は、iより前に1が何個連続してるか。

これを更新するにはセグ木に、

  • その区間の左端から何個1が連続してるか
  • その区間の右端から何個1が連続してるか

を持たせればいいです。

説明難しいので詳しくはコード内のコメントに書きます。

反転クエリで0と1が入れ替わる場合もあるので、上記の0バージョンも保持しておきます。

#include <bits/stdc++.h>
#define rep(i,n) for(int i=0;i<(n);++i)
using namespace std;
using ll = long long;

// aojではACLが使えません。
#include<atcoder/lazysegtree>
using namespace atcoder;


// Bの累積和配列
vector<ll> bsum;

struct S{
    ll A; //Aの総和
    ll AS; //A*Sの総和
    ll Bf1; // B*f(l,i)の総和
    ll Bf0; // B*f0(l,i)の総和 (f0はfの0連続バージョン)
    ll l1; // 左端から1が何個連続しているか
    ll r1; // 右端から1が何個連続しているか
    ll l0; // 左端から0が何個連続しているか
    ll r0; // 右端から0が何個連続しているか
    ll sz; // 区間の幅
    ll i; //区間の左端の添字
};
S op(S a, S b){
    
    S s;

    // 普通に足す
    s.A = a.A+b.A;
    s.AS = a.AS+b.AS;
    
    // 左から1が何個続くかはaのl1を参照すればいい。(遅延セグ木のライブラリの内部でaが左、bが右に揃えてくれてます)
    s.l1 = a.l1;


    // ただし、a.l1がa.szと同じ場合、つまりすべての要素が1の場合は、b.l1も加えます。(l1は左から何個1が連続してるかなので)
    if(a.l1==a.sz){
        s.l1 += b.l1;
    }
    // ...
    s.l0 = a.l0;
    if(a.l0==a.sz){
        s.l0 += b.l0;
    }
    // ...
    s.r1 = b.r1;
    if(b.r1==b.sz){
        s.r1 += a.r1;
    }
    // ...
    s.r0 = b.r0;
    if(b.r0==b.sz){
        s.r0 += a.r0;
    }
    

    // 一旦普通に足す。
    s.Bf1 = a.Bf1+b.Bf1;
    // bの左から連続している1のみ、f(i,l)の値が、aの右端に連続している1の分だけ、増えます。
    s.Bf1 += (bsum[b.i+b.l1]-bsum[b.i])*a.r1;

    // ...
    s.Bf0 = a.Bf0+b.Bf0;
    s.Bf0 += (bsum[b.i+b.l0]-bsum[b.i])*a.r0;
    

    // 区間の左端の添字なので小さい方
    s.i = a.i;
    // 区間の幅
    s.sz = a.sz+b.sz;

    return s;
}
S e(){
    return S{0,0,0,0,0,0,0,0,0,0};
}
using F = ll;
S mapping(F f, S a){
    S s=a;
    //奇数の場合のみ反転が行われる
    if(f==1){
        // Sは0か1なので、反転はAの総和-A*Sの総和で可能
        s.AS = a.A-a.AS;

        // 入れ替える
        swap(s.Bf0,s.Bf1);
        // 入れ替える2
        swap(s.l0,s.l1);
        // 入れ替える3
        swap(s.r0,s.r1);
    }
    return s;
}
F composition(F f, F g){
    return (f+g)%2;
}
F id(){
    return 0;
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int n,q;
    cin>>n>>q;
    vector<ll> a(n),b(n),s(n);
    rep(i,n)cin>>a[i];
    rep(i,n)cin>>b[i];
    rep(i,n)cin>>s[i];

    bsum.assign(n+1,0);
    rep(i,n) bsum[i+1] = bsum[i]+b[i];

    vector<S> init(n);
    rep(i,n){
        init[i] = S{a[i],a[i]*s[i],b[i]*s[i],b[i]*(1-s[i]),s[i],s[i],1-s[i],1-s[i],1,i};
    }

    lazy_segtree<S,op,e,F,mapping,composition,id> seg(init);

    rep(qi,q){
        int c,l,r;
        cin>>c>>l>>r;
        l--;
        if(c==1){
            seg.apply(l,r,1);
        }
        else{
            S res = seg.prod(l,r);
            cout << res.AS+res.Bf1 << endl;
        }
    }
}

H - Counting 1's
F - Vacation Query
これとか解いたことないと相当きつい。

衝突回数が円周率になるやつ、実験してみた

www.youtube.com
これです。面白そうなのでやってみた。

作った

質量は10000倍なので3桁です。
youtu.be

物理エンジンはBox2D、グラフィックスはSFMLを使っています。

気をつけること

3Blue1Brownさんの動画の通り、摩擦はなく弾性衝突のみにします。
b2FixtureDef.frictionとb2FixtureDef.restitutionがそれに当たります。
こういうのを完全弾性衝突っていうらしいです。

SetBullet(true); これを設定しないと物体が重なり合ったときの処理がちゃんと行われず、挙動がバグります。

また、動画より早く動かすと正確な値になりませんでした。
(物理演算って繊細)

obsidian 便利

obsidianというノートソフトを紹介?する

長所

  • ç„¡æ–™
  • データはローカルに保存される
  • カスタマイズ性が高い
  • ↑プラグインがつよい

    短所

  • オープンソースではない
  • 標準で十分な機能が提供されていない
  • 他有名ソフトとの連携が弱い

機能

youtbe twitter埋め込み

iframeを書く必要はない
通常の画像リンクのように
![](https://www.youtube.com/watch?v=Sy4QbpYX0XA)

バックリンク

グラフビュー

デイリーノート

プラグイン

自分は40個くらい入れてる

特に便利だなぁと思ったやつ

Templater

javascript

例: デイリーノートのテンプレート

---
aliases: 
created: <% tp.date.now("YYYY-MM-DD") %>
---
<% tp.date.now("YYYY-MM-DD", -1, tp.file.title, "YYYY-MM-DD") %> | <% tp.date.now("YYYY-MM-DD", 1, tp.file.title, "YYYY-MM-DD") %>

# <% tp.date.now("YYYY-MM-DD",0,tp.file.title) %>

# Day planner

## add
## visit
## memo

例: 同階層のノート一覧を出力

< %*
const folder = tp.file.folder();
const items = app.vault.getMarkdownFiles().filter(x=>x.parent.name===folder).map(x => "["+x.name+"]"+"("+x.name.replace(/ /g, "%20")+")");
return items.join("\n");
-% >

プラグイン開発で使われるobsidian apiもここから使えるので実質何でもできる。

なのでWorkspace - Developer Documentationこれも使える。
以下のtemplaterを実行するとファイルを開く度にnew Notice(file.name);が実行される
< %app.workspace.on("file-open", (file) => { new Notice(file.name); });% >
アプリを再起動すると消えてしまうのでstartup templatesに登録したり。

QuickAdd

指定したノートの指定した文字の下などに指定した文字を追加できる。

例: ### visitの下に渡した文字を挿入する

templateモードを使えば、新たなノートを作りそのノートへのリンクを前いた場所に挿入する処理をワンコマンドでできる。

Advanced URI

別のアプリからobsidianをいじれるとか?

例: 現在のサイトのタイトルとurlを引数に渡しquick addを実行する。
これをchromeのブックマークに登録するとか

javascript:Promise.all([]).then(async()=>{const doctitle=document.title;const url=document.URL;function gettitle(e){return e.replace(/\[/g,'\\[').replace(/\]/g,'\\]').split('/').map((e,t)=>0===t?e:%27/%27+e).join(%27%27)}const%20title=gettitle(doctitle);document.location.href=`obsidian://advanced-uri?eval=this.app.plugins.plugins.quickadd.api.executeChoice(%27visit%27,{%27content%27:%27[${title}](${url})%27})`});

urlを貼るだけで自動でマークダウンリンクに変換、タイトルも取得してくれる。

Haten Blog Publisher

Hatena Blogと連携できる。
画像も自動で取得してくれるのうれしい。

Paste image rename

コピペ画像のファイル名の形式を決めれる

Homepage

obsidian起動時に指定したノートを自動で開く。

Another Quick Switcher

標準より強いノート検索機能。
+ノートのフォルダ移動

Linter

Lint実行時にノートを整形する。
とにかく設定が多い
ファイル保存時にupdatedの日付を更新するのに使っている

Git

gitと連携

Remotely Save

dropboxなどと同期できる。
(課金機能代替)

Webpage HTML Export

htmlに変換して出力してくれる。
(課金機能代替)


慣れたら結構使いやすいです。

読んだ漫画載せてく

浅い感想

★

読んだ。

アカイリンゴ

名前覚えてないけど金髪の女の子が好き。

食糧人類

あまり覚えていない

回復術士のやり直し

えろい

少女null

リィンカーネーションの花弁

...最初のJKちょっと好き。後半面白くない

十字架のろくにん

グロい。顔が気持ち悪くてすき

BTOOOM

途中出てくるガキが厨二病で好き。

俺の現実は恋愛ゲーム??かと思ったら命がけのゲームだった

えろい

ブルーロック

ずっと同じことやってる(サッカーだからしょうがない...)。展開が遅くて飽きた。

僕たちがやりました

途中出てくる金髪の女の子好き。終わり方いい。

出会って5秒でバトル

厨二病。香椎鈴が好き。

スパイファミリー

アニメを見ろ。

★★

おもしろい

U12

おもしろい。主人公のことが好きな強い女の子が好き。

たとえ灰になっても

厨二病。キャラ好き。グロい。

カイジ

沼までは面白い

チェンソーマン

雰囲気がすき

リコリス・リコイル

かわいい。これもアニメがよき

カラダ探し

主人公結構すき。

出会って〇〇〇〇で〇〇除霊

かわいい。

...待ってます

今際の国のアリス

世界観すき。普通に主人公とヒロインが好き。

★★★

おすすめ

推しの子

かわいい。黒川あかねがすき。

Happy

作者の性癖もれすぎ(なんかわかる)
ちょっと主人公かわいそう。

地獄に堕ちてよお兄ちゃん

えろい

いじめるヤバイ奴

黒宮さん好き

五等分の花嫁

かわいい。

デスノート

おもしろい

★★★★

おすすめおすすめ

亜人

佐藤さんかっこいい。

ダーウィンズゲーム

厨二病。世界観が好き。

トモダチゲーム

主人公すき。

ワンピース

かっこいい。
特にゾロとドフラミンゴさん♡♡♡

デスティニーラバーズ

えろい。序盤と終盤(ちょっと)に出てきた黒髪の子供がすき。

進撃の巨人

リヴァイかっこいい、ユミル(そばかす)好き。

HUNTER×HUNTER

厨二病過ぎて好き。
船は...面白くない。

Flask Adminで管理画面を作る② ログイン機能

前回Flask Adminで管理画面を作る① - ぶろぐ

今回やること

Managerテーブルを作る
Flask-Loginでログイン機能を作り、アカウントを持ってる人のみアクセスできるようにする
BlackListの各人物に対し担当者を決める。
各管理者に権限を与え、操作の制限をする。

ログイン機能

app/models.py

# ...(BlackList)

from flask_login import UserMixin
from app.app import db, login_manager

# flask-loginで使うため、UserMixinも一緒に継承する
class Manager(UserMixin,db.Model):
    __tablename__ = 'manager'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), unique=True, nullable=False)
    email = db.Column(db.String(50))
    password_hash = db.Column(db.String(256), nullable=False)
    created_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
    
    role = db.Column(db.Integer, default=1, nullable=False)

    def set_password(self, password):
        self.password_hash = generate_password_hash(password)
    
    def check_password(self, password):
        return check_password_hash(self.password_hash, password)

@login_manager.user_loader
def load_user(user_id):
    return Manager.query.get(int(user_id))

パスワードはハッシュ化して保存する。

app/forms.py

from flask_wtf import FlaskForm
from wtforms import PasswordField, StringField, SubmitField
from wtforms.validators import DataRequired, EqualTo, Length


class LoginForm(FlaskForm):
    name = StringField('Name', validators=[DataRequired()])
    password = PasswordField('Password', validators=[DataRequired()])
    submit = SubmitField('Login')

app/admin_index.py

from flask import flash, redirect, url_for
from flask_admin import AdminIndexView, expose
from flask_login import current_user, login_user, logout_user

from app.app import db
from app.forms import LoginForm
from app.models import Manager


class MyAdminIndexView(AdminIndexView):
    
    # ログインしてないなら自動でログイン画面に遷移
    @expose('/')
    def index(self):
        if not current_user.is_authenticated:
            return redirect(url_for('.login_view'))
        return super(MyAdminIndexView, self).index()

    @expose('/login/', methods=('GET', 'POST'))
    def login_view(self):
        form = LoginForm()
        if form.validate_on_submit():
            manager = Manager.query.filter_by(name=form.name.data).first()
            if manager and manager.check_password(form.password.data):
                login_user(manager)
                flash('Logged in successfully.', 'success')
                return redirect(url_for('.index'))
            else:
                flash('Invalid name or password.', 'danger')
        # admin/login.htmlを返す。formをformとして渡す。
        return self.render('admin/login.html', form=form)


    @expose('/logout/')
    def logout_view(self):
        logout_user()
        flash('Logged out successfully.', 'success')
        return redirect(url_for('.login_view'))
    

MyAdminIndexViewは/admin/ルート全体を管轄しているビュー。

サインアップ画面は作らない。ユーザーを追加できるのはadminだけにしたいので

@exposeはModelViewの中で使えるapp.routeの代わりみたいなもの

app/templates/login.html

{% extends 'admin/master.html' %}
{% block body %}
{{ super() }}
<div class="container">
    <div class="row">
        <div class="col-md-4 col-md-offset-4">
            <h1>Login</h1>
            <form method="POST" action="">
                {{ form.hidden_tag() }}
                <div class="form-group">
                    {{ form.name.label }}
                    {{ form.name(class="form-control") }}
                </div>
                <div class="form-group">
                    {{ form.password.label }}
                    {{ form.password(class="form-control") }}
                </div>
                {{ form.submit(class="btn btn-primary") }}
            </form>
        </div>
    </div>
</div>
{% endblock %}

ログイン画面。jinjaテンプレート。

app/views.py

class BlackListModelView(ModelView):
    # ...

    # ビュー(ページ)が表示される前に呼び出される巻数
    # Trueを返す場合このビューにアクセスでき、Falseを返す場合はアクセスできない
    def is_accessible(self):
        return current_user.is_authenticated
    
    # アクセスできなかったときに呼ばれる関数。
    # ログイン画面にリダイレクト
    def inaccessible_callback(self, name, **kwargs):
        return redirect(url_for('admin.login_view', next=request.url))

ログイン必須のView全てに、この2つのメソッドを追加する。
適当な基底クラスを作って一緒に継承すると良い(今回はViewが少ないのでサボっている)

app/views.py

class ManagerModelView(ModelView):
    # idも表示させるようにする
    column_display_pk = True    
    
    # password列を非表示に
    column_exclude_list = ["password_hash"]

    form_create_rules = (
        "name",
        "email",
        "password_hash",
        "role"
    )
    form_edit_rules = (
        "name",
        "email",
        "role"
    )

    # フォーム送信時に呼ばれる関数
    # 作成時のみパスワードをハッシュ化してセットする。
    def on_model_change(self, form, model, is_created):
        if(is_created):    
            if form.password_hash.data:
                model.set_password(form.password_hash.data) 
    
    #ここ?

    # ビュー(ページ)が表示される前に呼び出される巻数
    # Trueを返す場合このビューにアクセスでき、Falseを返す場合はアクセスできない
    def is_accessible(self):
        return current_user.is_authenticated

    # アクセスできなかったときに呼ばれる関数。
    # ログイン画面にリダイレクト
    def inaccessible_callback(self, name, **kwargs):
        return redirect(url_for('admin.login_view', next=request.url))

app/app.py

import os

from dotenv import dotenv_values, load_dotenv
from flask import Flask
from flask_admin import Admin
from flask_admin.contrib.sqla import ModelView
from flask_login import LoginManager
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy

# 環境変数を読み込みconfigに代入
load_dotenv()
config = dotenv_values()

db = SQLAlchemy()
login_manager = LoginManager() 

def create_app():
    app = Flask(__name__)
    app.config.from_mapping(config)
    db.init_app(app)
    # flask db migrateを有効に
    migrate = Migrate(app,db)
    
    # flask-loginの設定
    login_manager.init_app(app)
    login_manager.login_view = 'admin.login_view'
    

    from app.models import BlackList, Manager
    from app.views.admin_index import MyAdminIndexView
    from app.views.blacklist import BlackListModelView
    from app.views.manager import ManagerModelView

    # adminアカウントが無い場合作成
    with app.app_context():    
        admin_user = Manager.query.filter_by(name='admin').first()
        if admin_user is None:
            admin_password = os.getenv('ADMIN_PASSWORD')
            if admin_password:
                admin_user = Manager(
                    name='admin',
                    email='[email protected]'
                )
                admin_user.set_password(admin_password)
                db.session.add(admin_user)
                db.session.commit()
                print("Admin user created.")
            
    
    # indexviewにMyAdminIndexViewを指定
    admin = Admin(app, name="管理画面だぇ", template_mode="bootstrap4", index_view=MyAdminIndexView())
    admin.add_view(BlackListModelView(BlackList, db.session,"ブラックリスト"))
    admin.add_view(ManagerModelView(Manager,db.session,"管理者"))
    admin.add_link(MenuLink(name="ログアウト",url="/admin/logout"))
    return app

環境変数にADMIN_PASSWORDを設定すること。



環境変数に設定したパスワードを入力すると、ログインでき中身を見れるようになる。

createから
タケシとタケルを追加してみた

担当者を決める

いわゆる一体多というやつで、Managerが複数のBlackListの参照を持っていて、BlackListは一つのManagerの参照を持っている。

app/models.py

class BlackList(db.Model):
    # ...
    
    manager_id = db.Column(db.Integer, db.ForeignKey('manager.id'))

class Manager(UserMixin,db.Model):
    # ...
    
    blacklist = db.relationship('BlackList', backref='manager', lazy='dynamic')
    def __repr__(self):
        return self.name

スキーマを変更したので、flask migrate flask upgradeを実行

このリレーションをUIに反映させるのは簡単で、親のModelViewのinline_modelsに子を渡せばいい

class ManagerModelView(ModelView):
    inline_models = [BlackList]


blacklistのフォームから担当者を選択できるようになる

権限を設定

role

  1. blacklistの閲覧のみ可
    • blacklistの追加編集削除可
    • managerの追加削除編集可

(3はAdminのみ)

class ManagerModelView(ModelView):
    # ...

    @property
    def can_edit(self):
        return current_user.role>=3
    @property
    def can_create(self):
        return current_user.role>=3
    @property
    def can_delete(self):
        return current_user.role>=3
class BlackListModelView(ModelView):
    # ...
    
    @property
    def can_edit(self):
        return current_user.role>=2
    @property
    def can_create(self):
        return current_user.role>=2
    @property
    def can_delete(self):
        return current_user.role>=2

ModelViewではcan_〇〇は変数のように呼び出されますが、値を動的に決定したいので関数を使い@propertyで修飾します。げったー

ログアウトして、タケシやタケルで入り直すと操作が制限されています。

親/子モデルへのリンクを貼る

例えばblasklistに載っているAさんを見ていて、そのAさんの担当者の詳細情報が見たいとき、わざわざManagerから検索かけるのは面倒。
blacklistのフォーム内から担当Managerの画面へ飛べるようにできると楽。
その逆も。

調べてみたら見つかりました。

app/rules.py

python - flask admin edit child objects on click - Stack Overflow
親/子が設定されてないときエラーが出ないように少し変えた

from flask import url_for
from flask_admin.form.rules import BaseRule, Markup


class Link(BaseRule):
    def __init__(self, endpoint, attribute, text):
        super(Link, self).__init__()
        self.endpoint = endpoint
        self.text = text
        self.attribute = attribute

    def __call__(self, form, form_opts=None, field_args=None):
        if not field_args:
            field_args = {}

        _id = getattr(form._obj, self.attribute, None)

        if _id:
            return Markup('<a href="{url}">{text}</a>'.format(url=url_for(self.endpoint, id=_id), text=self.text))


class MultiLink(BaseRule):
    def __init__(self, endpoint, relation, attribute):
        super(MultiLink, self).__init__()
        self.endpoint = endpoint
        self.relation = relation
        self.attribute = attribute

    def __call__(self, form, form_opts=None, field_args=None):
        if not field_args:
            field_args = {}
        _hrefs = []
        if form._obj:
            _objects = getattr(form._obj, self.relation)
            for _obj in _objects:
                _id = getattr(_obj, self.attribute, None)
                _link = '<div class="multilink"><a href="{url}">Edit {text}</a></div>'.format(
                    url=url_for(self.endpoint, id=_id), text=str(_obj))
                _hrefs.append(_link)

        return Markup('<br>'.join(_hrefs))

app/views.py

class BlackListModelView(ModelView):
    ...
    form_rules = [
        "name",
        "reason",
        "postcode",
        "address",
        "level",
        "state",
        "manager",
        Link(endpoint='manager.edit_view',attribute='manager_id', text='Edit Manager')
    ]

class ManagerModelView(ModelView):
    ...
    form_create_rules = [
        "name", 
        "email", 
        "password_hash", 
        "role", 
        "blacklist",
        MultiLink(endpoint='blacklist.edit_view',relation='blacklist', attribute='blacklist_id'),
    ]
    form_edit_rules = [
        "name", 
        "email", 
        "role", 
        "blacklist",
        MultiLink(endpoint='blacklist.edit_view',relation='blacklist', attribute='blacklist_id'),
    ]


終わり。

Flask Adminで管理画面を作る①

仕事で管ブラックリストを作りたくなったのでFlask-Adminを使ってみました。

  • 必要なライブラリをインストール
    pip install Flask Flask-Admin Flask-Migrate Flask-SQLAlchemy python-dotenv

app/models.py

from datetime import datetime

from app.app import db

class BlackList(db.Model):
    __tablename__ = "blacklist"
    
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255))
    address = db.Column(db.String(255))
    reason = db.Column(db.String(255))
    level = db.Column(db.Integer)
    state = db.Column(db.Boolean)
    created_at = db.Column(db.DateTime, nullable=False, default=datetime.now)

取り敢えずこれくらいで

.env

環境変数

FLASK_APP=app.app.py
FLASK_DEBUG=1
SECRET_KEY=babubabuakatyandetyu
SQLALCHEMY_DATABASE_URI=sqlite:///db.db
SQLALCHEMY_TRACK_MODIFICATIONS=False

app/app.py

from dotenv import dotenv_values, load_dotenv
from flask import Flask
from flask_admin import Admin
from flask_admin.contrib.sqla import ModelView
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy

# 環境変数を読み込みconfigに代入
load_dotenv()
config = dotenv_values()

db = SQLAlchemy()

def create_app():
    app = Flask(__name__)
    app.config.from_mapping(config)
    db.init_app(app)
    # flask db migrateを有効に
    migrate = Migrate(app,db)
        
    from app.models import BlackList
    
    admin = Admin(app, name="管理画面だぇ", template_mode="bootstrap4")
    admin.add_view(ModelView(BlackList, db.session,"ブラックリスト"))

    return app

ひとまず完成㊗️㊗️㊗️

flask db init
flask db migrate
flask db upgrade
を実行して、
flask runで起動
http://localhost:5000/admin/blacklist/にアクセス

createを押してフォームに情報を入力する。


dbの型から自動でバリデーションを設定してくれる。
DateTimeはbootstrapのpicker?を使えるようにしてくれてる。
おせっかいね。

色々追加してみた。

鉛筆マークで編集
ゴミ箱マークで削除
左のチェックボタンで複数選択して、上のWith selected→Deleteで一括削除(アクション)

ビューをカスタマイズ

ModelViewをオーバーライドする。

先程のコードの、
admin.add_view(ModelView(BlackList, db.session,"ブラックリスト"))
この部分を以下のように変更

 from views import BlackListModelView
 admin.add_view(BlackListModelView(BlackList, db.session,"ブラックリスト"))

app/views.py

from flask_admin.contrib.sqla import ModelView


class BlackListModelView(ModelView):
    # idも表示させるようにする
    column_display_pk = True    
    
    # address列を非表示に
    column_exclude_list = ["address"]
    
    # 検索可能な列
    column_searchable_list = ["id","name","address","reason"]
    
    # フィルタリング可能な列
    column_filters = ["id","name","address","reason","level","state","created_at"]
    
    # 表示させる列名
    column_labels = {
        "name":"名前",
        "address":"住所",
        "reason":"理由",
        "level":"危険度",
        "state":"状態",
    }
    
    # フィールドの値を変換する
    # state列がTrueなら済,Falseなら未済
    # reason列の表示させる文字数を20文字に制限
    column_formatters = {
        "state": lambda v, c, m, p: '済' if getattr(m, p) else '未済',
        'reason': lambda v, c, m, p: (getattr(m, p)[:20] + '...') if len(getattr(m, p, '')) > 20 else getattr(m, p),
    }
    
    # 詳細ビューを有効化
    can_view_details=True


上に検索窓とAddFillterが追加されてる。


左の目のマークを押したら詳細が見れる

フォームのカスタマイズ

コードだけのせます。コメントの通りです。

class BlackListModelView(ModelView):
    # ...


    # カスタムフィールド
    form_extra_fields = {
        "postcode": StringField(label="郵便番号")
    }
    
    # フォームに表示させるカラム
    # created_atは自動で決めるので非表示に
    form_columns = [
        "name",
        "reason",
        "postcode",
        "address",
        "level",
        "state",
    ]

    # input要素に渡す属性。htmlと同じです。
    form_widget_args = {
        "name":{
            "placeholder":"田中太郎",
        },
        "reason":{
            "rows": 10,
        }
    }

    # カラムの詳細
    # 各フォームの下に表示される。
    column_descriptions = {
        "name":"名前を入力してください",
        "postcode":"郵便番号を入力すると自動で住所が入力されます",
        "address": "住所を入力してください",
        "level": "危険度を1から5の整数で入力してください",
        "state": "制裁済みか",
    }
    
    # templateを指定
    create_template = "/blacklist/create.html"
    edit_template = "/blacklist/edit.html"

templateのhtmlファイルを指定することで、より細かくuiをカスタマイズできる

jinjaです。

template/blacklist/create.html

{% extends 'admin/model/edit.html' %}

{% block block %}
    <h1>ここは作成フォームです</h1>
    {{ super() }}
{% endblock %}

template/blacklist/edit.html

{% extends 'admin/model/crate.html' %}

{% block create_form %}
    <h1>ここは編集フォームです</h1>
    {{ super() }}
{% endblock %}

詳しくは公式のリポジトリを見てください
flask-admin/flask_admin/templates/bootstrap4/admin at master · pallets-eco/flask-admin · GitHub

フォーム画面

良くなった。

カスタムアクション

複数のレコードを選択し、一括でstateをTrueにするカスタムアクションを作ってみる。

class BlackListModelView(ModelView):
    # ...

    @action('update', 'Update', '一括 True ok?')
    def action_update_state(self, ids):
        try:
            #チェックが押されたレコードすべてを取得
            query = BlackList.query.filter(BlackList.id.in_(ids))
            for staff in query.all():
                staff.state = True
            db.session.commit()
            flash(f'Successfully')
        except Exception as ex:
            if not self.handle_view_exception(ex):
                raise
            flash(f'Failed')
    

左のチェックで複数のレコードを選択して、
With selected
→Update
→「一括 True ok?」と聞いてくるのでok

次回は認証機能を作る Flask Adminで管理画面を作る② ログイン機能 - ぶろぐ