Todo List Application in One File

This tutorial is intended to provide you with a feel of how a Pyramid web application is created. The tutorial is very short, and focuses on the creation of a minimal todo list application using common idioms. For brevity, the tutorial uses a "single-file" application development approach instead of the more complex (but more common) "scaffolds" described in the main Pyramid documentation .

在本教程的最后,您将得到一个最小的应用程序,它:

  • 提供要列出、插入和关闭任务的视图

  • 使用路由模式匹配URL以查看代码函数

  • 使用Mako模板呈现视图

  • 将数据存储在sqlite数据库中

以下是最终应用程序的屏幕截图:

../_images/single_file_tasks.png

Step 1 - Organizing the project

备注

要获得 Pyramid 设置的帮助,请尝试指南 Installing Pyramid .

要使用Mako模板,您需要安装 pyramid_mako 附加组件如下所示 Major Backwards Incompatibilities under What's New In Pyramid 1.5 .

简言之,您需要同时拥有 pyramidpyramid_mako 已安装程序包。使用 easy_install pyramid pyramid_makopip install pyramidpip install pyramid_mako 安装这些软件包。

在开始之前,我们将创建应用程序布局所需的目录层次结构。在文件系统上创建以下目录布局:

/tasks
    /static
    /templates

请注意 tasks directory will not be used as a Python package; it will just serve as a container in which we can put our project.

Step 2 - Application setup

要开始我们的应用程序,首先要添加一个名为 tasks.pytasks 目录。我们将在新创建的文件中添加一些基本导入。

1 import os
2 import logging
3
4 from pyramid.config import Configurator
5 from pyramid.session import UnencryptedCookieSessionFactoryConfig
6
7 from wsgiref.simple_server import make_server

然后我们将设置日志记录和当前工作目录路径。

 9 logging.basicConfig()
10 log = logging.getLogger(__file__)
11
12 here = os.path.dirname(os.path.abspath(__file__))

最后,在一个只在直接执行文件(即不导入)时运行的块中,我们将配置 Pyramid 应用程序,建立基本会话,获取wsgi应用程序,并为其提供服务。

14 if __name__ == '__main__':
15     # configuration settings
16     settings = {}
17     settings['reload_all'] = True
18     settings['debug_all'] = True
19     # session factory
20     session_factory = UnencryptedCookieSessionFactoryConfig('itsaseekreet')
21     # configuration setup
22     config = Configurator(settings=settings, session_factory=session_factory)
23     # serve app
24     app = config.make_wsgi_app()
25     server = make_server('0.0.0.0', 8080, app)
26     server.serve_forever()

我们现在有了运行应用程序所需的基本项目布局,但是我们仍然需要添加数据库支持、路由、视图和模板。

Step 3 - Database and schema

To make things straightforward, we'll use the widely installed SQLite database for our project. The schema for our tasks is simple: an id 要唯一标识任务,请 name 不超过100个字符,以及 closed 用于指示任务是否已关闭的布尔值。

添加到 tasks 目录名为的文件 schema.sql with the following content:

1create table if not exists tasks (
2    id integer primary key autoincrement,
3    name char(100) not null,
4    closed bool not null
5);
6
7insert or ignore into tasks (id, name, closed) values (0, 'Start learning Pyramid', 0);
8insert or ignore into tasks (id, name, closed) values (1, 'Do quick tutorial', 0);
9insert or ignore into tasks (id, name, closed) values (2, 'Have some beer!', 0);

tasks.py 按强调的行显示的文件。

1import os
2import logging
3import sqlite3
4
5from pyramid.config import Configurator
6from pyramid.events import ApplicationCreated
7from pyramid.events import NewRequest
8from pyramid.events import subscriber

To make the process of creating the database slightly easier, rather than requiring a user to execute the data import manually with SQLite, we'll create a function that subscribes to a Pyramid system event for this purpose. By subscribing a function to the ApplicationCreated 事件,每次启动应用程序时,都会执行订阅的函数。因此,我们的数据库将在应用程序启动时根据需要创建或更新。

