naritoブログ

【お知らせ】
新ブログができました。今後そちらで更新し、このサイトは更新されません(ウェブサイト自体は残しておきます)
このブログの内容に関してコメントしたい場合は、新ブログのフリースペースに書き込んでください

このブログの内容を新ブログに移行中です。このブログで見つからない記事は、新ブログにありま

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

約850日前 2016年9月22日12:45
プログラミング関連
Bootstrap4 Django Python
今後も使いそうなので、Githubにソースを置きました。
こちらのソースはたまに改良していく予定です。
https://github.com/naritotakizawa/django-easy-uploader

まずトップページ


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


更新するボタンや、上部ナビの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 %}
名無し 約142日前 2018年8月31日2:42 返信する
こんにちは、新ブログ設立おめでとうございます。
ImageFieldについての質問です。
signalを用いて、作成されたモデル(イメージフィールドをもつ)の画像パスを後から変えたいのですが、signal内で
old_path = instance.image.path
new_path = old_path + '.JPEG'
instance.image.paht = new_path
instance.save()
こうすることでパスを変更することは可能でしょうか?
なりと 約142日前 2018年8月31日13:42
old_path = instance.image.name
new_path = old_path + '.JPEG'
instance.image = new_path
instance.save()
多分、こちらのようなコードになると思います。

ImageFieldやFileFieldには直接文字列(ファイルパス)を入れれますが、その際は
my_upload_to_path/file.jpg のようなパスを入れます。
MEDIA_URL(/media/)の部分はつけず、upload_to引数の文字列部分から入力します。
image.nameがそのようなパスになっているので、末尾に何か足すならば、image.nameの後ろにそのまま.jpgなどを足せば良いです。
名無し 約136日前 2018年9月6日14:32 返信する
ありがとうございました。
勉強になります。
名無し 約73日前 2018年11月8日21:51 返信する
なりとさんこんばんは。
いつも困ったときはこのブログで勉強させていただいています。

現在、人の情報を閲覧できるようなサイトを作成しており、
ImageFieldを使用してプロフィール写真を自由にアップロードできるようにしたいと考えています。

UdemyのSimpleTubeの講義をベースに行い、フォームの作成はうまくいっているのですが、media/profilepics/の中に写真が入らず、そのせいか表示もされません。
urls.py, settings.pyはおそらく大丈夫だと思います。

models.py
class Picture(models.Model):
"""写真"""
profilepic = models.ImageField('Profile Picture', upload_to='profilepics/', null=True, blank=True) # /media/thumbnails/ファイル名
created_at = models.DateTimeField('Created at', auto_now_add=True) # default=timezone.nowと違い、入力欄は表示されない=(編集不可)
updated_at = models.DateTimeField('Updated at', auto_now=True) # 更新するたびにその日時が格納される

def get_profilepicname(self):
return os.path.basename(self.profilepic.name)


views.py
class PictureCreateView(LoginRequiredMixin, CreateView):
model = Picture
form_class = PictureCreateForm
success_url = reverse_lazy('People:index')


forms.py
class PictureCreateForm(forms.ModelForm):

class Meta:
model = Picture
fields = ('profilepic',)
# 少しツウな書き方
widgets = {
'profilepic': forms.ClearableFileInput(attrs={ # <input type="file" class="form-control-file"
'class': "form-control-file",
}),
}

html
<div class="card-body">
{% if picture.profilepic %} <!--thumbnailがあれば表示し、なければnoimageを表示-->
<img class="img-thumbnail" src="{{ picture.profilepic.url }}">
{% else %} <!-- field.urlでそのファイルの置き場所を取得できる-->
<img class="img-thumbnail" src="{% static 'People/noimage.jpg' %}">
{% endif %}
</div>


また、docker上で開発を行っているのですが、何かその影響はあるでしょうか。
どうぞよろしくお願いいたします。
なりと 約72日前 2018年11月9日16:33
<form>にenctype="multipart/form-data" とつけていることと、dockerのOSがCentOS等のLinuxならば、media/profilepics/の権限も確認してください。(場合によってはchmod 777 media/profilepics/ 等としてだれでもファイルを書き込める状態にしておく必要があります)
名無し 約69日前 2018年11月12日11:21 返信する
なりとさん

お返事ありがとうございます。
ご回答いただいた通りに権限を修正したところ、アップロードしたイメージがmedia/profilepicに格納されるようになりました!

表示はあまりうまくいっていないのですが、まずは一通り試してみたいと思います。

ありがとうございました。