Django ブログサイト templates/detail.html 詳細画面

2020/07/04 (更新:2020/11/19)

詳細画面です。

テンプレート

head

{% extends 'blog/blog_base.html' %}
{% load i18n static %}

{% block head_title %}{% block head_post_title %}{{ object.content.title_text }}{% endblock %} - {% block head_author_title %}{{ view.kwargs.author.title_text }}{% endblock %}{% endblock %}

{% block extra_css %}
  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/default.min.css">
{% endblock %}

blog_main

{% block blog_main %}
  <h1 class="mt-3">{% block post_title %}{{ object.content.title_text }}{% endblock %}</h1>
  <p class="ml-1 mb-0">
    <span class="post-created">{{ object.created_date|date:"Y/m/d" }}</span>
    <span class="post-updated">({% trans 'Updated' %}:{{ object.updated_date|date:"Y/m/d" }})</span>
  </p>
  <div>
    {% for bc in object.categories %}
      <a href="#" data-category-text="{{ bc.category.category_text }}" class="badge category badge-light"><span>{{ bc.category.category_text }}</span></a>
    {% endfor %}
  </div>
  <div class="mt-5">
    {% block post_content %}{% endblock %}
  </div>
  
  {% if object.is_comment %}
    <div>
      <h2>{% trans 'Comment' %}</h2>
      {% for comment in object.comments %}
        <div class="comment" data-id="{{ comment.id }}" data-status="{{ comment.status }}">
          <div>
            <div class="comment-summary">
              <span class="comment-name">{{ comment.name_text|default:'****' }}</span>
              <span class="comment-pipe">|</span>
              <span class="comment-created">{{ comment.created_date|date:"Y/m/d" }}</span>
              {% if request.user.is_authenticated %}
                <span class="comment-pipe">|</span>
                <span class="comment-status">{{ comment.status_name }}</span>
              {% endif %}
            </div>
            {% if request.user.is_authenticated %}
              <div class="float-right">
                {% if object.is_comment_edit %}
                  <button type="button" class="comment-edit btn btn-light">{% trans 'Edit' %}</button>
                {% endif %}
                {% if object.is_comment_reply %}
                  <button type="button" class="comment-reply btn btn-light">{% trans 'Reply' %}</button>
                {% endif %}
              </div>
            {% endif %}
          </div>
          {{ comment.comment_text|linebreaks }}

          {% if comment.replies %}
            <div class="reply-container ml-5">
              {% for reply in comment.replies %}
                  <div class="comment" data-id="{{ reply.id }}" data-status="{{ reply.status }}">
                    <div>
                      <div class="comment-summary">
                        <span class="comment-name">{{ reply.name_text|default:'****' }}</span>
                        <span class="comment-pipe">|</span>
                        <span class="comment-created">{{ reply.created_date|date:"Y/m/d" }}</span>
                        {% if request.user.is_authenticated %}
                          <span class="comment-pipe">|</span>
                          <span class="comment-status">{{ reply.status_name }}</span>
                        {% endif %}
                      </div>
                        <div class="float-right">
                          {% if object.is_comment_edit %}
                            <button type="button" class="comment-edit btn btn-light">{% trans 'Edit' %}</button>
                          {% endif %}
                          <button type="button" class="comment-reply btn btn-light invisible">{% trans 'Reply' %}</button>
                        </div>
                    </div>
                    {{ reply.comment_text|linebreaks }}
                  </div>
              {% endfor %}
            </div>
          {% endif %}

        </div>
      {% empty %}
        <p>{% trans 'COMMENT_NOTHING' %}</p>
      {% endfor %}
      {% if object.is_comment_entry %}
        <div class="mb-3">
          <button type="button" id="comment_entry" class="btn btn-light">{% trans 'Comment Entry' %}</button>
        </div>
      {% endif %}
    </div>
  {% endif %}

modal

<div id="code_modal" class="modal fade" tabindex="-1" role="dialog">
  <div class="modal-dialog modal-xl">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title">Modal title</h5>
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="modal-body">
        <div class="code-content">
          <div class="code-btn">
            <button type="button" id="code_modal_clipboard" alt="Copy to clipboard">Copy</button>
          </div>
          <div class="code-body">
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

