Start_python’s diary

ふたり暮らし

アラフィフ夫婦のフリーランスプラン

FlaskでWebアプリが完成しました!(全ソースコードはこちらです)

画像1

 

はじめに

Webアプリが完成したので全ソースコードを載せることにしました。

こちらから実際に操作できますのでお試しください。

http://startpython.pythonanywhere.com/blog/

ユーザー名とパスワードを何も入れずにログインするとゲストでログインできます。

※ユーザー名・パスワードは普段お使いのもの以外にしてください。セキュリティーは保証できません。

 

関連記事

 

動作環境

Windows10
Python 3.7.5
Flask 1.1.1

作業フォルダ/
 ├ blog/
 │  ├ templates/
 │  │  └ blog/
 │  │      ├ base.html
 │ │ ├ diary.html │ │ ├ edit.html │ │ ├ index.html │ │ ├ newcomoer.html │ │ └ top.html │ ├ models/ │ │ ├ __init__.py │ │ ├ database.py │ │ ├ models.py │ │ └ wiki.db │ └ server.py ├ static/ │ ├ images/ │ │ └ sample.jpg │ ├ back_image.jpg │ ├ base.css
 │ ├ diary.css
 │ ├ diary.js │ ├ edit.css │ ├ edit.js │ ├ image.jpg │ ├ index.css │ ├ login.css │ ├ login.js │ ├ newcomer.css │ └ newcomer.js ├ templates/ │ └ index.html ├ key.py └ main.py

 

ソースコード

「作業フォルダ/blog/templates/blog/」

base.html

<!DOCTYPE html>
<meta name="viewport" content="width=device-width,initial-scale=1">

<link rel="stylesheet" type="text/css" href="/static/base.css">

<body>
    <header>

        {% if name == "" %}
        <h3 id="user_name">ようこそ ゲスト さん</h3>
        {% elif name %}
        <h3 id="user_name">ようこそ {{name}} さん</h3>
        {% else %}
        <h3 id="user_name">ログインしてください</h3>
        {% endif %}

        <div id="search1">
        <form action="/blog/search" method="post">
            <input id="search2" type="text" name="search">
            <input class="btn2" type="submit" value="検  索">
        </form>
        </div>

        <div class="menu_box">
            <label id="menu" for="toggle">メニュー</label>
            <input type="checkbox" id="toggle">
            <ul class="dropdown_menu">
                <li><a href="/blog">ホーム</a></li>
                <li><a href="/blog/create">新規作成</a></li>
                <li><a href="/blog/logout">ログアウト</a></li>
            </ul>
        </div>

    </header>


{% block body %}{% endblock %}


    <footer>
        <ul>
            <li><a href="/blog">ホーム</a></li>
            {% if name == "レイルーク" %}
            <li><a href="/blog/all_delete">全削除</a></li>
            {% else %}
            <li>

            <span onclick="document.getElementById('search1').style.display = 'block'; document.getElementById('search2').focus();">
            検 索</span>

            </li>
            {% endif %}
            <li><a href="/blog/create">新規作成</a></li>
            <li><a href="/blog/logout">ログアウト</a></li>
        </ul>
        <span id="c">(c)2020 ふたり暮らし</span>
    </footer>
</body>

 

diary.html

{% extends "blog/base.html" %}
{% block body %}

<link rel="stylesheet" type="text/css" href="/static/diary.css">
<script src="/static/diary.js"></script>


<title>更新画面</title>

<form action="/blog/diaryupdate" method="post" enctype="multipart/form-data">

    <div id="main">
        <input id="ID" type="text" name="ID" value="{{diary.id}}">
        <label class="label">タイトル</label>
        <input id="text1" type="text" name="title" value="{{diary.title}}">
        <br><br>
        <div id=image_box>
            {% if name == diary.user %}
            <label class="btn1" for="file-sample">ファイルを選択</label>
            {% else %}
            <label class="btn0">ファイルを選択</label>
            {% endif %}
            <input id="file-sample" type="file" name="image" value="{{diary.image_url}}">
            <img id="file-preview" src="{{diary.image_url}}">
        </div>
        <label class="label">コメント</label>
        <textarea rows="12" cols="100" name="comment">{{diary.comment}}</textarea>
        <div class="time">{{diary.date.strftime("%Y/%m/%d %H:%M")}}</div>

        {% if name == diary.user or name == "レイルーク" %}
        <input class="btn1" type="submit" value="  更  新  ">
        <button class="btn1" id="btn2" type="button" onclick="location.href='/blog/delete/{{diary.id}}'">  削  除  </button>
        {% endif %}
    </div>

</form>

{% endblock %}

 

edit.html

{% extends "blog/base.html" %}
{% block body %}

<link rel="stylesheet" type="text/css" href="/static/edit.css">
<script src="/static/edit.js"></script>


<title>入力画面</title>

<form action="/blog/diarysave" method="post" enctype="multipart/form-data">

    <div id="main">
        <label class="label">タイトル</label>
        <input id="text1" type="text" name="title">
        <br><br>
        <div id=image_box>
            <label class="btn1" for="file-sample">ファイルを選択</label>
            <input id="file-sample" type="file" name="image">
            <img id="file-preview">
        </div>
        <label class="label">コメント</label>
        <textarea rows="12" cols="100" name="comment"></textarea>
        <div class="time"></div>
        <input class="btn1" type="submit" value="  登  録  ">
    </div>

</form>

{% endblock %}

 

index.html

{% extends "blog/base.html" %}
{% block body %}

<link rel="stylesheet" type="text/css" href="/static/index.css">

<title>インデックス画面</title>

<div id="main">
    {% for diary in all_diary[::-1] %}
        {% if search == "" or search in diary.user 
            or search in diary.title or search in diary.comment %}

    <form action="/blog/diary/{{diary.id}}">
        <label id="menu" for="{{diary.id}}">

    <div class="table">
        {% if diary.user != "" %}
        <h3>{{diary.title}} @{{diary.user}}</h3>
        {% else %}
        <h3>{{diary.title}}</h3>
        {% endif %}
        <p><img src={{diary.image_url}}></p>
        <p>{{diary.comment}}</p>
        <div class="time">{{diary.date.strftime("%Y/%m/%d %H:%M")}}</div>
    </div>

        </label>
        <input type="submit" id="{{diary.id}}" class="update">
    </form>

        {% endif %}
    {% endfor %}
</div>

{% endblock %}

 

newcomoer.html

<!DOCTYPE html>
<meta name="viewport" content="width=device-width,initial-scale=1">

<link rel="stylesheet" type="text/css" href="/static/newcomer.css">
<script src="/static/newcomer.js"></script>

<title>新規登録画面</title>

<div class="frame">
    <img src="/static/image.jpg">
    <h2>アカウントを作成</h2>
    {% if status == "exist_user" %}
    <p>そのユーザーは既に登録されています。</p>
    {% elif status == None %}
    <p>ユーザ名を入力してください。</p>
    {% endif %}
    <form action="/blog/registar" method="post">
        <div class="box1">
            <label for="text1" id="l_text1">名前</label>
            <input id="text1" type="text" name="user_name" placeholder="user name">
        </div>
        <div class="box1">
            <label for="text2" id="l_text2">パスワード</label>
            <input id="text2" type="password" name="password" placeholder="password">
        </div>
        <p>
            アカウントを作成すると、利用規約、およびCookieの使用を含むプライバシーポリシーに同意したことになります。
        </p>
        <button class="btn1" type="submit">登録する</button>
    </form>
    <p>
      <br>
      <a href="/blog/top">ログイン画面に戻る</a>
    </p>
</div>

 

top.html

<!DOCTYPE html>
<meta name="viewport" content="width=device-width,initial-scale=1">

<link rel="stylesheet" type="text/css" href="/static/login.css">
<script src="/static/login.js"></script>

<title>ログイン画面</title>

<div class="frame">
  <div class="box0">
    <img src="/static/image.jpg">
    <h3>ふたり暮らしにログイン</h3>

    {% if status == "user_notfound" %}
    <p>ユーザーが見つかりません。アカウント作成してください</p>
    {% elif status == "wrong_password" %}
    <p>パスワードが間違っています</p>
    {% elif status == "logout" %}
    <p>ログアウト完了</p>
    {% endif %}
  </div>
  <form action="/blog/login" method="post">
    <div class="box1">
      <label for="text1" id="l_text1">ユーザー名</label>
      <input id="text1" type="text" name="user_name" placeholder="user name">
    </div>
    <div class="box1">
      <label for="text2" id="l_text2">パスワード</label>
      <input id="text2" type="password" name="password" placeholder="password">
    </div>
    <button class="btn1" type="submit">ログイン</button>
  </form>

  <p>
    <br>
    <a href="/blog/newcomer">パスワードをお忘れですか?</a>
 
    <a href="/blog/newcomer">アカウント作成</a>
  </p>
</div>

 

「作業フォルダ/blog/models/」

__init__.py

 

 

database.py

from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
import os

#パスを定義してwiki.dbを作成します。
databese_file = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'wiki.db')
engine = create_engine('sqlite:///' + databese_file, convert_unicode=True)

#データベースにアクセスするためにセッションを作成します。
db_session = scoped_session(sessionmaker(autocommit=False,
                                         autoflush=False,
                                         bind=engine))
#Baseオブジェクトを作った後、query_property()を使ってBaseオブジェクトに検索クエリを持たせます。
#後でデータベースから検索する時に楽らしい。
Base = declarative_base()
Base.query = db_session.query_property()

#データベースの初期化用です。初期化したいときに呼びます。
def init_db():
    import models.models
    Base.metadata.create_all(bind=engine)

 

models.py

from sqlalchemy import Column, Integer, String, Text, DateTime
#from models.database import Base       # データベースの初期化用
from blog.models.database import Base   # 初期化が終わればこちらへ変更
from datetime import datetime
 
 
class WikiContent(Base):
    __tablename__ = 'wikicontents'
    id = Column(Integer, primary_key=True)
    user = Column(String(128))
    title = Column(String(128))
    comment = Column(Text)
    image_url = Column(String(128))
    date = Column(DateTime, default=datetime.now())
 
    def __init__(self, user=None, title=None, image_url=None, comment=None, date=None):
        self.user = user
        self.title = title
        self.image_url = image_url
        self.comment = comment
        self.date = date
 
    def __repr__(self):
        return '<Title %r>' % (self.title)
 
#Userクラスを追加。WikiContentの使い回し
class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    user_name = Column(String(128))
    hashed_password = Column(String(128))
 
    def __init__(self, user_name=None, hashed_password=None):
        self.user_name = user_name
        self.hashed_password = hashed_password
 
    def __repr__(self):
        return '<Name %r>' % (self.user_name)

 

「作業フォルダ/blog/」

server.py

# Blueprint(pyファイルを分割するための関数)をインポート
from flask import Blueprint

#「app」を「Blueprint()」を使って定義
#「blog」の部分は、url_for("blog.top")で使用
app = Blueprint('blog', __name__, template_folder='templates')


# 必要なモジュールをインポート
import os
from datetime import datetime, timedelta, timezone
from flask import Flask, session, redirect, url_for, render_template,request
#「/blog/models/models.py」の「WikiContent」と「User」を呼び出す
from blog.models.models import WikiContent,User
#「/blog/models/database.py」の「db_session」を呼び出す
from blog.models.database import db_session

from werkzeug.utils import secure_filename

# ハッシュ化するための関数をインポート(パスワードの暗号化用)
# 今回はsha256という暗号化方式を使用
import key
from hashlib import sha256


#「/」へアクセスがあった場合
@app.route('/')
def index():  # 一覧画面
    if "user_name" in session:
        # セッションがログイン状態であれば「blog/index.html」を返す
        name = session["user_name"]
        all_diary= WikiContent.query.all()
        search = ""
        return render_template('blog/index.html',name=name, all_diary=all_diary,search=search)
    else:
        # ログイン状態でなければ「/blog/top」ページへ移動
        return redirect(url_for("blog.top"))

#「/logout」へアクセスがあった場合「/blog/top」ページへ移動
@app.route('/logout')
def logout():
    session.pop("user_name", None)
    return redirect(url_for("blog.top",status="logout"))

#「/create」へアクセスがあった場合「blog/edit.html」を返す
@app.route('/create')
def create():  #新規作成
    name = session["user_name"]
    return render_template('blog/edit.html',name=name)


#「/diary/<ID>」へアクセスがあった場合「blog/diary.html」を返す
@app.route('/diary/<ID>')
def diary(ID):  #参照
    name = session["user_name"]
    diary = WikiContent.query.get(ID)
    return render_template('blog/diary.html',name=name,diary=diary)


#「/diarysave」へアクセスがあった場合「/blog/index」ページへ移動
@app.route('/diarysave', methods=['post'])
def diarysave():  #保存
    image_url = ""
    # 画像ファイルを保存
    if request.files['image']:
        f = request.files['image']
        filepath = 'static/images/' + secure_filename(f.filename)
        f.save(filepath)
        filepath = '/' + filepath
        image_url = filepath
    # データベースに保存
    name = name = session["user_name"]
    title = request.form["title"]
    comment = request.form["comment"]
    # JSTタイムゾーンを作成
    jst = timezone(timedelta(hours=9), 'JST')
    date = datetime.now(jst)
    content = WikiContent(name,title,image_url,comment,date)
    db_session.add(content)
    db_session.commit()
    return redirect(url_for("blog.index"))



#「/diaryupdate」へアクセスがあった場合「/blog/index」ページへ移動
@app.route('/diaryupdate', methods=['post'])
def diaryupdate():  #更新
    content = WikiContent.query.get(request.form.get("ID"))
    # 画像ファイルを更新
    if request.files['image']:
        f = request.files['image']
        filepath = 'static/images/' + secure_filename(f.filename)
        f.save(filepath)
        filepath = '/' + filepath
        content.image_url = filepath
    # データベースを更新
    content.title = request.form["title"]
    content.comment = request.form["comment"]
    db_session.commit()
    return redirect(url_for("blog.index"))


#「/delete/<ID>」へアクセスがあった場合「/blog/index」ページへ移動
@app.route('/delete/<ID>')
def delete(ID):  #削除
    content = WikiContent.query.get(ID)
    db_session.delete(content)
    db_session.commit()
    return redirect(url_for("blog.index"))


@app.route('/all_delete')
def all_delete():  #全削除
    WikiContent.query.delete()
    db_session.commit()
    return redirect(url_for("blog.index"))


#「/search」へアクセスがあった場合「blog/index.html」を返す
@app.route('/search', methods=['post'])
def search():  # 検索
    name = session["user_name"]
    all_diary= WikiContent.query.all()
    search = request.form["search"]
    return render_template('blog/index.html',name=name, all_diary=all_diary,search=search)


# top
#「/login」へアクセスがあった場合
@app.route("/login",methods=["post"])
def login():
    # 入力された「ユーザー名」が既に存在するか確認
    user_name = request.form["user_name"]
    user = User.query.filter_by(user_name=user_name).first()
    if user:
        #「ユーザー名」が存在した場合
        #「パスワード」が一致するか確認(「key.SALT」で暗号化)
        password = request.form["password"]
        hashed_password = sha256((user_name + password + key.SALT).encode("utf-8")).hexdigest()
        if user.hashed_password == hashed_password:
            #「パスワード」が一致した場合「/blog/index」ページへ移動
            session["user_name"] = user_name
            return redirect(url_for("blog.index"))
        else:
            #「パスワード」が一致しない場合「/blog/top」ページへ移動
            return redirect(url_for("blog.top",status="wrong_password"))
    else:
        #「ユーザー名」が存在しなかった場合「/blog/top」ページへ移動
        return redirect(url_for("blog.top",status="user_notfound"))

#「/registar」へアクセスがあった場合
# 新規登録画面で「新規登録」ボタンが押された時
@app.route("/registar",methods=["post"])
def registar():
    # 入力された「ユーザー名」が既に存在するか確認
    user_name = request.form["user_name"]
    user = User.query.filter_by(user_name=user_name).first()
    if user:
        #「ユーザー名」が存在した場合「/blog/newcomer」ページへ移動
        return redirect(url_for("blog.newcomer",status="exist_user"))
    else:
        #「ユーザー名」が存在しなかった場合
        #「ユーザー名」「パスワード」(「key.SALT」で暗号化)を
        password = request.form["password"]
        hashed_password = sha256((user_name + password + key.SALT).encode("utf-8")).hexdigest()
        user = User(user_name, hashed_password)
        # データベースに追加して保存
        db_session.add(user)
        db_session.commit()
        session["user_name"] = user_name
        #「/blog/index」ページへ移動
        return redirect(url_for("blog.index"))

#「/top」へアクセスがあった場合「blog/top.html」を返す
@app.route("/top")
def top():
    status = request.args.get("status")
    return render_template("blog/top.html",status=status)

#「/newcomer」へアクセスがあった場合「blog/newcomer.html」を返す
@app.route("/newcomer")
def newcomer():
    status = request.args.get("status")
    return render_template("blog/newcomer.html",status=status)

 

「作業フォルダ/static/」

base.css

