2014年5月3日土曜日

Pyramid 1.5 + jinja2-alchemy-starter で sqlite の authentication

Pyramid 1.5 で、前回までに行った修正に続けて作業していきます。
今までのものは、これ。
Pyramid 1.5 + jinja2-alchemy-starter で pcreate
Pyramid 1.5 + jinja2-alchemy-starter で @view_config()
Pyramid 1.5 + jinja2-alchemy-starter で authentication

前回はログインするページをつくりました。
ログイン ID とパスワードはソフト埋め込みでしたが、今回はデータベースに格納した ID とパスワードを使うことにします。
データベースはひとまず、標準で入っている sqlite を使います。次回は MySQL にします。

今のところ、development.ini に次の記述があります。sqlite のファイル名を指定しています。
sqlalchemy.url = sqlite:///%(here)s/MyProject.sqlite

あとは、myproject/models.py と myproject/security.py が関係しています。

models.py では・・・
class MyModel(Base) は不要ですね。
class User(Base): これがあれば良いと思われます。テーブルの名称は users で、フィールドは primary key の id と unique な name、それに password です。group も必要?
class RootFactory(object): は、__init__.py の Configurator() で使われていて、非常に重要っぽいものなので、そのままにしておきます。

security.py は・・・
group が editors と viewers があって、editors には editor が、viewers には viewer という名前の user がいます。
def groupfinder(userid, request): では、userid が属す group を返しています。
この部分を、sqlite から持ってくるようにすれば良さそうです。
というわけで、models.py の users テーブルは id、name、password のほかに group フィールドを持たせることにします。

models.py はこうなりました。User に group を追加しました。
#!/usr/bin/env python
# coding: UTF-8
from pyramid.security import (
    Allow,
    Everyone,
    )
from sqlalchemy import (
    Column,
    Integer,
    Text,
    )
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import (
    scoped_session,
    sessionmaker,
    )
from zope.sqlalchemy import ZopeTransactionExtension

DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
Base = declarative_base()

class User(Base):
    """ The SQLAlchemy declarative model class for a User object. """
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(Text, unique=True)
    password = Column(Text)
    group = Column(Text)

    def __init__(self, name, password, group):
        self.name = name
        self.password = password
        self.group = group

class RootFactory(object):
    __acl__ = [ (Allow, Everyone, 'view'),
                (Allow, 'group:editors', 'edit') ]
    def __init__(self, request):
        pass

def initialize_sql(engine):
    DBSession.configure(bind=engine)


この辺りを参考にします。
http://wiki.liris.org/article/python_intro/python03
http://seesaawiki.jp/w/kurt0027/d/python%20sqlalchemy%A4%CD%A4%BF
http://bty.sakura.ne.jp/wp/archives/634

development.ini にあるデータベースのファイル名を返す関数を持つ my_util.py を追加します。ちょっといい加減な関数です。
#!/usr/bin/env python
# coding: UTF-8
import pyramid

class MyUtil():
    def get_db_name(self):
        db_name = pyramid.threadlocal.get_current_registry().settings
        db_name = db_name['sqlalchemy.url']
        db_name = db_name[10:]
        return db_name

security.py はこうなりました。
#!/usr/bin/env python
# coding: UTF-8
import sqlite3
import sqlalchemy
import myproject.my_util

class myproject_db():
    def __init__(self, fname):
        self.con = None
        self.fname = fname
        if fname is not None:
            self.con = sqlite3.connect(fname)   # , isolation_level=None
            no_db = True
            try:
                cur = self.con.cursor()
                sql = 'SELECT name, password, user_group FROM users WHERE user_group=?'
                cur.execute(sql, (u'editor',))
                no_db = False
            except Exception as ex:
                print(ex)
            self.con.close()
            
            if no_db:
                # Create table
                self.con = sqlite3.connect(fname)
                cur = self.con.cursor()
                sql = '''create table users (
id integer primary key autoincrement not null,
name text,
password text,
user_group text)'''
                cur.execute(sql)
                tp = ('editor', 'editor', 'editors',)
                cur.execute(''' INSERT INTO users (name, password, user_group) VALUES (?, ?, ?)''', tp)
                self.con.commit()
                cur.close()
    
    
    def connect(self):
        if self.fname is not None:
            self.con = sqlite3.connect(self.fname)
        else:
            self.con = None
        return self.con
    
    
    def get_group_and_password_of_user(self, user):
        group = None
        password = None
        self.connect()
        if self.con is not None:
            try:
                cur = self.con.cursor()
                tp = (user,)
                cur.execute('SELECT user_group, password FROM users WHERE name=?', tp)
                for row in cur:
                    group = row[0]
                    password = row[1]
                cur.close()
            except Exception as ex:
                print(ex)
                group = None
                password = None
            self.con.close()
        return (group, password)
    
    def put_group_and_password_of_user(self, user, group, password):
        sql = 'UPDATE users SET user_group=?, password=? WHERE name=?;'
        self.connect()
        if self.con is not None:
            try:
                cur = self.con.cursor()
                tp = (group, password, user,)
                cur.execute(sql, tp)
                self.con.commit()
                cur.close()
            except Exception as ex:
                print(ex)
            self.con.close()

