torinaブログ

DjangoとBootstrap4で作成したブログ
Python, Django, Kivy, Bootstrap, Apache等のメモです
ソースコード

Djangoで、画像アプロダ(CRUD処理を、汎用ビューで)

Python Django Bootstrap4 Django Bitbucketにソースあり
2016年9月22日12:45
まずトップページ


ページ移動などもできます。


更新するボタンや、上部ナビのCreateを押すとこのように


削除は、このような画面


Django1.10
Python3.5
Bootstrap v4.0.0-alpha.4
プロジェクト名は「django_easy_uploader」
アプリケーション名は「easy_uploader」
です。

django_easy_uploader/django_easy_uploader/settings.py
MEDIA_URLと、MEDIA_ROOTを指定しときましょう。
"""
Django settings for upld project.

Generated by 'django-admin startproject' using Django 1.10.1.

For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
"""

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '2=9i@^8x%r#ke^j=%k)rfhk65728w$3qxbcq_8@5a_0r7(na1t'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'easy_uploader',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'django_easy_uploader.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'django_easy_uploader.wsgi.application'


# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}


# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/

STATIC_URL = '/static/'

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = "/media/"


django_easy_uploader/django_easy_uploader/urls.py
includeの設定と、開発環境での/media/というURLの紐づけ
本場環境では、ApacheならばAliasで設定しましょう。
from django.conf import settings
from django.conf.urls import url, include
from django.conf.urls.static import static
from django.contrib import admin

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^', include('easy_uploader.urls', namespace='easy_uploader')),
]
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL,
                          document_root=settings.MEDIA_ROOT)




django_easy_uploader/easy_uploader/urls.py
名前のとおりです。
from django.conf.urls import url
from . import views


urlpatterns = [
    url(r'^$', views.ImgIndexView.as_view(), name='index'),
    url(r'^create/$', views.ImgCreateView.as_view(), name='create'),
    url(r'^update/(?P<pk>[0-9]+)/$',
        views.ImgUpdateView.as_view(), name='update'),
    url(r'^delete/(?P<pk>[0-9]+)/$',
        views.ImgDeleteView.as_view(), name='delete'),
]



django_easy_uploader/easy_uploader/models.py
アップロードする画像のモデルです。
今回つかっていませんが、get_filenameメソッドはファイル名を返します。便利なので、この手のModelにはよく定義しています。
from django.db import models


class Img(models.Model):
    title = models.CharField("タイトル", max_length=255, blank=True)
    file = models.ImageField("ファイル", upload_to='images/', )
    created_at = models.DateTimeField("作成日", auto_now_add=True)
    updated_at = models.DateTimeField("更新日", auto_now=True)

    def __str__(self):
        return self.title

    def get_filename(self):
        return os.path.basename(self.file.name)


django_easy_uploader/easy_uploader/forms.py
widgets内でBootstrap用の設定をしています。classの指定ですね。
今回はMeta内でのwidgetsで行いました。
from django import forms
from .models import Img


class ImgForm(forms.ModelForm):

    class Meta:
        model = Img
        fields = '__all__'
        widgets = {
            'title': forms.TextInput(attrs={
                'class': "form-control",
            }),
            'file': forms.ClearableFileInput(attrs={
                'class': "form-control-file",
            }),
        }




django_easy_uploader/easy_uploader/views.py
コード量が減ってるってはっきりわかりますね。
今回はあえて、template_nameを全て指定していません。デフォルトの名前を使います。
from django.core.urlresolvers import reverse_lazy
from django.views import generic
from .models import Img
from .forms import ImgForm


class ImgIndexView(generic.ListView):
    model = Img
    paginate_by = 1


class ImgCreateView(generic.CreateView):
    model = Img
    form_class = ImgForm
    success_url = reverse_lazy("easy_uploader:index")


class ImgUpdateView(generic.UpdateView):
    model = Img
    form_class = ImgForm
    success_url = reverse_lazy("easy_uploader:index")


class ImgDeleteView(generic.DeleteView):
    model = Img
    success_url = reverse_lazy("easy_uploader:index")


まずListViewです。
modelは必須です。又は、querysetの指定か、get_queryset()をオーバーライドします。
paginate_byは、ページング処理を行う際に指定します。今回は1件づつの表示なので、1です。
今回の例ならば、template_nameは「easy_uploader/img_list.html」になります。
template側では、object_list、又はimg_listという名前でデータの一覧が渡されます。
class ImgIndexView(generic.ListView):
    model = Img
    paginate_by = 1