header{
    position: fixed; /*絶対位置で固定*/
    top: 0px; /*上に配置*/
    left: 0px; /*左の隙間をなくす*/
    padding: 0px 30px; /*範囲内の余白(上下、左右)*/
    width: 100%; /*幅*/
    height: 60px; /*高さ*/
    background-image: url("back_image.jpg");
    /*画像を背景に指定*/
}

#user_name{
    position: absolute; /*絶対位置で表示*/
    margin: 20px 0; /*範囲外の余白*/
}

#search1{
    display: none; /*非表示にする*/
    position: absolute; /*絶対位置で表示*/
    left: 0;
    right: 0;
    width: 90%; /*幅*/
    margin: 10px auto; /*中央寄せ*/
    max-width: 700px; /*最大の幅*/
}

#search2{
    padding: 8px; /*範囲内の余白*/
    width: calc(100% - 200px); /*幅*/
    font-size: 18px; /*文字の大きさ*/
    background-color: white; /*背景色*/
}

.btn2{
    padding: 5px 30px; /*余白*/
    color:#fff; /*文字の色*/
    font-size: 16px; /*文字のサイズ*/
    text-align: center; /*文字の位置*/
    background:#1B73BA; /*背景色*/
    border-radius: 10px; /*角の半径*/
    cursor: pointer; /*カーソル(指のかたち)*/
}

.btn2:hover{
    opacity: 0.8; /*透明度*/
}

.menu_box{
    margin: 30px 60px 0 calc(100% - 180px); /*範囲外の余白*/
    text-align: right; /*右に寄せる*/
}

#toggle{
    display: none; /*非表示にする*/
}

.dropdown_menu{
    display: none; /*非表示にする*/
}

/* .menu_box:hover .dropdown_menu{ */
#toggle:checked+.dropdown_menu{
    display: block; /*リストを表示する*/
}

.dropdown_menu li{
    display: block; /*要素をインライン表示にする*/
    width: 120px; /*幅*/
    text-align: center; /*中央に表示する*/
    background-color: white; /*背景色*/
    border: outset 2px; /*外枠を作る(スタイル、太さ)*/
    cursor: pointer; /*カーソル(指のかたち)*/
}

footer{
    position: fixed; /*絶対位置で固定*/
    bottom: 0px; /*下に配置*/
    left: 0px; /*左の隙間をなくす*/
    width: 100%; /*幅*/
    height: 60px; /*高さ*/
    font-weight: bold; /*文字を太字にする*/
    text-align: center; /*中央に表示する*/
    background-image: url("back_image.jpg");
    /*画像を背景に指定*/
}

ul{
    margin: 0; /*上の余白をなくすため*/
    padding: 5px 0; /*左の余白をなくすため*/
}

li{
    display: inline-block; /*要素をインライン表示にする*/
    width: 120px; /*幅*/
    text-align: center; /*中央に表示する*/
    border: outset 2px; /*外枠を作る(スタイル、太さ)*/
    cursor: pointer; /*カーソル(指のかたち)*/
}

a {
    color: #000; /*文字の色*/
    text-decoration: none; /*下線を消す*/
}

@media screen and (max-width: 600px) {
    li{
        width: 80px; /*スマホなどの時のボタン幅*/
    }
}

#c{
    position: absolute; /*絶対位置(rightのために必要)*/
    right: 30px; /*右に配置*/

 

diary.css

header{
    position: fixed; /*絶対位置で固定*/
    top: 0px; /*上に配置*/
    left: 0px; /*左の隙間をなくす*/
    padding:0px 30px; /*範囲内の余白(上下、左右)*/
    width: 100%; /*幅*/
    height: 60px; /*高さ*/
    background-image: url("back_image.jpg");
    /*画像を背景に指定*/
}

#main{
    margin: auto; /*中央寄せ*/
    padding:60px 20px; /*範囲内の余白(上下、左右)*/
    width: 90%; /*幅*/
    max-width: 800px; /*最大の幅*/
}

#ID{
    display: none; /*非表示にする*/
}

.label{
    font-size: 24px; /*文字のサイズ*/
}

#text1{
    padding: 8px; /*範囲内の余白*/
    width: 100%; /*幅*/
    font-size: 18px; /*文字の大きさ*/
    box-sizing: border-box; /*横幅の解釈をpadding, borderまでとする*/
}

textarea{
    padding: 10px; /*範囲内の余白*/
    width: calc(100% - 300px); /*幅*/
    font-size: 17px; /*文字の大きさ*/
    box-sizing: border-box; /*横幅の解釈をpadding, borderまでとする*/
    resize: vertical;
}

#image_box{
    float: right; /*右に寄せる*/
    width: 300px;
}

#file-sample {
     display: none; 
}

.btn1{
    display: inline-block;
    padding: 5px 30px; /*余白*/
    color:#fff; /*文字の色*/
    font-size: 16px; /*文字のサイズ*/
    text-align: center; /*文字の位置*/
    background:#1B73BA; /*背景色*/
    border-radius: 10px; /*角の半径*/
    cursor: pointer; /*カーソル(指のかたち)*/
}

.btn1:hover{
    opacity: 0.8; /*透明度*/
}

.btn0{
    display: inline-block;
    padding: 5px 30px; /*余白*/
    font-size: 16px; /*文字のサイズ*/
    opacity: 0; /*透明*/
}

#btn2{
    background:red; /*背景色*/
    float: right; /*右に寄せる*/
}

img{
    margin: 2px 0;
    width: 100%;
}

@media screen and (max-width: 600px) {
    #image_box{
        width: 100%; /*スマホなどの時の画像サイズ*/
    }
    textarea{
        width: 100%;
    }
}

.time{
    clear: both; /*「float」を解除*/
}

footer{
    position: fixed; /*絶対位置で固定*/
    bottom: 0px; /*下に配置*/
    left: 0px; /*左の隙間をなくす*/
    width: 100%; /*幅*/
    height: 60px; /*高さ*/
    font-weight: bold; /*文字を太字にする*/
    text-align: center; /*中央に表示する*/
    background-image: url("back_image.jpg");
    /*画像を背景に指定*/
}

ul.footer-menu li {
    display: inline; /*一行で表示する*/
}

#copy{
    position: absolute; /*絶対位置(rightのために必要)*/
    right: 30px; /*右に配置*/
}

 

diary.js

window.onload = function () {
    document.getElementById('file-sample').addEventListener('change', function (e) {
        var file = e.target.files[0];

        var blobUrl = window.URL.createObjectURL(file);

        var img = document.getElementById('file-preview');
        img.src = blobUrl;
    });
}

 

edit.css

header{
    position: fixed; /*絶対位置で固定*/
    top: 0px; /*上に配置*/
    left: 0px; /*左の隙間をなくす*/
    padding:0px 30px; /*範囲内の余白(上下、左右)*/
    width: 100%; /*幅*/
    height: 60px; /*高さ*/
    background-image: url("back_image.jpg");
    /*画像を背景に指定*/
}

#main{
    margin: auto; /*中央寄せ*/
    padding:60px 20px; /*範囲内の余白(上下、左右)*/
    width: 90%; /*幅*/
    max-width: 800px; /*最大の幅*/
}

.label{
    font-size: 24px; /*文字のサイズ*/
}

#text1{
    padding: 8px; /*範囲内の余白*/
    width: 100%; /*幅*/
    font-size: 18px; /*文字の大きさ*/
    box-sizing: border-box; /*横幅の解釈をpadding, borderまでとする*/
}

textarea{
    padding: 10px; /*範囲内の余白*/
    width: calc(100% - 300px); /*幅*/
    font-size: 17px; /*文字の大きさ*/
    box-sizing: border-box; /*横幅の解釈をpadding, borderまでとする*/
    resize: vertical;
}

#image_box{
    float: right; /*右に寄せる*/
    width: 300px;
}

#file-sample {
     display: none; 
}

.btn1{
    display: block;
    padding: 5px 30px; /*余白*/
    color:#fff; /*文字の色*/
    font-size: 16px; /*文字のサイズ*/
    text-align: center; /*文字の位置*/
    background:#1B73BA; /*背景色*/
    border-radius: 10px; /*角の半径*/
    cursor: pointer; /*カーソル(指のかたち)*/
}

.btn1:hover{
    opacity: 0.8; /*透明度*/
}

img{
    margin: 2px 0;
    width: 100%;
}

@media screen and (max-width: 600px) {
    #image_box{
        width: 100%; /*スマホなどの時の画像サイズ*/
    }
    textarea{
        width: 100%;
    }
}

.time{
    clear: both; /*「float」を解除*/
}

footer{
    position: fixed; /*絶対位置で固定*/
    bottom: 0px; /*下に配置*/
    left: 0px; /*左の隙間をなくす*/
    width: 100%; /*幅*/
    height: 60px; /*高さ*/
    font-weight: bold; /*文字を太字にする*/
    text-align: center; /*中央に表示する*/
    background-image: url("back_image.jpg");
    /*画像を背景に指定*/
}

ul.footer-menu li {
    display: inline; /*一行で表示する*/
}

#copy{
    position: absolute; /*絶対位置(rightのために必要)*/
    right: 30px; /*右に配置*/
}

 

edit.js

window.onload = function () {
    document.getElementById('file-sample').addEventListener('change', function (e) {
        var file = e.target.files[0];

        var blobUrl = window.URL.createObjectURL(file);

        var img = document.getElementById('file-preview');
        img.src = blobUrl;
    });
}

 

index.css

#main{
    width: 100%; /*幅*/
    margin: auto; /*中央寄せ*/
    max-width: 800px; /*最大の幅*/
    padding:60px 0; /*範囲内の余白(上下、左右)*/
}

.table{
    border: outset 2px; /*外枠を作る(スタイル、太さ)*/
    padding:10px 20px; /*範囲内の余白(上下、左右)*/
    margin: 10px 15px; /*範囲外の余白(上下、左右)*/
}

img{
    float: right; /*右に寄せる*/
    width: 300px; /*画像サイズを固定*/
}

@media screen and (max-width: 600px) {
    img{
        width: 100%; /*スマホなどの時の画像サイズ*/
    }
}

.time{
    clear: both; /*「float」を解除*/
}

.update{
    display: none; /*非表示にする*/
}

 

login.css

@media (max-width: 600px) {
    p {
        color: red;
        font-size: 80%;
    }
}

.frame{
    width: 90%; /*幅*/
    margin: 30px auto; /*上の位置と中央寄せ*/
    max-width: 600px; /*最大の幅*/
}

img{
    width: 80px; /*幅*/
    height: auto; /*縦横比を維持する高さを自動計算*/
}

.box0{
    text-align: center; /*中央寄せ*/
}

.box1{
    width: 100%; /*幅*/
    height: 60px; /*高さ*/
    margin: 15px 0px; /*領域外のスペース*/
    background-color: #eee; /*背景色*/
    border-bottom: 2px solid #c2c2c2; /*text1の下線*/
}

#l_text1{
    display: block; /*要素の表示形式*/
    font-size: 14px; /*文字の大きさ*/
    padding: 5px 10px; /*領域内のスペース*/
}

#text1{
    font-size: 18px; /*文字の大きさ*/
    padding: 0 0 7px 10px; /*ボックスを大きくする*/
    width: 100%; /*幅*/
    background-color: #eee; /*背景色*/
    border: none; /*外枠の線*/
    outline: none; /*外枠の線*/
    border-bottom: 2px solid #c2c2c2; /*text1の下線*/
    box-sizing: border-box; /*横幅の解釈をpadding, borderまでとする*/
}

#text1:focus{
    border-bottom: 2px solid #1B73BA; /*下線の太さと色*/
}

#l_text2{
    display: block; /*要素の表示形式*/
    font-size: 13px; /*文字の大きさ*/
    padding: 5px 10px; /*領域内のスペース*/
}

#text2{
    font-size: 18px; /*文字の大きさ*/
    padding: 2px 0 7px 10px; /*ボックスを大きくする*/
    width: 100%; /*幅*/
    background-color: #eee; /*背景色*/
    border: none; /*外枠の線*/
    outline: none; /*外枠の線*/
    border-bottom: 2px solid #c2c2c2; /*text1の下線*/
    box-sizing: border-box; /*横幅の解釈をpadding, borderまでとする*/
}

#text2:focus{
    border-bottom: 2px solid #1B73BA; /*下線の太さと色*/
}

.btn1{
    position:relative; /*相対位置(topのために必要)*/
    width:100%; /*幅*/
    height:50px; /*高さ*/
    top: 10px; /*上の位置*/
    color:#fff; /*文字の色*/
    border:none; /*外枠*/
    -webkit-border-radius: 25px;
       -moz-border-radius: 25px;
            border-radius: 25px; /*角の半径*/
    background:#1B73BA; /*背景色*/
    display: block; /*要素の表示形式*/
    -webkit-appearance: none;
    outline: 0; /*外枠(押した後)*/
    cursor: pointer; /*カーソル(指のかたち)*/
}
.btn1:hover{
    opacity: 0.8; /*透明度*/
}

a {
    text-decoration: none; /*下線を消す*/
}
a:hover{
    text-decoration: underline; /*下線を引く*/
}

p {
    text-align: center; /*中央寄せ*/
}

 

login.js

window.onload = function () {
  document.getElementById( "text1" ).onfocus = function(){
    document.getElementById("l_text1").style.color = '#1B73BA';
  };
  document.getElementById( "text1" ).onblur = function(){
    document.getElementById("l_text1").style.color = '';
  };

  document.getElementById( "text2" ).onfocus = function(){
    document.getElementById("l_text2").style.color = '#1B73BA';
  };
  document.getElementById( "text2" ).onblur = function(){
    document.getElementById("l_text2").style.color = '';
  };
}

 

newcomer.css

@media screen and (max-width: 600px) {
    p {
        font-size: 80%;
    }
}

.frame{
    width: 90%; /*幅*/
    margin: 30px auto; /*上の位置と中央寄せ*/
    max-width: 600px; /*最大の幅*/
}

img{
    display: block; /*要素の表示形式*/
    margin: auto; /*中央寄せ*/
    width: 40px; /*幅*/
    height: auto; /*縦横比を維持する高さを自動計算*/
}

.box1{
    width: 100%; /*幅*/
    height: 60px; /*高さ*/
    margin: 15px 0px; /*領域外のスペース*/
    background-color: #eee; /*背景色*/
    border-bottom: 2px solid #c2c2c2; /*text1の下線*/
}

#l_text1{
    display: block; /*要素の表示形式*/
    font-size: 14px; /*文字の大きさ*/
    padding: 5px 10px; /*領域内のスペース*/
}

#text1{
    font-size: 18px; /*文字の大きさ*/
    padding: 0 0 7px 10px; /*ボックスを大きくする*/
    width: 100%; /*幅*/
    background-color: #eee; /*背景色*/
    border: none; /*外枠の線*/
    outline: none; /*外枠の線*/
    border-bottom: 2px solid #c2c2c2; /*text1の下線*/
    box-sizing: border-box; /*横幅の解釈をpadding, borderまでとする*/
}

#text1:focus{
    border-bottom: 2px solid #1B73BA; /*下線の太さと色*/
}

#l_text2{
    display: block; /*要素の表示形式*/
    font-size: 13px; /*文字の大きさ*/
    padding: 5px 10px; /*領域内のスペース*/
}

#text2{
    font-size: 18px; /*文字の大きさ*/
    padding: 2px 0 7px 10px; /*ボックスを大きくする*/
    width: 100%; /*幅*/
    background-color: #eee; /*背景色*/
    border: none; /*外枠の線*/
    outline: none; /*外枠の線*/
    border-bottom: 2px solid #c2c2c2; /*text1の下線*/
    box-sizing: border-box; /*横幅の解釈をpadding, borderまでとする*/
}

#text2:focus{
    border-bottom: 2px solid #1B73BA; /*下線の太さと色*/
}

.btn1{
    position:relative; /*相対位置(topのために必要)*/
    width:100%; /*幅*/
    height:50px; /*高さ*/
    top: 10px; /*上の位置*/
    color:#fff; /*文字の色*/
    border:none; /*外枠*/
    -webkit-border-radius: 25px;
       -moz-border-radius: 25px;
            border-radius: 25px; /*角の半径*/
    background:#1B73BA; /*背景色*/
    display: block; /*要素の表示形式*/
    -webkit-appearance: none;
    outline: 0; /*外枠(押した後)*/
    cursor: pointer; /*カーソル(指のかたち)*/
}

.btn1:hover{
    opacity: 0.8; /*透明度*/
}

a {
    text-decoration: none; /*下線を消す*/
}
a:hover{
    text-decoration: underline; /*下線を引く*/
}

 

newcomer.js

window.onload = function () {
  document.getElementById( "text1" ).onfocus = function(){
    document.getElementById("l_text1").style.color = '#1B73BA';
  };
  document.getElementById( "text1" ).onblur = function(){
    document.getElementById("l_text1").style.color = '';
  };

  document.getElementById( "text2" ).onfocus = function(){
    document.getElementById("l_text2").style.color = '#1B73BA';
  };
  document.getElementById( "text2" ).onblur = function(){
    document.getElementById("l_text2").style.color = '';
  };
}

 

「作業フォルダ/templates/」

index.html

<!DOCTYPE html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>ふたり暮らし メニュー画面</title>
  </head>
  <body>

    <h2>メインメニュー</h2>
    <p><a href= "/blog" >ブログ(登録とログイン)</a></p>

  </body>
