Django de BBS
昨日友達からの要望でーBBSを作成してくれと…. ということでDjangoで!!
GenericViewsを今まであえて使わないで練習していたのでー今日はGenericViewsを使ってみた!! views.pyを一切書かないで終えたので,やっぱPythonの勉強にはならないねっ!!
とりあえず,書き込みできればいいのでーまた,メインは携帯からのアクセスなのでデザインなんてーシンプルこの上ない!!
まぁ?とりあえず書き込みできる形になったのでーapache(mod_wsgi)でアクセスっ!! ブラウザチェックOK!!
じゃー携帯からテスト…と思ったらーアクセスできない….アレ!?と思ったらー携帯はDigest認証になっているとーダメだったようで…(Basic認証はOK).
さてさて,めちゃくちゃシンプルなBBSはできたのですが…携帯の場合,認証・セッションとかどうするのだろう….あと端末固有IDで認証したいんだけどなぁ~.またPHPでいう「use_trans_sid=1」みたいなのってあるのかなぁ….
Django + 画像アップロード
Djangoで画像アップロードを試してみました!! 自分の環境が今まで,0.96だったのですがーnewformsがImageFieldと連動できていないのでー0.97(-pre-SVN-6085)に変えちゃいましたっ!!
# models.py
class Image(models.Model):
title = models.CharField(maxlength=30)
image = models.ImageField(upload_to='photos')
created_at = models.DateTimeField(editable=False, default=datetime.now)
updated_at = models.DateTimeField(editable=False)
is_active = models.BooleanField(editable=False, default=True)
def save(self):
self.updated_at = datetime.now()
super(Image, self).save()
ここで注目するのはimageカラムだけ!! upload_toオプションでMEDIA_ROOT配下にphotosディレクトリが作成され画像が保存されるように設定.
# upload.html
<form action="" method="post" enctype="multipart/form-data">
<label>Title : </label>
<p>{{ form.title }}</p>
{% if form.errors.title %}
{% for e in form.errors.title %}
<div class="error">{{ e }}</div>
{% endfor %}
{% endif %}
<label>Image : </label>
<p>{{ form.image }}</p>
{% if form.errors.image %}
{% for e in form.errors.image %}
<div class="error">{{ e }}</div>
{% endfor %}
{% endif %}
<input type="submit" value="Submit" />
</form>
Django-0.97preならばー「{{ form.image }}」とするだけでー 「<input id="id_image" type="file" name="image"/>」となる!!やっぱこうでないと!!
# views.py
def upload(request):
f = forms.form_for_model(Image)
if request.method == 'POST':
form = f(request_post, request.FILES)
if form.is_valid():
form.save()
return HttpResponseRedirect('/imagedb/')
else:
form = f(None)
t = loader.get_template('imagedb/upload.html')
c = RequestContext(request, {'form':form})
return HttpResponse(t.render(c))
- ここで,自分が失敗してしまったのがー
- form = f(request_post, request.FILES)
- を最初,
- request_post = request.POST.copy() request_post.update(request.FILES) form = f(request_post)
としていた….これだとimageが常にNoneとなってしまい.毎度毎度バリデートエラー(このフィールドは必須です)となってしまっていた!!
省略して書いている部分もありますが,てな感じでアップロードすることができました.
個人的には,アップロードする画像名はランダムな文字列に変更したい.また,アップロード先(upload_to)をユーザーIDなどで動的に変更したい. 日付にすることはできるようですが…. あとはーやっぱリサイズしないとねっ!! こちらのサイト(http://d.hatena.ne.jp/piro_suke/20070704/1183559610)で試されているようなのでーPhotoFieldにして今度やってみようかと思ってます.
最近,Djangoいじれていてー面白いね!!
さらに しつこく 画像アップロード
また,画像アップロードです. 今回は,都合によりPhotoFieldをカスタマイズしてみました.
というのも,前回アップロード作成のあと削除メソッドも….と思ったら説明は難しいのですがーなんともしっくり来ない! しかも,newformsからは「save_file()」なんて使われてないし….じゃー勉強ついでに色々いじってみることにしました.
仕様は, 1度に複数のリサイズファイルを作成する. ファイル名をランダムにする. 削除時はちゃんとリサイズされた複数のファイルを削除する.
なんつーのを目標にトライ!!
import Image as PILImage
import cStringIO, random, string, re, os
from django.db.models.fields import ImageField
from django.conf import settings
from django.utils.functional import curry
FIT = 0
CROP = 1
class PhotoField(ImageField):
EMPTY_VALUES = (None, (), '',)
def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, thumbs=None, mode=FIT, quality=None, **kwargs):
super(PhotoField, self).__init__(verbose_name, name, width_field, height_field, **kwargs)
self.thumbs, self.mode, self.quality = thumbs, mode, quality
def get_internal_type(self):
return 'ImageField'
# 動的に各リサイズされたイメージURL取得メソッド作成しようと思いつつどぼん….
#def contribute_to_class(self, cls, name):
# super(PhotoField, self).contribute_to_class(cls, name)
# for thumb in self.thumbs:
# setattr(cls, 'get_%s%s_url' % (self.name, thumb[2]), curry(cls._get_FIELD_SUFFIX_url, field=self, suffix=thumb[2]))
# #setattr(cls, 'get_%s%s_url' % (self.name, thumb[2]), curry(self._get_FIELD_SUFFIX_url, suffix=thumb[2]))
#def _get_FIELD_SUFFIX_url(self, suffix):
# return file_url
def save_form_data(self, instance, data):
if data:
extention = re.search(r"(?P<ext>\.[a-zA-Z]+)$", data.filename).group('ext')
data.filename = ''.join([random.choice(string.letters+string.digits) for x in range(15)])
for width, height, suffix in self.thumbs:
resized_filename = '%s%s%s' % (data.filename, suffix, extention)
getattr(instance, 'save_%s_file' % self.name)(resized_filename, self.resize(data.content, width, height), False)
data.filename += extention
getattr(instance, 'save_%s_file' % self.name)(data.filename, data.content, False)
def delete_file(self, instance):
file_name = getattr(instance, 'get_%s_filename' % self.name)()
directory, filename = os.path.split(file_name)
f = re.search(r"(?P<file>[a-zA-Z0-9]+)(?P<ext>\.[a-zA-Z]+)$", filename)
for thumb in self.thumbs:
thumb_file_name = os.path.join(directory, '%s%s%s' % (f.group('file'), thumb[2], f.group('ext')))
if os.path.exists(thumb_file_name):
os.remove(thumb_file_name)
if os.path.exists(file_name):
os.remove(file_name)
def resize(self, data, width, height):
data_file = cStringIO.StringIO(data)
pil_obj = PILImage.open(data_file)
if pil_obj.mode != 'RGB':
pil_obj = pil_obj.convert('RGB')
if self.mode == FIT:
pil_obj.thumbnail((int(width), int(height)), PILImage.ANTIALIAS)
elif self.mode == CROP:
x, y = pil_obj.size
if x >= width and y >= height:
aspect = float(width) / float(height)
crop_width = min(int(aspect*y), x)
crop_height = min(int(float(x)/aspect), y)
crop_point_x = (x-crop_width)/2
crop_point_y = (y-crop_height)/2
pil_obj = pil_obj.crop((crop_point_x, crop_point_y, crop_point_x+crop_width, crop_point_y+crop_height))
pil_obj.thumbnail((width, height), PILImage.ANTIALIAS)
else:
crop_width = min(width, x)
crop_height = min(height, y)
crop_point_x = (x-crop_width)/2
crop_point_y = (y-crop_height)/2
pil_obj = pil_obj.crop((crop_point_x, crop_point_y, crop_point_x+crop_width, crop_point_y+crop_height))
out_file = cStringIO.StringIO()
if self.quality:
pil_obj.save(out_file, 'JPEG', quality=self.quality)
else:
pil_obj.save(out_file, 'JPEG')
out_file.reset()
return out_file.read()
てな感じ. リサイズ部分は,オリジナルのPhotoFieldをそのまま.そのためJPEGにしか変換されていない!これはとりあえず無視!!! だもんで拡張子の処理はー今は意味ない!
では早速使ってみよう.(startapp image)
■ models.py
from django.db import models
from verdjnlib.fields import PhotoField, FIT # カスタマイズ済み
THUMBS = ((75, 75, '_thumb'), (250, 250, '_middle'))
class Image(models.Model):
image = PhotoField(upload_to='photos', thumbs=THUMBS)
# 暫定対応
def get_image_thumb_url(self):
return self.get_image_suffix_url(THUMBS[0][2])
# 暫定対応
def get_image_middle_url(self):
return self.get_image_suffix_url(THUMBS[1][2])
# 暫定対応
def get_image_suffix_url(self, suffix):
import re
filename = self.get_image_url()
extention = re.search(r'(?P<ext>\.[a-zA-Z]+$)', filename).group('ext')
return re.sub(r'\.[a-zA-Z]+$', suffix + extention, filename)
このようにすれば,2つのサムネイルを作成してくれるはず….
■ views.py
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.template import RequestContext, loader
from django import newforms as forms
from ENDLESS.image.models import Image
def view(request):
object_list = Image.objects.all()
t = loader.get_template('image/view.html')
c = RequestContext(request, {'object_list':object_list})
return HttpResponse(t.render(c))
def upload(request):
f = forms.form_for_model(Image)
if request.method == 'POST':
form = f(request.POST, request.FILES)
if form.is_valid():
form.save()
return HttpResponseRedirect('/image/')
else:
form = f(None)
t = loader.get_template('image/upload.html')
c = RequestContext(request, {'form':form})
return HttpResponse(t.render(c))
def delete(request, id):
object = get_object_or_404(Image, id=id)
object.delete()
return HttpResponseRedirect('/image/')
■ view.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>ENDLESS IMAGE TEST</title>
</head>
<body>
<p><a href="/image/upload/">UPLOAD</a></p>
{% for object in object_list %}
<p><img src="/site_media{{ object.get_image_url }}" /></p>
<p><img src="/site_media{{ object.get_image_middle_url }}" /></p>
<p><img src="/site_media{{ object.get_image_thumb_url }}" /></p>
<a href="/image/delete/{{ object.id }}/">Delete</a>
{% endfor %}
</body>
</html>
アップロード,削除ちゃんと出来ているようです. とりあえず,今回の目標は達成!! しかしーやっぱリサイズURLは動的に生成させたいね. また,JPEGのみ(?)ってーのはまずいねぇ?PILってどうなっているのかわからないのでー今度はそっちの使い方もみてゆかねば…. 拡張子やファイル名の処理部分がスッキリしていないのでーあれもなんとかしてみたいね…Python勉強しないと….
んーーー.個人的には 今回のこの作業を通じてー色んなことを得た気がします. Djangoのコードを追いかけていたんですがーやっぱ読めない!!(何度もnobuさんに聞いちゃいましたね.)何やっているのか?急にわからなくなってー推測になってしまう. あれをスラスラと読めたらー面白いのだろうなぁー.
携帯固有IDでユーザ認証
携帯端末固有IDでユーザ認証をしてみようとテストした. ちなみに,端末IDのみで認証することが危険だから実用的じゃない!!なんて話は受け流す.
端末固有IDの取得方法は,「 ウノウラボ Unoh Labs: Pythonで携帯の機種判別をする (perezvon さん)」からライブラリを利用しました.
まず,Userモデルを拡張で Profileモデルを作成.こちらにはデバイスIDを登録できる.
# project/common/models.py
class Profile(models.Model):
user = models.OneToOneField(User)
device_id = models.CharField(max_length=100, blank=True)
端末固有IDで認証するための DeviceIdBackend を作成. ModelBackendを継承して作ってみたんだが…どうなんでしょ!?
# project/common/auth/backends.py
from django.contrib.auth.backends import ModelBackend
from project.common.models import Profile
class DeviceIdBackend(ModelBackend):
def authenticate(self, device_id=None):
try:
profile = Profile.objects.get(device_id=device_id)
return profile.user
except Profile.DoesNotExist:
return None
これを settings.py に情報を追加
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'project.common.auth.backends.DeviceIdBackend',
)
とりあえず,動くかどうか views.py にログイン処理を書いてみる. forms.py を書いたりしたらスッキリに見えたりするんでしょうがーんなもん無視!
# project/app/views.py
from uamobile import detect, exceptions
from uamobile.nonmobile import NonMobileUserAgent
from django.contrib.auth import login, authenticate
from django.template import loader, RequestContext as Context
from django.http import HttpResponse, HttpResponseRedirect, Http404
def sign_in(request):
# デバイス情報の取得
try:
device = detect(request.META)
except exceptions.NoMatchingError, e:
raise Http404
# 携帯以外のアクセス処理
if isinstance(device, NonMobileUserAgent):
raise Http404
if request.method == 'POST':
# 端末固有IDからユーザ情報取得
user = authenticate(device_id = device.serialnumber)
if user is None or not user.is_active:
raise Http404
# ログイン処理(TODO:)
login(request, user)
return HttpResponseRedirect('app_template.html')
else:
# ログイン画面表示
t = loader.get_template('login_template.html')
c = Context(request, {})
return HttpResponse(t.render(c))
こんな感じでとりあえずユーザ認証はできたようだ. ちなみに,コレ携帯から試していない(笑).User Agent Switcherで偽装して成功した感じ. また,ログイン処理はSession系の処理をしないといけないはず,Cookieが使えるかとか/Getにセッションを埋め込むとか ね.面倒ね.
セッションをURLに付加
セッションIDをURLの後ろに付加させるカスタムタグを試しに作ってみた.
Cookieが無効な携帯は自動でセッションIDを付加させて欲しいなぁということでurlタグならぬーmobile_urlタグを作成. 今回は,前提として「django.core.context_processors.request」を有効へ.
# settings.py
TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.request',
)
また,MIDDLEWARE_CLASSに携帯用のUserAgentMobileMiddlewareを追記.これは 「 ウノウラボ Unoh Labs: Pythonで携帯の機種判別をする (perezvon さん)」を利用しました!! いつも参考にさせていただいてます☆ありがとうございます☆
さて,ここからmobile_urlタグの作成.内容は大したことなくおおもとのurlとURLNodeをそのまま利用しただけぇ.
# project/common/templatetags/custom_tags.py
from django.conf import settings
from django.template import Library, Node
from django.template.defaulttags import URLNode
from django.utils.encoding import iri_to_uri
from uamobile.nonmobile import NonMobileUserAgent
register = Library()
class MobileURLNode(URLNode):
def render(self, context):
url = super(MobileURLNode, self).render(context)
device = context['request'].device
# 携帯以外のアクセス処理
if isinstance(device, NonMobileUserAgent):
return url
# cookieが利用可能時処理
if device.supports_cookie():
return url
# session_idの付加
return url + iri_to_uri(
'?%s=%s' % (
settings.SESSION_COOKIE_NAME,
context['request'].session.session_key
)
)
def mobile_url(parser, token):
bits = token.contents.split(' ', 2)
if len(bits) < 2:
raise TemplateSyntaxError, "'%s' takes at least one argument (path to a view)" % bits[0]
args = []
kwargs = {}
if len(bits) > 2:
for arg in bits[2].split(','):
if '=' in arg:
k, v = arg.split('=', 1)
k = k.strip()
kwargs[k] = parser.compile_filter(v)
else:
args.append(parser.compile_filter(arg))
return MobileURLNode(bits[1], args, kwargs)
url = register.tag(mobile_url)
てな感じ,mobile_urlなんてurlをそのままコピー.ほんでもってーMobileURLNodeもURLNodeを継承してーそのまま利用. 結局,URLNodeでrender()されたURLのケツにセッションを付加させただけ. このとき,contextからデバイス情報とセッション情報を抜き取りたかったので最初に2つの作業をやったわけです.
これで,あとはテンプレートにて {% url %} を使うのと同じように {% mobile_url %} として使えば付加してくれるんじゃないかなぁ(笑)
ん~セッションIDを付加させるのを単純に ?session_key=session_value なんてしているので問題が多そう(苦笑).ここらはもう1ヒネリとか必要になりそうです. ちょいと適当なままBLOGに載せちゃった感がありますね.まぁ僕らしい僕らしい!!突っ込みがあれば是非宜しくお願いいたします!!