2013年5月23日木曜日

Python Pyramid + Raspberry Pi PWM つづき

前回作成した Raspberry Pi に Python Pyramid のウェブアプリをのせて、ウェブアプリから PWM の設定を行うというアプリの2回目。
今回は、そのウェブアプリにログインページを追加する。

  1. __init__.py
    auth なんとか、というのがいくつか追加されている。
    login/logout の config.add_route も追加。
    #!/usr/bin/env python
    # coding: UTF-8
    from pyramid.config import Configurator
    from pyramid.session import UnencryptedCookieSessionFactoryConfig
    from pyramid.authentication import AuthTktAuthenticationPolicy
    from pyramid.authorization import ACLAuthorizationPolicy
    from sqlalchemy import engine_from_config
    from pypwm.security import groupfinder
    
    from .models import (
        DBSession,
        Base,
        )
    
    my_session_factory = UnencryptedCookieSessionFactoryConfig('something_so_secret_strings')
    
    def main(global_config, **settings):
        """ This function returns a Pyramid WSGI application.
        """
        engine = engine_from_config(settings, 'sqlalchemy.')
        DBSession.configure(bind=engine)
        Base.metadata.bind = engine
        authn_policy = AuthTktAuthenticationPolicy(
            'sosecret', callback=groupfinder, hashalg='sha512')
        authz_policy = ACLAuthorizationPolicy()
        config = Configurator(settings=settings, root_factory='pypwm.models.RootFactory',
                              session_factory=my_session_factory)
        config.set_authentication_policy(authn_policy)
        config.set_authorization_policy(authz_policy)
        config.add_static_view('static', 'static', cache_max_age=3600)
        config.add_route('home', '/')
        config.add_route('home_org', '/home_org')
        config.add_route('update_slider_pwm', '/update_slider_pwm')
        config.add_route('login', '/login')
        config.add_route('logout', '/logout')
        config.add_renderer(".html", "pyramid.mako_templating.renderer_factory")  
        config.scan()
        return config.make_wsgi_app()
    

  2. models.py
    User と RootFactory Class が追加されている。
    #!/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 MyModel(Base):
        __tablename__ = 'models'
        id = Column(Integer, primary_key=True)
        name = Column(Text, unique=True)
        value = Column(Integer)
    
        def __init__(self, name, value):
            self.name = name
            self.value = value
    
    
    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)
    
        def __init__(self, name, password):
            self.name = name
            self.password = password
    
    class RootFactory(object):
        __acl__ = [ (Allow, Everyone, 'view'),
                    (Allow, 'group:editors', 'edit') ]
        def __init__(self, request):
            pass
    

  3. security.py
    これは新規に追加。ほんとうは、ここでデーターベースを参照してそれなりの値を返す?
    今回は、ログインID が user 、パスワードが password にしている。
    #!/usr/bin/env python
    # coding: UTF-8
    USERS = {
             'user': 'password',
             }
    GROUPS = {
              'user':['group:editors']
              }
    
    def groupfinder(userid, request):
        if userid in USERS:
            return GROUPS.get(userid, [])
    

  4. views.py
    import の追加・修正、login/logout 処理の追加。
    そして、@view_config に permission='edit' を追加している。これだけで、この関数にセッション管理などの必要な処理が追加される。
    #!/usr/bin/env python
    # coding: UTF-8
    from pyramid.httpexceptions import HTTPFound
    from pyramid.response import Response
    from pyramid.view import (
        view_config,
        forbidden_view_config,
        )
    
    from sqlalchemy.exc import DBAPIError
    
    from .models import (
        DBSession,
        MyModel,
        User,
        )
    
    from pyramid.security import (
        remember,
        forget,
        authenticated_userid,
        )
    
    from .security import USERS
    
    import time, string, os
    os_name = os.uname()
    if os_name[0] == 'Darwin':
        fname = '/Users/pi/env_pwm/PyPWM/pypwm/pwm.prefs'
    elif os_name[0] == 'Linux':
        fname = '/home/pi/env_pwm/PyPWM/pypwm/pwm.prefs'
    
    @view_config(route_name='home_org', renderer='templates/mytemplate.pt')
    def my_view(request):
        try:
            one = DBSession.query(MyModel).filter(MyModel.name == 'one').first()
        except DBAPIError:
            return Response(conn_err_msg, content_type='text/plain', status_int=500)
        return {'one': one, 'project': 'PyPWM'}
    
    conn_err_msg = """\
    Pyramid is having a problem using your SQL database.  The problem
    might be caused by one of the following things:
    
    1.  You may need to run the "initialize_PyPWM_db" script
        to initialize your database tables.  Check your virtual 
        environment's "bin" directory for this script and try to run it.
    
    2.  Your database server may not be running.  Check that the
        database server referred to by the "sqlalchemy.url" setting in
        your "development.ini" file is running.
    
    After you fix the problem, please restart the Pyramid application to
    try it again.
    """
    
    
    #---- pwm
    @view_config(route_name='home', renderer='index.html', permission='edit')
    def home(request):
        pwm_dict = {}
        pwm_val = read_prefs()
        pwm_dict['pwm_val'] = pwm_val
        return dict(pwm_dict=pwm_dict)
    
    
    @view_config(route_name='update_slider_pwm', xhr=True, renderer='json', permission='edit')
    def update_slider_pwm(request):
        """
        This function is call from ajax_slider_pwm.js send_option_request when slider is updated.
        receive json pwm data by ajax
                "pwm_val": slider_val,
                "channel": channel,
                "command": command
        command is 'read' or 'write'
        """
        pwm_val = request.GET.get('pwm_val')
        pwm_val = to_int(pwm_val)
        channel = request.GET.get('channel')
        channel = to_int(channel)
        command = request.GET.get('command')
        if command == 'read':
            pwm_val = read_prefs()
        elif command == 'write':
            write_prefs(pwm_val)
        return {
            'pwm_val': pwm_val,
            'channel': channel,
            'command': command,
        }
    
    
    def read_prefs():
        kMinVal = 0
        kMaxVal = 1999
        pwm_val = 0
    
        fd = None
        try:
            fd = open(fname, 'r')
        except IOError:
            fd = None
        if fd is not None:
            line = fd.readline()
            pwm_val = string.strip(line)
            try:
                pwm_val = int(pwm_val)
            except ValueError:
                pwm_val = 0
            if pwm_val < kMinVal:
                pwm_val = kMinVal
            if pwm_val > kMaxVal:
                pwm_val = kMaxVal
            fd.close()
        return pwm_val
    
    
    def write_prefs(pwm_val):
        my_error_count = 0
        fin = False
        while (not fin):
            try:
                fd = open(fname, 'w')
            except IOError:
                fd = None
            if fd is not None:
                fd.write(str(pwm_val))
                fd.close()
                fin = True
            else:
                if my_error_count < 10:
                    my_error_count += 1
                    time.sleep(0.1)
                else:
                    fin = True
    
    def to_int(the_str):
        try:
            int_value = int(the_str)
        except:
            int_value = 0
        return int_value
    
    
    #---- login/logout
    @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:
                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)
    

  5. templates/login.html
    新規追加。
    ## for mako
    <!DOCTYPE html>
    <html>
    <head>
      <title>PyPWM Login</title>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
      <meta name="apple-mobile-web-app-capable" content="yes" />  
      <meta name="keywords" content="python web application" />
      <meta name="description" content="pyramid web application" />
      <link rel="shortcut icon" href="${request.static_url('pypwm:static/favicon.ico')}" />
      <link rel="stylesheet" href="${request.static_url('pypwm:static/jquery/login.css')}" />
      <script type="text/javascript"></script>
    </head>
    <body>
      <div id="wrap">
        <div id="top-small">
          <div class="top-small align-center">
            <div>
            </div>
          </div>
        </div>
        <div id="middle">
          <div class="middle align-right">
            <div id="left" class="app-welcome align-left">
              <span style="background-color: #FFFF00">${message}</span>
            </div>
            <div id="right" class="app-welcome align-right"></div>
          </div>
        </div>
        <div id="bottom">
          <div class="bottom">
            <form action="${url}" method="post" id="id_login_form">
              <input type="hidden" name="came_from" value="${came_from}" />
              <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="submit" name="form.submitted" value="Log In" />
            </form>
          </div>
        </div>
      </div>
      <div id="footer">
        <div class="footer">&copy; Copyright 2013, .</div>
      </div>
    </body>
    </html>
    

  6. login.css
    #id_login_form {
     width: 220px;
     height: 155px;
     position: absolute;
     left: 50%;
     top: 30%;
     margin-left: -110px;
     margin-top: -75px;
    }
    
    #id_login_form input[type="text"],#id_login_form input[type="password"] {
        width: 100%;
        height: 40px;
        margin-top: 7px;
        font-size: 14px;
        color: #444;
        outline: none;  
        background-color: #fff;  
        border-radius: 6px;
        border: 1px solid rgba(0, 0, 0, .49);
    }
      
    /* button */
    #id_login_form input[type="submit"] {
        width: 100%;
        height: 50px;
        margin-top: 7px;
        color: #fff;
        font-size: 18px;
        font-weight: bold;
        outline: none;  
        background-color: #5466da;
        border-radius: 6px;
        border: 1px solid rgba(0, 0, 0, .49);
    }
      
    #id_login_form input[type="submit"]:active {
        background-color: #7588e1;
    }
    

0 件のコメント:

コメントを投稿