</html>

 

「作業フォルダ/」

key.py

SECRET_KEY = "内緒:好きなランダムの英数字"
SALT = "内緒:好きなランダムの英数字"

 

main.py

# Flaskとrender_template(HTMLを表示させるための関数)をインポート
from flask import Flask, render_template

# Flaskオブジェクトの生成
app = Flask(__name__)

# セキュリティーキーの設定(なりすましなどを防ぐため)
import key
app.secret_key = key.SECRET_KEY

# 前回作った基本的なWebページ(今回は使いません)
#from test.test_app import app1
#app.register_blueprint(app1)

# 今回使用するWebアプリ
from blog.server import app as app2
app.register_blueprint(app2, url_prefix="/blog")


#「/」へアクセスがあった場合に「index.html」を返す
@app.route('/')
def index():
    return render_template('index.html')


if __name__ == '__main__':
    app.run(debug=True)

 

まとめ

以上です。お疲れ様でした。すごく長くなりました。

ここまで見てくれている人はどれくらいいるのでしょうか。これでゼロから始める人も掲示板アプリを作成することができると思います。ここのソースコードはこのままコピペして使っていただいても構いません。著作権フリーです。

少しでもこれからWebアプリを作られる方のお役に立てればうれしいです。

 

 

↓よかったらポチッとしていってください。

にほんブログ村 IT技術ブログ Pythonへ
にほんブログ村

FlaskでWebアプリを作成 続き3(ユーザーの編集・削除と検索の機能をアップデートまで)

はじめに

前回の続きで、今回は「別のユーザーの投稿は編集・削除不可」と「検索の機能」を追加していきます。

おまけ:絶対位置を中央寄せにする方法「left: 0;  right: 0; を追加する

 

別のユーザーの投稿は編集・削除不可:

  • データベースにユーザー名を追加
  • タイトル表示の方法を変更
  • 編集画面の修正(ゲストはゲストの投稿を編集・削除できる)

検索の機能:

  • ヘッダーに検索ボックスを作成
  • フッターの検索ボタンを修正(フォーカスの移動)
  • 検索ワードで絞り込み処理

 

1.データベースにユーザー名を追加

「models.py」のSQLiteでデータベースを追加します。

user = Column(String(128))

ユーザー名を追加して、データベースを初期化しました。

 

2.タイトル表示の方法を変更

インデックス画面でタイトルを表示する時のみ「タイトル @ユーザー名」にして、タイトルのデータ自体はタイトルのみに戻しました。

 

3.編集画面の修正

編集画面でのタイトルにはユーザー名は付かないようにしました。投稿したユーザー名とログインしている現在のユーザー名が一致していない場合は更新ボタンと削除ボタンを非表示にします。ゲストはゲストの投稿を編集・削除できます。(ゲストが一度投稿したものを編集・削除できなくなるのは良くないと思ったので。ただし、ゲストで投稿したものは他人も編集・削除できます。)

 

4.ヘッダーに検索ボックスを作成

base.html(追加部分)

<div id="search1">
<form action="/blog/search" method="post">
    <input id="search2" type="text" name="search">
    <input class="btn2" type="submit" value="検  索">
</form>
</div>

親要素としてid「search1」の<div>~</div>を作りました。位置の調整や表示・非表示に使います。

base.css(追加部分)

#search1{
    display: none; /*非表示にする*/
    position: absolute; /*絶対位置で表示*/
    left: 0;
    right: 0;
    width: 90%; /*幅*/
    margin: 10px auto; /*中央寄せ*/
    max-width: 700px; /*最大の幅*/
}

非表示にしておきます。(検索ボタンが押されたら表示します)

苦労した点は、「絶対位置で表示した要素の中央寄せ」です。
left: 0;  right: 0;」で左右のpositionをゼロにするのと、「width: や max-width:」で幅を設定する必要があります。(幅の設定は忘れがちなので、幅を小さくしてみて中央寄せになるかテストしたほうがよさそうです)

 

5.フッターの検索ボタンを修正(フォーカスの移動)

base.html(変更部分)

<li>

<span onclick="document.getElementById('search1').style.display = 'block'; document.getElementById('search2').focus();">
検 索</span>

</li>

<span onclick="document.getElementById('search1').style.display = 'block';

クリックされた時にCSSで「display: block;」(非表示を表示する)設定をJavascriptで処理します。

document.getElementById('search2').focus();">

クリックされた時にJavascriptでid「search2」(検索のテキストボックス)にフォーカスを移動する処理をします。

 

6.検索ワードで絞り込み処理

server.py(追加部分)

#「/search」へアクセスがあった場合「blog/index.html」を返す
@app.route('/search', methods=['post'])
def search():  # 検索
    name = session["user_name"]
    all_diary= WikiContent.query.all()
    search = request.form["search"]
    return render_template('blog/index.html',name=name, all_diary=all_diary,search=search)

index.html

{% extends "blog/base.html" %}
{% block body %}

<link rel="stylesheet" type="text/css" href="/static/index.css">

<title>インデックス画面</title>

<div id="main">
    {% for diary in all_diary[::-1] %}
        {% if search == "" or search in diary.user 
            or search in diary.title or search in diary.comment %}

    <form action="/blog/diary/{{diary.id}}">
        <label id="menu" for="{{diary.id}}">

    <div class="table">
        {% if diary.user != "" %}
        <h3>{{diary.title}} @{{diary.user}}</h3>
        {% else %}
        <h3>{{diary.title}}</h3>
        {% endif %}
        <p><img src={{diary.image_url}}></p>
        <p>{{diary.comment}}</p>
        <div class="time">{{diary.date.strftime("%Y/%m/%d %H:%M")}}</div>
    </div>

        </label>
        <input type="submit" id="{{diary.id}}" class="update">
    </form>

        {% endif %}
    {% endfor %}
</div>

{% endblock %}

{% if search == "" or search in diary.user
  or search in diary.title or search in diary.comment %}

検索ワードが「ユーザー名」「タイトル」「コメント」のどれかに含まれていたらインデックス(見出し)に表示します。検索ワードがない場合は全て表示します。

 

まとめ

今回はこのへんで完成にして終わりたいと思います。約一ヶ月かかりました、ここまで長かったです。調べながらだったので一ヶ月かかりましたが、また同じようなWebアプリを作成するならガッツリやれば一週間ほどで出来るでしょうか。

検索機能を応用すれば、ユーザー名縛り(例えばログイン中のユーザーの書き込みのみを表示)やページ設定(idを「100まで」表示、次ページで「101から200まで」表示)などが出来そうです。

 

実際に使って軽くデバックしたら、次回は全ソースコードを載せたいと思います。

 

 

↓よかったらポチッとしていってください。

にほんブログ村 IT技術ブログ Pythonへ
にほんブログ村

FlaskでWebアプリを作成 続き2(編集と削除の機能をアップデートまで)

はじめに

前回の続きで、今回は編集画面を作って更新と削除の機能を追加していきます。

おまけ:CSSが反映されないときの対処法「リロードをshift+F5で行う

 

目標:

  • インデックス画面の「サンプル1」をユーザー名に変更
  • 編集画面のレイアウトを作成
  • 編集画面へ移動
  • 編集画面で更新処理
  • 編集画面に削除ボタン
  • ゲストは更新・削除をできないように

 

1.「サンプル1」をユーザー名に変更

「index.html」の「<h3>サンプル1</h3>」を消してタイトルの部分に「タイトル @ユーザー名」にすることにしました。

server.py(変更部分)

#「/diarysave」へアクセスがあった場合「/blog/index」ページへ移動
@app.route('/diarysave', methods=['post'])
def diarysave():  #保存
    image_url = ""
    # 画像ファイルを保存
    if request.files['image']:
        f = request.files['image']
        filepath = 'static/images/' + secure_filename(f.filename)
        f.save(filepath)
        filepath = '/' + filepath
        image_url = filepath
    # データベースに保存
    name = name = session["user_name"]
    if name == "":
        title = request.form["title"]
    else:
        title = request.form["title"] + "  @" + name
    comment = request.form["comment"]
    # JSTタイムゾーンを作成
    jst = timezone(timedelta(hours=9), 'JST')
    date = datetime.now(jst)
    content = WikiContent(title,image_url,comment,date)
    db_session.add(content)
    db_session.commit()
    return redirect(url_for("blog.index"))

title = request.form["title"] + " @" + name

「入力画面」で保存する時に「タイトル @ユーザー名」でタイトルを保存します。

 

2.編集画面を作成

「diary.html」を作成します。画面のレイアウトは入力画面とほとんど同じです。
※「diary.html」のコードは最後に載せます。

 

3.インデックス画面から編集画面へ移動

index.html

{% extends "blog/base.html" %}
{% block body %}

<link rel="stylesheet" type="text/css" href="/static/index.css">

<title>インデックス画面</title>

<div id="main">
    {% for diary in all_diary[::-1] %}
    <form action="/blog/diary/{{diary.id}}">
        <label id="menu" for="{{diary.id}}">

    <div class="table">
        <h3>{{diary.title}}</h3>
        <p><img src={{diary.image_url}}></p>
        <p>{{diary.comment}}</p>
        <div class="time">{{diary.date.strftime("%Y/%m/%d %H:%M")}}</div>
    </div>

        </label>
        <input type="submit" id="{{diary.id}}" class="update">
    </form>
    {% endfor %}
</div>

{% endblock %}

<form>を追加します。

<input type="submit" id="{{diary.id}}" class="update">

このボタンは非表示にして、テーブル全体をラベルにしてボタン化します。

 

server.py(追加部分)

#「/diary/<ID>」へアクセスがあった場合「blog/diary.html」を返す
@app.route('/diary/<ID>')
def diary(ID):  #参照
    name = session["user_name"]
    diary = WikiContent.query.get(ID)
    return render_template('blog/diary.html',name=name,diary=diary)

 

4.編集画面で更新処理

「diary.html」を編集します。更新ボタンを作成します。
※「diary.html」のコードは最後に載せます。

 

server.py(追加部分)

#「/diaryupdate」へアクセスがあった場合「/blog/index」ページへ移動
@app.route('/diaryupdate', methods=['post'])
def diaryupdate():  #更新
    content = WikiContent.query.get(request.form.get("ID"))
    # 画像ファイルを更新
    if request.files['image']:
        f = request.files['image']
        filepath = 'static/images/' + secure_filename(f.filename)
        f.save(filepath)
        filepath = '/' + filepath
        content.image_url = filepath
    # データベースを更新
    content.title = request.form["title"]
    content.comment = request.form["comment"]
    db_session.commit()
    return redirect(url_for("blog.index"))

 

5.画面に削除ボタン、ゲストは更新・削除できないように

「diary.html」を編集します。削除ボタンを作成します。

diary.html

{% extends "blog/base.html" %}
{% block body %}

<link rel="stylesheet" type="text/css" href="/static/diary.css">
<script src="/static/diary.js"></script>


<title>更新画面</title>

<form action="/blog/diaryupdate" method="post" enctype="multipart/form-data">

    <div id="main">
        <input id="ID" type="text" name="ID" value="{{diary.id}}">
        <label class="label">タイトル</label>
        <input id="text1" type="text" name="title" value="{{diary.title}}">
        <br><br>
        <div id=image_box>
            {% if name != "" %}
            <label class="btn1" for="file-sample">ファイルを選択</label>
            {% else %}
            <label class="btn0">ファイルを選択</label>
            {% endif %}
            <input id="file-sample" type="file" name="image" value="{{diary.image_url}}">
            <img id="file-preview" src="{{diary.image_url}}">
        </div>
        <label class="label">コメント</label>
        <textarea rows="12" cols="100" name="comment">{{diary.comment}}</textarea>
        <div class="time">{{diary.date.strftime("%Y/%m/%d %H:%M")}}</div>

        {% if name != "" %}
        <input class="btn1" type="submit" value="  更  新  ">
        <button class="btn1" id="btn2" type="button" onclick="location.href='/blog/delete/{{diary.id}}'"> 削 除 </button>
        {% endif %}
    </div>

</form>

{% endblock %}

<button type="button" onclick="location.href='/blog/delete/{{diary.id}}'"> 削 除 </button>

(<form>を使わずに)ボタンでページ移動する方法です。※更新ボタンで<form>を利用しているため、削除ボタンを<input>で作ってしまうと「1つの<form>に2つの<input>があるので」削除ボタンを押しても更新処理をしてしまいました。

{% if name != "" %}

ゲストでログイン時(ユーザー名がない時)の処理です。「更新」「削除」「ファイルを選択」のボタンを非表示にしています。※「ファイルを選択」ボタンは消してしまうとレイアウトがおかしくなったので不細工ですが透明に配置しています。

 

diary.css

header{
    position: fixed; /*絶対位置で固定*/
    top: 0px; /*上に配置*/
    left: 0px; /*左の隙間をなくす*/
    padding:0px 30px; /*範囲内の余白(上下、左右)*/
    width: 100%; /*幅*/
    height: 60px; /*高さ*/
    background-image: url("back_image.jpg");
    /*画像を背景に指定*/
}

#main{
    margin: auto; /*中央寄せ*/
    padding:60px 20px; /*範囲内の余白(上下、左右)*/
    width: 90%; /*幅*/
    max-width: 800px; /*最大の幅*/
}

#ID{
    display: none; /*非表示にする*/
}

.label{
    font-size: 24px; /*文字のサイズ*/
}

#text1{
    padding: 8px; /*範囲内の余白*/
    width: 100%; /*幅*/
    font-size: 18px; /*文字の大きさ*/
    box-sizing: border-box; /*横幅の解釈をpadding, borderまでとする*/
}

textarea{
    padding: 10px; /*範囲内の余白*/
    width: calc(100% - 300px); /*幅*/
    font-size: 17px; /*文字の大きさ*/
    box-sizing: border-box; /*横幅の解釈をpadding, borderまでとする*/
    resize: vertical;
}

#image_box{
    float: right; /*右に寄せる*/
    width: 300px;
}

#file-sample {
     display: none; 
}

.btn1{
    display: inline-block;
    padding: 5px 30px; /*余白*/
    color:#fff; /*文字の色*/
    font-size: 16px; /*文字のサイズ*/
    text-align: center; /*文字の位置*/
    background:#1B73BA; /*背景色*/
    border-radius: 10px; /*角の半径*/
    cursor: pointer; /*カーソル(指のかたち)*/
}

.btn1:hover{
    opacity: 0.8; /*透明度*/
}

.btn0{
    display: inline-block;
    padding: 5px 30px; /*余白*/
    font-size: 16px; /*文字のサイズ*/
    opacity: 0; /*透明*/
}

#btn2{
    background:red; /*背景色*/
    float: right; /*右に寄せる*/
}

img{
    margin: 2px 0;
    width: 100%;
}

@media screen and (max-width: 600px) {
    #image_box{
        width: 100%; /*スマホなどの時の画像サイズ*/
    }
    textarea{
        width: 100%;
    }
}

.time{
    clear: both; /*「float」を解除*/
}

footer{
    position: fixed; /*絶対位置で固定*/
    bottom: 0px; /*下に配置*/
    left: 0px; /*左の隙間をなくす*/
    width: 100%; /*幅*/
    height: 60px; /*高さ*/
    font-weight: bold; /*文字を太字にする*/
    text-align: center; /*中央に表示する*/
    background-image: url("back_image.jpg");
    /*画像を背景に指定*/
}

ul.footer-menu li {
    display: inline; /*一行で表示する*/
}

#copy{
    position: absolute; /*絶対位置(rightのために必要)*/
    right: 30px; /*右に配置*/
}

 

diary.js(edit.jsと同じ。なので「edit.js」を読み込めば、これは必要ないかも)

window.onload = function () {
    document.getElementById('file-sample').addEventListener('change', function (e) {
        var file = e.target.files[0];

        var blobUrl = window.URL.createObjectURL(file);

        var img = document.getElementById('file-preview');
        img.src = blobUrl;
    });
}

 

server.py(追加部分)

#「/delete/<ID>」へアクセスがあった場合「/blog/index」ページへ移動
@app.route('/delete/<ID>')
def delete(ID):  #削除
    content = WikiContent.query.get(ID)
    db_session.delete(content)
    db_session.commit()
    return redirect(url_for("blog.index"))

  

まとめ

http://startpython.pythonanywhere.com/

こちらから実際に操作できますのでお試しください。(ユーザー名・パスワードは普段お使いのもの以外にしてください。セキュリティーは保証できません)
ユーザー名とパスワードを何も入れずにログインするとゲストでログインできます。

 

これで完成にしたかったのですが、(ゲストログインの時は編集・削除はできないのですが)別のユーザーが登録したコメントや画像も編集・削除ができてしまいます。

次回はそのあたりをなんとかしたいのと、検索機能を付けたいと思います。

 

 

↓よかったらポチッとしていってください。

にほんブログ村 IT技術ブログ Pythonへ
にほんブログ村

FlaskでWebアプリを作成 続き1(インデックス画面と入力画面を機能させてアップデートまで)

はじめに

インデックス(見出し)画面と入力画面のレイアウトが出来上がったので、以前にログイン画面を作成したWebサイトに追加・変更していきます。