{% if object.is_comment %}
  <div id="comment_modal" class="modal" tabindex="-1" role="dialog">
    <div class="modal-dialog" role="document">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title">{% trans "Comment Entry" %}</h5>
          <button type="button" class="close" data-dismiss="modal" aria-label="Close">
            <span aria-hidden="true">&times;</span>
          </button>
        </div>
        <div class="modal-body">
          <div class="alert-container"></div>
          <div class="comment-wrapper"></div>
          <p id="comment_modal_message"></p>
          <form id="comment_form" action="{% url 'blog:comment' view.kwargs.author_name object.id %}" method="POST" onsubmit="return false;">
            <div class="form-group">
              <label for="name_text">{% trans 'Name' %}</label>
              <input type="text" id="name_text" name="name_text" placeholder="****">
            </div>
            <div class="form-group">
              <label for="comment_text">{% trans 'Comment' %}</label><span class="pl-1">*</span>
              <textarea id="comment_text" name="comment_text" class="form-control" rows="6"></textarea>
              <div class="text-right">
                <span class="comment-length"><span id="comment_length">0</span>/1000</span>
              </div>
            </div>
            {% csrf_token %}
          </form>
          <p class="comment-note">
          {% trans "COMMENT_NOTE" %}
          </p>
        </div>
        <div class="modal-footer">
          <button type="button" id="comment_confirm" class="btn btn-primary">{% trans 'Confirm' %}</button>
          <button type="button" id="comment_back" class="btn btn-second">{% trans 'Back' %}</button>
          <button type="button" id="comment_save" class="btn btn-primary">{% trans 'Save' %}</button>
        </div>
      </div>
    </div>
  </div>

  {% if object.is_comment_edit %}
    <div id="comment_edit_modal" class="modal" tabindex="-1" role="dialog">
      <div class="modal-dialog" role="document">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title">{% trans "Comment Edit" %}</h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
              <span aria-hidden="true">&times;</span>
            </button>
          </div>
          <div class="modal-body">
            <div class="comment-wrapper"></div>
            <form id="comment_edit_form" method="POST" onsubmit="return false;">
              <div class="form-group">
                {{ comment_form.status }}
              </div>
              {% csrf_token %}
            </form>
          </div>
          <div class="modal-footer">
            <button type="button" id="comment_update" class="btn btn-primary">{% trans 'Update' %}</button>
          </div>
        </div>
      </div>
    </div>
  {% endif %}
{% endif %}

URL とメッセージを定義し、ロジックは Javascript に実装します。

script

{% block extra_js %}
  <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js"></script>
  <script src="//cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.6/clipboard.min.js"></script>
  <script>hljs.initHighlightingOnLoad();</script>
  <script>
    $.extend(base, {
      detail: {
        actions: {
          comment: '{% url "blog:comment" view.kwargs.author_name object.id %}',
          reply: '{% url "blog:reply" view.kwargs.author_name 0 %}',
          comment_update: '{% url "blog:comment_update" view.kwargs.author_name 0 %}'
        },
        messages: {
          'Comment Entry': '{% trans "Comment Entry" %}',
          'Reply Entry': '{% trans "Reply Entry" %}',
          'COMMENT_ENTRY': '{% trans "COMMENT_ENTRY" %}',
          'COMMENT_CONFIRM': '{% trans "COMMENT_CONFIRM" %}'
        }
      }
    });
  </script>
  <script src="{% static 'blog/js/detail.js' %}"></script>
{% endblock %}

Javascript

コメントの登録は Ajax で処理します。

コメント登録comment
コメント更新comment_update
返信登録reply
detail.js

