百済茄子

アマチュアプログラマによる主にPythonとDjangoのブログ。

基礎の基礎「メモアプリ」Django2・Python3 ~Bootstrap4で飾り付け、その他

  2018/10/16         

前回作ったものを、Bootstrap4を使って仕上げる。
また、ちょっとした機能も追加する。


<完成品>





<変更点>

models.py
from datetime import datetime, timedelta  # 追加

...

class Memo(models.Model):

...

    # 以下、関数を2つ追加
    def comment_num(self):
        """Memoのコメント数を返す"""
        return self.comment_set.all().count()

    def is_new(self):
        """Memoのコメントが24時間以内のときTrueを返す"""
        if not self.comment_set.last():
            return False
        if datetime.now().astimezone() - self.comment_set.last().created_at >= timedelta(hours=24):
            return False
        return True

...

メモ一覧ページ(トップページ)に、コメント数とNew!マークを表示させたかった。
参考:他モデル参照_set
参考:SQLiteデータベース操作、Queryset API ~Django3・Python2で作るWebアプリ

コメント数が0のときに’NoneType’ object has no attribute ‘created_at’のエラーが出る。
よってコメントが存在しないときはFalseを返すようにする。

タイムゾーン云々でもエラーが出る。astimezone()を付けると治る。原理は謎。
参考:Pythonのタイムゾーンの扱い


views.py
...

class MemoList(ListView):
    """メモ一覧"""
    model = Memo
    ordering = ['done', '-created_at']
    paginate_by = 15  # 追加

テンプレートにページネーションを加えたので、paginate_byでコンテンツを1ページあたりいくつ表示させるのかを指定している。


テンプレート

base.html
<!doctype html>
<html lang="ja">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">

    <title>メモアプリ</title>

    <style>
      body {
        padding-bottom: 100px;
      }
      .footer {
        position: absolute;
        bottom: 0;
        width: 100%;
        height: 60px; /* ここでフッタの固定高さを設定 */
        line-height: 60px; /* ここでテキストを垂直に中央に配置 */
        background-color: #f5f5f5;
      }
    </style>

  </head>
  <body>

  <div class="container-fluid">   <!-- 全体を囲むコンテナ -->

    <div class="row">
      <!-- 左余白:大きい画面では左に余白を入れる -->
      <div class="col-xl-1"></div>

      <!-- 中央(メイン部分) -->
      <div class="col-xl-10">
        <div class="mt-5"></div>  <!-- 上余白 -->

        <span class="h1">
          <a href="{% url 'memo_list' %}" style="text-decoration: none; color:#000">メモの森</a>
        </span> 
        <a href="{% url 'admin:index' %}" target="_blank">管理画面へ</a>

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

      <!-- 右余白:大きい画面では右に余白を入れる -->
      <div class="col-xl-1"></div>
    </div>

  </div>   <!-- 全体を囲むコンテナ -->

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
  </body>

  <!-- フッター -->
  <footer class="footer">
  <div class="container text-center">
    <span class="text-muted">(C) 2018 Community site</span>
  </div>
  </footer>

</html>

大まかにはBootstrap4公式サイトからのコピペ。
参考:Bootstrap4 Starter template

上から、
フッターを付けるためのstyle。
フッターとページ内のコンテンツがかぶってしまい見えなくなる所があったので、bodyにpadding-bottom: 60px;を加えた。
参考:footerを常に最下部にするとfooterがコンテンツに重なってしまう(teratail)

container-fluidで全体をコンテナで囲み、PCの大きな画面で見ると画面の両端が詰まってしまうので
大きな画面で見たときはcol-xl-1で余白を入れた。
ほぼBootstrap4ファーストガイドを参考にした。

管理画面へリンクを作成するにはurlにadmin:indexを指定する。
参考:テンプレート(templates)の中身 ~Django3・Python2で作るWebアプリ


memo_list.html
{% extends 'memo/base.html' %}

{% block content %}

  {% if user.is_superuser %}
    <p class="mt-3">
      <a class="btn btn-primary" href="{% url 'admin:memo_memo_add' %}" target="_blank">メモ作成</a>
    </p>
  {% endif %}

  <div class="row">
  {% for memo in memo_list %}
    <div class="col-xl-3 col-lg-3 col-md-4 col-sm-6">
      <div class="card mb-3">

        <div class="card-header">
          {% if memo.done %}
            (完了) <a href="{% url 'memo_detail' memo.id %}">{{ memo.text |truncatechars:10 }}</a>
          {% else %}
            <a href="{% url 'memo_detail' memo.id %}">{{ memo.text |truncatechars:13 }}</a>
          {% endif %}
        </div>

        <div class="card-body">
          {% if memo.label %}{{ memo.label }}{% else %}ラベルなし{% endif %}<br>
          {{ memo.created_at | date:"Y/m/d" }}<br>
          <nobr>
            コメント: {{ memo.comment_num }} {% if memo.is_new %}<span class="text-danger">New!</span>{% endif %}
          </nobr>
        </div>

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

  {% include 'memo/page.html' %}