以前の内容はこちら

start-python.hateblo.jp

 

動作環境

Windows10
Python 3.7.5
Flask 1.1.1

作業フォルダ/
 ├ blog/
 │  ├ templates/
 │  │  └ blog/
 │  │      ├ base.html  ・・・(1)
 │  │      ├ edit.html  ・・・(4)
 │  │      ├ index.html  ・・・(3)
 │  │      ├ newcomoer.html
 │  │      └ top.html
 │  ├ models/
 │  │  ├ __init__.py
 │  │  ├ database.py
 │  │  ├ models.py  ・・・(2)
 │  │  └ wiki.db
 │  ├ __init__.py
 │  └ server.py    ・・・(5)
 ├ static/
 │  ├ images/
 │  │  └ sample.jpg
 │  ├ back_image.jpg
 │  ├ base.css
 │  ├ edit.css
 │  ├ edit.js
 │  ├ image.jpg
 │  ├ index.css
 │  ├ login.css
 │  ├ login.js
 │  ├ newcomer.css
 │  └ newcomer.js
 ├ templates/
 │  └ index.html
 ├ key.py
 └ main.py

 

1.ヘッダーにユーザー名を表示する

base.html

<link rel="stylesheet" type="text/css" href="/static/base.css">

<body>
    <header>

        {% if name == "" %}
        <h3 id="user_name">ようこそ ゲスト さん</h3>
        {% elif name %}
        <h3 id="user_name">{{name}}</h3>
        {% else %}
        <h3 id="user_name">ログインしてください</h3>
        {% endif %}

        <div class="menu_box">
            <label id="menu" for="toggle">メニュー</label>
            <input type="checkbox" id="toggle">
            <ul class="dropdown_menu">
                <li><a href="/blog">ホーム</a></li>
                <li><a href="/blog/create">新規作成</a></li>
                <li><a href="/blog/logout">ログアウト</a></li>
            </ul>
        </div>

    </header>


{% block body %}{% endblock %}


    <footer>
        <ul>
            <li>ホーム</li>
            <li>検 索</li>
            <li><a href="/blog/create">新規作成</a></li>
            <li><a href="/blog/logout">ログアウト</a></li>
        </ul>
        <span id="c">(c)2020 ふたり暮らし</span>
    </footer>
</body>

変更点:ヘッダーに「ユーザー名」を表示します。ユーザー名が無い場合には「ようこそ ゲスト さん」と表示します。

 

2.SQLiteでデータベースを追加・修正

models.py

from sqlalchemy import Column, Integer, String, Text, DateTime
#from models.database import Base       # データベースの初期化用
from blog.models.database import Base   # 初期化が終わればこちらへ変更
from datetime import datetime
 
 
class WikiContent(Base):
    __tablename__ = 'wikicontents'
    id = Column(Integer, primary_key=True)
    title = Column(String(128))
image_url = Column(String(128)) comment = Column(Text) date = Column(DateTime, default=datetime.now()) def __init__(self, title=None, image_url=None, comment=None, date=None): self.title = title self.image_url = image_url self.comment = comment self.date = date def __repr__(self): return '<Title %r>' % (self.title) #Userクラスを追加。WikiContentの使い回し class User(Base): __tablename__ = 'users' id = Column(Integer, primary_key=True) user_name = Column(String(128)) hashed_password = Column(String(128)) def __init__(self, user_name=None, hashed_password=None): self.user_name = user_name self.hashed_password = hashed_password def __repr__(self): return '<Name %r>' % (self.user_name)

追加・変更点:「id, title, image_url, comment, date」の5つのデータを扱います。

image_url・・画像のファイル名のみを保存します。(画像データは「作業フォルダ/static/images/」に保存します)

※データベースに保存する順番が「読み込む時の順番」になりますので、順番には注意が必要です。

データベースの初期化

「wiki.db」を作成します。コマンドプロンプトから下記を実行します。

cd 作業フォルダ
cd blog
python
from models.database import init_db
init_db()
quit()

modelsフォルダ内に「wiki.db」ファイルが出来ます。確認してみましょう。

※重要:初期化する時は「models.py」の2行目「#from models.database import Base」のコメント化を解除して、3行目「from blog.models.database import Base」をコメント化してください。
wiki.db」が作成されたら元に戻してください。(他にいい方法があればいいのですが、、、最初だけなので。)

 

3.インデックス(見出し)画面の修正

index.html

{% extends "blog/base.html" %}
{% block body %}

<link rel="stylesheet" type="text/css" href="/static/index.css">

<title>インデックス画面</title>

<div id="main">
    {% for diary in all_diary[::-1] %}

    <div class="table">
        <h3>サンプル1</h3>
        <h3>{{diary.title}}</h3>
        <p><img src={{diary.image_url}}></p>
        <p>{{diary.comment}}</p>
        <div class="time">{{diary.date.strftime("%Y/%m/%d %H:%M")}}</div>
    </div>

    {% endfor %}
</div>

{% endblock %}

{% for diary in all_diary[::-1] %}

Pythonでfor文を逆順で使います。(インデックスを新しい順に表示するため)

※データベースを「読み込む時の順番」に要素を配置できるように注意が必要です。

 

4.入力画面の修正

edit.html

{% extends "blog/base.html" %}
{% block body %}

<link rel="stylesheet" type="text/css" href="/static/edit.css">
<script src="/static/edit.js"></script>


<title>入力画面</title>

<form action="/blog/diarysave" method="post" enctype="multipart/form-data">

    <div id="main">
        <label>タイトル</label>
        <input id="text1" type="text" name="title">
        <br><br>
        <div id=image_box>
            <label class="btn1" for="file-sample">ファイルを選択</label>
            <input id="file-sample" type="file" name="image">
            <img id="file-preview">
        </div>
        <label>コメント</label>
        <textarea rows="12" cols="100" name="comment"></textarea>
        <div class="time"></div>
        <input class="btn1" type="submit" value="  登  録  ">
    </div>

</form>

{% endblock %}

<form action="/blog/diarysave" method="post" enctype="multipart/form-data">

入力フォームを作成します。送信ボタンをクリックすると転送処理されます「action」で送信先、「method」で転送方法、「enctype」でデータ形式、フォーム内の要素の「name」でPythonで使用する変数を設定します。

 

5.メインプログラムの追加・修正

server.py(長いので全文は後で載せます)

追加部分

#「/diarysave」へアクセスがあった場合「/blog/index」ページへ移動
@app.route('/diarysave', methods=['post'])
def diarysave():  #保存
    image_url = ""
    # 画像ファイルを保存
    if request.files['image']:
        f = request.files['image']
        filepath = 'static/images/' + secure_filename(f.filename)
        f.save(filepath)
        filepath = '/' + filepath
        image_url = filepath
    # データベースに保存
    title = request.form["title"]
    comment = request.form["comment"]
    # JSTタイムゾーンを作成
    jst = timezone(timedelta(hours=9), 'JST')
    date = datetime.now(jst)
    content = WikiContent(title,image_url,comment,date)
    db_session.add(content)
    db_session.commit()
    return redirect(url_for("blog.index"))

画像ファイルが選択されていた場合は「static/images/」に画像データを保存します。変数「image_url」に「/static/images/画像ファイル名」を設定します。

JSTタイムゾーンを作成

「pythonanywhere」では現在時刻が「日本時間の9時間前」になってしまうので、日本の現在時刻を「date」に設定しています。

 

「server.py」の全文です。

# Blueprint(pyファイルを分割するための関数)をインポート
from flask import Blueprint

#「app」を「Blueprint()」を使って定義
#「blog」の部分は、url_for("blog.top")で使用
app = Blueprint('blog', __name__, template_folder='templates')


# 必要なモジュールをインポート
import os
from datetime import datetime, timedelta, timezone
from flask import Flask, session, redirect, url_for, render_template,request
#「/blog/models/models.py」の「WikiContent」と「User」を呼び出す
from blog.models.models import WikiContent,User
#「/blog/models/database.py」の「db_session」を呼び出す
from blog.models.database import db_session

from werkzeug.utils import secure_filename

# ハッシュ化するための関数をインポート(パスワードの暗号化用)
# 今回はsha256という暗号化方式を使用
import key
from hashlib import sha256


#「/」へアクセスがあった場合
@app.route('/')
def index():  # 一覧画面
    if "user_name" in session:
        # セッションがログイン状態であれば「blog/index.html」を返す
        name = session["user_name"]
        all_diary= WikiContent.query.all()
        return render_template('blog/index.html',name=name, all_diary=all_diary)
    else:
        # ログイン状態でなければ「/blog/top」ページへ移動
        return redirect(url_for("blog.top"))

#「/logout」へアクセスがあった場合「/blog/top」ページへ移動
@app.route('/logout')
def logout():
    session.pop("user_name", None)
    return redirect(url_for("blog.top",status="logout"))

#「/create」へアクセスがあった場合「blog/edit.html」を返す
@app.route('/create')
def create():  #新規作成
    return render_template('blog/edit.html')

#「/diarysave」へアクセスがあった場合「/blog/index」ページへ移動
@app.route('/diarysave', methods=['post'])
def diarysave():  #保存
    image_url = ""
    # 画像ファイルを保存
    if request.files['image']:
        f = request.files['image']
        filepath = 'static/images/' + secure_filename(f.filename)
        f.save(filepath)
        filepath = '/' + filepath
        image_url = filepath
    # データベースに保存
    title = request.form["title"]
    comment = request.form["comment"]
    # JSTタイムゾーンを作成
    jst = timezone(timedelta(hours=9), 'JST')
    date = datetime.now(jst)
    content = WikiContent(title,image_url,comment,date)
    db_session.add(content)
    db_session.commit()
    return redirect(url_for("blog.index"))


@app.route('/all_delete')
def all_delete():  #全削除
    WikiContent.query.delete()
    db_session.commit()
    return redirect(url_for("blog.index"))


# top
#「/login」へアクセスがあった場合
@app.route("/login",methods=["post"])
def login():
    # 入力された「ユーザー名」が既に存在するか確認
    user_name = request.form["user_name"]
    user = User.query.filter_by(user_name=user_name).first()
    if user:
        #「ユーザー名」が存在した場合
        #「パスワード」が一致するか確認(「key.SALT」で暗号化)
        password = request.form["password"]
        hashed_password = sha256((user_name + password + key.SALT).encode("utf-8")).hexdigest()
        if user.hashed_password == hashed_password:
            #「パスワード」が一致した場合「/blog/index」ページへ移動
            session["user_name"] = user_name
            return redirect(url_for("blog.index"))
        else:
            #「パスワード」が一致しない場合「/blog/top」ページへ移動
            return redirect(url_for("blog.top",status="wrong_password"))
    else:
        #「ユーザー名」が存在しなかった場合「/blog/top」ページへ移動
        return redirect(url_for("blog.top",status="user_notfound"))

#「/registar」へアクセスがあった場合
# 新規登録画面で「新規登録」ボタンが押された時
@app.route("/registar",methods=["post"])
def registar():
    # 入力された「ユーザー名」が既に存在するか確認
    user_name = request.form["user_name"]
    user = User.query.filter_by(user_name=user_name).first()
    if user:
        #「ユーザー名」が存在した場合「/blog/newcomer」ページへ移動
        return redirect(url_for("blog.newcomer",status="exist_user"))
    else:
        #「ユーザー名」が存在しなかった場合
        #「ユーザー名」「パスワード」(「key.SALT」で暗号化)を
        password = request.form["password"]
        hashed_password = sha256((user_name + password + key.SALT).encode("utf-8")).hexdigest()
        user = User(user_name, hashed_password)
        # データベースに追加して保存
        db_session.add(user)
        db_session.commit()
        session["user_name"] = user_name
        #「/blog/index」ページへ移動
        return redirect(url_for("blog.index"))

#「/top」へアクセスがあった場合「blog/top.html」を返す
@app.route("/top")
def top():
    status = request.args.get("status")
    return render_template("blog/top.html",status=status)

#「/newcomer」へアクセスがあった場合「blog/newcomer.html」を返す
@app.route("/newcomer")
def newcomer():
    status = request.args.get("status")
    return render_template("blog/newcomer.html",status=status)

 

まとめ

http://startpython.pythonanywhere.com/blog/

こちらから実際に操作できますのでお試しください。(ユーザー名・パスワードは普段お使いのもの以外にしてください。セキュリティーは保証できません)
ユーザー名とパスワードを何も入れずにログインするとゲストでログインできます。

 

次回は編集と削除の機能を付けたいと思います。

 

 

↓よかったらポチッとしていってください。

にほんブログ村 IT技術ブログ Pythonへ
にほんブログ村

HTML メニューボタンの作り方2(CSSでメニュー一覧を開く・閉じる、表示・非表示)

はじめに

今回はボタンを押すとメニュー一覧が表示されるメニューボタンを作成します。CSSだけで(Javascriptは使わずに)ヘッダー部分に配置します。

f:id:Start_python:20200209013639p:plain

 

ソースコード

base.html

<link rel="stylesheet" type="text/css" href="/static/base.css">

<header>
    <h3 id="user_name">レイルーク@ふたり暮らし</h3>

    <div class="menu_box">
        <label id="menu" for="toggle">メニュー</label>
        <input type="checkbox" id="toggle">
        <ul class="dropdown_menu">
            <li><a href="/blog">ホーム</a></li>
            <li><a href="/blog/create">新規作成</a></li>
            <li><a href="/blog/logout">ログアウト</a></li>
        </ul>
    </div>

</header>

 

<label id="menu" for="toggle">メニュー</label>

チェックボックス」用にラベルを作っています。ラベルをクリックした時にチェックボックスのオンオフが可能になるので、チェックボックスは非表示にできます。

<input type="checkbox" id="toggle">

チェックボックスを作成します。チェック選択中にメニュー一覧を表示して、チェック選択されてない時はメニュー一覧を非表示にします。

<ul>~</ul>

リストを表示します。

<li>~</li>

リストの項目を記述します。<ul>~</ul>の間に複数入れることができます。

 

 

base.css

header{
    position: fixed; /*絶対位置で固定*/
    top: 0px; /*上に配置*/
    left: 0px; /*左の隙間をなくす*/
    padding: 0px 30px; /*範囲内の余白(上下、左右)*/
    width: 100%; /*幅*/
    height: 60px; /*高さ*/
    background-image: url("back_image.jpg");
    /*画像を背景に指定*/
}

#user_name{
    position: absolute; /*絶対位置で表示*/
    margin: 20px 0; /*範囲外の余白*/
}

.menu_box{
    margin: 30px 60px 0 calc(100% - 180px); /*範囲外の余白*/
    text-align: right; /*右に寄せる*/
}

#toggle{
    display: none; /*非表示にする*/
}

.dropdown_menu{
    display: none; /*非表示にする*/
}

#toggle:checked+.dropdown_menu{
    display: block; /*リストを表示する*/
}

.dropdown_menu li{
    display: block; /*要素をインライン表示にする*/
    width: 120px; /*幅*/
    text-align: center; /*中央に表示する*/
    background-color: white; /*背景色*/
    border: outset 2px; /*外枠を作る(スタイル、太さ)*/
    cursor: pointer; /*カーソル(指のかたち)*/
}