CreateViewです。
formは作成したのを使うので、form_classに渡しています。success_urlは、無事に作れた際のリダイレクト先です。
form_classを指定しない場合は、model(又はquerysetフィールドか、get_queryset())とfieldsを指定します。
(今回modelをわざわざ書いてるのは、template_nameを省略したため。他にも解決方法はあったけど、見栄えのためmodelを書く方法に)
今回の例ならば、template_nameは「easy_uploader/img_form.html」になります。
template側では、formという名前でフォームが渡されます。
class ImgCreateView(generic.CreateView):
    model = Img
    form_class = ImgForm
    success_url = reverse_lazy("easy_uploader:index")


UpdateViewです。
更新なので、modelは必須です。又は、querysetの指定か、get_queryset()をオーバーライドします。
form_classとsuccess_urlは上のCreateViewと同じです。
form_classを指定しない場合は、fieldsを指定します。
今回の例ならば、template_nameは「easy_uploader/img_form.html」になります。CreaeViewと同じ。
template側では、formという名前でフォームが渡され、object、又はimgという名前でフォームに紐づいたモデルも渡されます。
class ImgUpdateView(generic.UpdateView):
    model = Img
    form_class = ImgForm
    success_url = reverse_lazy("easy_uploader:index")


DeleteViewです。
modelは必須です。又は、querysetの指定か、get_queryset()をオーバーライドします。
今回の例ならば、template_nameは「easy_uploader/img_confirm_delete.html」になります。
template側では、object、又はimgという名前で削除するモデルが渡されます。
class ImgDeleteView(generic.DeleteView):
    model = Img
    success_url = reverse_lazy("easy_uploader:index")



django_easy_uploader/easy_uploader/templates/easy_uploader/base.html
Bootstrap4を使っています。
{% load static %}
<!DOCTYPE html>
<html lang="ja">
  <head>
    <!-- Required meta tags always come first -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta http-equiv="x-ua-compatible" content="ie=edge">
 
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.4/css/bootstrap.min.css" integrity="2hfp1SzUoho7/TsGGGDaFdsuuDL0LX2hnUp6VkX3CUQ2K4K+xjboZdsXyp4oUHZj" crossorigin="anonymous">
    
    <!-- Custom CSS -->
    <link rel="stylesheet" href="{% static 'easy_uploader/css/main.css' %}">
  </head>
  <body>
    <nav class="navbar navbar-fixed-top navbar-dark bg-info">
      <button class="navbar-toggler hidden-sm-up" type="button" data-toggle="collapse" data-target="#exCollapsingNavbar2">
        &#9776;
      </button>
      <div class="collapse navbar-toggleable-xs" id="exCollapsingNavbar2">
      <div class="container">
        <ul class="nav navbar-nav">
          <li class="nav-item">
            <a class="nav-link {% block nav_home %}{% endblock %}" href="{% url 'easy_uploader:index' %}">Home</a>
          </li>
          <li class="nav-item">
            <a class="nav-link {% block nav_create %}{% endblock %}" href="{% url 'easy_uploader:create' %}">Create</a>
          </li>
        </ul>
        </div>
      </div>
    </nav>

      {% block content %} {% endblock %}

    <!-- jQuery first, then Tether, then Bootstrap JS. -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.0.0/jquery.min.js" integrity="sha384-THPy051/pYDQGanwU6poAc/hOdQxjnOEXzbT+OuUAFqNqFjL+4IGLBgCJC3ZOShY" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.2.0/js/tether.min.js" integrity="sha384-Plbmg8JY28KFelvJVai01l8WyZzrYWG825m+cZ0eDDS1f7d/js6ikvy1+X+guPIB" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.4/js/bootstrap.min.js" integrity="VjEeINv9OSwtWFLAtmc4JCtEJXXBub00gtSnszmspDLCtC0I4z4nqz7rEFbIZLLU" crossorigin="anonymous"></script>
 
  </body>
</html>


これはナビバーです。常に上にくっついているタイプのやつです。
また、画面が小さいと見た目も変わるようになります。
{% block nav_home %}は、他のテンプレートで上書し、classにactive等を追加するためのものですね。
    <nav class="navbar navbar-fixed-top navbar-dark bg-info">
      <button class="navbar-toggler hidden-sm-up" type="button" data-toggle="collapse" data-target="#exCollapsingNavbar2">
        &#9776;
      </button>
      <div class="collapse navbar-toggleable-xs" id="exCollapsingNavbar2">
      <div class="container">
        <ul class="nav navbar-nav">
          <li class="nav-item">
            <a class="nav-link {% block nav_home %}{% endblock %}" href="{% url 'easy_uploader:index' %}">Home</a>
          </li>
          <li class="nav-item">
            <a class="nav-link {% block nav_create %}{% endblock %}" href="{% url 'easy_uploader:create' %}">Create</a>
          </li>
        </ul>
        </div>
      </div>
    </nav>