{% endblock %}

if user.is_superuserで囲むと、管理画面にログインしていないときは表示されなくなる。
参考:テンプレート(templates)の中身 ~Django3・Python2で作るWebアプリ
メモの作成・編集・削除は自作フォームを使わずに、全て管理画面に飛ばすことにした。

Bootstrap4のカードを使った。
画面が小さくなるにつれて4列、4列、3列、2列、1列と変化する。
Bootstrap4のグリッドシステムは横を12分割していて、col-3とすると12分の3ごと、つまり4つに分割される。
画面が最も大きいときがxlなのでcol-xl-3と指定する。

models.pyで定義したcomment_numとis_newを使った。
MemoDetailの中のコメントの数を表示させ、さらにコメント投稿24時間以内のものにNew!を付ける。

includeでページネーションを読み込む。
参考:テンプレート(templates)の中身 ~Django3・Python2で作るWebアプリ


page.html
<nav aria-label="Page navigation">
  <ul class="pagination">

      {% if page_obj.has_previous %}
        <li class="page-item">
        <a class="page-link" href="?page={{ page_obj.previous_page_number }}">
          <span aria-hidden="true">&laquo;</span>
        </a>
      </li>
      {% endif %}

      {% for link_page in page_obj.paginator.page_range %}
        {% if link_page == page_obj.number %}
          <li class="page-item active">
            <a class="page-link" href="?page={{ link_page }}">
              {{ link_page }}
            </a>
          </li>
        {% else %}
          <li class="page-item">
            <a class="page-link" href="?page={{ link_page }}">
              {{ link_page }}
            </a>
          </li>
        {% endif %}
      {% endfor %}

      {% if page_obj.has_next %}
        <li class="page-item">
        <a class="page-link" href="?page={{ page_obj.next_page_number }}" aria-label="Next">
          <span aria-hidden="true">&raquo;</span>
        </a>
      </li>
      {% endif %}

  </ul>
</nav>

これは(たしか)Bootstrap4 Cheatsheetからのコピペ。


memo_detail.html
{% extends 'memo/base.html' %}

{% block content %}

  <p class="mt-5">メモ: {{ memo.text | linebreaksbr }}</p>
  <p>ラベル: {{ memo.label }}</p>
  <p>完了: {% if memo.done %}済{% else %}未{% endif %}</p>
  <p>作成日時: {{ memo.created_at }}</p>
  <p>更新日時: {{ memo.updated_at }}</p>

  {% if user.is_superuser %}
    <a class="btn btn-success" href="{% url 'admin:memo_memo_change' memo.pk %}" target="_blank">編集</a>
    <a class="btn btn-danger" href="{% url 'admin:memo_memo_delete' memo.pk %}" target="_blank">削除</a>
  {% endif %}

  <p class="mt-5">
    <form method="post">{% csrf_token %}
      {{ form.as_p }}
      <button class="btn btn-primary" type="submit">送信</button>
    </form>
  </p>

  <p>コメント</p>
  {% for comment in comments %}
    <p>{{ comment.name | default:'名無し' }}: {{ comment.comment }}</p>
  {% endfor %}

{% endblock %}

編集と削除は管理画面に飛ばす。




  -  Bootstrap4, Django

SQLiteデータベース操作、Queryset API ~Django3・Python2で作るWebアプリ

  2018/10/16         

※自分なりの解釈が入っている部分がある。
※簡単で見れば理解できるものは載せない。



shellで操作する場合もあれば、models.pyの中やviews.pyの中で使う場合もある。
参考:Django Documentation


all()

Memo.objects.all()
self.comment_set.all().count()
のように使う。指定したオブジェクトを全て参照する。


count()

def comment_num(self):
    """Memoのコメント数を返す"""
    return self.comment_set.all().count()
オブジェクトの個数をカウントする。


last()

def is_new(self):
    """Memoのコメントが24時間以内のときTrueを返す"""
    if not self.comment_set.last():
        return False
    if datetime.now().astimezone() - self.comment_set.last().created_at >= timedelta(hours=24):
        return False
    return True
オブジェクトの最後の要素を参照する。first()もあることは容易に想像できる。








  -  Django

PAGE TOP