<h3>と<label>を同じ高さ(行)に表示させたかったので、<h3>(#user_name)のほうを「絶対位置で表示」にしました。

ラベル、ボタン、リストを<div>要素に入れて、<label>の位置は<div>(.menu_box)のほうで設定しています。※ここを理解するのに苦労しました。

margin: 30px 60px 0 calc(100% - 180px); /*範囲外の余白*/

左位置はGoogle Chromeの「デベロッパーツール」で確認しながら決めました。

#toggle:checked+.dropdown_menu{

チェックボタン(#toggle)が選択された時にリスト(.dropdown_menu)を装飾します。「+」で指定する要素は、元の要素に隣接している必要があります。 

 

まとめ

表示・非表示の設定や要素の装飾についてはそれほど難しくない気がします。一番難しく感じるのが要素の位置を決める方法だと思います。
親要素と子要素の関係や、親要素を新しく作るなど「CSSだけで考えずにHTMLとセットで考える」必要があります。※以前も同じようなことを言ってました。

「範囲外をクリックするとリストを消す」まで作りたかったのですが、Javascriptを使わないと出来なさそうなので今回はこのままでいきたいと思います。

 

 

 

↓よかったらポチッとしていってください。

にほんブログ村 IT技術ブログ Pythonへ
にほんブログ村

HTML メニューボタンの作り方1(CSS フッターの謎の隙間をなくす方法)

はじめに

今回はメニューボタンを作成します。フッター部分に、リストを一行で配置して外枠を付けてリンクできるようにします。前から少し気になってた「フッターの余白が消えない問題」も解決していきます。

f:id:Start_python:20200209001855p:plain

 

ソースコード

base.html

<link rel="stylesheet" type="text/css" href="/static/base.css">

<footer>
    <ul>
        <li>ホーム</li>
        <li>検 索</li>
        <li><a href="/blog/create">新規作成</a></li>
        <li><a href="/blog/logout">ログアウト</a></li>
    </ul>
    <span id="c">(c)2020 ふたり暮らし</span>
</footer>

<ul>~</ul>

リストを表示します。

<li>~</li>

リストの項目を記述します。<ul>~</ul>の間に複数入れることができます。

<span>~</span>

インライン要素としての範囲を指定します。idやclassを指定したい時に使います。
ブロック要素<div>のインライン要素版です。<div>では前後の行に改行が入ってしまうため、前後に改行を入れたくない時は<span>を使います。

 

base.css

footer{
    position: fixed; /*絶対位置で固定*/
    bottom: 0px; /*下に配置*/
    left: 0px; /*左の隙間をなくす*/
    width: 100%; /*幅*/
    height: 60px; /*高さ*/
    font-weight: bold; /*文字を太字にする*/
    text-align: center; /*中央に表示する*/
    background-image: url("back_image.jpg");
    /*画像を背景に指定*/
}

ul{
    margin: 0; /*上の余白をなくすため*/
    padding: 5px 0; /*左の余白をなくすため*/
}

li{
    display: inline-block; /*要素をインライン表示にする*/
    width: 120px; /*幅*/
    text-align: center; /*中央に表示する*/
    border: outset 2px; /*外枠を作る(スタイル、太さ)*/
    cursor: pointer; /*カーソル(指のかたち)*/
}  

a {
    text-decoration: none; /*下線を消す*/
}

@media screen and (max-width: 600px) {
    li{
        width: 80px; /*スマホなどの時のボタン幅*/
    }
}

#c{
    position: absolute; /*絶対位置(rightのために必要)*/
    right: 30px; /*右に配置*/

margin: 0; /*上の余白をなくすため*/

フッターの上に謎の余白がある場合は、「フッターの範囲内の余白(padding)」ではなく、「フッターの中の要素の範囲外の余白(margin)」が原因です。

padding: 5px 0; /*左の余白をなくすため*/

リスト「<ul><li>~</li></ul>」には初期値で左に「範囲内の余白」が付いてます。(中央寄せにしていても、なぜか少し右に寄っているのはこれが原因です)

 

まとめ

HTMLで<ul>を使ってリストを作って<li><a href=~>を使ってリンク先(ページの移動)を設定します。CSSで<li>をインライン表示にして横並びにします。横並びにした<li>に外枠を付けて装飾してボタンのように表示すれば完成です。今回はフッターに作成していますが、ヘッダーに作成してもよさそうです。

ボタンを押すとメニュー一覧が表示されるメニューボタンの作り方も一緒に解説したかったのですが、少し長くなりそうなので次回にします。

 

 

↓よかったらポチッとしていってください。

にほんブログ村 IT技術ブログ Pythonへ
にほんブログ村

HTML 共通テンプレートを作成する(HTMLで変数、if文、for文の使い方)

はじめに

Pythonのjinja2モジュールの使います。(HTMLだけでは機能しません)

PythonのFlaskでWebサイトを作る時に、複数のページで同じ内容(ヘッダーやフッターなど)を利用することが多いです。jinja2モジュールを使って共通テンプレートを作成しておくと、複数のページからテンプレートを呼び出して使えるので便利です。また、jinja2を利用してPythonでの変数をHTMLに読み込む方法も解説していきます。

 

jinja2での共通テンプレートの使い方

{% extends "ファイル名" %}

ベース(基本)となる「HTMLファイル」で共通テンプレート(ヘッダーやフッターの部分など)を作成します。その中の{% block 変数名 %}{% endblock %}の部分をテンプレートを継承する「HTMLファイル」に記述します。

わかりにくいと思うので実例をあげておきます。

base.html(共有テンプレート)

<!DOCTYPE html>
<meta name="viewport" content="width=device-width,initial-scale=1">

<link rel="stylesheet" type="text/css" href="/static/base.css">

<body>
    <header>
        <h2>レイルーク@ふたり暮らし</h2>
    </header>


{% block body %}{% endblock %}


    <footer>
        <ul>
            <li>ホーム</li>
            <li>検 索</li>
            <li><a href="/blog/create">新規作成</a></li>
            <li><a href="/blog/logout">ログアウト</a></li>
        </ul>
        <span id="copy">(c)2020 ふたり暮らし</span>
    </footer>
</body>

index.html

{% extends "base.html" %}
{% block body %}

<link rel="stylesheet" type="text/css" href="/static/index.css">

<title>インデックス画面</title>

<div id="main">
    <div class="table">
        <h3>サンプル1</h3>
        <h3>タイトル</h3>
        <p><img src="/static/images/sample1.jpeg"></p>
        <p>本文</p>
        <div class="time" id="time1"></div>
    </div>
    <div class="table">
        <h3>サンプル2</h3>
        <h3>タイトル</h3>
        <p><img src="/static/images/sample2.jpeg"></p>
        <p>本文</p>
        <div class="time" id="time2"></div>
    </div>
</div>

{% endblock %}

CSSファイルはそれぞれで指定できます。

共通連プレートの{% block 変数名 %}{% endblock %}部分に、メインHTMLファイルの{% block 変数名 %}{% endblock %}の間に記述します。

 

{% block 変数a %}{% endblock %}
{% block 変数b %}{% endblock %}
{% block 変数c %}{% endblock %}

と複数作成も可能です。

 

また「pyファイル」から変数を使って「HTMLファイル」で読み込むこともできます。

 

jinja2での変数、if文、for文の使い方

変数を「pyファイル」でこのように指定すると

name = "ユーザー名"
return render_template("index.html",name=name)

HTML側に送ることができます。

「HTMLファイル」でこのように変数を表示できます。

<title>{{name}}</title>

 

HTMLファイルでif文を使う

{% if name == "" %}
<h1>ゲスト</h1>
{% elif name %}
<h1>{{name}}</h1>
{% else %}
<h1>ログインしてください</h1>
{% endif %}

このように{%  %}の中でPythonコードを使うことができます。

 

HTMLファイルでfor文を使う

例えば、pyファイルのテーブル全て読み込んでHTMLファイルでfor文を使って1テーブルずつ読み込みます。

{% for table in all_table %}
<h3>{{table.title}}</h3>
<p><img src={{table.image_name}}></p>
<p>{{comment}}</p>
<div class="time">{table.date}}</div>
{% endfor %}

 

まとめ

index.html「{% extends "base.html" %}でテンプレートファイルの内容を呼び出して、新しく表示したい部分を{% block body %}~{% endblock %}で囲むことでテンプレートファイルにある{% block body %}{% endblock %}の部分に表示される」うーん、分かりづらいですね。理解するとそんなに難しいことではないのですが、実際にやってみないと説明を見ただけでは理解しにくいと思います。

 

 

↓よかったらポチッとしていってください。

にほんブログ村 IT技術ブログ Pythonへ
にほんブログ村

HTML 入力画面を作成(CSSでファイル選択した画像を表示する方法)

はじめに

今回は記事の入力画面を作成します。写真や画像をファイルから選択して張り付けます。ファイル選択の部分はJavascriptを使います。

f:id:Start_python:20200206112359p:plain

 

動作環境

Windows10
メモ帳
Google Chrome

作業フォルダ/
 ├ static/
 │   ├ back_image.jpg
 │   ├ edit.css
 │   └ edit.js
 └ edit.html

 

ソースコード

edit.hrml

<!DOCTYPE html>
<link rel="stylesheet" type="text/css" href="static/edit.css">
<script src="static/edit.js"></script>

<meta name="viewport" content="width=device-width,initial-scale=1">

<head>
    <title>入力画面</title>
</head>

<body>
    <header>
        <h2>レイルーク@ふたり暮らし</h2>
    </header>

    <div id="main">
        <label>タイトル</label>
        <input id="text1" type="text">
        <br><br>
        <div id=image_box>
            <label class="btn1" for="file-sample">ファイルを選択</label>
            <input id="file-sample" type="file">
            <img id="file-preview">
        </div>
        <label>コメント</label>
        <textarea rows="12" cols="100"></textarea>
        <div class="time"></div>
        <input class="btn1" type="submit" value="  登  録  ">
    </div>

    <footer>
        <ul class="footer-menu">
            <li>ホーム |</li>
            <li>利用規約 |</li>
            <li>ヘルプ |</li>
            <li>お問い合わせ</li>
            <br>
            <li id="copy">(c)2020 ふたり暮らし</li>
        </ul>
    </footer>
</body>

「ファイル選択ボタンと画像」の2つの要素をスマホ用では「コメント」の上に表示したかったので、<div>~</div>で1つのブロック要素にしました。(PCの時は、CSSで「float: right;」を使って右に寄せています)
※ここでハマりました。「2つの要素を1つのブロック要素にして右寄せする」方法を見つけたのはファインプレーでした。

<label class="btn1" for="file-sample">ファイルを選択</label>

「ファイル選択ボタン」用にラベルを作っています。<input type="file">ではボタンの見た目を変えられません。「ラベルをクリックすると指定した要素をクリックしたことになる」を利用して、ボタン自体を非表示してラベルにボタンのような装飾をすることで解決しました。(「登録ボタン」も「btn1クラス」にして同じ装飾にしました。)

 

edit.css

header{
    position: fixed; /*絶対位置で固定*/
    top: 0px; /*上に配置*/
    left: 0px; /*左の隙間をなくす*/
    padding:0px 30px; /*範囲内の余白(上下、左右)*/
    width: 100%; /*幅*/
    height: 60px; /*高さ*/
    background-image: url("back_image.jpg");
    /*画像を背景に指定*/
}

#main{
    margin: auto; /*中央寄せ*/
    padding:60px 20px; /*範囲内の余白(上下、左右)*/
    width: 90%; /*幅*/
    max-width: 800px; /*最大の幅*/
}

label{
    font-size: 24px; /*文字のサイズ*/
}

#text1{
    padding: 8px; /*範囲内の余白*/
    width: 100%; /*幅*/
    font-size: 18px; /*文字の大きさ*/
    box-sizing: border-box; /*横幅の解釈をpadding, borderまでとする*/
}

textarea{
    padding: 10px; /*範囲内の余白*/
    width: calc(100% - 300px); /*幅*/
    font-size: 17px; /*文字の大きさ*/
    box-sizing: border-box; /*横幅の解釈をpadding, borderまでとする*/
    resize: vertical;
}

#image_box{
    float: right; /*右に寄せる*/
    width: 300px;
}

#file-sample {
     display:none; 
}

.btn1{
    display: block;
    padding: 5px 30px; /*余白*/
    color:#fff; /*文字の色*/
    font-size: 16px; /*文字のサイズ*/
    text-align: center; /*文字の位置*/
    background:#1B73BA; /*背景色*/
    border-radius: 10px; /*角の半径*/
    cursor: pointer; /*カーソル(指のかたち)*/
}

.btn1:hover{
    opacity: 0.8; /*透明度*/
}

img{
    margin: 2px 0;
    width: 100%;
}

@media screen and (max-width: 600px) {
    #image_box{
        width: 100%; /*スマホなどの時の画像サイズ*/
    }
    textarea{
        width: 100%;
    }
}

.time{
    clear: both; /*「float」を解除*/
}

footer{
    position: fixed; /*絶対位置で固定*/
    bottom: 0px; /*下に配置*/
    left: 0px; /*左の隙間をなくす*/
    width: 100%; /*幅*/
    height: 60px; /*高さ*/
    font-weight: bold; /*文字を太字にする*/
    text-align: center; /*中央に表示する*/
    background-image: url("back_image.jpg");
    /*画像を背景に指定*/
}

ul.footer-menu li {
    display: inline; /*一行で表示する*/
}

#copy{
    position: absolute; /*絶対位置(rightのために必要)*/
    right: 30px; /*右に配置*/
}

ポイントは、HTMLのところでも触れた「ボタンと画像」の2つの要素を右寄せする部分です。レイアウトの基本である親要素と子要素の関係が理解できました。

CSSだけではどう考えても解決できないこともあるので、行き詰った時はHTMLとセットで考えたほうがよさそうです。

CSSで指定するスタイルが増えてくると順番がバラバラだと見返した時に編集しにくかったので、記述順を決めました。詳しくは、
【保存版】CSS 逆引き辞典(初心者用 随時更新予定) - 基本の書き方に載せました。

 

edit.js

window.onload = function () {
    document.getElementById('file-sample').addEventListener('change', function (e) {
        var file = e.target.files[0];
        var blobUrl = window.URL.createObjectURL(file);
        var img = document.getElementById('file-preview');
        img.src = blobUrl;
    });
}

ファイルを選択する部分です。

今回もまだ「Javascript」については勉強不足なので詳しい解説はできません。アップロードしてWebサイトで利用する時は、おそらくこのままでは利用できないと思うのでその時に勉強します。(たぶんPython側で作成するこになると思います)

 

まとめ

入力画面が作成できました。ファイル選択して画像表示は特に問題なかったのですが、レイアウトのところでかなりハマってしましました。ハマればハマるほど理解が深まるので良い傾向だと思います。

登録の方法はあとにして編集画面(インデックスから開いて更新する)を先に作成していきます。さらにその前に、ヘッダーやフッター部分は複数のページで利用するので次回は「共通テンプレート」を作りたいと思います。

 

 

↓よかったらポチッとしていってください。

にほんブログ村 IT技術ブログ Pythonへ
にほんブログ村

CSSで文字の右に画像を表示する方法/画像を背景に指定する

はじめに

今回は画像を表示させる場所についてです。文章を左に、画像を右に表示してみたいと思います。CSSで「float」を使う方法です。少しハマったところもあったので詳しく解説していきます。

f:id:Start_python:20200205112508g:plain

 

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

float: right; /*右に寄せる*/

画像サイズを固定(自動で縮小・拡大)させる場合は「width: 300px; /*画像サイズを固定*/」を使います。

「HTMLファイル」での文字の記述場所は、「float」で指定した「画像(要素)」の後ろに記述します。

<h3>サンプル3</h3>
<img src="static/images/sample3.gif">
<h3>タイトル</h3>
<p>本文</p>
<div id="time3"></div>

例えば上記の場合だと、「サンプル3」はそのまま表示されて「タイトル」から下の文字が「画像」の左側に表示されます。(画像より下の部分では文字は通常の全体表示に戻ります)

しかしこのままだと問題が起こります。

f:id:Start_python:20200205105521p:plain

出ました!「CSS完全に理解した」状態です。

www.ttrinity.jp

※Tシャツの宣伝ではありません。(画像をそのまま使うといろいろ面倒そうだったので。。。)

そこで「float」を解除する方法がこちらです。

clear: both; /*「float」を解除*/

解除したい要素(今回では日時の部分)で設定します。「folat」の基本だと思うのですが初めて使ったので、ここで少しハマりました。

 

まとめるとこうなります。

float: right; /*右に寄せる*/

CSSで右に寄せたい要素(画像)に使用します。

clear: both; /*「float」を解除*/

CSSで「float」を解除したい要素(文字)に使用します。(次の要素がない時は空白の<div class="解除用"></div>を作ればオッケーです)

この2つをセットで覚えておくと良いと思います。

 

ソースコード

作業フォルダ/
 ├ static/
 │  ├ images/
 │  │  ├ sample1.jpeg
 │  │  ├ sample2.jpeg
 │  │  ├ sample3.gif
 │  │  ├ sample4.jpg
 │  │  └ sample5.png
 │  ├ back_image.jpg
 │  └ index.css
 └ index.html

index.html

<!DOCTYPE html>
<link rel="stylesheet" type="text/css" href="static/index.css">

<meta name="viewport" content="width=device-width,initial-scale=1">

<head>
    <script type="text/javascript">
    window.onload = function() {
        document.getElementById("time1").innerHTML = getNow();
        document.getElementById("time2").innerHTML = getNow();
        document.getElementById("time3").innerHTML = getNow();
        document.getElementById("time4").innerHTML = getNow();
        document.getElementById("time5").innerHTML = getNow();
    }

    function getNow() {
        var now = new Date();
        var year = now.getFullYear();
        var mon = now.getMonth()+1; //1を足すこと
        var day = now.getDate()+3;
        var hour = 9; //now.getHours();
        var min = 00; //now.getMinutes();
        var sec = 00; //now.getSeconds();

        var s = year + "年" + mon + "月" + day + "日" + hour + "時" + min + "分" + sec + "秒"; 
        return s;
    }
    </script>
</head>

<body>
    <header>
        <h2>レイルーク@ふたり暮らし</h2>

    </header>

    <div id="main">
        <div class="table">
            <img src="static/images/sample1.jpeg">
            <h3>サンプル1</h3>
            <h3>タイトル</h3>
            <p>本文</p>
            <div class="time" id="time1"></div>
        </div>
        <div class="table">
            <p><img src="static/images/sample2.jpeg"></p>
            <h3>サンプル2</h3>
            <h3>タイトル</h3>
            <p>本文</p>
            <div class="time" id="time2"></div>
        </div>
        <div class="table">
            <h3>サンプル3</h3>
            <img src="static/images/sample3.gif">
            <h3>タイトル</h3>
            <p>本文</p>
            <div class="time" id="time3"></div>
        </div>
        <div class="table">
            <h3>サンプル4</h3>
            <h3>タイトル</h3>
            <img src="static/images/sample4.jpg">
            <p>本文</p>
            <div class="time" id="time4"></div>
        </div>
        <div class="table">
            <img src="static/images/sample5.png">
            <h3>サンプル5</h3>
            <h3>タイトル</h3>
            <p>本文</p>
            <div class="time" id="time5"></div>
        </div>
    </div>

    <footer>
        <ul class="footer-menu">
            <li>ホーム |</li>
            <li>利用規約 |</li>
            <li>ヘルプ |</li>
            <li>お問い合わせ</li>
            <br>
            <li id="copy">(c)2020 ふたり暮らし</li>
        </ul>
    </footer>