21@subscriber(ApplicationCreated)
22def application_created_subscriber(event):
23    log.warning('Initializing database...')
24    with open(os.path.join(here, 'schema.sql')) as f:
25        stmt = f.read()
26        settings = event.app.registry.settings
27        db = sqlite3.connect(settings['db'])
28        db.executescript(stmt)
29        db.commit()
30
31
32if __name__ == '__main__':

我们还需要使我们的数据库连接对应用程序可用。我们将提供连接对象作为应用程序请求的属性。通过订阅 Pyramid NewRequest event, we'll initialize a connection to the database when a Pyramid request begins. It will be available as request.db . 我们将安排在请求生命周期结束时使用 request.add_finished_callback 方法。

21@subscriber(NewRequest)
22def new_request_subscriber(event):
23    request = event.request
24    settings = request.registry.settings
25    request.db = sqlite3.connect(settings['db'])
26    request.add_finished_callback(close_db_connection)
27
28
29def close_db_connection(request):
30    request.db.close()
31
32
33@subscriber(ApplicationCreated)

To make those changes active, we'll have to specify the database location in the configuration settings and make sure our @subscriber decorator is scanned by the application at runtime using config.scan() .

44if __name__ == '__main__':
45    # configuration settings
46    settings = {}
47    settings['reload_all'] = True
48    settings['debug_all'] = True
49    settings['db'] = os.path.join(here, 'tasks.db')
54    # scan for @view_config and @subscriber decorators
55    config.scan()
56    # serve app

We now have the basic mechanism in place to create and talk to the database in the application through request.db .

步骤4-查看功能和路线

现在是时候以视图函数的形式向世界公开一些功能了。We'll start by adding a few imports to our tasks.py 文件。In particular, we're going to import the view_config decorator,它允许应用程序发现和注册视图:

 8from pyramid.events import subscriber
 9from pyramid.httpexceptions import HTTPFound
10from pyramid.session import UnencryptedCookieSessionFactoryConfig
11from pyramid.view import view_config

请注意,我们的进口商品在 pyramid Python-dotted name which makes them easier to find as their number increases.

现在,我们将向应用程序添加一些视图函数,以便列出、添加和关闭TODOS。

列表视图

This view is intended to show all open entries, according to our tasks 数据库中的表。它使用 list.mako 模板在下可用 templates 通过将目录定义为 rendererview_config 装饰者。The results returned by the query are tuples, but we convert them into a dictionary for easier accessibility within the template. The view function will pass a dictionary defining taskslist.mako 模板。

19here = os.path.dirname(os.path.abspath(__file__))
20
21
22# views
23@view_config(route_name='list', renderer='list.mako')
24def list_view(request):
25    rs = request.db.execute('select id, name from tasks where closed = 0')
26    tasks = [dict(id=row[0], name=row[1]) for row in rs.fetchall()]
27    return {'tasks': tasks}

当使用 view_config 装饰师,指定 route_name 匹配已定义的路由,以及 renderer 如果函数用于呈现模板。然后,view函数应该返回一个字典,定义渲染器要使用的变量。我们的 list_view 上面两个都有。

新观点

此视图允许用户向应用程序添加新任务。如果A name 在表单中提供,任务将添加到数据库中。然后,一条信息消息被闪烁,以便在下一个请求时显示,并且用户的浏览器被重定向回 list_view . 如果未提供任何信息,则会闪烁警告消息,并且 new_view 再次显示。在 list_view .

30@view_config(route_name='new', renderer='new.mako')
31def new_view(request):
32    if request.method == 'POST':
33        if request.POST.get('name'):
34            request.db.execute(
35                'insert into tasks (name, closed) values (?, ?)',
36                [request.POST['name'], 0])
37            request.db.commit()
38            request.session.flash('New task was successfully added!')
39            return HTTPFound(location=request.route_url('list'))
40        else:
41            request.session.flash('Please enter a name for the task!')
42    return {}