django_easy_uploader/easy_uploader/static/easy_uploader/css/main.css
常に上にくっついてる、fixed-navbarを使うときは、padding-topを指定します。Bootstrap3でもそうでしたね。
men-heightは、スクロールしてナビバーが機能してるか確認するのにつけただけです。
body {
  min-height: 75rem;
  padding-top: 6rem;
}

section.jumbotron {
  background-color: #fff;
}



django_easy_uploader/easy_uploader/templates/easy_uploader/index.html
飾りボタンは、なんとなく見た目のためにつけただけです。消してください。
{% extends "easy_uploader/base.html" %}
{% block nav_home %}active{% endblock %}
{% block content %}
  <section class="jumbotron text-xs-center">
    <div class="container">
      <h1 class="jumbotron-heading">アップローダーサンプル</h1>
      <p class="lead text-muted">アップローダーのサンプルです。Bootstrap4を使っています。</p>
      <p>
        <a href="#" class="btn btn-primary">飾りボタン</a>
        <a href="#" class="btn btn-secondary">飾りボタン</a>
      </p>
    </div>
  </section>

  <div class="container">
    {% for img in img_list %}
      <div class="row">
        <div class="col-xs-12 col-sm-6">
          <img src="{{ img.file.url }}" alt="{{ img.title }}">
        </div>
        <div class="col-xs-12 col-sm-6">
          <p>{{ img.title }}</p>
          <a href="{% url 'easy_uploader:update' img.pk %}" class="btn btn-outline-primary">
            更新する
          </a>
          <a href="{% url 'easy_uploader:delete' img.pk %}" class="btn btn-outline-primary">
            削除する
          </a>
        </div>
      </div>
    {% endfor %}

    <div class="text-xs-center">
      {% include "easy_uploader/page.html" %}
    </div>

  </div>
{% endblock %}



django_easy_uploader/easy_uploader/templates/easy_uploader/page.html
上で読み込まれてた、page.htmlです。
Bootstrap4仕様です。
少なくとも、他のGETパラメータがないならば、これは汎用的に使えます。
<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 }}" aria-label="Previous">
          <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>



django_easy_uploader/easy_uploader/templates/easy_uploader/img-form.html
作成、更新用テンプレートです。ファイルタグを扱うので、enctype='multipart/form-data'を忘れないように。
{% for field in form %}を使い、各フィールドのラベル名、ラベルのID、フィールドを出力しています。
多少Modelに変更があっても、治さなくて済むようにです。
{% extends "easy_uploader/base.html" %}
{% block nav_create %}active{% endblock %}
{% block content %}
  <div class="container">
    <form action="" method="POST" enctype='multipart/form-data'>
      {% for field in form %}
        <div class="form-group row">
          <label for="{{ field.id_for_label }}" class="col-sm-2 col-form-label">
            {{ field.label }}
          </label>
          <div class="col-sm-10">
            {{ field }}
          </div>
        </div>
      {% endfor %}
      {% csrf_token %}
      <div class="text-xs-center">
        <button type="submit" class="btn btn-outline-primary">送信する</button>
        <a href="{% url 'easy_uploader:index' %}" class="btn btn-outline-primary">やめる</a>
      </div>
    </form>
  </div>
{% endblock %}



django_easy_uploader/easy_uploader/templates/easy_uploader/img-confirm_delete.html
削除用のテンプレート。
{% extends "easy_uploader/base.html" %}
{% block content %}

  <div class="container">
    <form action="" method="POST" enctype='multipart/form-data'>
	  <div class="text-xs-center">
	    <img src="{{ img.file.url }}" alt="{{ img.title }}">
	    <p>{{ img.title }}</p>
	    <button type="submit" class="btn btn-outline-primary">削除する</button>
	    <a href="{% url 'easy_uploader:index' %}" class="btn btn-outline-primary">やめる</a>
	  </div>
	  {% csrf_token %}
    </form>
  </div>
{% endblock %}


今後も使いそうなので、Bitbucketにソースを置きました。
こちらのソースはたまに改良していく予定です。
https://bitbucket.org/toritoritorina/django-easy-uploader