</body>

index.css

header{
    width: 100%; /*幅*/
    height: 60px; /*高さ*/
    padding:0px 30px; /*範囲内の余白(上下、左右)*/

    position: fixed; /*絶対位置で固定*/
    top: 0px; /*上に配置*/
    left: 0px; /*左の隙間をなくす*/

    background-image: url("back_image.jpg");
    /*画像を背景に指定*/
}

#main{
    width: 100%; /*幅*/
    margin: auto; /*中央寄せ*/
    max-width: 800px; /*最大の幅*/
    padding:60px 0; /*範囲内の余白(上下、左右)*/
}

.table{
    border: outset 2px; /*外枠を作る(スタイル、太さ)*/
    padding:10px 20px; /*範囲内の余白(上下、左右)*/
    margin: 10px 15px; /*範囲外の余白(上下、左右)*/
}

img{
    float: right; /*右に寄せる*/
    width: 300px; /*画像サイズを固定*/
}

@media screen and (max-width: 600px) {
    img{
        width: 100%; /*スマホなどの時の画像サイズ*/
    }
}

.time{
    clear: both; /*「float」を解除*/
}

footer{
    width: 100%; /*幅*/
    height: 60px; /*高さ*/
    font-weight: bold; /*文字を太字にする*/
    text-align: center; /*中央に表示する*/

    position: fixed; /*絶対位置で固定*/
    bottom: 0px; /*下に配置*/
    left: 0px; /*左の隙間をなくす*/

    background-image: url("back_image.jpg");
    /*画像を背景に指定*/
}

ul.footer-menu li {
    display: inline; /*一行で表示する*/
}

#copy{
    position: absolute; /*絶対位置(rightのために必要)*/
    right: 30px; /*右に配置*/
}

 

おまけ:画像を背景に指定する

background-image: url("back_image.jpg"); /*画像を背景に指定*/

その要素の背景画像を指定します。CSSファイルからの相対パスで指定します。(同じフォルダにある場合、パス名は不要です)

通常、背景が画像より大きい場合は縦横に繰り返して表示します。

background-repeat: no-repeat; /*繰り返し表示しない*/

画像の位置を指定するには「background-position」を使います。

 

まとめ

今回は「CSS完全に理解した」状態から抜け出すのに苦労しました。floatでうまくいかない(問題が起こる)人も多いのではないでしょうか。それと今回初めて「classとidは同じ要素で同時に使える」ことがわかりました。これは便利です。

サンプル1~5で画像の位置を少しずつ変えてみました。サンプル3の位置がしっくりきたのでこれでいきたいと思います。

 

次回は記事の入力画面を作ってメニューも必要になるので、そのあたりを作成していきたいと思います。

 

 

↓よかったらポチッとしていってください。

にほんブログ村 IT技術ブログ Pythonへ
にほんブログ村

CSSでタイトルとメニュー画面を固定する(ヘッダーとフッターの使い方)

ヘッダーとフッターとは?

「<header>~</header>」「<footer>~</footer>」はHTML5から新たに追加された要素で、それまでは「<div id="header">~</div>」「<div id="footer">~</div>」と書くのが一般的だったらしいです。以前の書き方との違いはわかりませんでした。

<!DOCTYPE html>
<body>
  <p>ヘッダーの上</p>
    <header>
        <p>ここはヘッダーです<p>
    </header>
    <h1>表題</h1>
    <footer>
        <p>ここはフッターです<p>
    </footer>
  <p>フッターの下</p>
</body>

例えば上のような場合

f:id:Start_python:20200204110747p:plain

このようになります。(順番はそのままでヘッダーが一番上にはなりません)

つまり「IDだと書く人によってバラバラになるから統一しよう」ってことですかね。

 

スクロールさせずに固定する方法

これはヘッダーとフッターに限った方法ではなく、CSSのほうで要素の設定をします。

position: fixed; /*絶対位置で固定*/
top: 0px; /*上に配置*/

フッターの場合は「bottom: 0px; /*下に配置*/」

 

本文の部分がヘッダーとフッターに重なってしまう場合は「padding」を使って上下の余白を設定します。

padding:120px 0; /*上下に余白を取る*/

 

HTMLファイル

<!DOCTYPE html>
<link rel="stylesheet" type="text/css" href="test.css">

<body>
    <header>
        <p>ここはヘッダーです<p>

    </header>

    <div id="main">
        <h1>表題</h1>
        <p>1</p>
        <p>2</p>
        <p>3</p>
      : (省略)
        <p>14</p>
        <p>15</p>
    </div>

    <footer>
        ここはフッターです

        <ul class="footer-menu">
            <li>ホーム |</li>
            <li>利用規約 |</li>
            <li>ヘルプ |</li>
            <li>お問い合わせ</li>
        </ul>
        <p>(c)2020 ふたり暮らし</p>
    </footer>
</body>

test.css

header{
    width: 100%;
    height: 120px;
    background-color: #1B73BA;
    color: white;
    text-align: center;

    position: fixed; /*絶対位置で固定*/
    top: 0px; /*上に配置*/
}

#main{
    padding:120px 0; /*上下に余白を取る*/
}

footer{
    width: 100%;
    height: 120px;
    background-color: #1B73BA;
    color: white;
    text-align: center;

    position: fixed; /*絶対位置で固定*/
    bottom: 0px; /*下に配置*/
}

ul.footer-menu li {
    display: inline;
}

 

f:id:Start_python:20200204110928p:plain

出来ました!ついでに本文の部分をテーブル風にしてみます。

 

インデックス(見出し)を表示する方法

<div class="table">~</table>」でクラスを作ってCSSで外枠を付けます。

border: outset 2px; /*外枠(スタイル、太さ)を付ける*/
padding:10px 20px; /*範囲内の余白(上下、左右)*/
margin: 10px 15px; /*範囲外の余白(上下、左右)*/

この見出しをいくつか疑似的に作って表示してみます。

 

HTMLファイル

<!DOCTYPE html>
<link rel="stylesheet" type="text/css" href="test.css">

<body>
    <header>
        <p>ここはヘッダーです<p>

    </header>

    <div id="main">
        <div class="table">
            <h1>表題1</h1>
            <p>1</p>
            <p>2</p>
            <p>3</p>
        </div>
        <div class="table">
            <h1>表題2</h1>
            <p>4</p>
            <p>5</p>
            <p>6</p>
        </div>
      :  (省略)
        <div class="table">
            <h1>表題5</h1>
            <p>13</p>
            <p>14</p>
            <p>15</p>
        </div>
    </div>

    <footer>
        ここはフッターです

        <ul class="footer-menu">
            <li>ホーム |</li>
            <li>利用規約 |</li>
            <li>ヘルプ |</li>
            <li>お問い合わせ  </li>
        </ul>
        <p>(c)2020 ふたり暮らし</p>
    </footer>
</body>

test.css

header{
    width: 100%; /*幅*/
    height: 120px; /*高さ*/
    background-color: #1B73BA; /*背景色*/
    color: white; /*文字の色*/
    text-align: center; /*中央に表示する*/

    position: fixed; /*絶対位置で固定*/
    top: 0px; /*上に配置*/
}

#main{
    padding:120px 0; /*範囲内の余白(上下、左右)*/
}

.table{
    border: outset 2px; /*外枠を作る(スタイル、太さ)*/
    padding:10px 20px; /*範囲内の余白(上下、左右)*/
    margin: 10px 15px; /*範囲外の余白(上下、左右)*/
}

footer{
    width: 100%; /*幅*/
    height: 120px; /*高さ*/
    background-color: #1B73BA; /*背景色*/
    color: white; /*文字の色*/
    text-align: center; /*中央に表示する*/

    position: fixed; /*絶対位置で固定*/
    bottom: 0px; /*下に配置*/
}

ul.footer-menu li {
    display: inline; /*一行で表示する*/
}

 

f:id:Start_python:20200204121100g:plain

それっぽくなりました。

 

まとめ

これでなんとか形になりそうです。あとはレイアウトをどうするかセンスの問題になってきます。デザインなど苦手なのでどこかの真似(リスペクト)して決めていきたいと思います。

 

 

↓よかったらポチッとしていってください。

にほんブログ村 IT技術ブログ Pythonへ
にほんブログ村

新規登録画面をサクッと作成してアップロード/Flask Blueprintを利用した時のCSSファイルとjsファイルの保存場所

はじめに

今回は新規登録画面を作っていきます。完成したら「Pythonanywhere」へアップロードします。ログイン画面とほとんど同じなのでCSSは使いまわします。

f:id:Start_python:20200203203130p:plain

 

動作環境

Windows10
メモ帳
Google Chrome

作業フォルダ/
 ├ static/
 │   ├ image.jpg
 │   ├ newcomer.css
 │   └ newcomer.js
 └ newcomer.html

アップロードのことを考えて「static」フォルダを作りました。

 

ソースコード

newcomer.html

<!DOCTYPE html>
<meta name="viewport" content="width=device-width,initial-scale=1">

<link rel="stylesheet" type="text/css" href="static/newcomer.css">
<script src="static/newcomer.js"></script>

<title>新規登録画面</title>

<div class="frame">
    <img src="static/image.jpg">
    <h2>アカウントを作成</h2>
    <div class="box1">
        <label for="text1" id="l_text1">名前</label>
        <input id="text1" type="text">
    </div>
    <div class="box1">
        <label for="text2" id="l_text2">パスワード</label>
        <input id="text2" type="password">
    </div>
    <p>
        アカウントを作成すると、利用規約、およびCookieの使用を含むプライバシーポリシーに同意したことになります。
    </p>
    <button class="btn1" type="submit">登録する</button>

    <p>
      <br>
      <a href="/blog/top">ログイン画面に戻る</a>
    </p>
</div>

 

newcomer.css

@media screen and (max-width: 600px) {
    p {
        font-size: 80%;
    }
}


.frame{
    width: 90%; /*幅*/
    margin: 30px auto; /*上の位置と中央寄せ*/
    max-width: 600px; /*最大の幅*/
}

img{
    display: block; /*要素の表示形式*/
    margin: auto; /*中央寄せ*/
    width: 40px; /*幅*/
    height: auto; /*縦横比を維持する高さを自動計算*/
}

.box1{
    width: 100%; /*幅*/
    height: 60px; /*高さ*/
    margin: 15px 0px; /*領域外のスペース*/
    background-color: #eee; /*背景色*/
    border-bottom: 2px solid #c2c2c2; /*text1の下線*/
}


#l_text1{
    display: block; /*要素の表示形式*/
    font-size: 14px; /*文字の大きさ*/
    padding: 5px 10px; /*領域内のスペース*/
}

#text1{
    font-size: 18px; /*文字の大きさ*/
    padding: 0 0 7px 10px; /*ボックスを大きくする*/
    width: 100%; /*幅*/
    background-color: #eee; /*背景色*/
    border: none; /*外枠の線*/
    outline: none; /*外枠の線*/
    border-bottom: 2px solid #c2c2c2; /*text1の下線*/
    box-sizing: border-box; /*横幅の解釈をpadding, borderまでとする*/
}

#text1:focus{
    border-bottom: 2px solid #1B73BA; /*下線の太さと色*/
}

#l_text2{
    display: block; /*要素の表示形式*/
    font-size: 13px; /*文字の大きさ*/
    padding: 5px 10px; /*領域内のスペース*/
}

#text2{
    font-size: 18px; /*文字の大きさ*/
    padding: 2px 0 7px 10px; /*ボックスを大きくする*/
    width: 100%; /*幅*/
    background-color: #eee; /*背景色*/
    border: none; /*外枠の線*/
    outline: none; /*外枠の線*/
    border-bottom: 2px solid #c2c2c2; /*text1の下線*/
    box-sizing: border-box; /*横幅の解釈をpadding, borderまでとする*/
}

#text2:focus{
    border-bottom: 2px solid #1B73BA; /*下線の太さと色*/
}


.btn1{
    position:relative; /*相対位置(topのために必要)*/
    width:100%; /*幅*/
    height:50px; /*高さ*/
    top: 10px; /*上の位置*/
    color:#fff; /*文字の色*/
    border:none; /*外枠*/
    -webkit-border-radius: 25px;
       -moz-border-radius: 25px;
            border-radius: 25px; /*角の半径*/
    background:#1B73BA; /*背景色*/
    display: block; /*要素の表示形式*/
    -webkit-appearance: none;
    outline: 0; /*外枠(押した後)*/
    cursor: pointer; /*カーソル(指のかたち)*/
}
.btn1:hover{
    opacity: 0.8; /*透明度*/
}


a {
    text-decoration: none; /*下線を消す*/
}
a:hover{
    text-decoration: underline; /*下線を引く*/
}

 

newcomer.js

window.onload = function () {
  document.getElementById( "text1" ).onfocus = function(){
    document.getElementById("l_text1").style.color = '#1B73BA';
  };
  document.getElementById( "text1" ).onblur = function(){
    document.getElementById("l_text1").style.color = '';
  };

  document.getElementById( "text2" ).onfocus = function(){
    document.getElementById("l_text2").style.color = '#1B73BA';
  };
  document.getElementById( "text2" ).onblur = function(){
    document.getElementById("l_text2").style.color = '';
  };
}

 

ソースコードはほとんど「ログイン画面」と同じです。詳しい解説は「HTML ツイッター風のログイン画面を作成する(CSSでレイアウトの調整)」をご覧ください。

続けて「Pythonanywhere」へアップロードします。ここで問題が発生しました。

 

Blueprintを利用した時のCSSファイルとjsファイルの保存場所

main.py」のこの部分

app.register_blueprint(app2, url_prefix="/blog")

「url_prefix="/blog"」を入れることによって、分割されたpyファイルでは「/」が「/blog」扱いされるようになります。(「/top」は「/blog/top」扱いに)

前回はCSSファイルがなかったので問題ありませんでした。CSSファイルとjsファイルは「作業フォルダ/static」のフォルダに保存します。
しかし、このままではうまく読み込めなかったのでどう対処したかを残しておきます。

 

newcomer.html」の場合はこの部分

<link rel="stylesheet" type="text/css" href="/static/newcomer.css">
<script src="/static/newcomer.js"></script>

<img src="/static/image.jpg">

このように変更することでうまく読み込めるようになりました。
ポイントは「/static/newcomer.css」で、頭に「/」を付けるところです。

「newcomer.html」から直接開くときは「/」は必要ない(逆に「/」があると認識しない)のですが、Pythonanywhereへアップロードする際には変更が必要です。

 

現在のファイル構成

作業フォルダ/
 ├ blog/
 │  ├ templates/
 │  │  └ blog/
 │  │      ├ base.html
 │  │      ├ top.html
 │  │      ├ newcomoer.html
 │  │      └ index.html
 │  ├ models/
 │  │  ├ __init__.py
 │  │  ├ database.py
 │  │  ├ models.py
 │  │  └ wiki.db
 │  ├ __init__.py
 │  └ server.py
 ├ static/
 │   ├ image.jpg
 │   ├ login.css
 │   ├ login.js
 │   ├ newcomer.css
 │   └ newcomer.js
 ├ templates/
 │  └ index.html
 ├ key.py
 └ main.py

ログイン画面と新規登録画面だけでもこんなになるんですね。

 

おまけ:キャッシュ(クッキー)が邪魔でCSSが更新されない時の対処法

CSSファイルを編集して保存しても反映されない場合がよくあります。その時は、シークレットモード「シークレット ウィンドウを開く」で試してみましょう。

 

まとめ

CSSは特に詰まるところはなかったです。名前とパスワードの欄が選択中はラベルの色が青く変わるところだけJavascriptで対応しています。HTMLもブロック毎に見えてきました。frame要素の中に[画像][表題][テキストボックス][ボタン][段落][リンク]が入っているのが分かります。ログイン画面の復習のようでより理解ができるようになったと思います。

 

 

↓よかったらポチッとしていってください。

にほんブログ村 IT技術ブログ Pythonへ
にほんブログ村

作成したPC向けのサイトをスマホ対応にする方法(スマホでWebサイトを開くと小さく表示されてしまう問題)

結論

HTMLファイルの<head>~</head>の部分にこれを入れればオッケーです。

<meta name="viewport" content="width=device-width,initial-scale=1">

通常はスマホで画面を表示する時に、自動で縮小して全体を表示するため小さく表示されてしまいます。幅の初期値960pxをスマホの幅(320~414px:iphon6~8は375px)に合わせて縮小するためです。
そこで表示領域をdevice-width(その機種の横幅)に設定することで縮小することなくそのままの大きさで表示できます。「initial-scale=1」は倍率のことです。

Windows Phone・Windowsタブレット」では「meta name="viewport"が効かない」や「initial-scaleに対応していない 」など見かけましたが未検証です。

 

スマホ画面をGoogle Chrome(ブラウザ)で確認