警告

在通过以下方式生成SQL语句时,请确保使用问号: db.execute 否则,当使用字符串格式时,应用程序将容易受到SQL注入的攻击。

近景

此视图允许用户将任务标记为已关闭,闪烁成功消息,并重定向回 list_view 页。在 new_view .

45@view_config(route_name='close')
46def close_view(request):
47    task_id = int(request.matchdict['id'])
48    request.db.execute('update tasks set closed = ? where id = ?',
49                       (1, task_id))
50    request.db.commit()
51    request.session.flash('Task was successfully closed!')
52    return HTTPFound(location=request.route_url('list'))

未找到的视图

此视图允许我们自定义默认 NotFound 通过使用我们自己的模板, Pyramid 提供的视图。这个 NotFound 当URL无法映射到 Pyramid 视图时,视图按 Pyramid 显示。我们将在后续步骤中添加模板。在 close_view .

55@view_config(context='pyramid.exceptions.NotFound', renderer='notfound.mako')
56def notfound_view(request):
57    request.response.status = '404 Not Found'
58    return {}

添加路由

如果我们希望视图函数与应用程序URL匹配,那么最后需要向应用程序配置中添加一些路由元素。在配置设置代码之后立即插入以下代码。

95    # routes setup
96    config.add_route('list', '/')
97    config.add_route('new', '/new')
98    config.add_route('close', '/close/{id}')

我们现在通过定义通过Routes系统公开的视图向应用程序添加了功能。

Step 5 - View templates

视图执行这项工作,但它们需要呈现Web浏览器理解的内容:HTML。我们已经看到视图配置接受一个具有模板名称的呈现器参数。我们将使用模板引擎之一,Mako,由 Pyramid 插件支持, pyramid_mako .

我们还将使用mako模板继承。模板继承使跨多个模板重用通用布局成为可能,从而简化布局维护和统一性。

Create the following templates in the templates 目录及其各自的内容:

layout.mako

This template contains the basic layout structure that will be shared with other templates. Inside the body tag, we've defined a block to display flash messages sent by the application, and another block to display the content of the page, inheriting this master layout by using the mako directive ${{next.body()}} .

 1# -*- coding: utf-8 -*- 
 2<!DOCTYPE html>  
 3<html>
 4<head>
 5	
 6  <meta charset="utf-8">
 7  <title>Pyramid Task's List Tutorial</title>
 8  <meta name="author" content="Pylons Project">
 9  <link rel="shortcut icon" href="/static/favicon.ico">
10  <link rel="stylesheet" href="/static/style.css">
11
12</head>
13
14<body>
15
16  % if request.session.peek_flash():
17  <div id="flash">
18    <% flash = request.session.pop_flash() %>
19	% for message in flash:
20	${message}<br>
21	% endfor
22  </div>
23  % endif
24
25  <div id="page">
26    
27    ${next.body()}
28
29  </div>
30  
31</body>
32</html>

list.mako

此模板由 list_view 查看功能。此模板扩展了主控形状 layout.mako 提供任务列表的模板。循环使用传递的 tasks 从发送的词典 list_view 函数使用mako语法。我们也使用 request.route_url function to generate a URL based on a route name and its arguments instead of statically defining the URL path.

 1# -*- coding: utf-8 -*- 
 2<%inherit file="layout.mako"/>
 3
 4<h1>Task's List</h1>
 5
 6<ul id="tasks">
 7% if tasks:
 8  % for task in tasks:
 9  <li>
10    <span class="name">${task['name']}</span>
11    <span class="actions">
12      [ <a href="${request.route_url('close', id=task['id'])}">close</a> ]
13    </span>
14  </li>
15  % endfor
16% else:
17  <li>There are no open tasks</li>
18% endif
19  <li class="last">
20    <a href="${request.route_url('new')}">Add a new task</a>
21  </li>
22</ul>

new.mako