$(function() {
  // Comment Entry
  $('#comment_entry').click(function() {
    commentEntryModel(base.detail.actions.comment, 1);
    commentSaveDone = function(data) {
      showAlertModal('info', data.message);
    };
  });
  $('#comment_confirm').click(function() {
    commentConfirm();
  });
  $('#comment_save').click(function() {
    commentSave();
  });
  $('#comment_back').click(function() {
    commentEntry();
  });

  // Comment Reply
  $('.comment-reply').click(function() {
    var $comment = cloneComment($(this).closest('.comment'));
    $('#comment_modal .comment-wrapper').empty().append($comment);
    var action = base.actions.reply.replace(/\/0\//, f('/{0}/', $comment.data('id')));
    commentEntryModel(action, 2);
    commentSaveDone = function(data) {
      redirect(20, data.message);
    };
  });

  $('#comment_text').keydown(commentLength).blur(commentLength);

  // Comment Edit
  var $commentEdit;
  $('.comment-edit').click(function() {
    $commentEdit = $(this).closest('.comment');
    var $comment = cloneComment($commentEdit);
    var action = base.actions.comment_update.replace(/\/0\//, f('/{0}/', $comment.data('id')));
    $('#comment_edit_form').attr('action', action);
    $('#comment_edit_modal .comment-wrapper').empty().append($comment);
    $('#comment_edit_form select[name="status"]').val($comment.data('status'));
    $('#comment_edit_modal').modal('show');
  });
  $('#comment_update').click(function() {
    commentUpdate($commentEdit);
  });

  // Code Clipboard
  initClipboard('.clipboard', {
    text: function(trigger) {
      return $.trim($(trigger).closest('.code-content').find('code').text());
    }
  });

  // Code Popup
  var $modal = $('#code_modal');
  $('.popup').click(function() {
    var $codeContent = $(this).closest('.code-content');
    var $pre = $codeContent.find('pre').clone();
    $modal.find('.modal-title').text($codeContent.find('.code-title').text());
    $modal.find('.code-body').empty().append($pre);
    $modal.modal('show');
  }).attr('data-original-title', 'Show with modal').tooltip();
  initClipboard('#code_modal_clipboard', {
    container: $modal[0],
    text: function(trigger) {
      return $.trim($(trigger).closest('.code-content').find('code').text());
    }
  });
});

function initClipboard(selector, options) {
    new ClipboardJS(selector, options).on('success', function(e) {
      showTooltip($(e.trigger), 'Copied!');
      e.clearSelection();
    }).on('error', function(e) {
      showTooltip($(e.trigger), 'Error!');
      e.clearSelection();
    });
    $(selector).attr('data-original-title', 'Copy to clipboard').tooltip();
}
function showTooltip($target, msg) {
  var original = $target.attr('data-original-title');
  $target.attr('data-original-title', msg).tooltip('show').attr('data-original-title', original);
}

function commentLength() {
  var length = $(this).val().length;
  $('#comment_length').text(length);
  $('#comment_confirm').prop('disabled', !length);
}
function cloneComment($comment) {
  var $clone = $comment.clone();
  $clone.data('id', $comment.data('id'));
  $clone.data('status', $comment.data('status'));
  $clone.find('.comment-edit').parent().remove();
  $clone.find('.reply-container').remove();
  return $clone;
}
function commentEntryModel(action, mode) {
  var $modal = $('#comment_modal');
  $modal.find('form').attr('action', action);
  $modal.find('.alert-container').empty();
  $modal.find('.modal-title').text(mode == 1? base.detail.messages['Comment Entry'] : base.detail.messages['Reply Entry']);
  $modal.find('.comment-wrapper').toggle(mode != 1);
  $modal.find('.comment-note').toggle(mode == 1);
  $modal.modal('show');
  commentEntry();
  $('#name_text,#comment_text').val('');
}
function commentEntry() {
  $('#comment_modal_message').text(base.detail.messages['COMMENT_ENTRY']);
  enabled($('#name_text,#comment_text'), true);
  $('#comment_confirm').show();
  $('#comment_back,#comment_save').hide();
  $('#comment_text').focus();
}
function commentConfirm() {
  $('#comment_modal .alert-container').empty();
  $('#comment_modal_message').text(base.detail.messages['COMMENT_CONFIRM']);
  enabled($('#name_text,#comment_text'), false);
  $('#comment_confirm').hide();
  $('#comment_back,#comment_save').show();
}
var commentSaveDone;
function commentSave() {
  requestByForm($('#comment_form'),
    function(data, textStatus, jqXHR) {
      if (data.status == 1) {
        $('#comment_modal').modal('hide');
        commentSaveDone(data);
      } else {
        commentEntry();
        showAlert('error', data.message, $('#comment_modal .alert-container').empty());
      }
    }
  );
}

// Comment Edit
function commentUpdate($comment) {
  requestByForm($('#comment_edit_form'),
    function(data, textStatus, jqXHR) {
      if (data.status == 1) {
        showAlertModal('info', data.message);
        $comment.data('status', data.data.status);
        $comment.find('.comment-status').text(data.data.status_name);
      } else {
        showAlertModal('error', data.message);
      }
      $('#comment_edit_modal').modal('hide');
    }
  );
}

function enabled($targets, flag) {
  $targets.toggleClass('form-control', flag)
    .toggleClass('form-control-plaintext', !flag)
    .prop('readonly', !flag);
}