1.まず「Google Chrome」でサイトを開きます。

2.「デベロッパーツール」を開きます。開き方は3種類(以上?)あります。

  • 右クリックで一番下にある[検証]を押す
  • キーボードで[Shift]と[Ctrl]を押しながら[I]を押す
  • キーボードのファンクションキー[F12]を押す

f:id:Start_python:20200203121721g:plain

(少し広げて見やすくしています)

 

3.スマホ対応した画面を表示します。

f:id:Start_python:20200203122034p:plain

(赤丸の「スマホマーク」を押してサイト画面を切り替えられます)

少しわかりにくいかもしれませんが上の画像はスマホでWebサイトを開いた画面で、小さく表示されています。

 

HTMLファイルを編集して下の一行を入れて保存します。

<meta name="viewport" content="width=device-width,initial-scale=1">

編集後に再度スマホ画面を表示してみます。

f:id:Start_python:20200203122532p:plain

表示が大きくなり、先程の画面より見やすくなりました。

 

細かい部分を編集する

このままでもオッケーなのですが「パスワードをお忘れですか? アカウント作成」の文字を小さくして、スマホでも一行に収まるようにしたいです。(PCでの文字のサイズは今のままがいい)

 

login.cssに下記を追加します(一番上に追加しました)

@media screen and (max-width: 600px) {
    p {
        font-size: 80%;
    }
}

画面の幅が600px以下の時に、文字のサイズを通常の80%にします。

 

全体の幅も80%から90%に変更しました。

.frame{
    width: 90%; /*幅*/
    margin: 30px auto; /*上の位置と中央寄せ*/
    max-width: 600px; /*最大の幅*/
}

 

変更した結果がこちらです。

f:id:Start_python:20200203131318p:plain

スマホの画面でもいい感じになりました。

まとめ

はじめて「デベロッパーツール」の存在を知りました。実際の画面を確認しながらCSSを編集できるので、今後のCSS作成が捗りそうです。(編集した分はエディタにコピペして保存しないといけない?)

下記のブログで作成したログイン画面を新しくアップロードして変更しました。

start-python.hateblo.jp

こちらのサイトから確認できますので、お手元のスマホでも確認してみてください。

http://startpython.pythonanywhere.com/blog/top

 

 

↓よかったらポチッとしていってください。

にほんブログ村 IT技術ブログ Pythonへ
にほんブログ村

 

HTML ツイッター風のログイン画面を作成する(CSSでレイアウトの調整)

はじめに

前回のログイン画面を新しく作り直します。今回からCSSを本格的に勉強してします。新しく出てきた命令を「【保存版】CSS 逆引き辞典(初心者用 随時更新予定)」に追加していきます。

f:id:Start_python:20200201215230p:plain

Twitterのログイン画面を参考にゼロから作りました。

 

動作環境

Windows10
メモ帳
Google Chrome

作業フォルダ/
├─ login.html
├─ login.css
└─ login.js

cssとjsのファイル名がバラバラだと探すのが大変だと思ったので同じ「login」にしました。

 

HTMLの解説

login.html

<!DOCTYPE html>
<link rel="stylesheet" type="text/css" href="login.css"> <script src="login.js"></script> <title>ログイン画面</title> <div class="frame"> <div class="box0"> <img src="image.jpg"> <h3>ふたり暮らしにログイン</h3> </div> <div class="box1"> <label for="text1" id="l_text1">ユーザー名</label> <input id="text1" type="text"> </div> <div class="box1"> <label for="text2" id="l_text2">パスワード</label> <input id="text2" type="password"> </div> <button class="btn1" type="submit">ログイン</button> <p> <br> <a href="/blog/newcomer">パスワードをお忘れですか?</a>   <a href="/blog/newcomer">アカウント作成</a> </p> </div>

<html>~</html>、<head>~</head>、<body>~</body>は省略しました。

<div>~</div>

ひとかたまりの範囲として定義します。<div>のあいだに<div>を入れることもできます。(「frameクラス」を全体としてCSSで位置を指定しています。)

id="ID名"

タグの中に記載して、その要素にID名を付けて使用します。ID名は1つの名前で1つの要素にしか指定できません。同じ名前で付けると最初の要素にだけ適用されます。

class="クラス名"

タグの中に記載して、その要素にクラス名を付けて使用します。 クラス名は同じ名前で複数作ることができます。同じ名前の要素全てに適用されます。

<img src="ファイル名">

画像ファイルを表示します。終了タグはありません。

<label for="ラベルと付ける要素のID名" id="ID名">ラベルの文字</label>
<input id="ID名" type="text">

ラベルは、テキストボックスやチェックボックスなどに関連付ける文字です。ラベルの文字をクリックするとテキストボックスを指定したりチェックボックスにチェックを付けることができます。

<input id="ID名" type="text">

テキストボックスを作ります。

<input id="ID名" type="password">

「type="password"」にすることで、入力した文字が「●」になります。

<button class="クラス名" type="submit">ログイン</button>

ボタンを作ります。「type="submit"」にすることで送信ボタンを作ります。入力されたデータが「<form>タグ」を使って送信されますが、今回は未実装です。

<a href="リンク先">文字</a>

リンク先を指定して移動します。

 

CSSの解説

login.css

.frame{
    width: 80%; /*幅*/
    margin: 30px auto; /*上の位置と中央寄せ*/
    max-width: 600px; /*最大の幅*/
}

img{
    width: 80px; /*幅*/
    height: auto; /*縦横比を維持する高さを自動計算*/
}

.box0{
    text-align: center; /*中央寄せ*/
}

.box1{
    width: 100%; /*幅*/
    height: 60px; /*高さ*/
    margin: 15px 0px; /*領域外のスペース*/
    background-color: #eee; /*背景色*/
    border-bottom: 2px solid #c2c2c2; /*text1の下線*/
}


#l_text1{
    display: block; /*要素の表示形式*/
    font-size: 14px; /*文字の大きさ*/
    padding: 5px 10px; /*領域内のスペース*/
}

#text1{
    font-size: 18px; /*文字の大きさ*/
    padding: 0 0 7px 10px; /*ボックスを大きくする*/
    width: 100%; /*幅*/
    background-color: #eee; /*背景色*/
    border: none; /*外枠の線*/
    outline: none; /*外枠の線*/
    border-bottom: 2px solid #c2c2c2; /*text1の下線*/
    box-sizing: border-box; /*横幅の解釈をpadding, borderまでとする*/
}

#text1:focus{
    border-bottom: 2px solid #1B73BA; /*下線の太さと色*/
}

#l_text2{
    display: block; /*要素の表示形式*/
    font-size: 13px; /*文字の大きさ*/
    padding: 5px 10px; /*領域内のスペース*/
}

#text2{
    font-size: 18px; /*文字の大きさ*/
    padding: 2px 0 7px 10px; /*ボックスを大きくする*/
    width: 100%; /*幅*/
    background-color: #eee; /*背景色*/
    border: none; /*外枠の線*/
    outline: none; /*外枠の線*/
    border-bottom: 2px solid #c2c2c2; /*text1の下線*/
    box-sizing: border-box; /*横幅の解釈をpadding, borderまでとする*/
}

#text2:focus{
    border-bottom: 2px solid #1B73BA; /*下線の太さと色*/
}


.btn1{
    position: relative; /*相対位置(topのために必要)*/
    width:100%; /*幅*/
    height:50px; /*高さ*/
    top: 10px; /*上の位置*/
    color:#fff; /*文字の色*/
    border:none; /*外枠*/
    -webkit-border-radius: 25px;
       -moz-border-radius: 25px;
            border-radius: 25px; /*角の半径*/
    background:#1B73BA; /*背景色*/
    display: block; /*要素の表示形式*/
    -webkit-appearance: none;
    outline: 0; /*外枠(押した後)*/
    cursor: pointer; /*カーソル(指のかたち)*/
}
.btn1:hover{
    opacity: 0.8; /*透明度*/
}


a {
    text-decoration: none; /*下線を消す*/
}
a:hover{
    text-decoration: underline; /*下線を引く*/
}

p {
    text-align: center; /*中央寄せ*/
}

長いです。コメントを多めにしたので解説は少なくします。

.クラス名 {  }

class名の前には「.」を付けて呼び出します。

#ID名 {  }

id名の前には「#」を付けて呼び出します。

frameクラス

全体のブロック要素を作りました。全体の幅と上の位置を調整します。

box0クラス

画像と見出しを中央寄せにしています。

box1クラス

ユーザー名とパスワードで使います。幅、高さ、余白、背景色を設定して、下線を付けています。

ラベルとテキストボックス

文字のサイズや位置を調整して、クリックされた時に下線の色を付けています。

ボタン

幅、高さ、位置、色を調整して、角を丸めて、カーソルが乗った時の形を決めます。また、カーソルが乗った時に透明度(色を薄く)を付けています。

リンク<a>と段落<p>

リンクはカーソルが乗った時だけ下線を付けて、段落は中央寄せにしています。

※詳しい解説は【保存版】CSS 逆引き辞典(初心者用 随時更新予定)にあります。

 

javascriptの解説

login.js

window.onload = function () {
  document.getElementById( "text1" ).onfocus = function(){
    document.getElementById("l_text1").style.color = '#1B73BA';
  };
  document.getElementById( "text1" ).onblur = function(){
    document.getElementById("l_text1").style.color = '';
  };

  document.getElementById( "text2" ).onfocus = function(){
    document.getElementById("l_text2").style.color = '#1B73BA';
  };
  document.getElementById( "text2" ).onblur = function(){
    document.getElementById("l_text2").style.color = '';
  };
}

ユーザー名とパスワードのラベルの文字をそれぞれのテキストボックスが選択されている時だけ色を変えます。(本当はこの部分もCSSでやりたかったのですがうまくいきませんでした)

onfocusイベント

フォーカスした時のイベントです。今回は「テキストボックスが選択されたら」です。

onblurイベント

フォーカスを外した時のイベントです。

style.colorプロパティ

対象のHTML要素を取得してstyleプロパティで任意のCSSを変更します。今回は色を変更していますがほとんどのCSSが変更できそうです。
※注意:色情報は「'  '」もしくは「"  "」で囲んでください。

 

まとめ

javascriptはややこしそうなので後回しにして、CSSから勉強していきます。ログイン画面は要素も少なく、たったこれだけに感じますがCSSファイルはかなり長くなりました。やっている途中で覚えることが多すぎそうだったので、先に基本だけでもまとめてみようと【保存版】CSS 逆引き辞典(初心者用 随時更新予定)を作りました。

随時更新していきますのでご意見ご要望がございましたらよろしくお願いいたします。

 

 

↓よかったらポチッとしていってください。

にほんブログ村 IT技術ブログ Pythonへ
にほんブログ村

【保存版】CSS 逆引き辞典(初心者用 随時更新予定)

使い方

 「CSSの説明書(スタイルシートリファレンス)」「CSS命令一覧」「CSSプロパティ一覧」と、いろいろありますがやりたい事をどう命令するのかがわからないと調べられません。何度も同じことを調べて同じサイトに辿り着くことが多かったので、自分で見直す用に「逆引き辞典」を作ることにしました。

とりあえず「何を」「どうする」で調べられるようにしてみます。随時覚えたことを更新していきます。

 

なにを?

※特定の要素に見つからないときは「要素全体(共通)」にある場合があります。

 

 

基本の書き方

要素(タグ、ID、クラス)に対してのスタイルを決定します。
例えば、ページにある全ての<p>~</p>内の文字を赤色にする場合

p { color: red; }

 ID名の前には「#」を付ける。クラス名の前には「.」を付ける。

スタイルは1つの要素にいくつでも指定できる

例えば、ボタン「id="btn"」を装飾する場合

.btn{
    display: block;
    padding: 5px 30px; /*余白*/
    color:#fff; /*文字の色*/
    font-size: 16px; /*文字のサイズ*/
    text-align: center; /*文字の位置*/
    background:#1B73BA; /*背景色*/
    border-radius: 10px; /*角の半径*/
    cursor: pointer; /*カーソル(指のかたち)*/
}

このように書くと見やすい。記述順も決めておくと編集が楽になる。

記述順の例

ブロック・配置(display, position, float, top, left)
余白(margin, padding)
サイズ(width, height)
テキスト(color, font-size, font-weight, text-align)
背景(background, border, box-sizing)
その他(cursor, opacity)

 

 

 

 

要素をどうする?

 

 

 

特定の要素をどうする?

 

 

 

 

 

文字をどうする?

 

 

 

 

テキストボックスをどうする?

 

 

 

 

ボタンをどうする?

 

 

 

 

画像をどうする?

 

 

 

 

ヘッダー・フッターをどうする?

 

 

 

 

 

解説

要素を中央に表示する方法

1.margin: 30px auto; /*範囲外の余白(autoで中央表示)*/

上から30ピクセルの位置に中央寄せで表示する。
「margin(マージン)」は通常、領域外の余白を設定。[上][右][下][左]の順番。

2.text-align: center; /*中央に表示する*/

「text-align:right;」で右揃え。

3.position: absolute; /*絶対位置で表示*/
  left: 0;
  right: 0;
  margin: auto; /*中央寄せ*/

絶対位置の場合は「left: 0;  right: 0;」で左右のpositionをゼロにする。

※中央寄せにならない時の原因

要素(親要素)の幅が画面いっぱいになっている場合がある。

 

要素の位置を設定する方法

ブロック要素の場合(ブロック要素とは、前後に改行が入る要素)
例えば、h1~h6, p, div, ul, ol, li, table要素など。
※「display: block; /*要素をブロック表示にする*/」で指定もできる

1.top: 10px; /*上から10ピクセルの位置に配置*/
  left: 10px; /*左から10ピクセルの位置に配置*/

  ※「position:relative; /*相対位置への配置*/」

  これを先に指定しないと[top][bottom][left][right]は使えない。

2.margin: 15px 0px; /*範囲外の余白(上下、左右)*/

  値が4つの場合は[上][右][下][左]の順番。

3.padding: 5px 10px; /*範囲内の余白(上下、左右)*/

  値が4つの場合は[上][右][下][左]の順番。

4.text-align:center; /*文字の位置*/

  要素内の文字を中央に配置する

ブロック要素でない場合(インライン要素)
例えば、img, label, input, textarea, a, span要素など。
※「display: inline; /*要素をインライン表示にする*/」で指定もできる

1.float: right; /*右に寄せる*/

  右に寄せたい要素(画像など)に使用。

  clear: both; /*「float」を解除*/

  「float」を解除したい要素(文字など)に使用。

 

要素の幅を設定する方法

width: 80%; /*全体の80%の幅で表示*/

「px(ピクセル)」でも設定可。

 

要素の最大の幅を決める方法

max-width: 600px; /*幅の最大値を設定*/

「%」でも設定可。表示領域が変わる場合に幅が大きくなりすぎないように設定する。

 

要素の高さを設定する方法

height: 60px; /*60ピクセルの高さで表示*/

「%」でも設定可。

 

要素に背景色を付ける方法

background: #eee; /*背景色を薄灰色にする。「#eee」は「#eeeeee」と同じ*/

もしくは、background-color: #eee;

 

要素に外枠を作る方法

border: outset 2px; /*外枠を作る(スタイル、太さ)*/

※まわりの要素との間隔が気になる場合は、margin(領域外の余白)を見直す。

 

要素に下線を付ける方法

border-bottom: 2px solid #c2c2c2; /*下線を表示。太さ、1本線、色を設定*/

※要素との間隔が気になる場合は、padding(領域内の余白)を見直す。

 

要素の透明度を指定する方法

opacity: 0.8; /*透明度*/

0.0(完全に透明)~1.0(完全に不透明)の間で指定する。

 

 

 

 

特定の要素がクリックされたとき

#ID名:focus{  }  (この中に命令を入れる)

.クラス名:focus{  }  」や「a:focus {  }」でグループにも対応可。

 

特定の要素にカーソルが乗ったとき

#ID名:hover{  }  (この中に命令を入れる)

.クラス名:hover{  }  」や「a:hover {  }」でグループにも対応可。

 

カーソルの形を変える方法

cursor: pointer; /*カーソルを指の形に変える*/

「cursor: default;」で標準カーソル。「cursor: not-allowed;」なら禁止マークになる。

 

 

 

 

文字を太字にする方法

font-weight: bold; /*文字を太字にする*/

HTMLファイルで<b>~</b>でも可能だが、HTML4.01 では「非推奨(deprecated)」。

 

文字の大きさを変える方法

font-size: 18px; /*文字のサイズを18ピクセルにする*/

「%」でも設定可。ほかにも「em」や「ex」の単位がある。

 

文字に色を付ける方法

color: #ffffff; /*文字を白色にする。黒は「#000000」*/

「#ff0000」の代わりに「red」でも可。

 

文字に背景色を付ける方法

background: #1B73BA; /*背景色*/

もしくは、background-color: #1B73BA;

 

文字を中央に表示する方法

text-align: center; /*中央に表示する*/

「text-align:right;」で要素の右揃え。

 

文字を右に表示する方法

position: absolute; /*絶対位置(rightのために必要)*/
right: 30px; /*右に配置*/

親要素を基準にする時は「position: relative;」で相対位置を指定。
※h1~h6, p, div, ul, ol, li, table要素には必要ない。(デフォルトでブロック表示)

 