此模板由 new_view 查看函数。模板扩展了主控形状 layout.mako template by providing a basic form to add new tasks.

1# -*- coding: utf-8 -*- 
2<%inherit file="layout.mako"/>
3
4<h1>Add a new task</h1>
5
6<form action="${request.route_url('new')}" method="post">
7  <input type="text" maxlength="100" name="name">
8  <input type="submit" name="add" value="ADD" class="button">
9</form>

notfound.mako

此模板扩展了主控形状 layout.mako 模板。我们使用它作为自定义模板 NotFound 查看。

1# -*- coding: utf-8 -*- 
2<%inherit file="layout.mako"/>
3
4<div id="notfound">
5  <h1>404 - PAGE NOT FOUND</h1>
6  The page you're looking for isn't here.
7</div>

配置模板位置

To make it possible for views to find the templates they need by renderer name, we now need to specify where the Mako templates can be found by modifying the application configuration settings in tasks.py . 按如下所示插入强调线。

90    settings['db'] = os.path.join(here, 'tasks.db')
91    settings['mako.directories'] = os.path.join(here, 'templates')
92    # session factory
93    session_factory = UnencryptedCookieSessionFactoryConfig('itsaseekreet')
94    # configuration setup
95    config = Configurator(settings=settings, session_factory=session_factory)
96    # add mako templating
97    config.include('pyramid_mako')
98    # routes setup

步骤6-设置模板样式

It's now time to add some styling to the application templates by adding a CSS file named style.cssstatic 包含以下内容的目录:

 1body {
 2  font-family: sans-serif;
 3  font-size: 14px;
 4  color: #3e4349;
 5}
 6
 7h1, h2, h3, h4, h5, h6 {
 8  font-family: Georgia;
 9  color: #373839;
10}
11
12a {
13  color: #1b61d6;
14  text-decoration: none;
15}
16
17input {
18  font-size: 14px;
19  width: 400px;
20  border: 1px solid #bbbbbb;
21  padding: 5px;
22}
23
24.button {
25  font-size: 14px;
26  font-weight: bold;
27  width: auto;
28  background: #eeeeee;
29  padding: 5px 20px 5px 20px;
30  border: 1px solid #bbbbbb;
31  border-left: none;
32  border-right: none;
33}
34
35#flash, #notfound {
36  font-size: 16px;
37  width: 500px;
38  text-align: center;
39  background-color: #e1ecfe;
40  border-top: 2px solid #7a9eec;
41  border-bottom: 2px solid #7a9eec;
42  padding: 10px 20px 10px 20px;
43}
44
45#notfound {
46  background-color: #fbe3e4;
47  border-top: 2px solid #fbc2c4;
48  border-bottom: 2px solid #fbc2c4;
49  padding: 0 20px 30px 20px;
50}
51
52#tasks {
53  width: 500px;
54}
55
56#tasks li {
57  padding: 5px 0 5px 0;
58  border-bottom: 1px solid #bbbbbb;
59}
60
61#tasks li.last {
62  border-bottom: none;
63}
64
65#tasks .name {
66  width: 400px;
67  text-align: left;
68  display: inline-block;
69}
70
71#tasks .actions {
72  width: 80px;
73  text-align: right;
74  display: inline-block;
75}

为了使应用程序能够提供这个静态文件,我们必须向应用程序配置添加一个“static view”指令。

101    config.add_route('close', '/close/{id}')
102    # static view setup
103    config.add_static_view('static', os.path.join(here, 'static'))
104    # scan for @view_config and @subscriber decorators

步骤7-运行应用程序