def groupfinder(userid, request):
    my_util = myproject.my_util.MyUtil()
    db_name = my_util.get_db_name()
    db = myproject_db(db_name)
    (group, password) = db.get_group_and_password_of_user(userid)
    the_str = None
    # ['group:editors']
    if group is not None:
        the_str = 'group:' + group
        return [the_str]
    else:
        return []

    
if __name__ == '__main__':
    db_name = 'MyProject.sqlite'
    db = myproject_db(db_name)
    db.connect()
    if db.con is not None:
        try:
            cur = db.con.cursor()
            sql = 'SELECT name, password, user_group FROM users ORDER BY name;'
            cur.execute(sql)
            for xx in cur.fetchall():
                print(xx[0], xx[1], xx[2])
            cur.close()
        except Exception as ex:
            print(ex)
        db.con.close()

views.py もかなり修正しました。
#!/usr/bin/env python
# coding: UTF-8
from pyramid.i18n import TranslationStringFactory
from myproject.models import DBSession
from pyramid.httpexceptions import HTTPFound
from pyramid.response import Response
from pyramid.view import (
    view_config,
    forbidden_view_config,
    )
from pyramid.security import (
    remember,
    forget,
    authenticated_userid,
    )
import myproject.security
import myproject.my_util

_ = TranslationStringFactory('MyProject')

@view_config(route_name='home', renderer='home.html', permission='edit', request_method="GET")
def home(request):
    page_name = 'home'
    results = {
        'page_name': page_name,
        'next_uri': '/next',
        'next_name': 'next_page'
    }
    return results

@view_config(route_name='next_page', renderer='home.html', permission='edit', request_method="GET")
def next_page(request):
    page_name = 'next_page'
    results = {
        'page_name': page_name,
        'next_uri': '/',
        'next_name': 'home'
    }
    return results

@view_config(route_name='login', renderer='login.html')
@forbidden_view_config(renderer='login.html')
def login(request):
    login_url = request.route_url('login')
    referrer = request.url
    if referrer == login_url:
        referrer = '/' # never use the login form itself as came_from
    came_from = request.params.get('came_from', referrer)
    message = ''
    login = ''
    password = ''
    if 'form.submitted' in request.params:
        login = request.params['login']
        password = request.params['password']
        # if USERS.get(login) == password:
        my_util = myproject.my_util.MyUtil()
        db_name = my_util.get_db_name()
        my_db = myproject.security.myproject_db(db_name)
        (group, pwd) = my_db.get_group_and_password_of_user(login)
        if pwd == password:
            headers = remember(request, login)
            return HTTPFound(location = came_from,
                             headers = headers)
        message = 'Failed login'

    return dict(
        message = message,
        url = request.application_url + '/login',
        came_from = came_from,
        login = login,
        password = password,
        )

@view_config(route_name='logout')
def logout(request):
    headers = forget(request)
    return HTTPFound(location = request.route_url('home'), headers = headers)

最後にテンプレートの login.html。
ログインエラーなどのメッセージを表示するように修正しました。
<!DOCTYPE html>
<html>
<head>
  <title>Login</title>
  <meta charset="utf-8">
  <link rel="stylesheet" href="{{request.application_url}}/static/pylons.css" type="text/css" media="screen" charset="utf-8" />
</head>
<body>
  <form action="{{ url }}" method="post" id="id_login_form">
    <input type="text" name="login" value="{{ login }}"
        placeholder="login name" autofocus required/><br/>
    <input type="password" name="password" value="{{ password }}"
        placeholder="password" required /><br/>
    <input type="hidden" name="came_from" value="{{ came_from }}" />
    <input type="submit" name="form.submitted" value="Log In" />
  </form>
  {{ message }}
</body>
</html>

これで修正は終了です。
実行すると動きは変わりありませんが、MyProject.sqlite ファイルが作成されているのがわかります。
MyProject.sqlite が置かれているディレクトリで myproject/security.py を実行すると MyProject.sqlite の中を表示します。
$ ../bin/python myproject/security.py
(u'editor', u'editor', u'editors')
$ 

0 件のコメント:

コメントを投稿