文字を画像の右側に表示する方法

float: left; /*画像を左に寄せる*/

右に文字を表示したい要素(画像)に使用。

clear: both; /*「float」を解除*/

「float」を解除したい要素(文字)に使用。(次の要素がない時は空白の<div class="解除用"></div>を作ればオッケー)

 

スマホの時だけ小さくする方法

@media screen and (max-width: 600px) {  }; 

この中で要素と文字のサイズを指定

例えば、この中に「p { font-size: 80% }」を入れると、画面の幅が600px以下の時は<p>タグの文字のサイズが通常の80%になる。

 

 

 

 

テキストボックスの外枠を消す方法

border: none; /*外枠を表示しない*/
outline: none; /*テキストボックスがクリックされたときの外枠を表示しない*/

「none」は「0」でも可。

 

「width: 100%;」なのにはみ出す場合の対処法

box-sizing: border-box; /*横幅の解釈をpadding, borderまでとする*/

「padding」で左に余白を作ったときに起こる問題。

 

 

 

 

ボタンの外枠を消す方法

border: none; /*外枠を表示しない*/
outline: none; /*ボタンがクリックされたときの外枠を表示しない*/

「none」は「0」でも可。

 

ボタンの角を丸める方法

border-radius: 25px; /*半径25pxで角を丸める*/

半径25pxで高さが50pxのときは半円になる。

 

 

 

 

画像の縦横比を維持したままリサイズ(拡大/縮小)する方法

width: 80px; /*幅を基準に指定する場合。例:80ピクセルの幅で表示*/

height: auto; /*縦横比を維持する高さを自動計算*/

 

画像を中央に表示する方法

display: block; /*要素の表示形式*/
margin: auto;
 /*中央寄せ*/

先に要素をブロック表示にしないと[margin]が使えない。

 

画像を背景に指定する方法

background-image: url("back_image.jpg"); /*画像を背景に指定*/

その要素の背景画像を指定する。CSSファイルからの相対パスで指定。(同じフォルダにある場合、パス名は不要)

 

画像を文字の右側に表示する方法

float: right; /*右に寄せる*/

右に寄せたい要素(画像)に使用。

clear: both; /*「float」を解除*/

「float」を解除したい要素(文字)に使用。(次の要素がない時は空白の<div class="解除用"></div>を作ればオッケー)

 

 

 

 

ヘッダー・フッターを固定する方法

position: fixed; /*絶対位置で固定*/
top: 0px; /*上に配置*/

フッターの場合は「bottom: 0px; /*下に配置*/」

 

 

ヘッダー・フッターの左の隙間をなくす方法

left: 0px; /*左の隙間をなくす*/

通常はmargin(範囲外のスペース)の初期値が8pxのため隙間があいてしまう。

 

 

 

 

 

これからも随時更新していきますのでよろしくお願いします。

 

 

↓よかったらポチッとしていってください。

にほんブログ村 IT技術ブログ Pythonへ
にほんブログ村

Flask-Loginを使わずにログイン画面を実装する(Blueprintsでパス指定やSECRET_KEYを設置する方法)

f:id:Start_python:20200131094148p:plain

http://startpython.pythonanywhere.com/

こちらから実際に操作できますのでお試しください。(ユーザー名・パスワードは普段お使いのもの以外にしてください。セキュリティーは保証できません。)

 

はじめに

Webアプリに必要になることが多い「ログイン機能」を勉強していきます。複数のWebアプリを実行するために「Blueprints」を使用しましたが、結構行き詰ったところがありました。同じような苦労をした方にも参考になればうれしいです。

 

動作環境

Windows10
Python 3.7.5
Flask 1.1.1

作業フォルダ/
 ├ blog/
 │  ├ templates/
 │  │  └ blog/
 │  │      ├ base.html
 │  │      ├ top.html
 │  │      ├ newcomoer.html
 │  │      └ index.html
 │  ├ models/
 │  │  ├ __init__.py
 │  │  ├ database.py
 │  │  ├ models.py
 │  │  └ wiki.db
 │  ├ __init__.py
 │  └ server.py
 ├ templates/
 │  └ index.html
 ├ key.py
 └ main.py

 

1.ログイン画面と登録画面を作成

「作業フォルダ/blog/templates/blog」にHTMLファイルを作成します。

base.html(共通のテンプレート)

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <title>お試し日記</title>
  <code src="https://code.jquery.com/jquery-3.2.1.slim.min.j" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></code>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.j" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.cs" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">

</head>
<body>
<header>
  <div class="navbar navbar-dark box-shadow" style="background-color:#4169e1;">
    <div class="container d-flex justify-content-between">
      <a href="/blog/" class="navbar-brand d-flex align-items-center">
        <strong>お試し日記</strong>
      </a>
    </div>
  </div>
</header>
<div class="container">

{% block body %}{% endblock %}
</div>
</body>

top.html(ログイン画面)

{% extends "blog/base.html" %}
{% block body %}
 
<h1>ログイン</h1>
{% if status == "user_notfound" %}
<p>ユーザが見つかりません。新規登録してください</p>
{% elif status == "wrong_password" %}
<p>パスワードが間違っています</p>
{% elif status == "logout" %}
<p>ログアウト完了</p>
{% endif %}
<form action="/blog/login" method="post">
    <input type="text" name="user_name" placeholder="user name">
    <input type="password" name="password" placeholder="password">
    <input class="btn btn-primary" type="submit" value="Login">
</form>
<a href="/blog/newcomer">新規登録はこちら</a>
 
{% endblock %}

newcomer.html(新規登録画面)

{% extends "blog/base.html" %}
{% block body %}
 
<h1>新規登録</h1>
    <a href="/blog/top">ログイン画面に戻る</a>
    {% if status == "exist_user" %}
    <p>そのユーザは既に登録されています。</p>
    {% endif %}
    <form action="/blog/registar" method="post">
        <input type="text" name="user_name" placeholder="user name">
        <input type="password" name="password" placeholder="password">
        <input class="btn btn-primary" type="submit" value="新規登録">
    </form>
{% endblock %}

index.html(ログイン中の画面)

{% extends "blog/base.html" %}
{% block body %}
 
<p>ようこそ {{name}} さん</p>
<a href="/blog/logout">ログアウトする </a>
<a href="/blog/create">新しく日記を書く </a>
<table class="table table-striped table-hover">
  <tr>
    <th>id</th>
    <th>日時</th>
    <th>title</th>
    <th>date</th>
    <th> </th>
  </tr>
 
</table>
 
{% endblock %}

レイアウトの部分については次回勉強したいと思いますので、今回は参考サイト様からそのままお借りしています。

 

2.SQLAlchemyを使ってSQLiteのサーバーを作成する

ユーザー情報(ユーザー名、パスワード)を保存しておくデータベースを構築します。

SQLAlchemyモジュールをインストール

インストールがまだの場合は、コマンドプロンプトから下記を実行します。

pip install SQLAlchemy

「作業フォルダ/blog/models」にpyファイルを作成します。

__init__.py

 

中身は空のままです。「__init__.py」は同じフォルダ内のpyファイルをモジュールとして呼び出す際(インポートして使うため)に必要です。別のpyファイルから利用できるようなります。(前回より理解が深まった部分)

database.py(データベース情報)

from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
import os

#パスを定義してwiki.dbを作成します。
databese_file = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'wiki.db')
engine = create_engine('sqlite:///' + databese_file, convert_unicode=True)

#データベースにアクセスするためにセッションを作成します。
db_session = scoped_session(sessionmaker(autocommit=False,
                                         autoflush=False,
                                         bind=engine))
#Baseオブジェクトを作った後、query_property()を使ってBaseオブジェクトに検索クエリを持たせます。
#後でデータベースから検索する時に楽らしい。
Base = declarative_base()
Base.query = db_session.query_property()

#データベースの初期化用です。初期化したいときに呼びます。
def init_db():
    import models.models
    Base.metadata.create_all(bind=engine)

models.py(テーブル定義)

from sqlalchemy import Column, Integer, String, Text, DateTime
from models.database import Base        # データベースの初期化用
#from blog.models.database import Base  # 初期化が終わればこちらへ変更
from datetime import datetime
 
 
class WikiContent(Base):
    __tablename__ = 'wikicontents'
    id = Column(Integer, primary_key=True)
    title = Column(String(128))
    # title = Column(String(128), unique=True)
    body = Column(Text)
    date = Column(DateTime, default=datetime.now())
 
    def __init__(self, title=None, body=None, date=None):
        self.title = title
        self.body = body
        self.date = date
 
    def __repr__(self):
        return '<Title %r>' % (self.title)
 
#Userクラスを追加。WikiContentの使い回し
class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    user_name = Column(String(128))
    hashed_password = Column(String(128))
 
    def __init__(self, user_name=None, hashed_password=None):
        self.user_name = user_name
        self.hashed_password = hashed_password
 
    def __repr__(self):
        return '<Name %r>' % (self.user_name)

データベースの初期化

wiki.db」を作成します。コマンドプロンプトから下記を実行します。

cd 作業フォルダ
cd blog
python
from models.database import init_db
init_db()
quit()

modelsフォルダ内に「wiki.db」ファイルが出来ます。確認してみましょう。

※重要:「wiki.db」が作成されたら「models.py」の2行目の
from models.database import Base」を削除かコメント化して
from blog.models.database import Base」に変更かコメント化を解除してください。(他にいい方法があればいいのですが、、、最初だけなので。)

 

3.暗号化キー情報を作成

「作業フォルダ」にkey.pyファイルを作成します。中身は、セッション管理に利用するSECRET_KEYと、パスワードの暗号化の際に利用するSALTです。

key.py

SECRET_KEY = "任意の文字列"
SALT = "任意の文字列"

「任意の文字列」の部分をお好きな文字列(英数字のみ?)に変更してください。なるべく推測されにくいものにしましょう。

※この呼び出し方に苦労することになります。後ほど記載します。

 

4.メインプログラムの作成

「作業フォルダ」にpyファイルを作成します。コメントを多めにして解説は少なくします。

main.py(起動ファイル)

# Flaskとrender_template(HTMLを表示させるための関数)をインポート
from flask import Flask, render_template

# Flaskオブジェクトの生成
app = Flask(__name__)

# セキュリティーキーの設定(なりすましなどを防ぐため)
import key
app.secret_key = key.SECRET_KEY

# 前回作った基本的なWebページ(今回は使いません)
#from test.test_app import app1
#app.register_blueprint(app1)

# 今回使用するWebアプリ
from blog.server import app as app2
app.register_blueprint(app2, url_prefix="/blog")


#「/」へアクセスがあった場合に「index.html」を返す
@app.route('/')
def index():
    return render_template('index.html')


if __name__ == '__main__':
    app.run()

※セキュリティーキーの設定を「server.py」のほうでやろうとしてエラーが止まりませんでした。「main.py」でしか「app.config」の部分が触れないみたいです。
(ずっと「server.py」で考えていて、current_appを使ってapp.configを変更しようとしたり「server.py」の「app」の名前を変えたり、「main.py」でやろうと思い付くまでにすごく時間を使ってしまいました。)

前回より理解が深まった点

  • from blog.server import app as app2
    「as app2」を使うことによって、分割されたpyファイルで「app」のまま使えるようになりました。
  • app.register_blueprint(app2, url_prefix="/blog")
    「url_prefix="/blog"」を入れることによって、分割されたpyファイルでは「/」が「/blog」扱いされるようになります。(「/top」は「/blog/top」扱いに)

「作業フォルダ/blog」にpyファイルを作成します。

__init__.py

 

中身は空のままです。

server.py(Blueprintで分割された実行ファイル)

# Blueprint(pyファイルを分割するための関数)をインポート
from flask import Blueprint

#「app」を「Blueprint()」を使って定義
#「blog」の部分は、url_for("blog.top")で使用
app = Blueprint('blog', __name__, template_folder='templates')


# 必要なモジュールをインポート
import os
from datetime import datetime
from flask import Flask, session, redirect, url_for, render_template,request
#「/blog/models/models.py」の「WikiContent」と「User」を呼び出す
from blog.models.models import WikiContent,User
#「/blog/models/database.py」の「db_session」を呼び出す
from blog.models.database import db_session


# ハッシュ化するための関数をインポート(パスワードの暗号化用)
# 今回はsha256という暗号化方式を使用
import key
from hashlib import sha256


#「/」へアクセスがあった場合
@app.route('/')
def index():  # 一覧画面
    if "user_name" in session:
        # セッションがログイン状態であれば「blog/index.html」を返す
        name = session["user_name"]
        all_diary= WikiContent.query.all()
        return render_template('blog/index.html',name=name, all_diary=all_diary)
    else:
        # ログイン状態でなければ「/blog/top」ページへ移動
        return redirect(url_for("blog.top"))

#「/logout」へアクセスがあった場合「/blog/top」ページへ移動
@app.route("/logout")
def logout():
    session.pop("user_name", None)
    return redirect(url_for("blog.top",status="logout"))

#「/create」へアクセスがあった場合「準備中です」を表示
@app.route('/create')
def create():  # 新規作成
    return '準備中です'

#「/login」へアクセスがあった場合
@app.route("/login",methods=["post"])
def login():
    # 入力された「ユーザー名」が既に存在するか確認
    user_name = request.form["user_name"]
    user = User.query.filter_by(user_name=user_name).first()
    if user:
        #「ユーザー名」が存在した場合
        #「パスワード」が一致するか確認(「key.SALT」で暗号化)
        password = request.form["password"]
        hashed_password = sha256((user_name + password + key.SALT).encode("utf-8")).hexdigest()
        if user.hashed_password == hashed_password:
            #「パスワード」が一致した場合「/blog/index」ページへ移動
            session["user_name"] = user_name
            return redirect(url_for("blog.index"))
        else:
            #「パスワード」が一致しない場合「/blog/top」ページへ移動
            return redirect(url_for("blog.top",status="wrong_password"))
    else:
        #「ユーザー名」が存在しなかった場合「/blog/top」ページへ移動
        return redirect(url_for("blog.top",status="user_notfound"))

#「/registar」へアクセスがあった場合
# 新規登録画面で「新規登録」ボタンが押された時
@app.route("/registar",methods=["post"])
def registar():
    # 入力された「ユーザー名」が既に存在するか確認
    user_name = request.form["user_name"]
    user = User.query.filter_by(user_name=user_name).first()
    if user:
        #「ユーザー名」が存在した場合「/blog/newcomer」ページへ移動
        return redirect(url_for("blog.newcomer",status="exist_user"))
    else:
        #「ユーザー名」が存在しなかった場合
        #「ユーザー名」「パスワード」(「key.SALT」で暗号化)を
        password = request.form["password"]
        hashed_password = sha256((user_name + password + key.SALT).encode("utf-8")).hexdigest()
        user = User(user_name, hashed_password)
        # データベースに追加して保存
        db_session.add(user)
        db_session.commit()
        session["user_name"] = user_name
        #「/blog/index」ページへ移動
        return redirect(url_for("blog.index"))

#「/top」へアクセスがあった場合「blog/top.html」を返す
@app.route("/top")
def top():
    status = request.args.get("status")
    return render_template("blog/top.html",status=status)

#「/newcomer」へアクセスがあった場合「blog/newcomer.html」を返す
@app.route("/newcomer")
def newcomer():
    status = request.args.get("status")
    return render_template("blog/newcomer.html",status=status)

前回より理解が深まった点

  • return render_template('blog/index.html')
    こちらの「blog」の部分は「main.py」の「app.register_blueprint(app2, url_prefix="/blog")」にある「/bog」です。
  • return redirect(url_for("blog.top"))
    こちらの「blog」の部分は「server.py」の「app = Blueprint('blog', __name__, template_folder='templates')」にある「blog」です。
  • import key
    「server.py」ではSALTしか使っていません。(「key.SALT」として利用)
    「main.py」ではSECRET_KEYしか使っていません。(「key.SECRET_KEY」として利用)両方のpyファイルでインポートします。

 

最後に「作業フォルダ/templates」にHTMLファイルを作成します。

index.html(違うフォルダにも「index.html」があるので注意です)

<!DOCTYPE html>
<html lang="ja">
  <head>
    <title>ふたり暮らし メニュー画面</title>
  </head>
  <body>

    <h2>メインメニュー</h2>
    <p><a href= "/test_home.html" >ボタンでページを移動する</a></p>
    <p></p>
    <p><a href= "/blog" >ブログ(登録とログイン)</a></p>

  </body>
</html>

(「ボタンでページを移動する」は前回作ったWebページです。)

 

まとめ

Flask-Loginを使わないメリットはわかりませんが、ログイン機能をはじめて実装するときはシンプルなほうから勉強しようと考えてこのかたちになりました。これで新規登録とログインができるようになりましたが、課題がたくさん出てきました。たとえば現状のままだとユーザー名もパスワードも空のまま登録できてしまいます。

とりあえず一旦完成ということで、次回からはレイアウトを少しいじっていきます。

 

 

↓よかったらポチッとしていってください。

にほんブログ村 IT技術ブログ Pythonへ
にほんブログ村

 

こちらのサイトを参考にさせていただきました。

manecoco.com

qiita.com