We have now completed all steps needed to run the application in its final version. Before running it, here's the complete main code for tasks.py 进行复习。

  1import os
  2import logging
  3import sqlite3
  4
  5from pyramid.config import Configurator
  6from pyramid.events import ApplicationCreated
  7from pyramid.events import NewRequest
  8from pyramid.events import subscriber
  9from pyramid.httpexceptions import HTTPFound
 10from pyramid.session import UnencryptedCookieSessionFactoryConfig
 11from pyramid.view import view_config
 12
 13from wsgiref.simple_server import make_server
 14
 15
 16logging.basicConfig()
 17log = logging.getLogger(__file__)
 18
 19here = os.path.dirname(os.path.abspath(__file__))
 20
 21
 22# views
 23@view_config(route_name='list', renderer='list.mako')
 24def list_view(request):
 25    rs = request.db.execute('select id, name from tasks where closed = 0')
 26    tasks = [dict(id=row[0], name=row[1]) for row in rs.fetchall()]
 27    return {'tasks': tasks}
 28
 29
 30@view_config(route_name='new', renderer='new.mako')
 31def new_view(request):
 32    if request.method == 'POST':
 33        if request.POST.get('name'):
 34            request.db.execute(
 35                'insert into tasks (name, closed) values (?, ?)',
 36                [request.POST['name'], 0])
 37            request.db.commit()
 38            request.session.flash('New task was successfully added!')
 39            return HTTPFound(location=request.route_url('list'))
 40        else:
 41            request.session.flash('Please enter a name for the task!')
 42    return {}
 43
 44
 45@view_config(route_name='close')
 46def close_view(request):
 47    task_id = int(request.matchdict['id'])
 48    request.db.execute('update tasks set closed = ? where id = ?',
 49                       (1, task_id))
 50    request.db.commit()
 51    request.session.flash('Task was successfully closed!')
 52    return HTTPFound(location=request.route_url('list'))
 53
 54
 55@view_config(context='pyramid.exceptions.NotFound', renderer='notfound.mako')
 56def notfound_view(request):
 57    request.response.status = '404 Not Found'
 58    return {}
 59
 60
 61# subscribers
 62@subscriber(NewRequest)
 63def new_request_subscriber(event):
 64    request = event.request
 65    settings = request.registry.settings
 66    request.db = sqlite3.connect(settings['db'])
 67    request.add_finished_callback(close_db_connection)
 68
 69
 70def close_db_connection(request):
 71    request.db.close()
 72
 73
 74@subscriber(ApplicationCreated)
 75def application_created_subscriber(event):
 76    log.warning('Initializing database...')
 77    with open(os.path.join(here, 'schema.sql')) as f:
 78        stmt = f.read()
 79        settings = event.app.registry.settings
 80        db = sqlite3.connect(settings['db'])
 81        db.executescript(stmt)
 82        db.commit()
 83
 84
 85if __name__ == '__main__':
 86    # configuration settings
 87    settings = {}
 88    settings['reload_all'] = True
 89    settings['debug_all'] = True
 90    settings['db'] = os.path.join(here, 'tasks.db')
 91    settings['mako.directories'] = os.path.join(here, 'templates')
 92    # session factory
 93    session_factory = UnencryptedCookieSessionFactoryConfig('itsaseekreet')
 94    # configuration setup
 95    config = Configurator(settings=settings, session_factory=session_factory)
 96    # add mako templating
 97    config.include('pyramid_mako')
 98    # routes setup
 99    config.add_route('list', '/')
100    config.add_route('new', '/new')
101    config.add_route('close', '/close/{id}')
102    # static view setup
103    config.add_static_view('static', os.path.join(here, 'static'))
104    # scan for @view_config and @subscriber decorators
105    config.scan()
106    # serve app
107    app = config.make_wsgi_app()
108    server = make_server('0.0.0.0', 8080, app)
109    server.serve_forever()

And now let's run tasks.py

$ python tasks.py
WARNING:tasks.py:Initializing database...

它将监听端口8080。打开网页浏览器至url http://localhost:8080/查看并与应用程序交互。

结论

这个 Pyramid 的介绍灵感来自于瓶子和瓶子的教程,同样的最低限度的方法在头脑中。非常感谢克里斯·麦克多诺、卡洛斯·德·拉瓜迪亚和凯西·邓肯对他们的支持和友谊。