Web2Py-应用开发秘籍-全-
Web2Py 应用开发秘籍(全)
原文:
zh.annas-archive.org/md5/bd74c821f3f421a9d9964b9d98381270译者:飞龙
前言
we2py 是一个用于快速开发安全数据库驱动互联网应用程序的框架。它用 Python 编写,并可以用 Python 编程。它包括库、应用程序和可重用示例。
创建于 2007 年,web2py 在许多使用该框架的开发者的共同努力下,已经取得了巨大的成长、演变和改进。我们感谢他们所有人。
在过去两年中,web2py 发展迅速,以至于很难保持官方文档的时效性。尽管 web2py 始终向后兼容,但已创建了新的 API,提供了解决旧问题的新的方法。
在第三方网站上,如维基、博客和邮件列表中积累了大量的知识。特别是两个资源对 web2py 用户非常有价值:web2py Google Group 和www.web2pyslices.com/website。然而,那里提供的信息质量参差不齐,因为一些食谱已经过时。
这本书始于收集这些信息、清理它、更新它,并将用户试图解决的重要和常见问题与其他问题分开,这些问题并不代表普遍利益。
用户遇到的最常见问题包括在生产环境中部署 web2py、使用可重用组件构建复杂应用程序、生成 PDF 报告、自定义表单和身份验证、使用第三方库(特别是 jQuery 插件),以及与第三方 Web 服务接口。
收集这些信息并将它们组织成这本书花费了我们超过一年的时间。比列出的作者更多的人有意或无意地做出了贡献。实际上,这里使用的某些代码是基于已经在线发布的代码,尽管这些代码在这里已经被重构、测试和更好地记录。
本书中的代码在 BSD 许可下发布,除非另有说明,并且可在以下列出的专用 GitHub 存储库中在线获取。Python 代码应遵循称为 PEP 8 的风格约定。我们遵循了这一约定来发布在线代码,但在印刷版书籍中压缩了列表,以遵循 Packt 风格指南,并减少对长行的换行需求。
我们相信这本书将是对新 web2py 开发者和经验丰富的开发者都非常有价值的一份资源。我们的目标仍然是使网络变得更加开放和易于访问。我们通过提供 web2py 及其文档来做出贡献,使任何人都能以敏捷和高效的方式构建新的基础设施和服务。
本书涵盖的内容
第一章,部署 web2py。在本章中,我们讨论了如何配置各种 Web 服务器与 web2py 协同工作。这是生产环境的一个必要设置。我们考虑了最流行的服务器,如 Apache、Cherokee、Lighttpd、Nginx、CGI 和 IIS。相应的配方提供了不同适配器的使用示例,例如mod_wsgi、FastCGI、uWSGI和ISAPI。因此,它们可以轻松扩展到许多其他 Web 服务器。使用生产 Web 服务器可以保证静态文件更快地提供服务、更好的并发性和增强的日志记录功能。
第二章,构建您的第一个应用程序。我们指导读者通过创建几个非平凡应用程序的过程,包括Contacts应用程序、Reddit克隆和Facebook克隆。这些应用程序都提供了用户认证、通过关系连接的多个表以及 Ajax 功能。在第二部分的章节中,我们讨论了通用 web2py 应用程序的进一步定制,例如构建用于服务静态页面的插件、在页眉中添加标志、自定义菜单以及允许用户选择他们首选的语言。本章的主要重点是模块化和可重用性。
第三章,数据库抽象层。DAL 可以说是 web2py 最重要的组件之一。在本章中,我们讨论了从现有来源(csv 文件、mysql和postgresql数据库)导入模型和数据以及创建新模型的各种方法。我们处理了诸如标记数据和高效使用标签搜索数据库等常见情况。我们使用前序遍历方法实现树表示。我们展示了如何绕过 Google App Engine 平台的一些限制。
第四章,高级表单。web2py 的一个优势是它能够自动从数据表示生成表单。然而,不可避免的是,最苛刻的用户会感到需要自定义这些表单。在本章中,我们提供了典型定制的示例,例如添加按钮、添加上传进度条、添加工具提示以及为上传的图像添加缩略图。我们还展示了如何创建向导表单并在一个页面上添加多个表单。
第五章,添加 Ajax 效果。本章是上一章的扩展。在这里,我们进一步增强了表单和表格,使用各种 jQuery 插件通过 Ajax 使它们更具交互性。
第六章,使用第三方库。web2py 可以使用任何 Python 第三方库。在本章中,我们通过使用随 web2py 一起提供的库(feedparser、rss)以及不提供的库(matplotlib)来给出一些示例。我们还提供了一个允许在应用程序级别进行自定义日志记录的配方,以及一个可以检索和显示 Twitter 流的应用程序。
第七章,Web 服务。计算机可以通过协议进行通信,例如 JSON、JSONRPC、XMLRPC 和 SOAP。在本章中,我们提供了允许 web2py 基于这些协议创建服务并消费其他服务提供的服务的方法。特别是,我们提供了与 Flex、Paypal、Flickr 和 GIS 集成的示例。
第八章,认证和授权。web2py 内置了一个 Auth 模块,用于处理认证和授权。在本章中,我们展示了各种自定义方法,包括向注册和登录表单添加 CAPTCHA,为表示用户添加全球认可的头像(gravatars),以及与使用 OAuth 2.0(例如 Facebook)的服务集成。我们还展示了如何利用teacher/students模式。
第九章,路由配方。本章包括使用缩短、更简洁和旧 URL 公开 web2py 操作的配方。例如,向 URL 添加前缀或从 URL 中省略应用程序名称。我们还展示了如何使用 web2py 路由机制的高级用法来处理 URL 中的特殊字符,使用 URL 指定首选语言,以及映射特殊文件,如favicons.ico和robots.txt。
第十章,报告配方。在 web2py 中使用标准 Python 库(如reportlab或latex)创建报告有许多方法。然而,为了方便用户,web2py 附带了一个名为pyfpdf的库,由Mariano Reingart创建,可以将 HTML 直接转换为 PDF。本章介绍了使用 web2py 模板系统和pyfpdf库创建 PDF 报告、列表、标签、徽章和发票的配方。
第十一章,其他技巧和窍门。在这里,我们查看那些不适合其他任何章节,但典型 web2py 用户认为很重要的配方。一个例子是使用 Eclipse 与 web2py 结合,Eclipse 是一个非常流行的 Java IDE,可以与 Python 一起使用。其他示例包括如何开发适合移动设备的应用程序,以及如何开发使用 wxPython GUI 的独立应用程序。
您需要这本书的内容
所需的唯一软件是 web2py,这是所有配方共有的。web2py 提供源代码版本和适用于 Mac 和 Windows 的二进制版本。可以从web2py.com下载。
我们确实推荐从源代码运行 web2py,在这种情况下,用户还应该安装最新的 Python 2.7 解释器,可以从python.org下载。
当一个菜谱有额外要求时,会在菜谱中明确说明(例如,有些需要 Windows,有些需要 IIS,有些需要额外的 Python 模块或 jQuery 插件)。
本书面向对象
本书针对对 web2py 有基本知识的 Python 开发者,他们希望掌握这个框架。
惯例
在这本书中,您将找到许多不同风格的文本,以区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码词如下所示:“使用Lighttpd运行 web2py。”
代码块以如下格式设置:
from gluon.storage import Storage
settings = Storage()
settings.production = False
if settings.production:
settings.db_uri = 'sqlite://production.sqlite'
settings.migrate = False
else:
settings.db_uri = 'sqlite://development.sqlite'
settings.migrate = True
当我们希望引起您对代码块中特定部分的注意时,相关的行或项目将以粗体显示:
{{extend 'layout.html'}}
<h2>Companies</h2>
<table>
{{for company in companies:}}
<tr>
<td>
{{=A(company.name, _href=URL('contacts', args=company.id))}} </td>
<td>
{{=A('edit', _href=URL('company_edit', args=company.id))}} </td>
</tr>
{{pass}}
<tr>
<td>{{=A('add company', _href=URL('company_create'))}}</td>
</tr>
</table>
任何命令行输入或输出都写成如下格式:
python web2py.py -i 127.0.0.1 -p 8000 -a mypassword --nogui
新术语和重要词汇以粗体显示。屏幕上显示的词,例如在菜单或对话框中,在文本中显示如下:“一旦创建了网站,双击以下截图所示的URLRewrite:”。
注意
警告或重要注意事项以如下框的形式出现。
注意
小贴士和技巧看起来像这样。
读者反馈
我们欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正从中受益的标题非常重要。
要向我们发送一般反馈,只需发送一封电子邮件到 feedback@packtpub.com,并在邮件的主题中提及书名。
如果有您需要的书籍并希望我们出版,请通过www.packtpub.com上的建议标题表单或发送电子邮件至 suggest@packtpub.com 给我们留言。
如果您在某个主题上有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您已经是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从www.PacktPub.com上的您的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.PacktPub.com/support,并注册以将文件直接通过电子邮件发送给您。代码文件也上传到了以下仓库:github.com/mdipierro/web2py-recipes-source。
所有代码均在 BSD 许可证下发布(www.opensource.org/licenses/bsd-license.php),除非源文件中另有说明。
勘误
尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/support,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。您可以通过从www.packtpub.com/support选择您的标题来查看任何现有的勘误。
盗版
在互联网上,版权材料的盗版是所有媒体持续存在的问题。在 Packt,我们非常重视保护我们的版权和许可证。如果您在网上遇到我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过 copyright@packtpub.com 与我们联系,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们的作者和为您提供有价值内容的能力方面的帮助。
问题
您可以通过 questions@packtpub.com 与我们联系,如果您在本书的任何方面遇到问题,我们将尽力解决。
第一章。部署 web2py
在本章中,我们将涵盖以下食谱:
-
在 Windows 上安装 web2py(从源代码)
-
在 Ubuntu 上安装 web2py
-
在 Ubuntu 上设置生产部署
-
使用 Apache、
mod_proxy和mod_rewrite运行 web2py -
使用
Lighttpd运行 web2py -
使用 Cherokee 运行 web2py
-
使用 Nginx 和 uWSGI 运行 web2py
-
使用 CGI 在共享主机上运行 web2py
-
在共享主机上使用
mod_proxy运行 web2py -
从用户定义的文件夹运行 web2py
-
在 Ubuntu 上将 web2py 安装为服务
-
使用 IIS 作为代理运行 web2py
-
使用 ISAPI 运行 web2py
简介
在本章中,我们将讨论如何在不同的系统和不同的 Web 服务器上下载、设置和安装 web2py。
注意
它们都要求您从网站下载最新的 web2py 源代码:web2py.com,在 Unix 和 Linux 系统的 /home/www-data/web2py 下解压,在 Windows 系统的 c:/web2py 下解压。在各个地方,我们将假设主机的公共 IP 地址为 192.168.1.1;用您自己的 IP 地址或主机名替换它。我们还将假设 web2py 在端口 8000 上启动,但这个数字并没有什么特殊之处;如果需要,请更改它。
在 Windows 上安装 web2py(从源代码)
虽然为 Windows 环境提供了二进制发行版(打包可执行文件和标准库),但 web2py 是开源的,并且可以与正常的 Python 安装一起使用。
此方法允许使用 web2py 的最新版本,并自定义要使用的 python 模块。
准备工作
首先,您必须安装 Python。从以下网址下载您喜欢的 2.x 版本(不是 3.x):www.python.org/download/releases/。
虽然较新版本包括更多增强功能和错误修复,但旧版本具有更高的稳定性和第三方库覆盖范围。Python 2.5.4 在功能和经过验证的稳定性历史之间取得了良好的平衡,具有良好的二进制库支持。Python 2.7.2 是在撰写本文时此平台上的最新生产版本,因此我们将使用它进行示例。
下载您选择的 Windows Python 安装程序(即 python-2.7.2.msi),双击安装它。对于大多数情况,默认值都是可以的,所以按 下一步 直至安装完成。
您需要 Python Win32 扩展 来使用 web2py 任务栏或 Windows 服务。您可以从以下网址安装 pywin32:starship.python.net/~skippy/win32/Downloads.html。
在使用 web2py 之前,您可能还需要一些依赖项来连接到数据库。SQLite 和 MySQL 驱动程序包含在 web2py 中。如果您计划使用其他 RDBMS,您需要安装其驱动程序。
对于 PostgreSQL,您可以安装 psycopg2 二进制包(对于 Python 2.7,您应使用 psycopg2-2.3.1.win32-py2.7-pg9.0.1-release.exe):www.stickpeople.com/projects/python/win-psycopg/(请注意,web2py 需要 psycopg2 而不是 psycopg)。
对于 MS SQLServer 或 DB2,您需要 pyodbc:code.google.com/p/pyodbc/downloads/list。
如何操作...
在这一点上,您可以使用您首选的数据库使用 web2py。
-
从 web2py 官方网站下载源代码包:
www.web2py.com/examples/static/web2py_src.zip,并解压它。由于 web2py 不需要安装,您可以在任何文件夹中解压它。使用
c:\web2py很方便,以保持路径名短。 -
要启动它,双击
web2py.py。您也可以从控制台启动它:cd c:\web2py c:\python27\python.exe web2py.py -
在这里,您可以添加命令行参数(例如
-a用于设置管理员密码,-p用于指定备用端口等)。您可以使用以下命令查看所有启动选项:
C:\web2py>c:\python27\python.exe web2py.py --help
工作原理...
web2py 是用 Python 编写的,Python 是一种便携、解释和动态的语言,不需要编译或复杂的安装即可运行。它使用虚拟机(如 Java 和 .Net),并且可以在运行脚本时透明地即时字节编译您的源代码。
为了方便新手用户,官方网站上提供了 web2py 的 Windows 二进制发行版,它预先编译成字节码,打包在 zip 文件中,包含所有必需的库(dll/pyd),并附带一个可执行入口点文件(web2py.exe),但使用源代码运行 web2py 并没有明显的区别。
还有更多...
在 Windows 中从源代码包运行 web2py 有许多优点,以下列出其中一些:
-
您可以更轻松地使用第三方库,例如 Python Imaging(查看 Python 软件包索引,您可以在那里安装超过一万个模块!)。
-
您可以从其他 Python 程序中导入 web2py 功能(例如,数据库抽象层 (DAL))。
-
您可以使用最新的更改保持 web2py 更新,帮助测试它,并提交补丁。
-
您可以浏览 web2py 的源代码,根据您的定制需求进行调整等。
在 Ubuntu 中安装 web2py
本教程涵盖如何在 Ubuntu 桌面环境中安装 web2py。在生产系统中的安装将在下一教程中介绍。
我们假设您知道如何使用控制台和通过控制台安装应用程序。我们将使用最新的 Ubuntu 桌面,即本文撰写时的 Ubuntu Desktop 10.10。
准备工作
我们将在您的家目录中安装 web2py,因此请启动控制台。
如何操作...
-
下载 web2py。
cd /home mkdir www-dev cd www-dev wget http://www.web2py.com/examples/static/web2py_src.zip (get web2py) -
下载完成后,解压它:
unzip -x web2py_src.zip -
如果您想使用 GUI,可以选择安装 Python 的
tk库。sudo apt-get install python-tk注意
下载示例代码
您可以从您在
www.PacktPub.com的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了此书,您可以访问www.PacktPub.com/support,并注册以将文件直接通过电子邮件发送给您。代码文件也上传到了以下存储库:github.com/mdipierro/web2py-recipes-source。所有代码均在 BSD 许可下发布(
www.opensource.org/licenses/bsd-license.php),除非源文件中另有说明。 -
要启动 web2py,请访问 web2py 目录并运行 web2py。
cd web2py python web2py.py![如何做到这一点...]()
- 安装后,每次运行它时,web2py 都会要求您选择一个密码。这个密码是您的管理员密码。如果密码留空,则管理界面将被禁用。
-
在您的浏览器中输入
127.0.0.1:8000/以检查一切是否正常工作。
注意
管理界面:http://127.0.0.1:8000/admin/default/index 只能通过 localhost 访问,并且始终需要密码。它也可以通过 SSH 隧道访问。
还有更多...
您可以使用一些其他选项。例如,您可以使用选项 -p port 指定端口,使用选项 -i 127.0.0.1 指定 IP 地址。指定密码很有用,这样您就不必每次启动 web2py 时都输入它;使用选项 -a 指定密码。如果您需要其他选项的帮助,请使用带有 -h 或 help 选项的 web2py 运行。
例如:
python web2py.py -i 127.0.0.1 -p 8000 -a mypassword --nogui
在 Ubuntu 上设置生产部署
本菜谱描述了如何在 Ubuntu 服务器上使用生产环境安装 web2py。这是在生产环境中部署 web2py 的推荐方法。
准备工作
我们假设您知道如何使用控制台,并使用存储库和命令安装应用程序。我们将使用写作时的最新 Ubuntu 服务器:Ubuntu Server 10.04 LTS。
在这个菜谱中,我们将学习如何:
-
在 Ubuntu 上安装运行 web2py 所需的所有模块
-
在
/home/www-data/中安装 web2py -
创建自签名 SSL 证书
-
使用
mod_wsgi设置 web2py -
覆盖
/etc/apache2/sites-available/default -
重启 Apache

首先,我们需要确保系统是最新的。使用以下命令升级系统:
sudo apt-get update
sudo apt-get upgrade
如何做到这一点...
-
让我们从安装
postgreSQL:开始。sudo apt-get install postgresql -
如果尚未安装,我们需要解压并打开
ssh-server。sudo apt-get install unzip sudo apt-get install openssh-server -
安装 Apache 2 和
mod-wsgi:sudo apt-get install apache2 sudo apt-get install libapache2-mod-wsgi -
可选地,如果您计划操作图像,我们可以安装 Python Imaging Library (PIL) :
sudo apt-get install python-imaging -
现在我们需要安装 web2py。我们将在
/home中创建www-data并在那里提取 web2py 源代码。cd /home sudo mkdir www-data cd www-data -
从 web2py 网站获取 web2py 源代码:
sudo wget http://web2py.com/examples/static/web2py_src.zip sudo unzip web2py_src.zip sudo chown -R www-data:www-data web2py -
启用 Apache SSL 和 EXPIRES 模块:
sudo a2enmod expires sudo a2enmod ssl -
创建自签名证书:
您应该从受信任的证书颁发机构获取您的 SSL 证书,例如
verisign.com,但出于测试目的,您可以生成自己的自签名证书。您可以在:help.ubuntu.com/10.04/serverguide/C/certificates-and-security.html.了解更多。 -
创建
SSL文件夹,并将 SSL 证书放入其中:sudo openssl req -new -x509 -nodes -sha1 -days 365 -key \ /etc/apache2/ssl/self_signed.key > \ /etc/apache2/ssl/self_signed.cert sudo openssl x509 -noout -fingerprint -text < \ /etc/apache2/ssl/self_signed.cert > \ /etc/apache2/ssl/self_signed.info -
如果您遇到权限问题,请使用
sudo -i。 -
使用您的编辑器编辑默认的 Apache 配置。
sudo nano /etc/apache2/sites-available/default -
将以下代码添加到配置中:
NameVirtualHost *:80 NameVirtualHost *:443 <VirtualHost *:80> WSGIDaemonProcess web2py user=www-data group=www-data WSGIProcessGroup web2py WSGIScriptAlias / /home/www-data/web2py/wsgihandler.py <Directory /home/www-data/web2py> AllowOverride None Order Allow,Deny Deny from all <Files wsgihandler.py> Allow from all </Files> </Directory> AliasMatch ^/([^/]+)/static/(.*) \ /home/www-data/web2py/applications/$1/static/$2 <Directory /home/www-data/web2py/applications/*/static/> Options -Indexes Order Allow,Deny Allow from all </Directory> <Location /admin> Deny from all </Location> <LocationMatch ^/([^/]+)/appadmin> Deny from all </LocationMatch> CustomLog /var/log/apache2/access.log common ErrorLog /var/log/apache2/error.log </VirtualHost> <VirtualHost *:443> SSLEngine on SSLCertificateFile /etc/apache2/ssl/self_signed.cert SSLCertificateKeyFile /etc/apache2/ssl/self_signed.key WSGIProcessGroup web2py WSGIScriptAlias / /home/www-data/web2py/wsgihandler.py <Directory /home/www-data/web2py> AllowOverride None Order Allow,Deny Deny from all <Files wsgihandler.py> Allow from all </Files> </Directory> AliasMatch ^/([^/]+)/static/(.*) \ /home/www-data/web2py/applications/$1/static/$2 <Directory /home/www-data/web2py/applications/*/static/> Options -Indexes ExpiresActive On ExpiresDefault "access plus 1 hour" Order Allow,Deny Allow from all </Directory> CustomLog /var/log/apache2/access.log common ErrorLog /var/log/apache2/error.log </VirtualHost> -
重新启动 Apache 服务器:
sudo /etc/init.d/apache2 restart cd /home/www-data/web2py sudo -u www-data python -c "from gluon.widget import console; \ console();" sudo -u www-data python -c "from gluon.main \ import save_password; \ save_password(raw_input('admin password: '),443)" -
在您的浏览器中输入
http://192.168.1.1/以检查一切是否正常工作,将192.168.1.1替换为您的公网 IP 地址。
还有更多...
我们所做的一切都可以使用 web2py 提供的脚本自动完成:
wget http://web2py.googlecode.com/hg/scripts/setup-web2py-\
ubuntu.sh
chmod +x setup-web2py-ubuntu.sh
sudo ./setup-web2py-ubuntu.sh
使用 Apache、mod_proxy 和 mod_rewrite 运行 web2py
Apache httpd 是最受欢迎的 HTTP 服务器,在大型安装中拥有 Apache httpd 是必需的,就像在意大利圣诞节那天必须有潘内托尼一样。就像潘内托尼一样,Apache 有很多口味和不同的填充物。您必须找到您喜欢的。
在此配方中,我们使用 mod_proxy 配置 Apache,并通过 mod_rewrite 规则对其进行优化。这是一个简单但稳健的解决方案。它可以用来提高 web2py 的可伸缩性、吞吐量、安全性和灵活性。这些规则应该能满足专家和初学者的需求。
此配方将向您展示如何在主机上创建一个 web2py 安装,使其看起来像网站的一部分,即使它托管在其他地方。我们还将展示如何使用 Apache 来提高您的 web2py 应用程序的性能,而不需要接触 web2py。
准备工作
您应该有以下内容:
-
web2py 已安装在
localhost上并运行,使用内置的 Rocket 服务器(端口 8000) -
Apache HTTP 服务器 (
httpd) 版本 2.2.x 或更高版本 -
mod_proxy和mod_rewrite(包含在标准的 Apache 发行版中)
在 Ubuntu 或其他基于 Debian 的服务器上,您可以使用以下命令安装 Apache:
apt-get install apache
在 CentOS 或其他基于 Fedora 的 Linux 发行版上,您可以使用以下命令安装 Apache:
yum install httpd
对于大多数其他系统,您可以从网站 httpd.apache.org/ 下载 Apache,并按照提供的说明自行安装。
如何操作...
现在我们已经本地运行了 Apache HTTP 服务器(从现在起我们将简单地称之为 Apache)和 web2py,我们必须对其进行配置。
Apache 通过在纯文本配置文件中放置指令来配置。主要的配置文件通常称为 httpd.conf。此文件的默认位置在编译时设置,但可以使用 -f 命令行标志进行覆盖。httpd.conf 可能包含其他配置文件。额外的指令可以放置在这些配置文件中的任何一个。
配置文件可能位于 /etc/apache2、/etc/apache 或 /etc/httpd,具体取决于操作系统和 Apache 版本的细节。
-
在编辑任何文件之前,请确保从命令行 shell (
bash) 启用了所需的模块,输入:a2enmod proxy a2enmod rewrite- 在
mod_proxy和mod_rewrite启用后,我们现在可以设置一个简单的重写规则,将 Apache 收到的 HTTP 请求代理转发到我们希望的其他任何 HTTP 服务器。Apache 支持多个VirtualHosts,也就是说,它能够在单个 Apache 实例中处理不同的虚拟主机名称和端口。默认的VirtualHost配置位于名为/etc/<apache>/ sites-available/default的文件中,其中<apache>是 apache、apache2 或 httpd。`
- 在
-
在此文件中,每个
VirtualHost都是通过创建以下条目来定义的:<VirtualHost *:80> ... </VirtualHost>- 您可以在
http://httpd.apache.org/docs/2.2/vhosts/.阅读关于VirtualHost的深入文档。
- 您可以在
-
要使用
RewriteRules,我们需要在VirtualHost:内部激活 Rewrite Engine。<VirtualHost *:80> RewriteEngine on ... </VirtualHost> -
然后,我们可以配置重写规则:
<VirtualHost *:80> RewriteEngine on # make sure we handle the case with no / at the end of URL RewriteRule ^/web2py$ /web2py/ [R,L] # when matching a path starting with /web2py/ do use a reverse # proxy RewriteRule ^/web2py/(.*) http://localhost:8000/$1 [P,L] ... </VirtualHost>-
第二条规则告诉 Apache 对
http://localhost:8000执行反向代理连接,传递用户调用的 URL 的所有路径组件,除了第一个,即 web2py。规则使用的语法基于正则表达式 (regex),其中第一个表达式与传入的 URL(用户请求的 URL)进行比较。如果有匹配,则使用第二个表达式来构建一个新的 URL。
[and]内部的标志决定了如何处理生成的 URL。前面的例子匹配任何以/web2py开头的默认VirtualHost路径的传入请求,并生成一个新的 URL,将http://localhost:8000/预先添加到匹配路径的剩余部分;与表达式 .* 匹配的传入 URL 的部分替换第二个表达式中的$1。标志
P告诉 Apache 在将其传递回请求的浏览器之前,使用其代理检索 URL 所指向的内容。假设 Apache 服务器响应域名 www.example.com;那么如果用户的浏览器请求
www.example.com/web2py/welcome,它将收到来自 web2py 框架应用的响应内容。也就是说,这就像浏览器请求了http://localhost:8000/welcome一样。
-
-
有一个陷阱:web2py 可能会发送一个 HTTP 重定向,例如将用户的浏览器指向默认页面。问题是重定向是相对于 web2py 的应用程序布局的,即 Apache 代理试图隐藏的那个布局,因此重定向很可能会指向错误的位置。为了避免这种情况,我们必须配置 Apache 来拦截重定向并纠正它们。
<VirtualHost *:80> ... #make sure that HTTP redirects generated by web2py are reverted / -> /web2py/ ProxyPassReverse /web2py/ http://localhost:8000/ ProxyPassReverse /web2py/ / # transform cookies also ProxyPassReverseCookieDomain localhost localhost ProxyPassReverseCookiePath / /web2py/ ... </VirtualHost> -
还有一个问题。由 web2py 生成的许多 URL 也相对于 web2py 的上下文。这包括图像或 CSS 样式表的 URL。我们必须指导 web2py 如何编写正确的 URL,当然,由于它是 web2py,所以很简单,我们不需要修改应用程序代码中的任何代码。我们需要在 web2py 安装根目录下定义一个名为
routes.py的文件,如下所示:routes_out=((r'^/(?P<any>.*)', r'/web2py/\g<any>'),) -
在此阶段,Apache 可以在将内容发送回客户端之前对其进行转换。我们有几种方法可以提高网站速度。例如,如果浏览器接受压缩内容,我们可以在将内容发送回浏览器之前对其进行压缩。
# Enable content compression on the fly,
# speeding up the net transfer on the reverse proxy.
<Location /web2py/>
# Insert filter
SetOutputFilter DEFLATE
# Netscape 4.x has some problems...
BrowserMatch ^Mozilla/4 gzip-only-text/html
# Netscape 4.06-4.08 have some more problems
BrowserMatch ^Mozilla/4\.0[678] no-gzip
# MSIE masquerades as Netscape, but it is fine
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
# Don't compress images
SetEnvIfNoCase Request_URI \
\.(?:gif|jpe?g|png)$ no-gzip dont-vary
# Make sure proxies don't deliver the wrong content
Header append Vary User-Agent env=!dont-vary
</Location>
- 同样,只需配置 Apache,就可以执行其他有趣的任务,例如 SSL 加密、负载均衡、通过内容缓存加速,以及许多其他事情。您可以在以下网站找到有关这些和许多其他配置的信息:
httpd.apache.org.
这里是以下配方中使用的默认虚拟主机完整配置:
<VirtualHost *:80>
ServerName localhost
# ServerAdmin: Your address, where problems with the server
# should
# be e-mailed. This address appears on some server-generated
# pages,
# such as error documents. e.g. admin@your-domain.com
ServerAdmin root@localhost
# DocumentRoot: The directory out of which you will serve your
# documents. By default, all requests are taken from this
# directory,
# but symbolic links and aliases may be used to point to other
# locations.
# If you change this to something that isn't under /var/www then
# suexec will no longer work.
DocumentRoot "/var/www/localhost/htdocs"
# This should be changed to whatever you set DocumentRoot to.
<Directory "/var/www/localhost/htdocs">
# Possible values for the Options directive are "None", "All",
# or any combination of:
# Indexes Includes FollowSymLinks
# SymLinksifOwnerMatch ExecCGI MultiViews
#
# Note that "MultiViews" must be named *explicitly* ---
# "Options All"
# doesn't give it to you.
#
# The Options directive is both complicated and important.
# Please
# see http://httpd.apache.org/docs/2.2/mod/core.html#options
# for more information.
Options Indexes FollowSymLinks
# AllowOverride controls what directives may be placed in
# .htaccess
# It can be "All", "None", or any combination of the keywords:
# Options FileInfo AuthConfig Limit
AllowOverride All
# Controls who can get stuff from this server.
Order allow,deny
Allow from all
</Directory>
### WEB2PY EXAMPLE PROXY REWRITE RULES
RewriteEngine on
# make sure we handle when there is no / at the end of URL
RewriteRule ^/web2py$ /web2py/ [R,L]
# when matching a path starting with /web2py/ do a reverse proxy
RewriteRule ^/web2py/(.*) http://localhost:8000/$1 [P,L]
# make sure that HTTP redirects generated by web2py are reverted
# / -> /web2py/
ProxyPassReverse /web2py/ http://localhost:8000/
ProxyPassReverse /web2py/ /
# transform cookies also
ProxyPassReverseCookieDomain localhost localhost
ProxyPassReverseCookiePath / /web2py/
# Enable content compression on the fly speeding up the net
# transfer on the reverse proxy.
<Location /web2py/>
# Insert filter
SetOutputFilter DEFLATE
# Netscape 4.x has some problems...
BrowserMatch ^Mozilla/4 gzip-only-text/html
# Netscape 4.06-4.08 have some more problems
BrowserMatch ^Mozilla/4\.0[678] no-gzip
# MSIE masquerades as Netscape, but it is fine
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
# Don't compress images
SetEnvIfNoCase Request_URI \
\.(?:gif|jpe?g|png)$ no-gzip dont-vary
# Make sure proxies don't deliver the wrong content
Header append Vary User-Agent env=!dont-vary
</Location>
</VirtualHost>
您必须重新启动 Apache 以使任何更改生效。您可以使用以下命令进行相同操作:
apachectl restart
使用 Lighttpd 运行 web2py
Lighttpd 是一个安全、快速、兼容且非常灵活的 Web 服务器,它针对高性能环境进行了优化。与其他 Web 服务器相比,它具有非常低的内存占用,并关注 cpu-load。其高级功能集(FastCGI、CGI、认证、输出压缩、URL 重写等)使 Lighttpd 成为每个遭受负载问题的服务器的完美 Web 服务器软件。
这个配方是从官方 web2py 书籍中提取的,但尽管书中使用 FastCGI mod_fcgi 在 Ligthttpd Web 服务器后面公开 web2py 功能,这里我们使用 SCGI。我们在这里使用的 SCGI 协议在意图上与 FastCGI 类似,但更简单、更快。它描述在以下网站上:
SCGI 是一种用于 IP 上进程间通信的二进制协议。SCGI 专为 Web 服务器与 CGI 应用程序之间的通信任务量身定制。CGI 标准定义了 Web 服务器如何将动态生成 HTTP 响应的任务委托给外部应用程序。
CGI 的问题在于,对于每个传入的请求,都必须创建一个新的进程。在某些情况下,进程创建可能比响应生成所需的时间更长。这在大多数解释语言环境中都是正确的,其中加载新解释器实例的时间可能比程序本身的执行时间更长。
FastCGI 通过使用长时间运行的进程来回答多个请求而不退出,从而解决了这个问题。这对于解释程序特别有益,因为每次不需要重新启动解释器。SCGI 是在 FastCGI 经验之后开发的,以减少将 CGI 转换为 FastCGI 应用程序所需的复杂性,从而提高性能。SCGI 是 Lighttpd 的标准模块,也适用于 Apache。
准备工作。
您应该有:
-
web2py 已安装在本地主机(端口
8000)上。 -
Lighttpd(从
www.lighttpd.net下载并安装)。 -
SCGI(从
python.ca/scgi下载并安装)。 -
Python Paste(从
pythonpaste.org/下载并安装),或 WSGITools(http://subdivi.de/helmut/wsgitools)。
如果您有 setuptools,您可以安装 SCGI、paste 和 wsgitools,如下所示:
easy_install scgi
easy_install paste
easy_install wsgitools
您还需要一个脚本来启动一个配置为 web2py 的 SCGI 服务器,这个脚本可能随 web2py 一起提供,也可能不提供,这取决于版本,因此我们为这个配方提供了一个。
如何做到这一点...
现在,您必须编写一个脚本来启动将监听 Lighttpd 请求的 SCGI 服务器。别担心,即使它非常短且简单,我们在这里提供了一个可以复制的示例:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
LOGGING = False
SOFTCRON = False
import sys
import os
path = os.path.dirname(os.path.abspath(__file__))
os.chdir(path)
sys.path = [path]+[p for p in sys.path if not p==path]
import gluon.main
if LOGGING:
application = gluon.main.appfactory(
wsgiapp=gluon.main.wsgibase,
logfilename='httpserver.log',
profilerfilename=None)
else:
application = gluon.main.wsgibase
if SOFTCRON:
from gluon.settings import global_settings
global_settings.web2py_crontype = 'soft'
try:
import paste.util.scgiserver as scgi
scgi.serve_application(application, '', 4000).run()
except ImportError:
from wsgitools.scgi.forkpool import SCGIServer
SCGIServer(application, port=4000).run()
-
复制前面的脚本,并将其放在您的 web2py 安装根目录中,命名为
scgihandler.py。启动 SCGI 服务器,并在后台运行:$ nohup python ./scgihandler.py &-
现在,我们已经准备好配置
lighttpd。我们提供了一个简单的
lighttpd.conf配置文件作为示例。当然,现实世界的配置可能更加复杂,但重要部分不会有太大差异。
-
-
将以下行追加到您的
lighttpd.conf文件中:server.modules += ( "mod_scgi" ) server.document-root="/var/www/web2py/" # for >= linux-2.6 server.event-handler = "linux-sysepoll" url.rewrite-once = ( "^(/.+?/static/.+)$" => "/applications$1", "(^|/.*)$" => "/handler_web2py.scgi$1", ) scgi.server = ( "/handler_web2py.scgi" => ("handler_web2py" => ( "host" => "127.0.0.1", "port" => "4000", "check-local" => "disable", # important! ) ) ) -
此配置执行以下操作:
-
将 SCGI 模块加载到 Lighttpd 中。
-
将服务器文档根配置为 web2py 安装根目录。
-
使用
mod_rewrite重写 URL,以便静态文件的请求直接由 Lighttpd 服务,而所有其他请求都被重写到以/handler_web2py.scgi开头的 假 URL。 -
创建一个 SCGI 服务器段落: 对于以
/handler_web2py.scgi开头的每个请求,请求会被路由到运行在127.0.0.1的4000端口的 SCGI 服务器,跳过检查文件系统上是否存在相应的本地文件。
-
-
现在,检查您的配置是否正确:
$ lighttpd -t -f lighttpd.conf -
然后启动服务器进行测试:
$ lighttpd -D -f lighttpd.conf -
您可以使用以下命令启动/停止/重启服务器:
$ /etc/init.d/lighttpd start|stop|restart
您将看到您的 web2py 应用程序达到光速(ttpd)的速度。
使用 Cherokee 运行 web2py。
此配方解释了如何在 Cherokee 网络服务器后面运行 web2py,使用 uWSGI。
Cherokee 是用 C 语言编写的 web 服务器,其意图与 Lighttpd 相似:快速、紧凑且模块化。Cherokee 附带一个管理界面,允许用户管理其配置,否则配置难以阅读和修改。uWSGI 在其网站上描述为一种快速(纯 C)、自我修复、开发者/系统管理员友好的应用程序容器服务器。Cherokee 包含一个用于与 uWSGI 服务器通信的模块。
如何操作...
-
安装软件包或下载、编译和安装所需的组件。在 web2py 安装根目录中创建以下文件,并将其命名为
uwsgi.xml:<uwsgi> <pythonpath>/home/web2py</pythonpath> <module>wsgihandler</module> <socket>127.0.0.1:37719</socket> <master/> <processes>8</processes> <memory-report/> </uwsgi>- 此配置会启动八个进程来管理来自 HTTP 服务器的多个请求。根据需要更改它,并将
<pythonpath>配置为 web2py 的安装根目录。
- 此配置会启动八个进程来管理来自 HTTP 服务器的多个请求。根据需要更改它,并将
-
以拥有 web2py 安装的用户的身份启动 uWSGI 服务器:
$ uWSGI -d uwsgi.xml -
现在,启动 Cherokee 管理界面以创建新的配置:
$ cherokee-admin -
使用浏览器通过以下链接连接到管理界面:
http://localhost:9090/。![如何操作...]()
-
前往源部分 - (A),然后点击+按钮 - (B)。
-
在 (C) 处选择远程主机,然后在 (D) 处的文本字段中填写 IP 地址和端口,以匹配上一个
uswgi.xml文件中的配置。配置了 uWGI 源之后,现在可以配置一个虚拟主机,并通过它重定向请求。在这个菜谱中,我们选择当没有其他虚拟主机更适合传入请求时使用的默认虚拟主机。
-
点击按钮
(C)进入规则管理。![如何操作...]()
-
删除左侧列出的所有规则。只有默认规则将保留。
![如何操作...]()
-
使用 uWSGI 处理器配置默认规则。其他值保持不变。
![如何操作...]()
-
如果你想让 Cherokee 直接从 web2py 文件夹中服务静态文件,你可以添加一个正则表达式规则。点击按钮 (A),然后从 (B) 处的下拉菜单中选择正则表达式。请注意,此配置仅在 web2py 目录位于同一文件系统且可由 Cherokee 访问时才有效。
![如何操作...]()
-
配置正则表达式:
![如何操作...]()
-
现在,您可以配置指向您的 web2py 安装的应用程序子目录的静态处理器:
![如何操作...]()
- 记得保存配置,并从管理界面重新加载或重启 Cherokee;然后您就可以开始启动 uWSGI 服务器了。
-
切换到用于安装 web2py 的正确用户 ID;请注意,不建议使用 root 用户。
-
进入 web2py 安装根目录,其中保存了配置文件
uwsgi.xml。 -
使用
-d <logfile>选项运行 uWSGI,使其在后台运行:
$ su - <web2py user>
$ cd <web2py root>
$ uwsgi -x uwsgi.xml -d /tmp/uwsgi.log
享受速度!
准备工作
您应该具备以下条件:
-
web2py(已安装但未运行)
-
uWSGI(从
projects.unbit.it/uwsgi/wiki下载并安装) -
Cherokee(从
www.cherokee-project.com/下载并安装)
使用 Nginx 和 uWSGI 运行 web2py
本菜谱解释了如何使用 uWSGI 在 Nginx 网络服务器上运行 web2py。
Nginx是一个免费、开源、高性能的 HTTP 服务器和反向代理,由Igor Sysoev编写。
与传统服务器不同,Nginx 不依赖于线程来处理请求,而是实现了一个异步架构。这意味着 Nginx 即使在重负载下也能使用可预测的内存量,从而实现更高的稳定性和低资源消耗。Nginx 现在托管了全球所有域的超过七个百分点。
应强调的是,即使 Nginx 是异步的,web2py 也不是。因此,web2py 处理的并发请求越多,它使用的资源就越多。uWSGI 在其网站上被描述为一个快速(纯 C)、自我修复、开发者/系统管理员友好的应用程序容器服务器。我们将配置 Nginx 通过 uWSGI 服务动态的 web2py 页面,并直接服务静态页面,利用其低内存占用能力。
准备工作
您应该具备以下条件:
-
web2py(已安装但未运行)
-
uWSGI(从
projects.unbit.it/uwsgi/wiki下载并安装) -
Nginx(从
nginx.net/下载并安装)
在 Ubuntu 10.04 LTS 上,您可以使用apt-get安装 uWSGI 和 Nginx,如下所示:
apt-get update
apt-get -y upgrade
apt-get install python-software-properties
add-apt-repository ppa:nginx/stable
add-apt-repository ppa:uwsgi/release
apt-get update
apt-get -y install nginx-full
apt-get -y install uwsgi-python
如何操作...
-
首先,我们需要配置 Nginx。创建或编辑一个名为
/etc/nginx/sites-available/web2py的文件。 -
在文件中,写入以下内容:
server { listen 80; server_name $hostname; location ~* /(\w+)/static/ { root /home/www-data/web2py/applications/; } location / { uwsgi_pass 127.0.0.1:9001; include uwsgi_params; } } server { listen 443; server_name $hostname; ssl on; ssl_certificate /etc/nginx/ssl/web2py.crt; ssl_certificate_key /etc/nginx/ssl/web2py.key; location / { uwsgi_pass 127.0.0.1:9001; include uwsgi_params; uwsgi_param UWSGI_SCHEME $scheme; } }- 如您所见,它将所有动态请求传递到
127.0.0.1:9001。我们需要在那里运行 uWSGI。
- 如您所见,它将所有动态请求传递到
-
在 web2py 的安装根目录下创建以下文件,并将其命名为
web2py.xml:<uwsgi> <socket>127.0.0.1:9001</socket> <pythonpath>/home/www-data/web2py/</pythonpath> <app mountpoint="/"> <script>wsgihandler</script> </app> </uwsgi>- 此脚本假设 web2py 通常安装在
/home/www-data/web2py/.。
- 此脚本假设 web2py 通常安装在
-
现在禁用默认配置,并启用新的配置:
rm /etc/nginx/sites-enabled/default rm /etc/nginx/sites-available/default ln -s /etc/nginx/sites-available/web2py /etc/nginx/sites-enabled/\ web2py ln -s /etc/uwsgi-python/apps-available/web2py.xml /etc/uwsgi-\ python/apps-enabled/web2py.xml -
为了使用 HTTPS,您可能需要创建一个自签名证书:
mkdir /etc/nginx/ssl cd /etc/nginx/ssl openssl genrsa -out web2py.key 1024 openssl req -batch -new -key web2py.key -out web2py.csr openssl x509 -req -days 1780 -in web2py.csr -signkey web2py.key \ -out web2py.crt -
您还需要启用 web2py 管理员:
cd /var/web2py sudo -u www-data python -c "from gluon.main import save_password;\ save_password('$PW', 443)" -
完成后,重新启动 uWSGI 和 Nginx:
/etc/init.d/uwsgi-python restart
/etc/init.d/nginx restart
web2py 附带一个脚本,可以自动为您完成此设置:
scrips/setup-web2py-nginx-uwsgi-ubuntu.sh
在共享主机上使用 CGI 运行 web2py
本菜谱解释了如何配置 web2py 在具有登录(但不是 root)访问权限的共享主机上运行。
使用共享主机的登录或 FTP 访问,用户无法配置网络服务器,必须遵守主机的配置限制。本菜谱假设运行 Apache 的典型基于 Unix 或 Linux 的共享主机。
根据系统的配置,有两种部署方法。如果 Apache 的mod_proxy可用,并且主机允许长时间运行进程,将 web2py 的内置服务器作为 Apache 代理运行是简单且高效的。如果mod_proxy不可用,或者主机禁止长时间运行进程,我们将局限于 CGI 接口,该接口配置简单且几乎无处不在,但速度较慢,因为 Python 解释器必须为每个请求运行和加载 web2py。
我们将从 CGI 部署开始,这是一个简单的情况。
准备工作
我们假设你的网站根目录是/usr/www/users/username,而/usr/www/users/username/cgi-bin是你的 CGI 二进制目录。如果你的详细信息不同,从你的服务提供商那里获取实际值,并相应地修改这些说明。
由于安全原因,这里我们假设你的主机支持以本地用户(cgiwrap)的身份运行 CGI 脚本。如果它可用,这个程序可能因主机而异;请咨询你的服务提供商。
将 web2py 源代码下载到你的cgi-bin目录。例如:
cd cgi-bin
wget http://www.web2py.com/examples/static/web2py_src.zip
unzip web2py_src.zip
rm web2py_src.zip
或者,在本地解压 web2py 源代码,并通过 FTP 上传到主机。
如何操作...
-
在你的 Web 根目录中,如果需要,创建文件
.htaccess,并添加以下行(根据需要更改路径):SuexecUserGroup <yourusername> <yourgroup> RewriteEngine on RewriteBase /usr/www/users/username RewriteRule ^(welcome|examples|admin)(/.*)?$ \ /cgi-bin/cgiwrap/username/web2py/cgihandler.py -
使用以下命令更改其权限:
chown 644 .htaccess -
现在访问
yourdomain.com/welcome,或者(根据你的服务提供商)hostingdomain.com/username/welcome。 -
如果你在这个阶段遇到访问错误,请使用
tail命令检查web2py/applications/welcome/errors/目录中最新的文件。这种格式并不特别友好,但它可以提供有用的线索。如果errors目录为空,你可能需要再次检查errors目录是否可由 Web 服务器写入。
在共享主机上使用 mod_proxy 运行 web2py
使用mod_proxy比之前菜谱中讨论的 CGI 部署有两个主要优势:web2py 持续运行,因此性能显著更好,并且它以你的本地用户身份运行,这提高了安全性。因为从 web2py 的角度来看,它似乎是在 localhost 上运行的,所以管理应用程序可以运行,但如果你没有 SSL 操作可用,你可能出于安全原因想要禁用管理。SSL 设置在在 Ubuntu 上设置生产部署菜谱中讨论。
准备工作
这里我们假设你已经将 web2py 下载并解压到你的家目录中的某个位置。我们还假设你的 Web 托管提供商已启用 mod_proxy,支持长时间运行进程,允许你打开一个端口(例如示例中的 8000,但如果你发现该端口已被其他用户占用,你可以更改它)。
如何操作...
-
在你的基本 Web 目录中,如果需要,创建文件
.htaccess,并添加以下行:RewriteEngine on RewriteBase /usr/www/users/username RewriteRule ^((welcome|examples|admin)(/.*)?)$ \ http://127.0.0.1:8000/$1 [P] -
按照之前描述的 CGI 操作方式下载和解压 web2py,除了 web2py 不需要安装在你的
cgi-bin目录中,甚至不需要在你的网页文档树中。对于这个配方,我们假设你将其安装在登录主目录$HOME中。 -
使用以下命令在本地主机和端口
8000上启动 web2py 运行:
nohup python web2py.py -a password -p 8000 -N
password是你选择的单次管理员密码。-N是可选的,它禁用了web2py的 cron 以节省内存。(注意,最后一步不能通过 FTP 完成,因此需要登录访问。)
从用户定义的文件夹运行 web2py
这个配方解释了如何移动 web2py 的applications文件夹。
在 web2py 中,每个应用程序都位于applications/文件夹下的一个文件夹中,而applications/文件夹又位于 web2py 的base或root文件夹中(该文件夹还包含gluon/,web2py 的核心代码)。
当使用 web2py 内置的 web 服务器部署时,applications/文件夹可以被移动到文件系统中的其他位置。当applications/被移动时,某些其他文件也会随之移动,包括logging.conf、routes.py和parameters_port.py。此外,位于移动后的applications/文件夹同一目录下的site-packages会被插入到sys.path中(这个site-packages目录不必存在)。
如何操作...
当 web2py 从命令行运行时,文件夹移动位置可以通过-f选项指定,该选项应指定移动后的applications/文件夹的父文件夹,例如:
python web2py.py -i 127.0.0.1 -p 8000 -f /path/to/apps
还有更多...
当 web2py 以 Windows 服务的方式运行(web2py.exe -W)时,移动位置可以在 web2py 主文件夹中的options.py文件中指定。将默认文件夹os.getcwd()改为指定移动后的applications/文件夹的父文件夹。以下是一个options.py文件的示例:
import socket
import os
ip = '0.0.0.0'
port = 80
interfaces=[('0.0.0.0',80),
('0.0.0.0',443,'ssl_key.pem','ssl_certificate.pem')]
password = '<recycle>' # <recycle> means use the previous password
pid_filename = 'httpserver.pid'
log_filename = 'httpserver.log'
profiler_filename = None
#ssl_certificate = 'ssl_cert.pem' # certificate file
#ssl_private_key = 'ssl_key.pem' # private key file
#numthreads = 50 # ## deprecated; remove
minthreads = None
maxthreads = None
server_name = socket.gethostname()
request_queue_size = 5
timeout = 30
shutdown_timeout = 5
folder = "/path/to/apps" # <<<<<<<< edit this line
extcron = None
nocron = None
当 web2py 与外部 web 服务器一起部署时,不可用应用程序移动功能。
如何操作...
-
首先,创建一个 web2py 无权限用户:
sudo adduser web2py -
为了安全起见,禁用 web2py 用户密码以防止远程登录:
sudo passwd -l web2py -
从 web2py 的官方网站下载源代码包,将其解压到合适的目录中(例如
/opt/web2py),并适当地设置访问权限:wget http://www.web2py.com/examples/static/web2py_src.zip sudo unzip -x web2py_src.zip -d /opt sudo chown -Rv web2py. /opt/web2py -
在
/etc/inid.d/web2py中创建一个init脚本(你可以使用web2py/scripts/中的作为起点):sudo cp /opt/web2py/scripts/web2py.ubuntu.sh /etc/init.d/web2py -
编辑
init脚本:sudo nano /etc/init.d/web2py -
设置基本配置参数:
PIDDIR=/opt/$NAME DAEMON_DIR=/opt/$NAME APPLOG_FILE=$DAEMON_DIR/web2py.log DAEMON_ARGS="web2py.py -p 8001 -i 127.0.0.1 -c server.crt -k server.key -a<recycle> --nogui --pid_filename=$PIDFILE -l \ $APPLOG_FILE" -
将
127.0.0.1和8001改为你想要的 IP 和端口。你可以使用0.0.0.0作为通配符 IP,以匹配所有接口。 -
如果计划远程使用管理员权限,创建一个自签名证书:
sudo openssl genrsa -out /opt/web2py/server.key 1024 sudo openssl req -new -key /opt/web2py/server.key -out /opt/\ web2py/server.csr sudo openssl x509 -req -days 365 -in /opt/web2py/server.csr \ -signkey /opt/web2py/server.key -out /opt/web2py/server.crt -
如果你使用
print语句进行调试,或者想要记录 web2py 的输出消息,你可以在web2py.py中的imports之后添加以下行来重定向标准输出:sys.stdout = sys.stderr = open("/opt/web2py/web2py.err","wa", 0) -
最后,启动你的 web2py 服务:
sudo /etc/init.d/web2py start -
要永久安装(使其与操作系统的其他服务一起自动启动和停止),请执行以下命令:
sudo update-rc.d web2py defaults
如果一切正常,你将能够打开你的 web2py 管理员:
https://127.0.0.1:8001/welcome/default/index
在 Ubuntu 上安装 web2py 作为服务
对于简单的网站和内部网络,你可能需要一个简单的安装方法,以保持 web2py 运行。这个菜谱展示了如何以简单的方式启动 web2py,而不需要进一步的依赖(没有 Apache Web 服务器!)。
还有更多...
你可以使用bash来调试init脚本,查看正在发生的事情:
sudo bash -x /etc/init.d/web2py start
此外,你也可以更改start-stop-daemon选项以更详细地输出,并使用 web2py 用户来防止与其他 Python 守护进程的干扰:
start-stop-daemon --start \
${DAEMON_USER:+--chuid $DAEMON_USER} --chdir $DAEMON_DIR \
--background --user $DAEMON_USER --verbose --exec $DAEMON \
--$DAEMON_ARGS || return 2
记住设置一个密码,以便能够使用管理界面。可以通过执行以下命令来完成(将mypass更改为你想要的密码):
sudo -u web2py python /opt/web2py/web2py.py -p 8001 -a mypasswd
使用 IIS 作为代理运行 web2py
IIS 是 Windows 操作系统的首选 Web 服务器。它可以运行多个并发域和多个应用程序池。当你将 web2py 部署到 IIS 上时,你想要设置一个新的站点,并为它的根应用程序创建一个单独的应用程序池。这样,你就有独立的日志和启动/停止应用程序池的能力,独立于其他应用程序池。以下是具体操作方法。
这是三个菜谱中的第一个,我们将使用不同的配置重复这个过程。在这个第一个菜谱中,我们将 IIS 设置为作为 web2py Rocket Web 服务器的代理。
当 IIS 默认站点已经处于生产状态,并且启用了 ASP.NET、ASP 或 PHP 应用程序时,这种配置是可取的,同时,你的 web2py 站点可能处于开发中,可能需要频繁重启(例如,由于routes.py中的更改)。
准备工作
在这个菜谱中,我们假设你已经安装了 IIS 7 或更高版本。我们不讨论安装 IIS7 的步骤,因为它是商业产品,并且它们在其他地方有很好的文档记录。
你还需要将 web2py 解压缩到本地文件夹中。在端口8081上启动 web2py。
python web2py -p 8081 -i 127.0.0.1 -a 'password'
注意,当以代理方式运行 web2py 时,你应该小心不要无意中暴露未加密的 admin。
最后,你需要能够使用 IIS 代理。为此,你需要应用程序请求路由(ARR) 2.5。ARR 可以从以下 Microsoft Web 平台安装程序下载和安装:
http://www.microsoft.com/web/downloads/platform.aspx
如何操作...
-
下载 ARR 的 Web 平台安装程序后,打开应用程序,浏览到屏幕左侧的产品,如下面的截图所示:
![如何操作...]()
-
接下来,点击添加 - 应用程序请求路由 2.5,然后点击安装。这将带您到一个新屏幕,如下面的截图所示;点击我接受:
![如何操作...]()
-
Web 平台安装程序将自动选择并安装应用程序请求路由 2.5运行所需的所有依赖项。点击完成,这将带您到下载和安装屏幕。
![如何操作...]()
-
一旦收到成功消息,您可以关闭 Microsoft Web 平台应用程序。
-
现在打开 IIS 管理器,并按照指示创建一个新的网站。
-
首先,在IIS 管理器的左上角右键点击网站,然后选择新建网站。这将带您进入以下屏幕。按照此处所示填写详细信息:
![如何操作...]()
- 确保您选择了您的网站将要运行的正确 IP。
-
一旦创建了网站,双击以下截图所示的URL 重写:
![如何操作...]()
-
一旦进入URL 重写模块,点击右上角的添加规则,如图所示。
-
在入站和出站规则下选择反向代理模板。
-
按照此处所示填写详细信息:
![如何操作...]()
-
由于服务器 IP字段是最重要的,它必须包含 web2py 运行的 IP 和端口:
127.0.0.1:8081。同时,请确保SSL 卸载被勾选。在TO字段的出站规则中,写入分配给网站的域名。完成后,点击确定。在这个阶段,你的 web2py 安装应该一切正常,除了管理界面。当非 localhost 服务器的请求指向管理界面时,Web2py 要求我们使用 HTTPS。在我们的例子中,web2py 的 localhost 是
127.0.0.1:8081,而 IIS 目前运行在127.0.0.1:80. -
要启用管理,您需要一个证书。创建一个证书并将其添加到 IIS 7 的服务器证书中,然后重复之前的步骤将
443绑定到之前创建的 web2py 网站。 -
现在,访问:
https://yourdomain.com/admin/,您将能够浏览 web2py 管理 Web 界面。输入您的 web2py 管理界面的密码,然后正常进行。
使用 ISAPI 运行 web2py
在这里,我们展示了一个生产质量的配置,它使用一个在 IIS 中本地运行的专用应用程序池,并使用 ISAPI 处理程序。它与典型的 Linux/Apache 配置类似,但它是 Windows 本地的。
准备工作
如前所述,您将需要安装 IIS。
您应该已经下载并解压了 web2py。如果您已经在 localhost 的8081(或其它端口)上运行,您可以保留它,因为它不应该干扰此安装。我们将假设 web2py 已安装到C:\path\to\web2py。
您可以将其放置在您喜欢的任何位置。
然后您需要下载并安装isapi-wsgi。这将在下面解释。
如何操作...
-
首先,您需要从:
code.google.com/p/isapi-wsgi/下载isapi-wsgi。它是基于 pywin32 的成熟 WSGI 适配器,大部分的配置基于
isapi-wsgi的文档和示例。您可以使用 win32 安装程序安装 isapi-wsgi:
http://code.google.com/p/isapi-wsgi/downloads/detail?name=isapi_wsgi-0.4.2\. win32.exe.你也可以简单地下载 Python 文件并将其放置在
"c:\Python\Lib\site-packages"中安装它。isapi-wsgi.googlecode.com/svn/tags/isapi_wsgi-0.4.2/isapi_wsgi.py.isapi_wsgi在 IIS 5.1、6.0 和 7.0 上运行。但 IIS 7.x 必须安装IIS 6.0 管理兼容性。你可能想尝试运行以下测试来确认它已正确安装:
cd C:\Python\Lib\site-packages C:\Python\Lib\site-packages> python isapi_wsgi.py install Configured Virtual Directory: isapi-wsgi-test Extension installed Installation complete. -
现在转到
http://localhost/isapi-wsgi-test/。 -
如果你遇到一个显示“这不是一个有效的 Win32 应用程序”的
500 错误,那么可能存在问题,这里进行了讨论:support.microsoft.com/kb/895976/en-us。 -
如果你看到一个正常的
Hello响应,那么安装就成功了,你可以移除测试:C:\Python\Lib\site-packages> python isapi_wsgi.py remove- 我们还没有准备好配置 web2py 处理器。你需要启用 32 位模式。
-
我们现在准备好配置 web2py 处理器。将你的 web2py 安装添加到
PYTHONPATH:。set PYTHONPATH=%PYTHONPATH%;C:\path\to\web2py -
如果它还不存在,请在
C:\path\to\web2py文件夹中创建一个名为isapiwsgihandler.py的文件,其中包含以下内容:import os import sys import isapi_wsgi # The entry point for the ISAPI extension. def __ExtensionFactory__(): path = os.path.dirname(os.path.abspath(__file__)) os.chdir(path) sys.path = [path]+[p for p in sys.path if not p==path] import gluon.main application = gluon.main.wsgibase return isapi_wsgi.ISAPISimpleHandler(application) # ISAPI installation: if __name__=='__main__': from isapi.install import ISAPIParameters from isapi.install import ScriptMapParams from isapi.install import VirtualDirParameters from isapi.install import HandleCommandLine params = ISAPIParameters() sm = [ScriptMapParams(Extension="*", Flags=0)] vd = VirtualDirParameters(Name="appname", Description = "Web2py in Python", ScriptMaps = sm, ScriptMapUpdate = "replace") params.VirtualDirs = [vd] HandleCommandLine(params)- web2py 的最近版本可能已经包含了这个文件,甚至是一个更好的版本。
-
第一部分是处理器,第二部分将允许从命令行自动安装:
cd c:\path\to\web2py
python isapiwsgihandler.py install --server=sitename
- 默认情况下,这将在
Default Web Site下的虚拟目录appname中安装扩展。
更多...
检查 Web 应用程序的当前模式(32 位或 64 位):
cd C:\Inetpub\AdminScripts
cscript.exe adsutil.vbs get W3SVC/AppPools/Enable32BitAppOnWin64
cscript %systemdrive%\inetpub\AdminScripts\adsutil.vbs get w3svc/\
AppPools/Enable32bitAppOnWin64
如果答案是"Enable32BitAppOnWin64"参数在此节点未设置或Enable32BitAppOnWin64 : (BOOLEAN) False,那么你必须将 Web 服务器从 64 位模式切换到 32 位模式。ISAPI 在 64 位模式的 IIS 上不工作。你可以使用以下命令进行切换:
cscript %systemdrive%\inetpub\AdminScripts\adsutil.vbs set w3svc/\
AppPools/Enable32bitAppOnWin64 1
然后按照以下步骤重新启动应用程序池:
IIsExt /AddFile %systemroot%\syswow64\inetsrv\httpext.dll 1 ^
WEBDAV32 1 "WebDAV (32-bit)"
或者按照以下步骤设置一个单独的池:
system.webServer/applicationPool/add@enable32BitAppOnWin64.
第二章. 构建你的第一个应用程序
在本章中,我们将涵盖以下食谱:
-
改进脚手架应用程序
-
构建一个简单的联系人应用程序
-
构建 Reddit 克隆
-
构建 Facebook 克隆
-
使用
crud.archive -
将现有静态网站转换为 web2py 应用程序
-
创建半静态页面(flatpages)
-
添加你的自定义标志
-
创建菜单和子菜单
-
使用图标自定义菜单
-
创建导航栏
-
使用 cookies 设置语言
-
设计模块化应用程序
-
加快下载速度
简介
现在你已经安装并运行了 web2py,你就可以开始构建你的第一个应用程序了。本章中的食谱将提供完整应用程序的示例,包括模型、视图和控制器。它们从简单的联系人应用程序到更复杂的Facebook克隆不等。本章中的其他食谱将向你展示如何解决新用户通常遇到的一些常见问题,从添加标志到创建导航栏。
改进脚手架应用程序
在这个食谱中,我们讨论如何创建自己的脚手架应用程序并添加自己的配置文件。脚手架应用程序是任何新 web2py 应用程序附带文件的集合。
如何做这件事...
脚手架应用程序包括几个文件。其中之一是models/db.py,它从gluon.tools(Mail、Auth、Crud和Service)导入四个类,并定义了以下全局对象:db、mail、auth、crud和service。
脚手架应用程序还定义了auth对象所需的表,例如db.auth_user。
默认脚手架应用程序旨在最小化文件数量,而不是模块化。特别是,模型文件db.py包含配置,在生产环境中,最好将其保存在单独的文件中。
在这里,我们建议创建一个配置文件models/0.py,其中包含以下内容:
from gluon.storage import Storage
settings = Storage()
settings.production = False
if settings.production:
settings.db_uri = 'sqlite://production.sqlite'
settings.migrate = False
else:
settings.db_uri = 'sqlite://development.sqlite'
settings.migrate = True
settings.title = request.application
settings.subtitle = 'write something here'
settings.author = 'you'
settings.author_email = 'you@example.come'
settings.keywords = ''
settings.description = ''
settings.layout_theme = 'Default'
settings.security_key = 'a098c897-724b-4e05-b2d8-8ee993385ae6'
settings.email_server = 'localhost'
settings.email_sender = 'you@example.com'
settings.email_login = ''
settings.login_method = 'local'
settings.login_config = ''
我们还修改了models/db.py,使其使用配置文件中的信息,并显式定义auth_user表(这使得添加自定义字段更容易):
from gluon.tools import *
db = DAL(settings.db_uri)
if settings.db_uri.startswith('gae'):
session.connect(request, response, db = db)
mail = Mail() # mailer
auth = Auth(db) # authentication/authorization
crud = Crud(db) # for CRUD helpers using auth
service = Service() # for json, xml, jsonrpc, xmlrpc, amfrpc
plugins = PluginManager()
# enable generic views for all actions for testing purpose
response.generic_patterns = ['*']
mail.settings.server = settings.email_server
mail.settings.sender = settings.email_sender
mail.settings.login = settings.email_login
auth.settings.hmac_key = settings.security_key
# add any extra fields you may want to add to auth_user
auth.settings.extra_fields['auth_user'] = []
# user username as well as email
auth.define_tables(migrate=settings.migrate,username=True)
auth.settings.mailer = mail
auth.settings.registration_requires_verification = False
auth.settings.registration_requires_approval = False
auth.messages.verify_email = 'Click on the link http://' \
+ request.env.http_host + URL('default','user',
args=['verify_email']) \
+ '/%(key)s to verify your email'
auth.settings.reset_password_requires_verification = True
auth.messages.reset_password = 'Click on the link http://' \
+ request.env.http_host + URL('default','user',
args=['reset_password']) \
+ '/%(key)s to reset your password'
if settings.login_method=='janrain':
from gluon.contrib.login_methods.rpx_account import RPXAccount
auth.settings.actions_disabled=['register', 'change_password',
'request_reset_password']
auth.settings.login_form = RPXAccount(request,
api_key = settings.login_config.split(':')[-1],
domain = settings.login_config.split(':')[0],
url = "http://%s/%s/default/user/login" % \
(request.env.http_host, request.application))
通常,在 web2py 安装或升级后,欢迎应用程序会被 tar-gzipped 成welcome.w2p,并用作脚手架应用程序。你可以使用以下命令从现有应用程序创建自己的脚手架应用程序,这些命令来自bash shell:
cd applications/app
tar zcvf ../../welcome.w2p *
更多...
web2py 向导使用类似的方法,并创建类似的0.py配置文件。根据需要,你可以向0.py文件添加更多设置。
0.py文件可能包含敏感信息,例如用于加密密码的security_key、包含 smtp 账户密码的email_login以及包含 Janrain 密码的login_config(www.janrain.com/)。你可能希望将这些敏感信息写入 web2py 树之外的只读文件中,并从你的0.py中读取它们,而不是硬编码。这样,如果你选择将应用程序提交到版本控制系统,你将不会提交敏感信息。
框架应用程序包括其他你可能想要定制的文件,包括views/layout.html和views/default/users.html。其中一些是即将到来的菜谱的主题。
构建一个简单的联系人应用程序
当你开始设计一个新的 web2py 应用程序时,你会经历三个阶段,这些阶段的特点是寻找以下三个问题的答案:
-
应用程序应该存储哪些数据?
-
应该向访客展示哪些页面?
-
对于每个页面,页面内容应该如何呈现?
这三个问题的答案分别体现在模型、控制器和视图中。
对于良好的应用程序设计来说,按照这个顺序,尽可能准确地回答这些问题是很重要的。这些答案可以稍后进行修改,并以迭代的方式添加更多表格、更多页面和更多功能。一个优秀的 web2py 应用程序就是这样设计的,你可以更改表定义(添加和删除字段)、添加页面和更改页面视图,而不会破坏应用程序。
web2py 的一个显著特点是所有东西都有一个默认值。这意味着你可以工作在第一步,而无需编写第二步和第三步的代码。同样,你也可以工作在第二步,而无需编写第三步的代码。在每一步,你将能够立即看到你工作的结果;这要归功于appadmin(默认数据库管理界面)和通用视图(每个动作都有一个默认视图,直到你编写一个自定义视图)。
在这里,我们考虑的第一个示例是一个用于管理我们的商业联系人的应用程序,一个 CRM。我们将称之为Contacts。该应用程序需要维护公司列表以及在这些公司工作的个人列表。
如何操作...
-
首先,我们创建模型。
在这一步中,我们确定需要哪些表以及它们的字段。对于每个字段,我们确定它们:
-
必须包含唯一值(
unique=True) -
包含空值(
notnull=True) -
是否是引用(包含另一个表中记录的列表)
-
用于表示记录(格式属性)
从现在开始,我们将假设我们正在使用默认框架应用程序的副本,我们只描述需要添加或替换的代码。特别是,我们将假设默认的
views/layout.html和models/db.py。这里是一个可能的模型,表示我们需要在
models/db_contacts.py中存储的数据:
# in file: models/db_custom.py db.define_table('company', Field('name', notnull=True, unique=True), format='%(name)s') db.define_table('contact', Field('name', notnull=True), Field('company', 'reference company'), Field('picture', 'upload'), Field('email', requires=IS_EMAIL()), Field('phone_number', requires=IS_MATCH('[\d\-\(\) ]+')), Field('address'), format='%(name)s') db.define_table('log', Field('body', 'text',notnull=True), Field('posted_on', 'datetime'), Field('contact', 'reference contact'))当然,更复杂的数据表示是可能的。您可能希望允许,例如,多个用户使用系统,允许同一个人为多个公司工作,并跟踪时间的变化。在这里,我们将保持简单。
这个文件名很重要。特别是,模型按字母顺序执行,并且这个必须跟在
db.py之后。 -
-
在创建此文件后,您可以通过访问以下 URL 进行测试:
127.0.0.1:8000/contacts/appadmin,以访问 web2py 数据库管理界面appadmin。在没有控制器或视图的情况下,它提供了一种插入、选择、更新和删除记录的方法。 -
现在,我们准备构建控制器。我们需要确定应用程序需要哪些页面。这取决于所需的流程。至少我们需要以下页面:
-
一个索引页面(主页)
-
一个列出所有公司的页面
-
一个列出所选公司所有联系人的页面
-
一个创建公司的页面
-
一个编辑/删除公司的页面
-
一个创建联系人的页面
-
一个编辑/删除联系人的页面
-
一个页面,可以阅读一个联系人的信息和通信日志,以及添加新的通信日志
-
-
这些页面可以按以下方式实现:
# in file: controllers/default.py def index(): return locals() def companies(): companies = db(db.company).select(orderby=db.company.name) return locals() def contacts(): company = db.company(request.args(0)) or redirect(URL('companies')) contacts = db(db.contact.company==company.id).select( orderby=db.contact.name) return locals() @auth.requires_login() def company_create(): form = crud.create(db.company, next='companies') return locals() @auth.requires_login() def company_edit(): company = db.company(request.args(0)) or redirect(URL('companies')) form = crud.update(db.company, company, next='companies') return locals() @auth.requires_login() def contact_create(): db.contact.company.default = request.args(0) form = crud.create(db.contact, next='companies') return locals() @auth.requires_login() def contact_edit(): contact = db.contact(request.args(0)) or redirect(URL('companies')) form = crud.update(db.contact, contact, next='companies') return locals() @auth.requires_login() def contact_logs(): contact = db.contact(request.args(0)) or redirect(URL('companies')) db.log.contact.default = contact.id db.log.contact.readable = False db.log.contact.writable = False db.log.posted_on.default = request.now db.log.posted_on.readable = False db.log.posted_on.writable = False form = crud.create(db.log) logs = db( db.log.contact==contact.id).select(orderby=db.log.posted_on) return locals() def download(): return response.download(request, db) def user(): return dict(form=auth()) -
确保您不要删除 scaffolding 中的现有
user、download和service函数。 -
注意所有页面都是使用相同的成分构建的:选择查询和CRUD 表单。您很少需要其他任何东西。
-
还请注意以下内容:
-
一些页面需要
request.args(0)参数(联系人及company_edit的联系人 ID,contact_edit的联系人 ID,以及contact_logs)。 -
所有选择都有
orderby参数。 -
所有 CRUD 表单都有一个 next 参数,用于确定表单提交后的重定向。
-
所有操作都返回
locals(),这是一个包含在函数中定义的局部变量的 Python 字典。这是一个快捷方式。当然,也可以返回包含locals()任何子集的字典。 -
contact_create为新的联系人公司设置默认值,该值作为args(0)传递。 -
contacts_logs在为新日志条目处理crud.create后检索过去的日志。这避免了在插入新日志时无必要地重新加载页面。
-
-
到目前为止,我们的应用程序已经完全功能化,尽管外观和导航可以改进:
-
您可以在以下位置创建一个新的公司:
http://127.0.0.1:8000/contacts/default/company_create -
您可以在以下位置列出所有公司:
http://127.0.0.1:8000/contacts/default/companies -
您可以在以下位置编辑公司
#1:http://127.0.0.1:8000/contacts/default/company_edit/1 -
您可以在以下位置创建一个新的联系人:
http://127.0.0.1:8000/contacts/default/contact_create -
您可以在以下位置列出公司
#1的所有联系人:http://127.0.0.1:8000/contacts/default/contacts/1 -
你可以在以下位置编辑联系人
#1:http://127.0.0.1:8000/contacts/default/contact_edit/1 -
你可以通过以下链接访问联系人
#1的通信日志:http://127.0.0.1:8000/contacts/default/contact_logs/1
-
-
你还应该编辑
models/menu.py文件,并将其内容替换为以下内容:response.menu = [['Companies', False, URL('default', 'companies')]]应用程序现在可以正常工作,但我们可以通过为操作设计更好的外观和感觉来改进它。这是在视图中完成的。
-
创建并编辑文件
views/default/companies.html:{{extend 'layout.html'}} <h2>Companies</h2> <table> {{for company in companies:}} <tr> <td>{{=A(company.name, _href=URL('contacts', args=company.id))}}</td> <td>{{=A('edit', _href=URL('company_edit', args=company.id))}}</td> </tr> {{pass}} <tr> <td>{{=A('add company', _href=URL('company_create'))}}</td> </tr> </table>这是这个页面的样子:
![如何操作...]()
-
创建并编辑文件
views/default/contacts.html:{{extend 'layout.html'}} <h2>Contacts at {{=company.name}}</h2> <table> {{for contact in contacts:}} <tr> <td>{{=A(contact.name, _href=URL('contact_logs', args=contact.id))}}</td> <td>{{=A('edit', _href=URL('contact_edit', args=contact.id))}}</td> </tr> {{pass}} <tr> <td>{{=A('add contact', _href=URL('contact_create', args=company.id))}}</td> </tr> </table>这是这个页面的样子:
![如何操作...]()
-
创建并编辑文件
views/default/company_create.html:{{extend 'layout.html'}} <h2>New company</h2> {{=form}} -
创建并编辑文件
views/default/contact_create.html:{{extend 'layout.html'}} <h2>New contact</h2> {{=form}} -
创建并编辑文件:
views/default/company_edit.html:{{extend 'layout.html'}} <h2>Edit company</h2> {{=form}} -
创建并编辑文件
views/default/contact_edit.html:{{extend 'layout.html'}} <h2>Edit contact</h2> {{=form}} -
创建并编辑文件
views/default/contact_logs.html:{{extend 'layout.html'}} <h2>Logs for contact {{=contact.name}}</h2> <table> {{for log in logs:}} <tr> <td>{{=log.posted_on}}</td> <td>{{=MARKMIN(log.body)}}</td> </tr> {{pass}} <tr> <td></td> <td>{{=form}}</td> </tr> </table>这是这个页面的样子:

注意,在最后一个视图中,我们使用了 MARKMIN 函数来渲染 db.log.body 的内容,使用的是 MARKMIN 标记。这允许在日志中嵌入链接、图片、锚点、字体格式化信息以及表格。有关 MARKMIN 语法的详细信息,请参阅:web2py.com/examples/static/markmin.html。
构建 Reddit 克隆
这里我们展示如何构建一个发布和排名在线新闻链接的应用程序,类似于www.reddit.com/网站。链接被组织到类别中,用户可以发布、投票和评论它们。与先前的菜谱一样,代码只显示了默认脚手架应用程序的添加或更改。我们将我们的应用程序称为 reddit。
在这个菜谱中,我们不会支持带线程的评论(如实际www.reddit.com/网站上的那样),因为这会是一个不必要的复杂性。我们将在后续的菜谱中讨论带线程的评论。
我们将遵循先前的菜谱中讨论的相同步骤。
如何操作...
这个应用程序与先前的 contacts 菜谱非常相似。事实上,数据模型几乎是相同的,前提是我们将表 company 映射到表 category,将表 contact 映射到表 news。主要区别在于新闻条目没有 name,但它们有 title 和 link。此外,新闻条目必须按用户投票排序,而不是按字母顺序。我们还需要添加一个机制来允许用户投票、记录投票并防止重复计数。为此我们需要一个额外的表。我们也不会处理分页,因为这在单独的菜谱中讨论过。
这是完整的模型:
# in file: models/db_reddit.py
db.define_table('category',
Field('name' ,notnull=True, unique=True),
format='%(name)s')
db.define_table('news',
Field('title', notnull=True),
Field('link', requires=IS_URL()),
Field('category', 'reference category', readable=False,
writable=False),
Field('votes', 'integer', readable=False, writable=False),
Field('posted_on', 'datetime', readable=False, writable=False),
Field('posted_by', 'reference auth_user', readable=False, writable=False),
format='%(title)s')
db.define_table('comment',
Field('news', 'reference news', readable=False, writable=False),
Field('body', 'text', notnull=True),
Field('posted_on', 'datetime', readable=False, writable=False),
Field('posted_by', 'reference auth_user', readable=False,
writable=False))
db.define_table('vote',
Field('news', 'reference news'),
Field('value', 'integer'),
Field('posted_on', 'datetime', readable=False, writable=False),
Field('posted_by', 'reference auth_user', readable=False,
writable=False))
-
如前所述,许多所需操作与之前菜谱中的
contacts应用程序等效。特别是,我们需要列出类别、列出给定类别的新闻、创建和编辑类别、创建和编辑新闻、列出评论以及为news项目投票的操作。def index(): return locals() def categories(): categories = db(db.category).select(orderby=db.category.name) return locals() def news(): category = db.category(request.args(0)) or redirect(URL('categories')) news = db(db.news.category==category.id).select( orderby=~db.news.votes, limitby=(0, 25)) return locals() @auth.requires_membership('manager') def category_create(): form = crud.create(db.category, next='categories') return locals() @auth.requires_membership('manager') def category_edit(): category = db.category(request.args(0)) or redirect(URL('categories')) form = crud.update(db.category, category, next='categories') return locals() @auth.requires_login() def news_create(): db.news.category.default = request.args(0) db.news.votes.default = 0 form = crud.create(db.news, next='news_comments/[id]') return locals() @auth.requires_login() def news_edit(): news = db.news(request.args(0)) or redirect(URL('categories')) if not news.posted_by==auth.user.id: redirect(URL('not_authorized')) form = crud.update(db.news, category, next='news_comments/[id]') return locals() def news_comments(): news = db.news(request.args(0)) or redirect(URL('categories')) if auth.user: db.comment.news.default = news.id db.comment.posted_on.default = request.now db.comment.posted_by.default = auth.user.id form = crud.create(db.comment) comments = db(db.comment.news==news.id).select( orderby=db.comment.posted_on) return locals() @auth.requires_login() def vote(): if not request.env.request_method=='POST': raise HTTP(400) news_id, mode = request.args(0), request.args(1) news = db.news(id=news_id) vote = db.vote(posted_by=auth.user.id, news=news_id) votes = news.votes value = (mode=='plus') and +1 or -1 if vote and value*vote.value==1: message = 'you voted already' else: if vote: votes += value - vote.value vote.update_record(value=value) else: votes += value db.vote.insert(value=value, posted_by=auth.user.id, posted_on=request.now, news=news_id) news.update_record(votes=votes) message = 'vote recorded' return "jQuery('#votes').html('%s');jQuery('.flash').\ html('%s').slideDown();" % (votes, message)大多数这些操作都非常标准,由常用的
select和crud表单组成。 -
我们使用了两种类型的装饰器来确保只有登录用户可以编辑内容,只有管理员可以创建和编辑类别。您可以使用
appadmin创建一个manager组并将用户添加为成员:![如何做...]()
-
唯一的特殊操作是最后的
vote。投票操作被设计为 Ajax 回调。为了避免间接引用攻击,第一行确保操作通过POST请求调用。然后我们解析请求参数:它期望一个新闻 ID 作为args(0),以及正负号作为args(0),取决于我们是要对新闻项进行点赞还是踩。如果我们点赞(plus),它将创建一个值为+1的新db.vote条目。如果我们踩(minus),它将创建一个值为-1的新db.vote条目。操作还检查我们是否已经投票。我们允许更改我们的投票,但不能重复投票。此操作返回一个 JavaScript 字符串,用于更新投票 HTML 元素的最新投票数,并显示一条新消息。操作的最后一行与将要执行 Ajax 调用的视图紧密耦合(views/default/news_comments.html)。
-
-
我们还希望在菜单中列出所有可能的类别:
# in file: models/menu.py" categories = db(db.category).select(orderby=db.category.name, cache=(cache.ram, 60)) response.menu = [(c.name, False, URL('default', 'news', args=c.id)) for c in categories] -
最后,我们需要创建以下视图:
views/default/categories.html:
{{extend 'layout.html'}} <h2>Categories</h2> <table> {{for category in categories:}} <tr> <td>{{=A(category.name, _href=URL('news', args=category.id))}}</td> <td>{{=A('edit', _href=URL('category_edit', args=category.id))}} </td> </tr> {{pass}} <tr> <td>{{=A('add category', _href=URL('category_create'))}}</td> </tr> </table>views/default/news.html:
{{extend 'layout.html'}} <h2>News at {{=category.name}}</h2> <table> {{for news in news:}} <tr> <td>{{=A(news.title, _href=news.link)}}</td> <td>{{=A('comments', _href=URL('news_comments', args=news.id))}} </td> <td>{{=A('edit', _href=URL('news_edit', args=news.id))}}</td> </tr> {{pass}} <tr> <td>{{=A('post news item', _href=URL('news_create', args=category.id))}} </td> <td></td> </tr> </table>下面是这个页面的样子:
![如何做...]()
-
views/default/category_create.html:{{extend 'layout.html'}} <h2>New category</h2> {{=form}} -
views/default/news_create.html:{{extend 'layout.html'}} <h2>Post news item</h2> {{=form}} -
views/default/category_edit.html:{{extend 'layout.html'}} <h2>Edit category</h2> {{=form}} -
views/default/categories.html:{{extend 'layout.html'}} <h2>Edit news item</h2> {{=form}} -
views/default/news_comments.html:{{extend 'layout.html'}} <h2>Comments for {{=A(news.title, _href=news.link)}}</h2> {{if auth.user:}} <span id="votes">{{=news.votes}}</span> <button id="plus" onclick="ajax('{{=URL('vote', args=(news.id, 'plus'))}}', [], ':eval')"> plus </button> <button id="minus" onclick="ajax('{{=URL('vote', args=(news.id, 'minus'))}}', [], ':eval')"> minus </button> {{=form}} {{pass}} <table> {{for comment in comments:}} <tr> <td>{{=comment.posted_on}}</td> <td>{{=comment.posted_by.first_name}} says </td> <td>{{=MARKMIN(comment.body)}}</td> </tr> {{pass}} </table>注意代码:
<button id="plus" onclick="ajax('{{=URL('vote', args=(news.id, 'plus'))}}', [], ':eval')"> plus </button>点击时,它执行一个 Ajax 请求来记录我们的投票。Ajax 请求的返回值将被评估(
:eval)。URL(vote)返回将被评估的 JavaScript 代码:def vote(): ... return "jQuery('#votes').html('%s');jQuery('.flash'). html('%s').slideDown();" % (votes, message)
-
特别是,它将更改以下代码的内容,并显示一条新消息(滑动下降):
<span id="votes">{{=news.votes}}</span>下面是这个页面的样子:
![如何做...]()
构建 Facebook 克隆
在其基本层面,Facebook 处理用户之间的友谊关系,并允许朋友看到彼此的帖子。用户可以注册、登录、搜索其他用户、请求友谊和接受友谊。当用户发布一条消息时,该消息将显示在所有朋友的朋友墙上(网页)。
当然,真正的 Facebook 应用程序相当复杂,而我们版本大大简化了,但它捕捉了最重要的功能。特别是,我们将省略在帖子后附加评论的能力,以及省略电子邮件通知功能。我们还将省略处理照片、视频和聊天的代码。我们只对基于好友关系的友谊关系和显示墙帖子感兴趣。我们将我们的应用程序称为 friends。
如何操作...
我们设计的核心是一个连接两个人的表:友谊关系的 source 和 target。友谊关系由 source 请求,必须由 target 批准。批准后,source 用户可以看到 target 的帖子和个人资料信息。虽然真实的 Facebook 友谊关系是双向的(尽管朋友可以被隐藏/阻止),但在我们的情况下,我们假设单向友谊(两个用户必须互相给予友谊才能看到对方的帖子)。
-
因此,模型非常简单,我们只需要两个表:
# in file: models: # a table to store posted messages db.define_table('post', Field('body', 'text', requires=IS_NOT_EMPTY(), label='What is on your mind?'), Field('posted_on', 'datetime', readable=False, writable=False), Field('posted_by', 'reference auth_user', readable=False, writable=False)) # a table to link two people db.define_table('link', Field('source', 'reference auth_user'), Field('target', 'reference auth_user'), Field('accepted', 'boolean', default=False)) # and define some global variables that will make code more compact User, Link, Post = db.auth_user, db.link, db.post me, a0, a1 = auth.user_id, request.args(0), request.args(1) myfriends = db(Link.source==me)(Link.accepted==True) alphabetical = User.first_name|User.last_name def name_of(user): return '%(first_name)s %(last_name)s' % user最后五行定义了各种快捷方式,这将使我们的控制器和视图更加紧凑。例如,它们允许用户使用
User而不是db.user,以及使用orderby=alphabetical而不是更冗长的等效选项。myfriends是接受我们友谊的人的集合,这意味着我们可以看到他们的帖子。下面的列表行允许我们打印用户对象或用户引用的姓名和姓氏:
{{=name_of(user)}} -
我们将需要以下页面:
-
一个索引页面,如果登录,则重定向到我们的主页
-
一个私人主页,显示我们的消息、朋友的帖子,并允许我们发布新的帖子
-
一个页面用于按名称搜索新朋友
-
一个页面用于查看我们的当前好友,检查待处理的好友请求,并批准或拒绝好友关系
-
一个墙页面用于查看特定朋友的状况(或我们自己的)
-
-
我们还需要实现一个回调操作,允许用户请求好友关系,接受好友关系,拒绝好友请求,以及取消之前的好友请求。我们通过一个名为
friendship:的单个 Ajax 回调函数来实现这些功能。# in file: controllers/default.py def index(): if auth.user: redirect(URL('home')) return locals() def user(): return dict(form=auth()) def download(): return response.download(request, db) def call(): session.forget() return service() # our home page, will show our posts and posts by friends @auth.requires_login() def home(): Post.posted_by.default = me Post.posted_on.default = request.now crud.settings.formstyle = 'table2cols' form = crud.create(Post) friends = [me]+[row.target for row in myfriends.select(Link.target)] posts = db(Post.posted_by.belongs(friends))\ .select(orderby=~Post.posted_on, limitby=(0, 100)) return locals() # our wall will show our profile and our own posts @auth.requires_login() def wall(): user = User(a0 or me) if not user or not (user.id==me or \ myfriends(Link.target==user.id).count()): redirect(URL('home')) posts = db(Post.posted_by==user.id)\ .select(orderby=~Post.posted_on, limitby=(0, 100)) return locals() # a page for searching friends and requesting friendship @auth.requires_login() def search(): form = SQLFORM.factory(Field('name', requires=IS_NOT_EMPTY())) if form.accepts(request): tokens = form.vars.name.split() query = reduce(lambda a,b:a&b, [User.first_name.contains(k)|User.last_name.contains(k) \ for k in tokens]) people = db(query).select(orderby=alphabetical) else: people = [] return locals() # a page for accepting and denying friendship requests @auth.requires_login() def friends(): friends = db(User.id==Link.source)(Link.target==me)\ .select(orderby=alphabetical) requests = db(User.id==Link.target)(Link.source==me)\ .select(orderby=alphabetical) return locals() # this is the Ajax callback @auth.requires_login() def friendship(): """Ajax callback!""" if request.env.request_method != 'POST': raise HTTP(400) if a0=='request' and not Link(source=a1, target=me): # insert a new friendship request Link.insert(source=me, target=a1) elif a0=='accept': # accept an existing friendship request db(Link.target==me)(Link.source==a1).update(accepted=True) if not db(Link.source==me)(Link.target==a1).count(): Link.insert(source=me, target=a1) elif a0=='deny': # deny an existing friendship request db(Link.target==me)(Link.source==a1).delete() elif a0=='remove': # delete a previous friendship request db(Link.source==me)(Link.target==a1).delete() -
我们还在菜单中包括了主页、墙、好友和搜索页面:
# in file: models/menu.py response.menu = [ (T('Home'), False, URL('default', 'home')), (T('Wall'), False, URL('default', 'wall')), (T('Friends'), False, URL('default', 'friends')), (T('Search'), False, URL('default', 'search')), ]大多数视图都很直接。
- 这里是
views/default/home.html:
{{extend 'layout.html'}} {{=form}} <script>jQuery('textarea').css('width','600px'). css('height','50px');</script> {{for post in posts:}} <div style="background: #f0f0f0; margin-bottom: 5px; padding: 8px;"> <h3>{{=name_of(post.posted_by)}} on {{=post.posted_on}}:</h3> {{=MARKMIN(post.body)}} </div> {{pass}}注意到调整输入消息框大小的 jQuery 脚本,以及使用 MARKMIN 进行消息标记渲染。
- 这里是
views/default/wall.html,它与前面的视图非常相似(区别在于没有表单,帖子与单个用户相关,由request.args(0)指定):
{{extend 'layout.html'}} <h2>Profile</h2> {{=crud.read(db.auth_user, user)}} <h2>Messages</h2> {{for post in posts:}} <div style="background: #f0f0f0; margin-bottom: 5px; padding: 8px;"> <h3>{{=name_of(post.posted_by)}} on {{=post.posted_on}}:</h3> {{=MARKMIN(post.body)}} </div> {{pass}}这个页面的样子如下:
- 这里是

-
这里是
views/default/search.html:{{extend 'layout.html'}} <h2>Search for friends</h2> {{=form}} {{if people:}} <h3>Results</h3> <table> {{for user in people:}} <td> {{=A(name_of(user), _href=URL('wall', args=user.id))}} </td> <td> <button onclick="ajax( '{{=URL('friendship', args=('request', user.id))}}', [], null); jQuery(this).parent().html('pending')"> request friendship </button> </td> {{pass}} </table> {{pass}} -
这里是这个页面的样子:

-
注意按钮是如何执行 Ajax 调用到
request以user.id为对象的友谊的。点击后,按钮会被一个显示“待处理”的消息所替换。- 下面是
views/default/friends.html。它列出了当前的朋友和待处理的友谊请求:
{{extend 'layout.html'}} <h2>Friendship Offered</h2> <table> {{for friend in friends:}} <tr> <td> {{=A(name_of(friend.auth_user), _href=URL('wall', args=friend.auth_user.id))}} </td> <td> {{if friend.link.accepted:}}accepted{{else:}} <button onclick="ajax( '{{=URL('friendship', args=('accept', friend.auth_user.id))}}', [], null); jQuery(this).parent().html('accepted')"> accept </button> {{pass}} </td> <td> <button onclick="ajax( '{{=URL('friendship', args=('deny', friend.auth_user.id))}}', [], null); jQuery(this).parent().html('denied')"> deny </button> </td> </tr> {{pass}} </table> <h2>Friendship Requested</h2> <table> {{for friend in requests:}} <tr> <td> {{=A(name_of(friend.auth_user), _href=URL('wall', args=friend.auth_user.id))}} </td> <td> {{if friend.link.accepted:}}accepted{{else:}} pending{{pass}} </td> <td> <button onclick="ajax( '{{=URL('friendship', args=('deny', friend.auth_user.id))}}', [], null); jQuery(this).parent().html('removed')"> remove </button> </td> </tr> {{pass}} </table> - 下面是
-
下面是这个页面的样子:

-
这个视图显示了两个表:一个是向我们提供的友谊列表(通过接受,我们赋予他们查看我们个人资料和帖子的权限),以及我们发送的友谊请求(我们希望查看其个人资料和帖子的人)。在第一个表中的每个用户都有一个按钮。一个按钮执行 Ajax 调用以接受待处理的友谊请求,另一个按钮用于拒绝友谊。在第二个表中的每个用户都有一个列,告诉我们我们的请求是否被接受,以及一个带有按钮的列,用于取消友谊关系(无论是否待处理或已建立)。
注意
{{=name_of(user)}}和{{=name_of(message.posted_by)}}需要数据库查找。我们的应用程序可以通过缓存此函数的输出来加速。
使用crud.archive
在这个菜谱中,我们讨论了如何在任何应用程序中为记录创建完整的版本控制。
如何操作...
如果你有一个需要版本控制的表,例如db.mytable,并且你使用crud.update,你可以通过将onaccept=crud.archive传递给crud.update来为你的记录存储完整的修订历史。下面是一个例子:
form = crud.update(db.mytable, myrecord,
onaccept=crud.archive,
deletable=False)
crud.archive将创建一个隐藏的表db.mytable_archive,并在更新之前将旧记录存储在新创建的表中,包括对当前记录的引用。
通常,这个新表是隐藏的,并且只对 web2py 内部可见,但你可以通过在模型中明确定义它来访问它。如果原始表被命名为db.mytable,归档表必须命名为db.mytable_archive(在原始表名后加上_archive),并且它必须通过一个名为current_record的引用字段扩展原始表。下面是一个具体的例子:
db.define_table('mytable_archive',
Field('current_record', db.mytable),
db.mytable)
对于不同的表,只需将mytable替换为实际的表名。其他所有内容保持不变。
注意这样的表包含了db.mytable的所有字段加上一个current_record。
还有更多...
让我们看看crud.archive的其他功能。
存储记录的时间戳
crud.archive不会为存储的记录添加时间戳,除非你的原始表有时间和签名。例如:
db.define_table('mytable',
...
auth.signature)
通过向表中添加auth.signature,我们添加了以下字段:
Field('is_active', 'boolean', default=True),
Field('created_on', 'datetime', default=request.now,
writable=False, readable=False),
Field('created_by', db.auth_user, default=auth.user_id, writable=False, readable=False),
Field('modified_on', 'datetime',
update=default.now, default=request.now,
writable=False, readable=False),
Field('modified_by', db.table_user,
default=auth.user_id, update=auth.user_id,
writable=False, readable=False)
你也可以手动进行(不使用auth.signature),并为签名和时间戳字段命名。crud.archive会透明地处理它们。它们由SQLFORM.accepts函数填充。
存储每个记录的历史
crud.archive 的主要思想是存储每个被编辑的记录的历史,并将以前的版本存储在单独的表中。这允许你在不破坏对它的引用的情况下编辑记录。此外,如果你修改(迁移)原始表,存档表也会迁移。唯一的缺点是,在原始表中删除记录将在存档表中引起级联删除,并且整个记录的先前历史都将被删除。因此,你可能永远不想删除记录,而是通过取消选中 is_active 字段来将它们设置为禁用。
你还必须在某些 select 语句中更改查询,以隐藏已禁用的记录,通过以下方式过滤记录:
db.mytable.is_active==True
将现有静态网站转换为 web2py 应用程序
我们将假设你有一个静态 HTML 文件、CSS 文件、JavaScript 文件和图像的集合在一个文件夹中,并且你希望将它们转换为一个 web2py 应用程序。有两种方法可以实现:一种简单的方法,其中现有的 HTML 文件继续被视为静态,另一种更复杂的方法,其中 HTML 文件与控制器操作相关联,以便以后可以添加一些动态内容。
如何操作...
-
一种简单的方法是创建一个新的 web2py 应用程序(或使用现有的一个),并使用一个静态文件夹。例如,创建一个名为
app的新应用程序,并将现有站点的整个目录结构复制到applications/app/static/之下。这样,一个静态文件,
applications/app/static/example.html,可以通过以下 URL 访问:http://127.0.0.1:8000/app/static/example.html。虽然这个过程不会破坏相对 URL(不以正斜杠开头的 URL),例如:
<a href="../path/to/example2.html">点击我</a>,但它可能会破坏绝对 URL(以正斜杠开头),例如:<a href="/path/to/example2.html">点击我</a>。这不是一个 web2py 特定的问题,而是一个表明那些 HTML 文件设计不佳的迹象,因为绝对链接会在文件夹结构移动到另一个文件夹时被破坏。
-
如果出现这种情况,解决这个问题的正确方法是将所有绝对 URL 替换为相对 URL。这里有一个例子。
如果一个文件,
static/path1/example1.html,包含一个像<a href="/path/to/example2.html">点击我</a>这样的链接,并且文件example2.html出现在static/path2/example2.html之下,那么链接应该替换为<a href="../path2/example2.html">点击我</a>。这里,
../从static/path1 文件夹移动到静态文件夹,其余路径(path2/example2.html)正确地标识了所需的文件。 -
通过简单搜索
href, src和url,你应该可以定位到 HTML、CSS 和 JavaScript 文件中的所有 URL。所有以/开头的 URL 都需要修复。 -
一种更复杂的方法是将所有图像、电影、CSS 和 JavaScript 文件移动到静态文件夹中,并将 HTML 文件转换为视图。
我们按照以下五个步骤进行:
-
我们将所有静态文件(除了以
html结尾的文件)移动到应用程序的static/文件夹。 -
我们创建一个新的控制器(例如,名为
controllers/legacy.py),以及一个新的views文件夹(例如views/legacy)。 -
我们将所有 HTML 文件(例如,
page.html)移动到新的视图文件夹下。 -
对于每个
view文件,我们创建一个同名的控制器动作,返回dict()。 -
我们将所有内部链接和引用替换为
URL(...)。
-
-
让我们考虑一个具体的例子,它包括以下文件:
page.html image.png在这里,page.html 包含
。在我们的 web2py 应用程序文件夹中,我们最终得到以下文件结构:controllers/legacy.py views/legacy/page.html static/image.png在这里,legacy.py 包含
def page(): return dict()并且在 page.html 中的
被替换为<img src="img/{{=URL('static', 'image.png')}}"/>
页面现在可以通过以下 URL 访问:
http://127.0.0.1:8000/app/legacy/page.html。
创建半静态页面(平面页面)
任何网络应用程序都包含静态页面,其内容不经常更改。它们被称为平面页面。可以通过将 CMS 嵌入到应用程序中(例如plugin_wiki)或使用本菜谱中描述的显式机制来处理它们。
平面页面的例子包括:
-
基本主页和索引
-
关于我们
-
许可证和免责声明
使用 web2py,对于这些页面,我们可以设置简单的控制器,例如:
def about(): return dict()
然后,您可以直接在视图中编写页面代码,或者将页面内容存储在数据库中。第二种方法更好,因为它将允许用户轻松就地编辑,支持多语言国际化,记录更改历史以供审计,等等。
这个菜谱的思路是将平面页面存储在数据库中,并根据用户请求(控制器、函数、参数、首选语言等)显示它们。
如何做到这一点...
-
首先,定义一个平面页面表以存储页面内容,在模型中创建一个名为
flatpages.py的文件,并添加以下定义:LANGUAGES = ('en', 'es', 'pt', 'fr', 'hi', 'hu', 'it', 'pl', 'ru') FLATPAGES_ADMIN = 'you@example.com' DEFAULT_FLATPAGE_VIEW = "flatpage.html" db.define_table('flatpage', Field('title', notnull=True), Field('subtitle', notnull=True), Field('c', label='controller'), Field('f', label='function'), Field('args', label='arguments'), Field('view', default=DEFAULT_FLATPAGE_VIEW), Field('lang', requires=IS_IN_SET(LANGUAGES), default='en'), Field('body', 'text', default=''), auth.signature, )字段包括:
-
title:这是主标题 -
subtitle:这是可选的副标题 -
c:这是属于此页面的控制器(参见 URL 辅助工具) -
f:这是属于此页面的函数(参见 URL 辅助工具) -
args:这是添加多个页面到函数的字符串参数(参见 URL 辅助工具) -
lang:这是匹配用户偏好的语言 -
body:这是页面的 HTML 主体注意,
FLATPAGES_ADMIN将用于限制对平面页面的编辑访问。此变量包含允许编辑的用户电子邮件。
-
-
到目前为止,您应该能够使用
appadmin管理界面填充此表,或者您可以编程实现,即在控制器中创建一个setup_flatpage函数,如下所示:if not db(db.flatpage).count(): db.flatpage.insert(title="Home", subtitle="Main Index", c="default", f='index', body="<h3>Hello world!</h3>") db.flatpage.insert(title="About us", subtitle="The company", c="company", f='about_us', body="<h3>My company!</h3>") db.flatpage.insert(title="Mision & Vision", subtitle="The company", c="company", f='mision_vision', body="<h3> Our vision is...</h3>") db.flatpage.insert(title="Our Team", subtitle="Who we are", c="company", f='our_team', body="<h1>We are...</h3>") db.flatpage.insert(title="Contact Us", subtitle="Where we are", c="company", f='contact_us', body="<h3>Contact form:...</h3>")此示例页面将如下所示:
Home: Hello world About Us The Company : My company! Mission & Vision: Our vision is... Our Team: We are... Contact Us: Contact Form:... -
为了能够渲染一个页面,将以下函数
flatpage添加到之前创建的文件models/flatpage.py中:def flatpage(): # define languages that don't need translation: T.current_languages = ['en', 'en-en'] # select user specified language (via session or browser config) if session.lang: lang = session.lang elif T.accepted_language is not None: lang = T.accepted_language[:2] else: lang = "en" T.force(lang) title = subtitle = body = "" flatpage_id = None form = '' view = DEFAULT_FLATPAGE_VIEW if request.vars and auth.user and auth.user.email==FLATPAGES_ADMIN: # create a form to edit the page: record = db.flatpage(request.get_vars.id) form = SQLFORM(db.flatpage, record) if form.accepts(request, session): response.flash = T("Page saved") elif form.errors: response.flash = T("Errors!") else: response.flash = T("Edit Page") if not form: # search flatpage according to the current request query = db.flatpage.c==request.controller query &= db.flatpage.f==request.function if request.args: query &= db.flatpage.args==request.args(0) else: query &= (db.flatpage.args==None)|(db.flatpage.args=='') query &= db.flatpage.lang==lang # execute the query, fetch one record (if any) flatpage = db(query).select(orderby=~db.flatpage.created_on, limitby=(0, 1), cache=(cache.ram, 60)).first() if flatpage: flatpage_id = flatpage.id title = flatpage.title subtitle = flatpage.subtitle body = flatpage.body view = flatpage.view else: response.flash = T("Page Not Found!") if auth.user and auth.user.email==FLATPAGES_ADMIN: # if user is authenticated, show edit button: form = A(T('edit'), _href=URL(vars=dict(id=flatpage_id))) # render the page: response.title = title response.subtitle = subtitle response.view = view body = XML(body) return dict(body=body, form=form)这个功能:
-
检查用户语言偏好(或会话设置)
-
检查操作 URL(根据请求控制器、函数和参数)
-
获取存储的扁平页面
-
渲染页面 HTML
如果用户
FLATPAGES_ADMIN已登录,函数flatpage(): -
在编辑页面时准备/处理
SQLFORM -
或者显示一个编辑按钮来编辑页面
-
-
最后,你应该创建一个
flatpage.html视图,以便 web2py 可以渲染页面,例如:{{extend 'layout.html'}} <h1>{{=response.title}}</h1> <h2>{{=response.subtitle}}</h2> {{=form}} {{=body}}占位符是:
-
form:这是编辑FORM(或编辑链接) -
body:这些是实际的页面内容(作为 HTML 存储在数据库中)
-
-
要告诉 web2py 在期望的控制器(即在
default.py中的index)中显示扁平页面,请编写以下内容:def index(): return flatpage()这将渲染主页的扁平页面。
company.py示例控制器将如下所示:def about_us(): return flatpage() def mision_vision(): return flatpage() def our_team(): return flatpage()当你访问
default/index.php或company/about_us时,如果你已经登录,你应该会看到一个带有EDIT按钮的扁平页面。
请记住,出于性能原因,扁平页面是缓存的。因此,更改可能不会立即显示(你可以通过清除缓存或从数据库查询中删除缓存参数来更改这一点)。
它是如何工作的...
使用 web2py,你可以完全选择要显示的内容、要使用的视图等。
在这种情况下,当 web2py 执行你的控制器时,函数flatpage()将尝试根据请求变量从数据库中获取存储的页面。
如果按下编辑按钮,将渲染一个SQLFORM,允许普通认证用户编辑页面。页面更新将被插入,因此你会得到页面更改的历史记录。它显示页面的最新记录,尝试匹配首选语言(例如,使用session.lang = 'es'来更改语言)。
你可以向扁平页面表添加一个view字段,这样你就可以有多个视图来显示这种内容。你甚至可以添加一个format字段,这样你可以在 HTML 之外的其他标记语言中渲染页面主体(wiki、ReST 等)。
添加您的自定义标志
我们将更改随 web2py 一起提供的默认标志,并添加我们的标志。我们需要一个图像编辑器;使用你喜欢的,或者使用操作系统提供的,如 Paint、GIMP 或 Photoshop 都是合适的。

这是默认应用程序的外观:

这是自定义标志的结果。
如何操作...
-
首先,我们需要创建一个新的应用程序。您可以通过
admin应用程序来完成此操作。选择创建一个新的应用程序,并为其命名。我的应用程序名称是changelogo。默认情况下,新应用程序是welcome脚手架应用程序的副本。现在,如果您运行应用程序,您将在应用程序顶部看到您的应用程序标题后跟单词App,在我的情况下是changelogoApp。 -
启动您的图像编辑器,如果您要开始一个新的标志。根据您使用的布局选择像素尺寸。我选择了
300x90像素的新标志尺寸。编辑完成后,将其保存为 PNG 或 JPEG 格式。将其命名为(例如,mylogoapp.png),并将其复制到您应用程序内部的static/images目录中。 -
下一步是编辑您应用程序的
views/layout.html。您可以使用管理员或您自己的编辑器。向下滚动到标题部分,寻找以下内容:<div id="header"> <!-- header and login nav --> {{block header}} <!-- this is default header --> {{try:}}{{=auth.navbar(action=URL('default', 'user'))}}{{except:pass}} <h1> <span id="appname"> {{=request.application.capitalize()}} </span> App </h1> <div style="clear: both;"></div><!-- Clear the divs --> {{end}} </div><!-- header --> -
让我稍微解释一下这段代码。
以下代码打印用户操作,例如登录、注册和找回密码:
{{try:}}{{=auth.navbar(action=URL('default', 'user'))}}{{except:pass}}以下代码打印应用程序名称后跟
App:。<h1> <span id="appname"> {{=request.application.capitalize()}} </span> App </h1>我们需要将其更改为显示新的标志。我们将用以下内容替换
<h1>...</h1>:{{=IMG(_src=URL('static', 'images/mylogoapp.png'), _style="width: 100%;")}}这将打印标志图像而不是标题。
标题部分现在看起来如下:
<div id="header"> <!-- header and login nav --> {{block header}} <!-- this is default header --> {{try:}}{{=auth.navbar(action=URL('default', 'user'))}}{{except:pass}} {{=IMG(_src=URL('static', 'images/mylogoapp.png'))}} <div style="clear: both;"></div><!-- Clear the divs --> {{end}} </div><!-- header --> -
最后,将标志链接到主页是一种良好的做法,因此我们将创建一个链接到
default/index:。{{=A(IMG(_src=URL('static', 'images/mylogoapp.png')), _href=URL('default', 'index'))}}
创建菜单和子菜单
web2py 以透明的方式处理菜单,如下所示:
-
按惯例,菜单项列表存储在
response.menu中。 -
菜单嵌入在带有
{{=MENU(response.menu)}}的视图中。
您可以在同一视图的不同位置或不同视图中拥有多个菜单。response.menu的值是菜单项的列表。通常,每个菜单项都是一个包含以下元素的元组的列表:标题,状态,链接,和子菜单。
其中标题是菜单的标题,状态是一个布尔值,可以用来确定菜单链接是否是当前页面,链接是在选择菜单项时要重定向到的链接,子菜单是菜单项的列表。
以下是一个通常放在文件models/menu.py中的代码示例:
response.menu = [
('Home', URL()==URL('default', 'home'), URL('default', 'home'),
[]),
('Search', URL()==URL('default', 'search'), URL('default',
'search'), []),
]
我们用作第二个参数的条件检查当前页面的URL()是否是链接的页面。
如何做到这一点...
-
子菜单可以很容易地显式构建,如下所示:
response.menu = [ ('Home', URL()==URL('default', 'home'), URL('default', 'home'), []), ('Search', False, None, [ ('Local', URL()==URL('default', 'search'), URL('default', 'search')), ('Google', False, 'http://google.com'), ('Bing', False, 'http://bing.com'), ] ), ] -
子菜单的父菜单可能包含或不包含链接。在这个例子中,我们将链接从
搜索移动到其本地子菜单项。将菜单标题国际化是一种良好的做法。response.menu = [ (T('Home'), URL()==URL('default', 'home'), URL('default', 'home'), []), ] -
对于每个链接指定控制器名称(例如示例中的
default)也很重要;否则,当有多个控制器时(这几乎是必然的情况;想想appadmin),菜单会出错。
使用图标自定义菜单
有时,你可能想要比通常的语法允许的更多自定义菜单项,例如,通过向菜单项添加图标。这个菜谱展示了如何做到这一点。
如何做...
-
首先要意识到的是以下内容:
response.menu = [ ('Home', False, URL('default', 'home'), []), ...]等价于以下内容:
response.menu = [ (A('Home', _href=URL('default', 'home')), False, None, []), ...]在这里,A 是锚点(链接)助手。你可以使用后面的语法,并且你可以用任何其他组合的助手替换 A 助手。例如:
response.menu = [ (A(IMG(_src=URL('static', 'home.png'), _href=URL('default', 'home'))), False, None, []), ... ]或者:
response.menu = [ (SPAN(IMG(_src=URL('static', 'home.png')), A('home', _href=URL('default', 'home'))), False, None, []), ... ] -
你可以创建构建你的菜单项的函数:
def item(name): return SPAN(IMG(_src=URL('static', name+'.png')), A(name, _href=URL('default', name))) response.menu = [ (item(home), False, None, []), ...
创建导航栏
web2py 包括对菜单的内置支持,使用基本的 Python 结构进行渲染。在大多数情况下,这已经足够了,但对于更复杂的菜单,仅使用 Python 代码来维护它们是困难的。这个菜谱展示了如何创建一个更动态的菜单,将菜单条目存储在数据库中,并自动构建菜单树。
如何做...
-
首先,让我们定义一个导航栏表来存储菜单条目。在
models中创建一个名为navbar.py的文件,并添加以下定义:db.define_table('navbar', Field("title", "string"), Field("url", "string", requires=IS_EMPTY_OR(IS_URL())), Field("c", label="Controller"), Field("f", label="Function"), Field("args", label="Arguments"), Field("sortable", "integer"), Field("parent_id", "reference navbar"), format="%(title)s", )字段包括:
-
title:这是在用户界面中显示的文本 -
url:这是可选的链接 URL -
c:这是构建链接的控制器(参见 URL 助手) -
f:这是构建链接的函数(参见 URL 助手) -
args:这是构建链接的字符串参数 -
sortable:这是一个用于排序条目的数值 -
parent_id:这是高级别菜单祖先(其条目是子菜单)的引用(navbar.id)
-
-
到目前为止,你应该能够使用
appadmin管理界面填充此表,或者你可以通过在控制器中创建一个名为setup_navbar的函数来程序化地完成它,如下所示:if not db(db.navbar).count(): # create default index entry: home_id = db.navbar.insert(title="Home", c="default") # create a "Company" leaf with typical options: company_id = db.navbar.insert(title="Company", c="company") db.navbar.insert(title="About Us", f='about_us', parent_id=company_id) db.navbar.insert(title="Mision & Vision", f='mision_vision', parent_id=company_id) db.navbar.insert(title="Our Team", f='our_team', parent_id=company_id) products_id = db.navbar.insert(title="Products", c="products") # Add some "Computers models" to products entry: computers_id = db.navbar.insert(title="Computers", f='computers', parent_id=products_id) for model in 'basic', 'pro', 'gamer': db.navbar.insert(title="Model %s" % model, args=model, parent_id=computers_id)这个示例菜单看起来如下所示:
Home Company About Us Mission & Vision Our Team Products Computers Model basic Model pro Model gamer每个顶级菜单链接到一个特定的控制器,二级子菜单链接到该控制器上的函数,三级条目为这些函数提供参数(注意,你可以为 URL 参数 c 和 f 使用相同的默认值,这些值将按层次结构使用继承的值)。
-
现在,为了显示菜单,将以下
get_sub_menus函数添加到之前创建的models中的navbar.py文件:def get_sub_menus(parent_id, default_c=None, default_f=None): children = db(db.navbar.parent_id==parent_id) for menu_entry in children.select(orderby=db.navbar.sortable): # get action or use defaults: c = menu_entry.c or default_c f = menu_entry.f or default_f # is this entry selected? (current page) sel = (request.controller==c and request.function==f and (request.args and request.args==menu_entry.args or True)) # return each menu item yield (T(menu_entry.title), sel, menu_entry.url or URL(c, f, args=menu_entry.args), get_sub_menus(menu_entry.id, c, f) )此函数递归地构建在 HTML 页面上渲染菜单所需的 Python 结构,并执行以下操作:
-
获取请求级别的
navbar菜单条目 -
检查操作目标或使用默认值(对于
URL助手) -
计算此条目是否为当前选中的条目
-
创建并返回用于
MENU助手的条目
-
-
要告诉 web2py 构建菜单,请在相同的
navbar模型中使用以下内容:response.menu = get_sub_menus(parent_id=None)这将在每次查看页面时构建菜单。
-
如果你有一个不经常改变的复杂菜单,你可以多次重用它,使用缓存将其保存在内存中:
response.menu = cache.ram('navbar_menu', lambda: get_sub_menus(parent_id=None), time_expire=60)
菜单通常在包含MENU助手的页面上渲染,该助手解释这个结构(参见views, layout.html):
{{=MENU(response.menu, _class='sf-menu')}}
使用 cookies 设置语言
默认情况下,web2py 从HTTP Accept-Language 头中确定首选用户语言。
这里是一个正常工作流程的示例:
-
用户将浏览器首选项设置为
en(英语)、en-us(美国英语)和fr-fr(法国法语) -
访问我们的网站时,浏览器在 HTTP 头
Accept-Language中发送接受的语言列表。 -
web2py 解析 HTTP 头,验证
Accept-Language列表,并遍历其语言。 -
当一个语言出现在
T.current_languages中,或者当在请求应用程序的语言子文件夹中找到相应的语言文件(例如fr-fr.py)时,web2py 停止循环。
如果 web2py 因为一个语言,例如en-en,出现在T.current_languages中而停止循环,这意味着该语言不需要翻译。如果相反,web2py 因为找到一个语言文件而停止循环,那么将使用该语言文件进行翻译。如果对于Accept-Language中的所有语言都没有满足这两个条件,则没有翻译。
通常,这个选择是在调用应用程序代码之前由 web2py 执行的。
在应用程序代码中(例如,在模型中),你可以覆盖默认设置。你可以更改当前语言列表:
T.set_current_languages('en', 'en-en')
你也可以强制 web2py 从一个不同于 HTTP 头中提供的列表中选择语言:
T.force('it-it')
通常情况下,你不想让网络应用程序依赖于浏览器来确定语言偏好,而是希望通过按钮、链接或下拉框明确询问访客。当这种情况发生时,应用程序需要记住用户在浏览应用程序页面时的选择。
如果应用程序需要登录,这个偏好可以存储在用户配置文件中,例如,在auth_user表的自定义字段中。
但并非所有应用程序都需要登录,而且通常语言偏好是在注册之前表达的。一种方便且透明地设置和记住语言的方法是不需要数据库,而是在 cookie 中存储偏好设置。这可以通过以下方式实现。
如何操作...
-
创建一个
model文件,例如0_select_language.py,其中包含以下代码:if 'all_lang' in request.cookies and not (request.cookies['all_lang'] is None): T.force(request.cookies['all_lang'].value) -
在
views/layout.html中的某个位置插入以下内容:<span> <script> function set_lang(lang) { var date = new Date(); cookieDate=date.setTime(date.getTime()+(100*24*60*60*1000)); document.cookie='all_lang='+lang+';expires='+cookieDate+'; path=/{{=request.application}}'; window.location.reload(); }; </script> <select name="adminlanguage" onchange="set_lang(jQuery(this).val())"> {{for language in T.get_possible_languages():}} <option {{=T.accepted_language==language and 'selected' or ''}}> {{=T(language)}} </option> {{pass}} </select> </span>上述代码生成一个选择语言的下拉框,列出 web2py 可以找到翻译文件的所有语言。当下拉框的值改变时,它会强制页面重新加载。在重新加载后,代码设置一个名为
all_lang的 cookie,其中包含所选语言。当加载另一个页面时,如果上述代码找到 cookie,它将使用 cookie 中的信息来选择并强制语言选择。 -
使用链接而不是下拉框可以更明确地实现相同的效果:
<span> <script> function set_lang(lang) { var date = new Date(); cookieDate=date.setTime(date.getTime()+(100*24*60*60*1000)); document.cookie='all_lang='+lang+';expires='+cookieDate+'; path=/{{=request.application}}'; window.location.reload(); return false; }; </script> {{for language in T.get_possible_languages():}} {{if not T.accepted_language==language:}} <a href="#" onclick="set_lang('{{=language}}')"> {{=T(language)}} </a> {{else:}}{{=T(language)}}{{pass}}{{pass}} </select> </span>即使应用程序不使用会话,这种解决方案也是有效的。
注意,语言名称本身也需要翻译,即{{=T(language)}},因为它应该列在当前选定的语言中。
也要注意以下字符串位于 cookie 中,确保偏好设置仅适用于当前应用程序。此行由客户端解释,如果通过路由启用了自定义 URL,可能需要更改:
path=/{{=request.application}}
在这种情况下,一个简单的解决方案是将它替换为以下内容,并且偏好设置将适用于同一 web2py 安装下的所有应用程序,无论 URL 如何:
path=/
设计模块化应用程序
在这个菜谱中,我们将向您展示如何使用 web2py 组件创建一个模块化应用程序。
特别是,我们将考虑一个示例应用程序,该应用程序将允许您创建项目、列出项目,并在创建/更新新项目时动态更新列表。
准备就绪
我们将考虑以下附加模型scaffolding application的models/db_items.py:
db.define_table('mytable',
Field('name'),
Field('quantity','integer'))
注意,此表及其字段结构没有特定之处,但以下示例中我们将使用db.mytable。
如何操作...
-
在
controllers/default.py中,创建一个基本动作来加载视图和控制器中的组件,以实际列出和编辑项目:def index(): "index will load the list and the create/edit forms as components" return dict() def list_items(): """ shows a list of items that were created each items is clickable and can be edited """ rows = db(db.mytable.id>0).select() return dict(rows=rows) def edit_item(): """ return a creation form if no item is specified, return an edit form if the item is known """ def display_changed(data): response.ajax = \ 'web2py_component("%s","show_itemlist")' % URL('showitems') form = crud.update(db.mytable, request.args(0),onaccept=display_changed) return form -
在视图
views/default/index.html中,只需加载组件以列出项目并链接编辑功能(使用占位符,该占位符将用于插入创建或编辑项目的表单):{{=LOAD('default', 'list_items', ajax = True, target = 'showitems')}} {{=A('create',component=URL('edit_item'),target='placeholder')}} <div id="placeholder"></div> -
在视图
views/default/list_items.html中,项目列表中的每个项目都会将指定的 URL 加载到 ID 为placeholder的div中。<ul> {{for item in rows:}} {{=LI(A('edit %s' % item.name, component=URL('edit_item',args=item.id), target='placeholder'))}} {{pass}} </ul>
对于edit_item动作的视图不是必需的,因为它返回一个辅助器,而不是字典。
它是如何工作的...
索引视图通过 Ajax 将项目列表加载到div#showitems中。它还显示一个链接和一个div#placeholder。点击链接会导致对edit_item的 Ajax 请求,没有args返回一个在div#placeholder内渲染的create form。列表还包含一个到edit_item的链接,该链接也会在div#placeholder中显示一个update form。表单不仅通过 Ajax 显示。组件始终通过点击A(...,component=URL(...),target="placeholder")来加载。
这确保了组件通过 Ajax 加载,组件中的表单将通过 Ajax 提交,从而仅刷新组件。任何表单提交都将返回一个response.ajax,该响应刷新其他组件div@list_items。
注意所有逻辑都在服务器端生成,在嵌入页面的 JS 代码中幕后翻译,并在客户端执行。点击链接会显示一个表单。提交表单会导致处理表单,如果被接受,项目列表将被刷新。
加快下载速度
默认情况下,scaffolding 控制器中的下载函数设置以下 HTTP 响应头,防止客户端缓存:
Expires: Thu, 27 May 2010 05:06:44 GMT
Pragma: no-cache
Cache-Control: no-store, no-cache, must-revalidate, post-check=0,
pre-check=0
这可能对某些动态内容有益,但例如,一个客户端在浏览包含多个非静态图像的网站时,会看到每次页面显示时每个图像的加载情况,这会减慢导航速度。
使用 @cache 装饰器缓存下载没有帮助,因为缓存将在服务器端完成,而我们希望客户端缓存。
此外,下载函数还执行一些授权检查,在某些情况下,这些检查是不必要的,因此会导致不必要的延迟。
一个更好的方法是使用自定义的下载函数,它允许客户端缓存,并跳过授权。
如何做到这一点...
我们需要编写一个自定义的下载函数。我们可以编辑现有的脚手架,但最好是简单地添加另一个名为 fast_download 的函数,这样我们就可以在不同的应用部分选择使用其中一个。
-
首先,我们希望我们的应用程序返回以下 HTTP 头部信息:
Last-Modified: Tue, 04 May 2010 19:41:16 GMT -
但省略这些:
Expires removed Pragma removed Cache-control removed这可以通过在流回文件之前显式删除不需要的头部信息来完成:
def fast_download(): filename = request.args(0) if not qualify_for_fast_download(filename) return download() else: del response.headers['Cache-Control'] del response.headers['Pragma'] del response.headers['Expires'] filepath = os.path.join(request.folder, 'uploads', filename) response.headers['Last-Modified'] = \ time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.localtime(os.path.getmtime(filename))) return response.stream(open(filepath, 'rb')) -
注意,
response.stream会为你处理Range 请求和If-Modified-Since。同时请注意,这样的操作可能会下载比预期更多的文件,因此我们插入了一个检查,如下所示:qualify_for_fast_download(filename)我们把这个留给你来实现。
它将检查这个函数是否可以使用,或者应该使用正常的下载函数。
-
在你的视图中,记得使用
fast_download而不是下载来构造 URL:URL('fast_download', args='filename')
还有更多...
如果你想要确保 fast-download 保存的文件名在一个 上传 类型的字段中,例如,mytable.myfield,那么你可以配置你的 web 服务器直接提供它,并绕过 web2py。例如,如果你正在使用 Apache:
AliasMatch ^/([^/]+)/static/(mytable\.myfield.*) \
/home/www-data/web2py/applications/$1/static/$2
<Directory /home/www-data/web2py/applications/*/static/>
Options -Indexes
Order Allow,Deny
Allow from all
</Directory>
这之所以有效,是因为存储在 mytable.myfield 中的所有文件名在上传时都会被 web2py 重命名,并且它们的名称以 mytable.myfield 开头。
第三章。数据库抽象层
在本章中,我们将介绍以下食谱:
-
创建新模型
-
从 csv 文件创建模型
-
批量上传你的数据
-
将你的数据从一个数据库迁移到另一个数据库
-
从现有的 MySQL 和 PostgreSQL 数据库创建模型
-
通过标签高效搜索
-
从多个应用程序访问你的数据库
-
层次分类树
-
按需创建记录
-
或者,LIKE,BELONGS,以及更多关于 Google App Engine 的内容
-
用 DB 视图替换慢速虚拟字段
简介
数据库抽象层(DAL)可能是 web2py 的主要优势。DAL 向底层的 SQL 语法暴露了一个简单的应用程序编程接口(API),这可能会隐藏其真正的力量。在本章的食谱中,我们提供了 DAL 的非平凡应用示例,例如构建高效按标签搜索的查询和构建层次分类树。
创建新模型
如前一章的食谱所示,大多数应用程序都需要数据库,构建数据库模型是应用程序设计的第一步。
准备工作
在这里我们假设你有一个新创建的应用程序,你将把模型放入一个名为models/db_custom.py的文件中。
如何做到这一点...
-
首先,你需要一个数据库连接。这是由 DAL 对象创建的。例如:
db = DAL('sqlite://storage.sqlite')注意,这一行已经存在于
models/db.py文件中,因此你可能不需要它,除非你删除了它或需要连接到不同的数据库。默认情况下,web2py 连接到存储在文件存储.sqlite 中的sqlite数据库。此文件位于应用程序的数据库文件夹中。如果该文件不存在,则在应用程序首次执行时由 web2py 创建。SQLite 速度快,并且将所有数据存储在一个单独的文件中。这意味着你的数据可以轻松地从应用程序转移到另一个应用程序。实际上,
sqlite数据库(们)是由 web2py 与应用程序一起打包的。它提供了完整的 SQL 支持,包括翻译、连接和聚合。此外,SQLite 从 Python 2.5 及以后的版本开始就自带了,因此,它已经包含在你的 web2py 安装中了。SQLite 有两个缺点。一个是它不强制执行列类型,除了添加和删除列之外,没有
ALTER TABLE。另一个缺点是任何需要写访问权限的事务都会锁定整个数据库。因此,数据库除了读取之外不能并发访问。这些特性使其成为开发目的和低流量网站的不错选择,但不是高流量网站的可行解决方案。
在下面的食谱中,我们将向你展示如何连接到不同类型的数据库。
-
一旦我们有了
db对象,我们就可以使用define_table方法来定义新表。例如:db.define_table('invoice',Field('name'))语法始终相同。第一个参数是表名,其后跟一个字段列表。字段构造函数接受以下参数:
-
字段名
-
字段类型:这可以接受以下数据类型之一的值 -
string(默认),text, boolean, integer, double, password, date, time, datetime, upload, blob, reference other_table, list:string, list:integer,和list:reference other_table。内部,upload, password和list类型等同于string,但在 web2py 级别,它们被不同地处理。 -
length=512:这是基于字符串的字段的最大长度。对于非文本字段,此值被忽略。 -
default=None:这是插入新记录时的默认值。此属性的值可以是当需要值时调用的函数(例如,在记录插入时,如果没有指定值)。 -
update=None:这与默认值相同,但仅在更新时使用该值,而不是在插入时使用。 -
ondelete='CASCADE':这映射到相应的 SQLON DELETE属性。 -
notnull=False:这指定字段值是否可以是NULL(在数据库级别强制执行)。 -
unique=False:这指定字段值是否必须是唯一的(在数据库级别强制执行)。 -
requires=[]:这是一组 web2py 验证器(在 web2py 表单级别强制执行)。大多数字段类型都有默认验证器。 -
required=False:这不要与 requires 混淆,并且它告诉 web2py 在插入和更新期间必须指定此字段的值。对于required字段,默认值和更新值将被忽略。除非与notnull=True一起使用,否则即使字段是必需的,None值也是可接受的。 -
readable=True:这指定字段在表单中是否可读。 -
writable=True:这指定字段在表单中是否可写。 -
represent=(lambda value: value):这是一个用于在表单和表中显示字段值的函数。 -
widget=SQLHTML.widgets.string.widget:这是一个将在表单中构建输入小部件的函数。 -
label="Field Name":这是用于表单中此字段的标签。 -
comment="...":这是在表单中添加到该字段的注释。Field构造函数还有其他特定于上传类型字段的属性。有关更多信息,请参阅 web2py 书籍。
-
-
define_table方法还接受三个命名参数:db.define_table('....', migrate=True, fake_migrate=False, format='%(id)s')-
migrate=True:这指示 web2py 在不存在时创建表,或在它不匹配模型定义时修改它。此过程伴随着元数据文件的创建。元数据文件的形式为databases/<hash>_<name>.table,并将用于跟踪模型的变化,并执行自动迁移。将migrate=False设置为禁用自动迁移。 -
fake_migrate=False:有时上述元数据会损坏(或意外删除),需要重新创建。如果模型与数据库表内容匹配,则设置fake_migrate=True,web2py 将重新构建元数据。 -
format='%(id)s':这是一个格式化字符串,它决定了当其他表在表单(例如在选择下拉框中)中引用此表的记录时应如何表示。格式可以是一个函数,它接受一个行对象并返回一个字符串。
-
还有更多...
在所有数据库中,但 SQLite 和 Google App Engine 数据存储,如果您更改表定义,则会发出ALTER TABLE以确保数据库与模型匹配。在 SQLite 中,只有在添加或删除列时才会执行ALTER TABLE,而不是当字段类型更改时(因为 SQLite 不强制执行)。在 Google App Engine 数据存储中,没有ALTER TABLE的概念,可以添加列但不能删除;web2py 将忽略模型中未列出的列。
完全从模型中删除define_table不会导致DROP TABLE。该表只是直到相应的define_table被放回,对 web2py 不可访问。这防止了数据的意外删除。您可以使用db.<name>.drop()命令在 web2py 中删除表。
从 CSV 文件创建模型
考虑这样一个场景:您有一个 CSV 文件,您对它知之甚少。但您仍然想创建一个 Web 应用程序来访问 CSV 文件中的数据。
准备工作
我将假设您有一个 csv 文件在文件夹中
/tmp/mydata.csv
您还需要一个名为csvstudio的程序,您可以从csvstudio.googlecode.com/hg/csvstudio.py下载。
如何操作...
-
第一步是查看 csv 文件:
python csvstudio.py -a < /tmp/mydata.csv-
如果文件没有损坏,并且是标准的 csv 格式,那么 csvstudio 将生成一个报告,列出 CSV 列、数据类型和数据范围。
如果文件是非标准的 CSV 格式,或者例如是 XLS 格式,尝试在 Excel 中导入它,然后再以 CSV 格式保存。
您还可能想尝试使用Google Refine来清理 CSV 文件。
-
-
一旦您知道
csvstudio可以正确读取文件,运行以下命令:python csvstudio.py -w mytable -i /tmp/mydata.csv > db1.py- csvstudio 创建一个名为 db1.py 的文件,其中包含一个与数据兼容的 web2py 模型。在这里,mytable 是您为表选择的名称。
-
将此文件移动到您的应用程序的
models文件夹中。 -
现在您需要清理数据,以便可以在 web2py 中导入。
python csvstudio.py -f csv -i /tmp/mydata.csv -o /tmp/mydata2.csv- 文件
mydata2.csv现在包含与原始文件相同的数据,但列名已被清理以与生成的模型兼容。字段值已去除任何前导和尾随空格。
- 文件
-
到目前为止,您只需运行您的应用程序并调用
appadmin。http://.../app/appadmin -
你应该能看到你生成的模型。点击模型名称,你将在底部看到一个上传链接。上传
mydata2.csv文件以填充你的表格。
还有更多...
如果你更喜欢从 shell 上传 csv 文件而不是使用appadmin界面,你可以这样做。
从主 web2py 文件夹内部,运行以下命令:
python web2py.py -S app -M -N
你将得到一个 web2py shell(-S app 在应用程序上下文中打开 shell,-M 加载模型,-N 防止 cron 作业运行)。
在 shell 内部执行以下操作:
>>> f = open('/tmp/mydata2.csv','rb')
>>> db.mytable.import_from_csv_file(f)
>>> db.commit()
嘿,数据已经在数据库中了。当你使用 shell 时,别忘了执行db.commit()。
如果由于任何原因这不起作用(可能是因为 CSV 文件是非标准的,无法进行标准化),请尝试按照我们的下一个食谱操作。
批量上传你的数据
在这里,我们假设你有一个已知结构的平面文件中的数据。你想要创建一个数据库模型并将数据导入数据库。
准备工作
为了简化,我们假设文件位于/tmp/data.txt,具有以下结构:
Clayton Troncoso|234523
Malinda Gustavson|524334
Penelope Sharpless|151555
Serena Ruggerio|234565
Lenore Marbury|234656
Amie Orduna|256456
Margery Koeppel|643124
Loraine Merkley|234555
Avis Bosserman|234523
...
Elinor Erion|212554
每一行都是一个以\n结尾的记录。字段由—分隔。第一列包含<first name> <last name>。第二列包含年薪值。
如同往常,我们假设你有一个名为app的新应用程序。
如何做...
-
你首先需要做的是在你的
app中创建一个名为models/db1.py的模型,包含以下数据:db.define_table('employees', Field('first_name'), Field('last_name'), Field('salary','double')) -
然后,你会编写一个脚本,例如:
applications/app/private/importer.py
-
此脚本可以读取数据,解析它,并将其放入数据库中,如下所示:
for line in open('/tmp/data.txt','r'): fullname,salary = line.strip().split('|') first_name,last_name = fullname.split(' ') db.employees.insert(first_name=first_name, last_name=last_name, salary=float(salary)) db.commit() -
最后,从 web2py 文件夹运行以下脚本:
python web2py.py -S app -M -N -R applications/app/private/
importer.py
注意,导入器是一个 Python 脚本,而不是一个模块(这就是为什么我们把它放在private文件夹而不是modules文件夹中。它在我们的应用程序上下文中执行,就像是一个控制器。实际上,你可以将代码复制到一个控制器中,并通过浏览器运行它。
还有更多...
如果数据是干净的,前面的脚本运行良好。你可能需要在插入之前验证每个记录。这又是一个两步过程。首先,你需要向你的模型添加验证器,例如:
db.define_table('employees',
Field('first_name', requires=IS_NOT_EMPTY()),
Field('last_name', requires=SI_NOT_EMPTY()),
Field('salary','double', requires=IS_FLOAT_IN_RANGE(0,10**7)))
然后你需要调用导入时的验证器并检查错误:
for line in open('/tmp/data.txt','r'):
fullname,salary = line.strip().split('|')
first_name,last_name = fullname.split(' ')
r = db.employee.validate_and_insert(
first_name=first_name,
last_name=last_name,
salary=float(salary))
if r.errors: print line, r.errors
db.commit()
导致错误的记录将不会被插入,你可以手动处理它们。
将数据从一个数据库迁移到另一个数据库
因此,到目前为止,你已经构建了你的应用程序,并且你的 SQLite 数据库中有数据。但是假设你需要迁移到生产 MySQL 或 PostgreSQL 环境。
准备工作
假设你有一个名为app的应用程序,数据在sqlite://storage.sqlite数据库中,你想要将数据迁移到不同的数据库:
mysql://username:password@hostname:port/dbname
如何做...
-
编辑你的模型
db.py,并替换以下内容:db=DAL('sqlite://storage.sqlite')使用以下:
production=False URI = 'mysql://username:password@hostname:port/dbname' if production: db=DAL(URI, pool_size=20) else: db=DAL('sqlite://storage.sqlite') -
创建一个名为
applications/app/private/mover.py的文件,包含以下数据:def main(): other_db = DAL(URI) print 'creating tables...' for table in db: other_db.define_table(table._tablename,*[field for field in table]) print 'exporting data...' db.export_to_csv_file(open('tmp.sql','wb')) print 'importing data...' other_db.import_from_csv_file(open('tmp.sql','rb')) other_db.commit() print 'done!' if __name__() == "__main__": main() -
使用以下命令运行此文件(只运行一次,否则你会得到重复的记录):
python web2py.py -S app -M -N -R applications/app/private/mover.py -
修改模型
db.py,并修改以下:production=False到以下:
production=True
还有更多...
实际上,web2py 附带以下脚本:
script/cpdb.py
此脚本使用命令行选项执行任务和变体。阅读文件以获取更多信息。
从现有的 MySQL 和 PostgreSQL 数据库创建模型
通常需要从 web2py 应用程序访问现有的数据库。在某些条件下这是可能的。
准备工作
为了连接到现有的数据库,它必须是被支持的数据库。在撰写本文时,这包括MySQL、PostgreSQL、MSSQL、DB2、Oracle、Informix、FireBase和Sybase。您必须知道数据库类型(例如mysql或postgres),数据库名称(例如,mydb),以及数据库服务器运行的主机名和端口号(例如mysql的127.0.0.1:3306或postgres的127.0.0.1:5432)。您必须有一个有效的用户名和密码来访问数据库。总之,您必须知道以下 URI 字符串:
-
mysql://username:password@127.0.0.1:3306/mydb -
postgres://username:password@127.0.0.1:5432/mydb
假设您可以连接到此数据库,您只能访问满足以下条件的表:
-
每个要访问的表都必须有一个唯一的自增整数主键(无论是否称为
id)。对于 PostgreSQL,您也可以有复合主键(由多个字段组成),并且不一定必须是SERIAL类型(参见 web2py 书籍中的键表)。 -
记录必须通过其主键进行引用。
-
web2py 模型必须为每个要访问的表包含一个
define_table语句,列出所有字段及其类型。
在以下内容中,我们还将假设您的系统支持使用mysql命令本地访问数据库(以提取 MySQL 模型),或者您的系统已安装了psycopg2 Python 模块(以提取 PostgreSQL 模型,请参阅安装说明)。
如何做到这一点...
-
首先,您需要查询数据库,并制定一个与数据库内容兼容的可能模型。这可以通过运行以下随 web2py 提供的脚本完成:
-
要从 MySQL 数据库构建 web2py 模型,请使用:
python scripts/extract_mysql_models.py username:password@databasename > db1.py -
要从 PostgreSQL 数据库构建 web2py 模型,请使用:
python scripts/extract_pgsql_models.py databasename localhost 5432 username password > db1.py这些脚本并不完美,但它们将生成一个 db1.py 文件,描述数据库表。
-
-
编辑此模型以删除您不需要访问的表。改进字段类型(例如,字符串字段可能是密码),并添加验证器。
-
然后将此文件移动到您的应用程序的
models/文件夹中。 -
最后,编辑原始的
db.py模型,并用此数据库的 URI 字符串替换它。- 对于 MySQL,请编写:
db = DAL('mysql://username:password@127.0.0.1:8000/databasename', migrate_enabled=False, pool_size=20)- 对于 PostgreSQL,请编写:
db = DAL( "postgres://username:password@localhost:5432/databasename", migrate_enabled=False, pool_size=10) migrate = False # you can control migration per define_table我们禁用了所有迁移,因为表已经存在,web2py 不应该尝试创建或修改它。
不幸的是,访问现有的数据库是 web2py 中最棘手的任务之一,因为数据库不是由 web2py 创建的,web2py 需要做出一些猜测。解决这些问题的唯一方法是手动编辑模型文件,并使用对数据库内容的独立知识。
更多...
实际上,extract_pgsql_models.py还具有以下附加功能:
-
它使用 ANSI 标准
INFORMATION_SCHEMA(这可能与其他 RDBMS 一起工作)。 -
它检测具有
id作为其主键的键表(没有id)。 -
它直接连接到正在运行的数据库,因此不需要进行 SQL 转储。
-
它处理
notnull, unique和引用约束。 -
它检测最常见的数据类型和默认值。
-
它支持 PostgreSQL 列注释(即,用于文档)。
如果您必须使用它来对抗支持 ANSI INFORMATION_SCHEMA的其他 RDBMS(例如,MSSQL Server),则导入并使用适当的 Python 连接器,并删除特定的postgreSQL查询(pg_ tables用于注释)。
注意
您不能在普通自增主键表(type='id')和键表(primarykey=['field1','field2'])之间混合引用。如果您在数据库中使用两者,您必须在 web2py 模型中将键表手动定义为自增主键(移除id类型,并将主键参数添加到define_table)。
高效的标签搜索
无论您是在构建社交网络、内容管理系统还是 ERP 系统,您最终都需要记录标记的能力。这个配方向您展示了一种通过标签高效搜索记录的方法。
准备工作
在这里,我们假设以下两个模型:
-
包含数据的模型:
db.define_table('data', Field('value')) -
存储标签的模型:
db.define_table('tag', Field('record_id', db.data), Field('name'))
这里,name是标签名称。
如何做到这一点...
-
我们想搜索列表中至少有一个标签的所有记录:
tags = [...]为了这个目的,我们创建了一个搜索函数:
def search_or(data=db.data, tag=db.tag, tags=[]): rows = db(data.id==tag.record_id)\ (tag.name.belongs(tags)).select( data.ALL, orderby=data.id, groupby=data.id, distinct=True) return rows -
同样,如果您想搜索具有所有标签的记录(而不是列表中的任何一个):
def search_and(data=db.data,tag=db.tag,tags=[]): n = len(tags): rows = db(data.id==tag.record_id)\ (tag.name.belongs(tags)).select( data.ALL, orderby=data.id, groupby=data.id, having=data.id.count()==n) return rows
注意,这两个函数适用于任何作为第一个参数传递的表。
在这两个函数中,查询涉及两个表。
data.id==tag.record_id
web2py 将其解释为连接。
更多...
这个系统如果用户可以自由选择标签名称,效果很好。有时,您可能希望将标签限制在定义良好的集合中。在这种情况下,模型需要更新:
db.define_table('data', Field('value'))
db.define_table('tag', Field('name', unique=True))
db.define_table('link', Field('record_id',db.data), Field('tag_id',db.
tag))
这里,链接表实现了数据记录和标签项之间的多对多关系。
在这种情况下,我们需要修改我们的搜索函数,因此首先我们将标签名称列表(tags)转换为标签 ID 列表,然后执行之前的查询。这可以通过使用subquery:来完成。
def search_or(data=db.data, tag=db.tag,link=db.link,tags=[]):
subquery = db(db.tag.name.belongs(tags)).select(db.tag.id)
rows = db(data.id==link.record_id)\
(link.tag_id.belongs(subquery)).select(
data.ALL,
orderby=data.id,
groupby=data.id,
distinct=True)
return rows
def search_and(data=db.data, tag=db.tag, link=db.link, tags=[]):
n = len(tags)
subquery = db(db.tag.name.belongs(tags)).select(db.tag.id)
rows = db(data.id==link.record_id)\
(link.tag_id.belongs(subquery)).select(
data.ALL,
orderby=data.id,
groupby=data.id,
having=data.id.count()==n)
return rows
我们在这里实施的技术被称为Toxi方法,并在以下链接中以更通用和抽象的方式描述:
www.pui.ch/phred/archives/2005/04/tags-database-schemas.html.
从多个应用程序访问你的数据库
构建分布式应用程序的一种方法,是让多个应用程序可以访问相同的数据库。不幸的是,这不仅仅是连接到数据库的问题。实际上,不同的应用程序需要了解表内容和其他元数据,这些元数据存储在模型定义中。
有三种方法可以实现这一点,它们并不等价。这取决于应用程序是否共享文件系统,以及你希望给予两个应用程序多少自主权。
准备工作
我们假设你已经有两个 web2py 应用程序,一个叫做app1,另一个叫做app2,其中app1通过以下方式连接到数据库:
db = DAL(URI)
在这里,URI 是一些连接字符串。无论是 SQLite 还是客户端/服务器数据库,这都无关紧要。我们还将假设app1使用的模型存储在models/db1.py中,尽管这里的名称并不重要。
现在我们希望app2连接到同一个数据库。
如何操作...
这也是一个常见的场景,你希望两个应用程序是自主的,尽管能够共享数据。自主意味着你希望能够独立分发每个应用程序,而无需另一个应用程序。
如果是这样的话,每个应用程序都需要自己的模型副本和自己的数据库元数据。实现这一点的唯一方法是通过代码的复制。
你必须遵循以下步骤:
-
编辑
app2的 URI 字符串,使其看起来与app1相同,但禁用迁移:db = DAL(URI, migrate_enabled=False) -
将
app1中的模型文件models/d1.py复制到app2中。
注意,只有app1能够执行迁移(如果两个都能做,情况会变得非常混乱)。如果你在app1中更改模型,你必须再次复制模型文件。
虽然这个解决方案打破了不要重复自己(DRY)模式,但它保证了每个应用程序的完全自主性,即使它们在不同的服务器上运行,也可以访问相同的数据库。
如果两个应用程序运行在同一台服务器上,你不需要复制模型文件,只需创建一个符号链接即可:
ln applications/app1/models/db1.py applications/app2/models/db1.py
现在你只有一个模型文件。
还有更多...
有时候你需要一个脚本(而不是一个网络应用程序)来访问 web2py 模型。这可以通过仅访问元数据来实现,而不需要执行实际的模型文件。
这里有一个可以做到这一点的 Python 脚本(而不是 web2py 模型):
# file myscript.py
from gluon.dal import DAL
db = DAL(URI, folder='/path/to/web2py/applications/app1', auto_
import=True)
print db.tables
# add your code here
注意auto_import=True。它告诉 DAL 在指定的文件夹中查找与 URI 连接关联的元数据,并在内存中动态重建模型。以这种方式定义的模型具有正确的名称和字段类型,但它们将不具有其他属性的正确值,例如可读性、可写性、默认值、验证器等。这是因为这些属性不能在元数据中进行序列化,并且在这个场景中可能也不需要。
层次化分类树
任何应用程序迟早都需要一种对数据进行分类的方法,并且类别必须以树的形式存储,因为每个类别都有一个父类,可能还有子类别。没有子类别的类别是树的叶子。如果有没有父类的类别,我们创建一个虚构的根树节点,并将它们作为根的子类别附加。
主要问题是如何在数据库表中存储具有父子关系的类别,以及高效地添加节点和查询节点的祖先和后代。
这可以通过修改的先序树遍历算法来实现,如下所述。
如何操作...
关键技巧是将每个节点存储在其自己的记录中,带有两个整数属性,左和右,这样所有祖先的左属性都小于或等于当前节点的左属性,而右属性大于当前节点的右属性。同样,所有后代都将有一个大于或等于当前左的左属性和小于当前右的右属性。在公式中:
如果 A.ileft<=B.ileft 且 A.iright>B.iright,则 A 是 B 的父类。
注意到 A.iright - A.ileft 总是后代的数量。
以下是一个可能的实现:
from gluon.dal import Table
class TreeProxy(object):
skeleton = Table(None,'tree',
Field('ileft','integer'),
Field('iright','integer'))
def __init__(self,table):
self.table=table
def ancestors(self,node):
db = self.table._db
return
db(self.table.ileft<=node.ileft)(self.table.iright>node.iright)
def descendants(self,node):
db = self.table._db
return
db(self.table.ileft>=node.ileft)(self.table.iright<node.iright)
def add_leaf(self,parent_id=None,**fields):
if not parent_id:
nrecords = self.table._db(self.table).count()
fields.update(dict(ileft=nrecords,iright=nrecords))
else:
node = self.table(parent_id)
fields.update(dict(ileft=node.iright,iright=node.iright))
node.update_record(iright=node.iright+1)
ancestors = self.ancestors(node).select()
for ancestor in ancestors:
ancestor.update_record(iright=ancestor.iright+1)
ancestors = self.ancestors(node).select()
for ancestor in ancestors:
ancestor.update_record(iright=ancestor.iright+1)
return self.table.insert(**fields)
def del_node(self,node):
delta = node.iright-node.ileft
deleted = self.descendants(node).delete()
db = self.table._db
db(self.table.iright>node.iright).
update(iright=self.table.iright-delta)
del self.table[node.id]
return deleted + 1
这允许我们执行以下操作:
-
定义自己的树表(mytree)和代理对象(treeproxy):
treeproxy = TreeProxy(db.define_table('mytree',Field('name'),Tree.skeleton)) -
插入一个新的节点:
id = treeproxy.add_leaf(name="root") -
添加一些节点:
treeproxy.add_leaf(parent_id=id,name="child1") treeproxy.add_leaf(parent_id=id,name="child2") -
搜索祖先和后代:
for node in treeproxy.ancestors(db.tree(id)).select(): print node.name for node in treeproxy.descendants(db.tree(id)).select(): print node.name -
删除一个节点及其所有后代:
treeproxy.del_node(db.tree(id))
按需创建记录
我们通常需要根据条件获取或更新记录,但记录可能不存在。如果记录不存在,我们希望创建它。在这个菜谱中,我们将展示两个可以满足此目的的实用函数:
-
get_or_create -
update_or_create
为了使这可行,我们需要传递足够的 field:value 对来创建缺失的记录。
如何操作...
-
这里是
get_or_create的代码:def get_or_create(table, **fields): """ Returns record from table with passed field values. Creates record if it does not exist. 'table' is a DAL table reference, such as 'db.invoice' fields are field=value pairs """ return table(**fields) or table.insert(**fields)注意如何通过表(**字段)选择与请求字段匹配的记录,如果记录不存在则返回 None。在这种情况下,将插入记录。然后,table.insert(...) 返回插入记录的引用,对于实际目的来说,就是获取刚刚插入的记录。
-
这里是一个使用示例:
db.define_table('person', Field('name')) john = get_or_create(db.person, name="John") -
update_or_create的代码非常相似,但我们需要两组变量— 用于 搜索(在更新之前)的变量和用于 更新的变量:def update_or_create(table, fields, updatefields): """ Modifies record that matches 'fields' with 'updatefields'. If record does not exist then create it. 'table' is a DAL table reference, such as 'db.person' 'fields' and 'updatefields' are dictionaries """ row = table(**fields) if row: row.update_record(**updatefields) else: fields.update(updatefields) row = table.insert(**fields) return row -
这里是一个使用示例:
tim = update_or_create(db.person, dict(name="tim"), dict(name="Tim"))
OR、LIKE、BELONGS 以及更多在 Google App Engine 上的应用
Google App Engine (GAE) 的一个主要限制是无法执行使用 OR、BELONGS(IN) 和 LIKE 操作符的查询。
web2py DAL 提供了一个抽象数据库查询的系统,它不仅适用于 关系数据库 (RDBS),也适用于 GAE,但仍然受到前面提到的限制。这里我们展示了些解决方案。
我们创建了一个额外的 API,允许在从 GAE 存储中提取记录后在 web2py 层面上合并、过滤和排序记录。它们可以用来模拟缺失的功能,并将您的 GAE 代码也移植到 RDBS。
当前支持的 RDBS 是 SQLite、MySQL、PostgreSQL、MSSQL、DB2、Informix、Oracle、FireBird 和 Ingres。
GAE 是目前唯一支持的 NoDB。其他适配器正在开发中。
准备工作
在以下菜谱中,我们计划开发一个在 GAE 上运行的应用程序,并使用以下逻辑连接到数据库:
if request.env.web2py_runtime_gae:
db = DAL('google:datastore')
else:
db = DAL('sqlite://storage.sqlite')
我们假设以下模型作为示例:
product = db.define_table('product',
Field('name'),
Field('price','double'))
buyer = db.define_table('buyer',
Field('name'))
purchase = db.define_table('purchase',
Field('product',db.product),
Field('buyer',db.buyer),
Field('quantity','integer'),
Field('order_date','date',default=request.now))
如何做到这一点...
在设置我们之前描述的 GAE 模型之后,让我们看看如何在以下部分中执行插入和更新记录、执行连接和其他操作。
记录插入
为了测试其余的代码,您可能想在表中插入一些记录。您可以使用 appadmin 或以编程方式完成此操作。以下代码在 GAE 上运行良好,但有警告,即 insert 方法返回的 ID 在 GAE 上不是顺序的:
icecream = db.product.insert(name='Ice Cream',price=1.50)
kenny = db.buyer.insert(name='Kenny')
cartman = db.buyer.insert(name='Cartman')
db.purchase.insert(product=icecream,buyer=kenny,quantity=1,
order_date=datetime.datetime(2009,10,10))
db.purchase.insert(product=icecream,buyer=cartman,quantity=4,
order_date=datetime.datetime(2009,10,11))
记录更新
GAE 上的 update 操作与您预期的正常操作一样。两种语法都受支持:
icecream.update_record(price=1.99)
以及:
icecream.price=1.99
icecream.update_record()
连接
在关系数据库中,您可以执行以下操作:
rows = db(purchase.product==product.id)
(purchase.buyer==buyer.id).select()
for row in rows:
print row.product.name, row.product.price,
row.buyer.name, row.purchase.quantity
这会产生以下结果:
Ice Cream 1.99 Kenny 1
Ice Cream 1.99 Cartman 4
这在 GAE 上不起作用。您必须在不使用连接的情况下执行查询,使用递归 selects。
rows = db(purchase.id>0).select()
for row in rows:
print row.product.name, row.product.price, row.buyer.name,
row.quantity
在这里,row.product.name 执行递归 selects,并获取由 row.product. 引用的产品的名称。
逻辑 OR
在 RDBS 上,您可以使用 — 操作符在查询中实现 OR:
rows = db((purchase.buyer==kenny)|(purchase.buyer==cartman)).select()
这在 GAE 上不起作用,因为不支持 OR 操作(在撰写本文时)。如果查询涉及相同的字段,可以使用 IN 操作符:
rows = db(purchase.buyer.contains((kenny,cartman))).select()
这是一个便携且高效的解决方案。在最一般的情况下,您可能需要在 web2py 层面上而不是在数据库层面上执行 OR 操作。
rows_kenny = db(purchase.buyer==kenny).select()
rows_cartman = db(purchase.buyer==cartman).select()
rows = rows_kenny|rows_cartman
在这种后一种情况下,— 不是在查询之间,而是在行对象之间,并且是在记录检索之后执行的。这带来了一些问题,因为原始顺序丢失了,并且由于增加了内存和资源消耗的惩罚。
带有 orderby 的 OR
在关系数据库中,您可以执行以下操作:
rows = db((purchase.buyer==kenny)|(purchase.buyer==cartman))\
.select(orderby=purchase.quantity)
但是,再次在 GAE 上,您必须在 web2py 层面上执行 OR 操作。因此,您还必须在 web2py 层面上进行排序:
rows_kenny = db(purchase.buyer==kenny).select()
rows_cartman = db(purchase.buyer==cartman).select()
rows = (rows_kenny|rows_cartman).sort(lambda row:row.quantity)
rows 对象的 sort 方法接受一个行函数,并必须返回一个用于排序的表达式。它们也可以与 RDBS 一起使用以实现排序,当表达式过于复杂而无法在数据库级别实现时。
带有复杂 orderby 的 OR
考虑以下涉及 OR、JOIN 和排序的查询,并且仅在 RDBS 上工作:
rows = db((purchase.buyer==kenny)|(purchase.buyer==cartman))\
(purchase.buyer==buyer.id).select(orderby=buyer.name)
您可以使用 sort 方法以及 sort 参数中的递归 select 来重写它:
rows = (rows_kenny|rows_cartman).sort( \
lambda row:row.buyer.name)
这可以工作,但可能效率不高。您可能希望缓存 row.buyer 到 buyer_names: 的映射。
buyer_names = cache.ram('buyer_names',
lambda:dict(*[(b.id,b.name) for b in db(db.buyer).select()]),
3600)
rows = (rows_kenny|rows_cartman).sort(
lambda row: buyer_names.get(row.buyer,row.buyer.name))
在这里,buyer_names 是 ids 和 names 之间的映射,并且每小时(3600 秒)缓存一次。sort 尝试从 buyer_names 中选择名称,如果可能的话,否则执行递归选择。
LIKE
在关系数据库中,例如,你可以搜索所有以字母 C 开头后跟任何内容(%)的记录:
rows = db(buyer.name.like('C%')).select()
print rows
但 GAE 既不支持全文搜索,也不支持类似 SQL LIKE 操作符的任何内容。再一次,我们必须选择所有记录并在 web2py 层面上执行过滤。我们可以使用 rows 对象的 find 方法:
rows = db(buyer.id>0).select().find(lambda
row:row.name.startswith('C'))
当然,这很昂贵,不推荐用于大型表(超过几百条记录)。如果这种搜索对你的应用程序至关重要,也许你不应该使用 GAE。
日期和日期时间操作
对于涉及其他表达式(如日期和日期时间操作)的查询,也会出现相同的问题。考虑以下在关系数据库上工作但在 GAE 上不工作的查询:
rows = db(purchase.order_date.day==11).select()
在 GAE 上,你必须将其重写如下:
rows = db(purchase.id>0).select().find(lambda
row:row.order_date.day==11)
用数据库视图替换慢速虚拟字段
考虑以下表:
db.define_table('purchase', Field('product'),
Field('price', 'double'),
Field('quantity','integer'))
你需要添加一个字段,称为 total price,在检索记录时计算,定义为每个记录的价格乘以数量。
正常的做法是使用 虚拟字段:
class MyVirtualFields(object):
def total_price(self):
return self.purchase.price * self.purchase.quantity
db.purchase.virtualfields.append(MyVirtualFields())
然后,你可以执行以下操作:
for row in db(db.purchase).select():
print row.name, row.total_price
这是可以的,但在 web2py 层面上计算虚拟字段可能会很慢。此外,你可能不习惯在查询中涉及虚拟字段。
这里我们提出了一种替代方案,该方案涉及为表创建一个数据库视图,该视图包括包含计算字段的列,并为 web2py 提供了访问它的方式。
如何做...
给定表,执行以下操作:
if not db.executesql("select * from information_schema.tables where
table_name='purchase_plus' limit 1;"):
db.executesql("create view purchase_plus as select purchase.*,
purchase.price * purchase.quantity as total_price from purchase")
db.define_table('purchase_plus', db.purchase, Field('total_price',
'double'),
migrate=False)
现在,你可以在任何使用 db.numbers_plus 的地方使用 db.purchase_plus,除了插入操作,与 VirtualFields 解决方案相比,性能有所提升。
它是如何工作的...
以下行检查视图是否已经创建:
if not db.executesql("select ...")
如果没有,它指示数据库创建它:
db.executesql("create view ...")
最后,它定义了一个新的 web2py 模型,该模型映射到表:
db.define_table('purchase_plus',...)
这个模型包括 db.purchase 表中的所有字段,新的字段 total_price,并将 migrate=False 设置为,这样 web2py 就不会尝试创建表(它不应该尝试创建,因为这个不是新表,而是一个视图,并且已经创建)。
还有更多...
注意,并非所有支持的数据库都支持视图,并且并非所有支持视图的数据库都有 information_schema.tables。因此,这个菜谱不能保证在所有支持的数据库上都能工作,并且会使你的应用程序不可移植。
第四章. 高级表单
在本章中,我们将介绍以下配方:
-
在表单中添加取消按钮
-
在表单提交时添加确认
-
动态搜索数据
-
在一页中嵌入多个表单
-
检测和阻止并发更新
-
创建表单向导
-
临时去规范化数据
-
移除表单标签
-
使用
fileuploader.js -
使用
LOADed组件上传文件 -
从上传的图像创建图像缩略图
-
监控上传进度
-
表单中的自动提示
-
颜色选择器小部件
-
缩短文本字段
-
创建多表单
-
创建具有引用的多表单
-
创建多表更新表单
-
星级评分小部件
简介
Web2py 提供了强大的表单生成功能。在本章中,我们提供了从添加按钮到创建自定义表单小部件的表单定制的示例。我们还提供了复杂表单的示例,例如向导和多表单。
在表单中添加取消按钮
这个配方解释了一种向表单添加取消按钮的方法,即不提交表单、忽略任何更改并返回到上一页(或根据设置继续到下一页)的按钮。取消按钮实际上是此处描述的更一般机制的特殊情况,用于向你的表单添加按钮。
准备工作
我们的配方假设了一个通用模型。
如何操作...
-
控制器使用以下语句构建表单和按钮:
form=SQLFORM(db.mytable, record=mytable_index, deletable=True, submit_button=T('Update')) -
你可以使用以下语句添加按钮:
form[0][-1][1].append(TAG.BUTTON('Cancel', _onclick="document.location='%s';"%URL('index')))最后一行显示了如何将“取消”按钮添加到表单中,这就像将按钮附加到表单一样简单。
SQLFORM的索引,你在其中选择附加(或插入)你的取消按钮,决定了你的按钮将在你的页面上出现的位置。这里,表单
[0]是表单内的 TABLE。form[0][-1]是最后一个 TR。form[0][-1][1]是第一列(最后一个 TR 中的第二个 TD)。_onclick参数将用户带到window.location=语句右侧指定的 URL。 -
将“取消”按钮放在“提交”按钮之后的等效表示法将是:
form.element('input[type=submit]').parent.append(TAG.BUTTON(...))这里,元素方法部分接受 CSS3 语法。
-
通常,可以使用相同的机制将任何类型的按钮添加到表单中。
如果你更喜欢对创建你的“取消”按钮或其他按钮有更多控制和透明度,那么自定义视图可能是有序的。然而,你不得将此方法与已附加到表单中混合使用。以下示例显示了一个自定义表单,表单是通过以下方式创建的:
form=SQLFORM.factory(db.mytable)
示例假设了一个具有编号为 1 到 N 的字段的通用表。
{{=form.custom.begin}}
{{=form.custom.widget.field1}}
{{=form.custom.widget.field2}}
{{=form.custom.widget.field3}}
{{=form.custom.submit}}
{{=TAG.BUTTON(T('Cancel'), _onclick='...')}}
{{=form.custom.end}}
在这里,field1...field3 必须是实际的字段名称。再次强调,_onclick 动作可以是任何类型和风味。
在表单提交时添加确认
经常,你需要再次确认用户不是意外提交了错误表单。你可以通过在用户点击提交按钮时提示用户确认来实现这一点。这可以通过两种方式完成。
如何操作...
-
一种方法是通过使用
jQuery仅编辑渲染表单的视图。在视图中添加以下代码:<script> jQuery(function(){ jQuery('input[type=submit]').click( function(){return confirm('Are you sure?'); }); }); </script>这里,
confirm是一个 JavaScript 函数,它指示浏览器创建一个确认对话框。如果你按下[是],onclick函数返回 true,表单将被提交。如果你按下[否],onclick函数返回 false,表单将不会被提交。 -
同样,你可以在创建表单时将字符串添加到按钮的
onclick属性中来实现这一点。return confirm('Are you sure?') -
在 web2py 中,有一个简单的方法来做这件事:
def my_action(): form = SQLFORM.factory(...) form.element('input[type=submit]')['_onclick'] = "return confirm('Are you sure?');" return dict(form=form)
注意我们如何在服务器端(在表单实际在 HTML 中渲染之前)使用jQuery语法获取form.element(...),并修改其onclick属性(使用带有前导下划线的 web2py 表示法)。
动态搜索数据
Web2py 自带了一个crud.search机制,允许你执行以下操作:
def index():
form, results = crud.search(db.things)
return dict(form=form, results=results)
在这里,form是一个搜索表单,records是搜索的结果。为了理解它是如何工作的,我们在这个菜谱中提供了一个简化版的函数实现,你可以根据你的需求进一步自定义。在这里,db.things是一个包含我们的东西的表。实际的表名或其结构在这里并不重要。
如何做...
-
首先,创建一个新的模型,例如
dynamic_search.py,并将以下代码添加到其中:def build_query(field, op, value): if op == 'equals': return field == value elif op == 'not equal': return field != value elif op == 'greater than': return field > value elif op == 'less than': return field < value elif op == 'starts with': return field.startswith(value) elif op == 'ends with': return field.endswith(value) elif op == 'contains': return field.contains(value) def dynamic_search(table): tbl = TABLE() selected = [] ops = ['equals','not equal','greater than','less than', 'starts with','ends with','contains'] query = table.id > 0 for field in table.fields: chkval = request.vars.get('chk'+field,None) txtval = request.vars.get('txt'+field,None) opval = request.vars.get('op'+field,None) row = TR(TD(INPUT(_type="checkbox",_name="chk"+field, value=chkval=='on')), TD(field),TD(SELECT(ops,_name="op"+field, value=opval)), TD(INPUT(_type="text",_name="txt"+field, _value=txtval))) tbl.append(row) if chkval: if txtval: query &= build_query(table[field], opval,txtval) selected.append(table[field]) form = FORM(tbl,INPUT(_type="submit")) results = db(query).select(*selected) return form, results -
现在,你可以使用
dynamic_search作为crud.search的替代品。def index(): form,results = dynamic_search(db.things) return dict(form=form,results=results)我们可以用以下视图来渲染它:
{{extend 'layout.html'}} {{=form}} {{=results}}它看起来是这样的:

在一个页面上嵌入多个表单
这个菜谱解释了如何在页面上嵌入多个表单。这样做可以通过减少 HTTP 调用来提高用户的生产力,但可能会增加页面布局的杂乱。
如何做...
-
为了说明具有多个表单的页面,我们创建了一个用于存储个人教育简历(CV)的简化系统。我们首先定义了学校、学生和他们所获得的学位的表。
YEARS = range(1910, 2011) DEGREES = ('BA', 'BS', 'MA', 'MS', 'MBA', 'JD', 'PhD') db.define_table('school', Field('name', 'string', unique=True), Field('address', 'string'), Field('established', 'integer', requires=IS_IN_SET(YEARS)), format='%(name)s') db.define_table('student', Field('name', 'string', unique=True), Field('birthday', 'date'), format='%(name)s') db.define_table('education', Field('student', db.student), Field('school', db.school), Field('degree', 'string', requires=IS_IN_SET(DEGREES)), Field('graduated', 'integer', requires=IS_IN_SET(YEARS))) -
index()控制器为每个表创建一个表单:def index(): student_form = SQLFORM(db.student) if student_form.accepts(request, session): response.flash = 'Student Form Accepted' elif student_form.errors: response.flash = 'Form has errors' school_form=SQLFORM(db.school) if school_form.accepts(request, session): redirect(URL('index')) response.flash = 'School Form Accepted' elif school_form.errors: response.flash = 'Form has errors' education_form=SQLFORM(db.education) if education_form.accepts(request, session): response.flash = 'Education Form Accepted' elif education_form.errors: response.flash = 'Form has errors' return locals() -
在典型的 web2py 控制器中,你只会看到一个
form=SQLFORM(...)语句和一个if form.accepts(...)子句。由于我们需要渲染和处理三个表单,我们需要三个SQLFORM(...)语句和三个if specific_form.accepts(...)子句。每个表单都必须有一个唯一的名称,以便当一个表单被 POST 时,其相应的form.accepts子句将被触发。注意,关于包含其他表引用的表格的表单必须按照依赖关系的顺序定义和处理。因此,如果添加了新的
school或新的student,它将显示education表单的下拉菜单。在单个页面上显示所有三个表单的最简单视图如下所示:
{{extend 'layout.html'}} <h2>Education CV</h2> <div id='form1'>{{=education_form}}</div> <h2>Student</h2> <div id='form1'>{{=student_form}}</div> <h2>School</h2> <div id='form1'>{{=school_form}}</div>
如果两个或多个表单与同一表相关,必须将formname参数传递给accepts,并且对于两个表单必须不同。
更多...
另一个选项是使用 LOAD 命令实现主(索引)页面中加载的不同表单和组件。请注意,提交 education 表单不会影响其他两个,而其他两个会影响 education 表单中的下拉列表。这允许我们为每个表单创建不同的操作:
def index():
return dict()
def create_student():
return crud.create(db.student, message='Student Form Accepted')
def create_school():
return crud.create(db.school, message='School Form Accepted')
def create_education():
return crud.create(db.education, message='Education Form Accepted')
视图 views/default/index.html 包含了三个表单,并捕获了 education 表单,因此当提交此表单时,其他两个表单不会被处理和重新加载:
{{extend 'layout.html'}}
<h2>Education CV</h2>
<div id='form1'>
{{=LOAD('default','create_eduction',ajax_trap=True)}}
</div>
<h2>Student</h2>
<div id='form1'>{{=LOAD('default', 'create_student')}}</div>
<h2>School</h2>
<div id='form1'>{{=LOAD('default', 'create_school')}}</div>
可以使用 FORM、SQLFORM.factory 和 crud 语句,或者所有表单生成语句的组合来创建多表单页面。可以将自定义表单与自动生成的表单混合使用。使用 web2py 生成美观的表单输入页面具有无限的灵活性。
检测和阻止并发更新
例如,考虑一个维基页面。你打开页面,编辑它,并保存它。在你编辑页面的同时,可能有人访问了同一页面,并在你之前保存了页面的新版本。你的保存操作将导致之前的编辑丢失。
当然,你可以通过实现锁定机制来防止并发编辑,但正确实现这样的机制是困难的。如果用户打开一个页面进行编辑,然后关闭浏览器并忘记它,会发生什么?其他人将无法编辑同一页面。实现超时机制会重新引入原始问题。
有一个简单的解决方案。每次你保存一个页面(或任何相关的记录)时,请让 web2py 检查自记录最初检索以来,原始记录是否在服务器上被修改。
在 web2py 中,这很容易,我们将在本菜谱中解释。
准备工作
我们将以以下模型为例来考虑一个应用:
db.define_table('page', Field('title', notnull=True), Field('body'))
以及以下编辑表单:
def edit():
page = db.page(request.args(0))
form = SQLFORM(db.page,page)
if form.accepts(request,session):
response.flash = "page saved"
return dict(form=form)
如何做到这一点...
-
你需要做的只是向
form.accepts传递一个额外的属性detect_record_change,并检查记录是否已更改:def edit(): page = db.page(request.args(0)) form = SQLFORM(db.page,page) if form.accepts(request,session, detect_record_change=True): response.flash = "page saved" elif form.record_changed: response.flash = "page not saved because changed on server" return dict(form=form) -
在
record-changed事件中,你可以编写自己的逻辑来处理冲突。服务器上的数据始终在页面中(page.title和page.body);提交的值在request.vars.title和request.vars.body中。
更多内容...
那么 crud 表单怎么办?实际上,crud.create 和 crud.update 表单默认具有 detect_record_change=True(而正常的 SQLFORMs 默认为 False)。因此,如果服务器上的记录被修改,新提交的值不会被保存。然而,crud 表单不提供任何处理这种情况的逻辑,而是将其留给开发者。例如,你可以使用 crud 重写前面的示例,如下所示:
def edit():
page = db.page(request.args(0))
form = crud.update(db.page,page)
if form.record_changed:
response.flash = "page not saved; try resubmit"
return dict(form=form)
注意,当提交被拒绝,因为服务器上的记录已更改时,第二次提交将成功。
创建表单向导
我们经常需要从用户那里收集信息(例如,为了填充数据库或执行某些其他操作),但我们不希望用一个非常大的表单让用户感到不知所措。一个更好的方法是把表单分成多个页面,用户可以通过一个 [下一步] 按钮来导航。这种做法被称为 向导。
如何做...
-
在这里,我们假设我们想要使用向导来填充
mytable:表中的多个字段db.define_table('mytable', Field('field1'), Field('field2'), ... Field('fieldN'))你有多少字段并不重要。
-
我们可以用一个单独的操作来处理向导。这个操作需要知道有多少步骤,每个步骤要查询哪些字段,以及最后一步之后要去哪里。以下是一个可能的实现:
def wizard(): STEPS = {0: ('field1','field2'), # fields for 1st page 1: ('field3','field4'), # fields for 2nd page 2: ('field5,''field6'), # fields for 3rd page 3: URL('done')} # url when wizard completed step = int(request.args(0) or 0) if not step in STEPS: redirect(URL(args=0)) fields = STEPS[step] if step==0: session.wizard = {} if isinstance(fields,tuple): form = SQLFORM.factory(*[f for f in db.mytable if f.name in fields]) if form.accepts(request,session): session.wizard.update(form.vars) redirect(URL(args=step+1)) else: db.mytable.insert(**session.wizard) session.flash = T('wizard completed') redirect(fields) return dict(form=form,step=step) -
你可以用以下方式渲染向导:
{{extend 'layout.html'}} <h1>Wizard Step {{=step}}</h1> {{=form}}
它是如何工作的...
实际上,这很简单。向导操作从 request.args(0) 获取其页面编号,并在 STEPS 中查找要显示的字段。它使用 SQLFORM.factory 来构建部分表单。完成的数据存储在 session.wizard 中。最后一页不是字段列表的元组,而是一个 URL('done') 字符串。当向导遇到这个条件时,它知道是时候在新的表中插入 session.wizard 变量并将它们重定向到该 URL。注意,每个步骤都会对显示的字段进行验证。
临时去规范化数据
在这个菜谱中,我们考虑菜谱中描述的模型 通过标签高效搜索,我们想要创建插入、更新表单,或者一个允许用户在同一个表单中输入标签的 data 表格。换句话说,我们想要创建一个表单,它可以自动从 data 中填充,并且所有引用此 data 记录的 tag 记录。提交时,表单应更新 data 和 tag 表。
准备工作
我们假设我们的常规应用程序,以及以下模型:
db.define_table('data',Field('value'))
db.define_table('tag',Field('record_id',db.data),Field('name'))
我们还将假设在 controllers/default.py 中的以下函数:
def edit():
record = db.data(request.args(0))
form = crud.update(db.data,record)
return dict(form=form)
如何做...
我们需要分两步来做这件事,每一步都由一个函数表示。一个函数将假设我们有新的标签,删除旧的标签,并存储新的标签。另一个函数将修改 crud 表单并添加一个包含当前标签的输入字段。这两个函数然后可以用来修改我们的原始表单。
def update_tags(form):
db(db.tag.record_id==form.record.id).delete()
new_tags = [tag.strip() for tag in request.vars.tags.split(',')]
for tag in new_tags:
if tag:
db.tag.insert(record_id=form.record.id,name=tag)
def make_taggable(form):
tags = [tag.name for tag in db(db.tag.record_id==form.record.id).
select()]
value = ', '.join(tags)
form.element('table').insert(-2, TR(LABEL('Tags:'),
INPUT(_name='tags', value=value)))
return form
def edit():
record = db.data(request.args(1))
form = make_taggable(crud.update(db.data, record,
onaccept=update_tags))
return dict(form=form)
它是如何工作的...
make_taggable 函数接受一个表单对象(表单总是 FORM 类的派生),并向表单表注入一个包含标签(Tags:)和 INPUT 元素的新行。INPUT 的值默认为包含当前标签的字符串。
当表单提交并被接受时,crud.update 会忽略 request.vars.tags,因为它不是 db.data 表的字段。如果表单被接受,会调用 onaccept 函数,该函数指向 update_tags。这个函数会删除当前的标签并更新它们。
注意,这个机制非常通用,并没有针对db.data表的具体内容。实际上,update_tags和make_taggable这两个函数可以与任何表一起使用,只要它通过一个db.tags表引用,并且通过crud.update和crud.create表单。
还有更多...
如果需要验证标签字段,我们需要进行一些小的调整。我们将假设每个标签名称都需要验证,验证器如下所示:
db.tag.name.requires=IS_MATCH('\w[\w\-\./]+')
即,每个标签至少包含两个字符。第一个必须是字母数字(\w),而后续的可以是字母数字(\w)、破折号(\-)、点(\.)或正斜杠(/)。
为了执行验证,我们需要一个智能验证函数:
def validate_tags(form):
new_tags = [tag.strip() for tag in request.vars.tags.split(',')]
if tag in new_tags:
(value, error) = db.tag.name.validate(tag)
if error:
form.errors['tags'] = error + '(%s)' % value
然后,我们需要在验证时强制调用它:
def edit():
record = db.data(request.args(0))
form = make_taggable(crud.update(db.data,record,
onvalidation=validate_tags,
onaccept=update_tags))
return dict(form=form)
如果所有其他字段都已验证,则调用onvalidation函数。此函数遍历所有标签,并使用db.tag.name验证器进行验证。如果其中之一未通过,错误将存储在form.errors中,它是一个Storage对象。表单错误的存在阻止了表单的接受。当表单渲染时,INPUT(...,_name='tags')对象将从表单中获取错误,并适当地显示它。
移除表单标签
当你使用SQLFORM或 crud 时,生成的表单有标签。你可以使用表单的formstyle属性来决定标签应该如何显示:
-
table3cols(位于输入小部件的左侧) -
table2cols(位于输入小部件的顶部) -
divs(在单独的divs中,没有表格,这样你可以通过坐标定位它们) -
ul(位于输入小部件的左侧,但使用无序列表而不是表格)
有时你只是想隐藏标签。
如何做到这一点...
有两种方法可以做到这一点:
-
一种方法包括生成表单并从表单中移除它们:
db.define_table('mytable',Field('myfield')) def index(): form = SQLFORM(db.mytable) for row in form.element('table'): del row[0] return dict(form=form) -
另一种方法是在视图中使用自定义表单:
{{=form.custom.begin}} <table> <tr> <td>{{=form.custom.widget.myfield}}</td> <td>{{=db.mytable.myfield.comment}}</td> </tr> <tr> <td>{{=form.custom.submit}}</td> </tr> </table> {{=form.custom.end}}
最终效果是相同的。
使用 fileuploader.js
在这个菜谱中,我们将假设你有一个数据库表来存储上传的文件,并且你想要创建一个允许用户使用 Ajax 上传多个文件的接口。fileuploader.js是一个 jQuery 插件,它使用 XHR 上传多个文件,并显示进度条。它在 Firefox 3.6+、Safari 4+和 Chrome 中工作,在其他浏览器中回退到基于隐藏 iframe 的上传。
准备工作
首先,你需要从github.com/valums/file-uploader下载插件,并将文件fileuploader.js放入应用的static/js/目录中。同时,将fileuploader.css放入应用的static/css目录中。
第二种方法是假设你有一个模型,例如以下示例,你将在这里存储上传的文件:
db.define_table('document',
Field('filename', 'upload'),
Field('uploaded_by', db.auth_user))
如何做到这一点...
我们需要在controllers/default.py:中创建以下上传操作:
@auth.requires_login()
def upload_callback():
if 'qqfile' in request.vars:
filename = request.vars.qqfile
newfilename = db.document.filename.store(request.body, filename)
db.document.insert(filename=newfilename,
uploaded_by=auth.user.id)
return response.json({'success': 'true'})
@auth.requires_login()
def upload():
return dict()
upload_callback 动作将在 request.body 中接收一个文件,其名称在 request.vars.qqfile 中。它将重命名该文件,存储它,将新名称插入数据库,并返回成功。而 upload 动作则不做任何事情,但其视图将显示 jQuery 插件:
{{response.files.append(URL(request.application,'static','js/
fileuploader.js'))}}
{{response.files.append(URL(request.application,'static','css/
fileuploader.css'))}}
{{extend 'layout.html'}}
<script>
jQuery(document).ready(function() {
var uploader = new qq.FileUploader({
// pass the dom node (ex. jQuery(selector)[0] for jQuery users)
element: document.getElementById('file-uploader'),
// path to server-side upload script
action: '{{=URL("upload_callback")}}',
sizeLimit: 15000000,
minSizeLimit: 0,
allowedExtensions: ['xls','jpg', 'jpeg', 'pdf',
'txt','doc','htm','html','xml','xmls', 'txt','ppt','png',
'gif'],
// set to true to output server response to console
debug: true,
// events
// you can return false to abort submit
onSubmit: function(id, fileName){},
onProgress: function(id, fileName, loaded, total){},
onComplete: function(id, fileName, responseJSON){},
onCancel: function(id, fileName){},
messages: {
// error messages, see qq.FileUploaderBasic for content
typeError: "{file} {{=T('has invalid extension.')}}
{{=T('Only')}} {extensions} {{=T('are allowed.')}}",
sizeError: "{file} {{=T('is too large, maximum file size
is')}} {sizeLimit}.",
minSizeError: "{file} {{=T('is too small, minimum file size
is')}} {minSizeLimit}.",
emptyError: "{file} {{=T('is empty, please select files again
without it.')}}",
onLeave: "{{=T('The files are being uploaded, if you leave now
the upload will be cancelled.')}}"
},
showMessage: function(message){ alert(message); }
});
});
</script>
<div id="file-uploader">
<noscript>
<p>Please enable JavaScript to use file uploader.</p>
<!-- or put a simple form for upload here -->
</noscript>
</div>
此插件功能非常强大,并且具有许多配置选项。要了解更多信息,请参考其网站:valums.com/ajax-upload/。
结果的截图可以在这里看到:

使用加载的组件上传文件
web2py 允许您以模块化方式设计页面,并使用 Ajax 在页面中加载组件。组件是其自身动作所服务的页面的一部分。例如,组件可以渲染一个表单。组件会捕获表单提交,并在提交时才刷新自身。这种魔法之所以可能,归功于 static/js/web2py_ajax.js 工具和 LOAD 辅助函数。问题是,这种机制在多部分表单中会失效,并且当加载的组件中的表单包含文件 upload 字段时,它将不起作用。
为了解决这个问题,我们需要一个名为 jquery.form.js 的 jQuery 插件。
准备工作
首先,您需要从 github.com/malsup/form/raw/master/jquery.form.js?v2.43 下载所需的 jQuery 插件,并将其放置在 static/js 文件夹中,命名为 jquery.form.js。
我们还将假设以下模型(与前面的食谱相同),但我们将忽略身份验证:
db.define_table('document',
Field('filename','upload',requires=IS_NOT_EMPTY()),
Field('uploaded_by',db.auth_user))
以下控制器:
def index():
return dict()
@auth.requires_signature()
def component_list():
db.document.filename.represent = lambda f,r: f and A('file',_href\
=URL('download',args=f))
return db(db.document).select()
@auth.requires_signature()
def component_form():
db.document.uploaded_by.default = auth.user_id
db.document.uploaded_by.writable = False
form = SQLFORM(db.document)
if form.accepts(request):
response.flash = 'Thanks for filling the form'
response.js = "web2py_component('%s','doc_list');" % \
URL('component_list.load',user_signature=True)
elif form.errors:
response.flash = 'Fill the form correctly'
else:
response.flash = 'Please fill the form'
return dict(form=form)
并且 views/default/index.html:
{{extend 'layout.html'}}
<h1>{{=T("Change the user's image!")}}</h1>
{{=LOAD('default', 'component_list.load', ajax=True,
target='doc_list', user_signature=True)}}
{{=LOAD('default', 'component_form.load', ajax=True,
user_signature=True)}}
对于除我们创建的表单之外的所有表单,这将正常工作。它不会与我们的表单一起工作,因为它包含一个 upload 字段。注意,在这个食谱中,我们使用了 user_signature=True 和 auth.requires_signature() 装饰器。这将确保所有 URL 都被签名,并且我们应用于父页面 index 的任何身份验证/授权都将传播到组件。
如何实现...
-
为了解决这个问题,我们需要两个步骤。首先,我们需要通过在
views/web2py_ajax.html中添加以下行来包含插件:response.files.insert(2,URL('static','js/jquery.form.js')) -
然后,我们需要修改
static/js/web2py_ajax.js,通过添加捕获表单并使用ajaxForm函数处理上传的逻辑,该函数在jqeury.form.js中定义。为了实现这一点,编辑web2py_ajax.js并将函数web2py_trap_form替换为以下内容:function web2py_trap_form(action,target) { jQuery('#'+target+' form').each(function(i){ var form=jQuery(this); if(!form.hasClass('no_trap')) if(form.find('.upload').length>0) { form.ajaxForm({ url: action, success: function(data, statusText, xhr) { jQuery('#'+target).html(xhr.responseText); web2py_trap_form(action,target); web2py_ajax_init(); } }); } else { form.submit(function(e){ jQuery('.flash').hide().html(''); web2py_ajax_page('post',action,form.serialize(),target); e.preventDefault(); }); } }); }它将处理表单上传,仅当表单包含一个上传类输入元素时使用 ajaxForm。
-
然后,我们需要为名为
component_form的动作创建一个视图,称为views/default/component_form.load,它包含以下内容:{{=form}} <script> /* hack because jquery.form.js does not properly passes headers */ jQuery('.flash').hide().html("{{=response.flash}}").slideDown(); eval("{{=XML(response.js or '')}}"); </script>
脚本可能不是必需的,但 ajaxForm 函数没有正确地在服务器之间传递头信息。因此,我们需要在视图中显式包含显示 response.flash 的逻辑,并执行 response.js。
从上传的图片制作图像缩略图
标题已经说明了一切。我们想要上传图片,并从它们中动态创建缩略图。我们将把缩略图的引用存储在相同的记录中,与上传的图片一样。
准备工作
要使用这个食谱,你必须安装Python Imaging Library (PIL)。你可以在以下链接找到它:
www.pythonware.com/products/pil/
这需要从源代码运行 web2py。像 Python 一样,你可以使用easy_install:。
easy_install PIL
或者从与 Debian 兼容的发行版中,以下为:
sudo apt-get install python-imaging
如何做...
为了这个目的,我们将修改在两个先前食谱中使用的模型,添加一个名为thumbnail的字段,并且我们将忽略身份验证,因为它是一个正交问题。
db.define_table('document',
Field('filename','upload'),
Field('thumbnail','upload', readable=False, writable=False))
这里是控制器:
def make_thumbnail(table, image_id, size=(150, 150)):
import os
from PIL import Image
this_image = table(image_id)
im = Image.open(os.path.join(request.folder, 'uploads',
this_image.filename))
im.thumbnail(size, Image.ANTIALIAS)
thumbnail = 'document.thumbnail.%s.jpg' %
this_image.filename.split('.')[2]
im.save(os.path.join(request.folder, 'uploads', thumbnail), 'jpeg')
this_image.update_record(thumbnail=thumbnail)
def uploadimage():
form = SQLFORM(db.document)
if form.accepts(request, session):
response.flash = 'form accepted'
make_thumbnail(db.document,form.vars.id,(175,175))
elif form.errors:
response.flash = 'form has errors'
docs = db(db.document).select()
return dict(form=form,docs=docs)
监控上传进度
在这个食谱中,我们将展示如何创建一个显示进度条并显示上传进度的 JavaScript 小部件。我们的解决方案是基于服务器的,比纯 JavaScript 解决方案更可靠。请注意,没有浏览器可以处理超过 2GB 的文件。
这个食谱基于以下食谱,经过修改以适应 web2py:
www.motobit.com/help/scptutl/pa98.htm
www.djangosnippets.org/snippets/679/
如何做...
-
主要思想是使用
cache.ram在服务器端存储进度,并公开一个查询此变量值的操作。这是通过两个步骤完成的。在第一步中,我们选择一个 X-Progress-ID 键,这样我们就可以稍后检索缓存值:
<form action="http://127.0.0.1:8000/example/upload/post?X- Progress-ID=myuuid"> -
然后我们从
cache.ram:检索上传的总长度:cache.ram("X-Progress-ID:myuuid:length",lambda:0,None)以及当前上传的长度:
cache.ram('X-Progress-ID:myuuid:uploaded', lambda: 0, None)在这里,
myuuid必须被服务器生成的 UUID 替换到所有地方。 -
让我们用一个具体的例子更详细地来做。考虑以下控制器操作在
controllers/default.py:中:def post(): if request.extension=='json' and 'X-Progress-ID' in request.get_vars: cache_key = 'X-Progress-ID:'+request.get_vars['X-Progress-ID'] length=cache.ram(cache_key+':length', lambda: 0, None) uploaded=cache.ram(cache_key+':uploaded', lambda: 0, None) from gluon.serializers import json return json(dict(length=length, uploaded=uploaded)) form = FORM(INPUT(_type='file', _name='file',requires=IS_NOT_EMPTY()), INPUT(_type='submit', _value='SUBMIT')) return dict(form=form, myuuid = "[server generated uuid]")注意,这个操作有两个目的:
-
它创建并处理表单
-
如果使用
.json调用,并传递一个X-Progress-ID,它将返回 json 中的长度和上传变量。
-
-
现在我们需要在
views/default/post.html:中自定义表单:{{extend 'layout.html'}} <script type="text/javascript"> // Add upload progress for multipart forms. jQuery(function() { jQuery('form[enctype="multipart/form- data"]').submit(function(){ // Prevent multiple submits if (jQuery.data(this, 'submitted')) return false; // freqency of update in ms var freq = 1000; // id for this upload so we can fetch progress info. var uuid = ''+Math.floor(Math.random() * 1000000); // ajax view serving progress info var progress_url = '{{ =URL( extension= "json" )}}'; // Append X-Progress-ID uuid form action this.action += ((this.action.indexOf('?') == -1)?'?':'&') + 'X-Progress-ID=' + uuid; var progress = jQuery('<div id="upload-progress" class="upload-progress"></div>').insertAfter( jQuery('input[type="submit"]')).append('<div class="progress-container"> <span class="progress-info">uploading 0%</span> <div class="progress-bar"></div></div>'); jQuery('input[type="submit"]').remove(); // style the progress bar progress.find('.progress-bar').height('1em').width(0); progress.css("background-color", "red"); // Update progress bar function update_progress_info() { progress.show(); jQuery.getJSON(progress_url, {'X-Progress-ID': uuid, 'random': Math.random()}, function(data, status){ if (data) { var progress_coefficient= parseInt(data.uploaded)/parseInt(data.length); var width=progress.find('.progress-container').width(); var progress_width = width * progress_coefficient; progress.find('.progress-bar').width(progress_width); progress.find('.progress-info').text('uploading ' + progress_coefficient*100 + '%'); } window.setTimeout(update_progress_info, freq); }); }; window.setTimeout(update_progress_info, freq); // mark form as submitted. jQuery.data(this, 'submitted', true); }); }); </script> {{=form}}
它是如何工作的...
这里的关键部分如下:
this.action += (this.action.indexOf('?') == -1 ? '?' : '&')
+ 'X-Progress-ID=' + uuid;
它将uuid变量作为GET变量传递。其余的魔法由 web2py 自动完成,它读取这些变量,计算上传程序,并将其存储在cache.ram中。
这些行也很重要:
var progress_url = '{{=URL(extension='json')}}';
jQuery.getJSON(progress_url,
{'X-Progress-ID': uuid, 'random': Math.random()},
...)
它们告诉 web2py 使用相同的 URL,但带有.json扩展名来获取更新进度条所需的长度和上传值。
表单中的自动提示
这个食谱展示了如何通过使用字段的comment属性在通过 Crud 或 SQLFORM 创建的表单中显示提示。
准备工作
首先,你必须填写你想要提示出现的field定义中的comment属性。例如:
db.define_table('board',
Field('message', comment='Let your message here.'))
如果你只做这些,提示将在通过 Crud 或 SQLFORM 生成的表单字段右侧出现。
记住,您可以使用辅助工具在评论中放置 HTML 代码:
db.define_table('recados',
Field('message', comment=SPAN('Let here your ',B('message'))))
如何操作...
您需要使用 jQuery 插件来显示提示,因此您可以 Google 搜索并选择一个。或者您可以使用此链接:jquery.bassistance.de/tooltip/jquery.tooltip.zip。在此处查看其外观:jquery.bassistance.de/tooltip/demo/.
-
从
static/js中提取jquery.tooltip.min.js,并将jquery.tooltip.css放到static/css中分别。 -
编辑您的布局文件,并在
{{include 'web2py_ajax.html'}}之前在 head 中添加以下内容:{{ response.files.append(URL('static','js/jquery.tooltip.min.js')) response.files.append(URL('static','css/jquery.tooltip.css')) }} -
现在您可以在您想要工具提示的每个页面上使用此脚本:
<script type="text/javascript"> jQuery(function() { // iterates over all form widgets jQuery(".w2p_fw").each(function (){ // set title for the widget taken from the comment column jQuery(this).attr('title',jQuery(this).next().html()); // clear the comment (optional) jQuery(this).next().html(''); // create the tooltip with title attribute set jQuery(this).tooltip(); }); }); </script>您的评论列将被转换为漂亮的工具提示。
您也可以将此脚本包含在web2py_ajax.html或layout.html中,以重用代码。或者您可以将此代码放在另一个文件中,并在需要时包含它;也许这是一种更好的方法。
颜色选择器小部件
如果您有一个应该包含颜色(红色、绿色、#ff24dc 等)的表字段,您可能希望有一个小部件来表示该字段,允许您通过从颜色画布中选择颜色来更改/选择颜色。这里我们向您展示如何构建这样一个小部件。
准备工作
您需要从www.bertera.it/software/web2py/mColorPicker-w2p.tgz下载mColorPicker,并在您应用程序的static/文件夹中解压缩。
如何操作...
-
在文件
models/plugin_colorpicker.py中定义小部件:class ColorPickerWidget(object): """ Colorpicker widget based on http://code.google.com/p/mcolorpicker/ """ def __init__ (self, js = colorpicker_js, button=True, style="", transparency=False): import uuid uid = str(uuid.uuid4())[:8] self._class = "_%s" % uid self.style = style if transparency == False: self.transparency = 'false' else: self.transparency = 'true' if button == True: self.data = 'hidden' if self.style == "": self.style = "height:20px;width:20px;" else: self.data = 'display' if not js in response.files: response.files.append(js) def widget(self, f, v): wrapper = DIV() inp = SQLFORM.widgets.string.widget(f,v, _value=v,\ _type='color',\ _data_text='hidden', _style=self.style, _hex='true',\ _class=self._class) scr = SCRIPT("jQuery.fn.mColorPicker.init.replace = false; \ jQuery.fn.mColorPicker.init.allowTransparency=%s; \ jQuery('input.%s').mColorPicker(\ {'imageFolder': '/%s/static/mColorPicker/'});"\ % (self.transparency, self._class, request.application)) wrapper.components.append(inp) wrapper.components.append(scr) return wrapper color_widget = ColorPickerWidget() -
为了测试它,创建一个表,并将小部件设置为我们的新
colorpicker小部件:db.define_table('house', Field('color', widget = color_widget.widget)) -
最后,在您的控制器中创建表单:
def index(): form = SQLFORM(db.house) if form.accepts(request, session): response.flash = T('New house inserted') return dict(form=form)
缩短文本字段
在这个菜谱中,我们假设我们有一个如下所示的表,我们想要显示所选帖子正文列表,但缩短。
db.define_table('post', Field('body', 'text'))
如何操作...
如何做这取决于帖子是否包含 HTML 或 wiki 语法。
-
我们首先考虑 HTML。
这分为三个步骤。在控制器中我们选择行:
def index(): posts = db(db.post).select() return dict(posts=posts) -
然后通过序列化和截断 HTML 来缩短:
def index(): posts = db(db.post).select() for post in posts: post.short = TAG(post.body).flatten()[:100]+'...' return dict(posts=posts) -
然后在相关的视图中显示:
{{for post in posts:}}<div class="post">{{=post.short}}</div>{{pass}}注意,TAG(
post.body)解析 HTML,然后flatten()将解析的 HTML 序列化为文本,省略标签。然后我们提取前 100 个字符并添加'...'. -
如果正文包含 wiki 语法而不是 HTML,那么事情会更简单,因为我们不需要解析,可以直接渲染缩短后的文本。这里我们假设使用
MARKMINwiki 语法:def index(): posts = db(db.post).select() for post in posts: post.short = post.body[:100]+'...' return dict(posts=posts) -
在视图中:
{{for post in posts:}}<div class="post">{{=MARKMIN(post.short)}}</div>{{pass}}
更多内容...
在后一种情况下,如果您使用的是关系型数据库,截断可以在数据库服务器上完成,从而减少从db服务器到db客户端传输的数据量。
def index():
posts = db(db.post).select(db.post.body[:100]+'...')
for post in posts:
post.short = post(db.post.body[:100]+'...')
return dict(posts=posts)
更好的方法是存储缩短后的文本在不同的数据库字段中,而不是每次需要时都缩短。这将导致应用程序运行更快。
创建多表单
让我们考虑一个数据库表bottles的例子,其中字段表示携带一瓶酒参加品酒会的客人。每个瓶子可以有一个或两个品酒者。请放心,品尝也存在一对一的关系,但这里我们假设只有两个品酒者。我们的目标是创建一个自定义表单,允许插入瓶子的描述,并填写两个品酒者的名字,即使一对一的关系是通过一个单独的表实现的。
准备中
我们将假设以下最小化模型,其中后一个表实现了一对一关系:
db.define_table('bottle', Field('name'), Field('year', 'integer'))
db.define_table('taster', Field('name'), Field('bottle', db.bottle))
如何操作...
-
首先,我们要求
工厂为我们制作一个包含瓶子描述和品酒者列表字段的表单:form=SQLFORM.factory( db.bottle, Field('tasters', type='list:string', label=T('Tasters'))) -
现在,我们可以按照以下两个步骤来处理
accept:-
我们将
bottle插入到db.bottle表中 -
我们将每个
tasters插入到db.taster表中
def register_bottle(): form=SQLFORM.factory( db.bottle, Field('tasters', type='list:string', label=T('Tasters'))) if form.accepts(request,session): bottle_id = db.bottle.insert(**db.bottle._filter_fields(form.vars)) if isinstance(form.vars.tasters, basestring): db.taster.insert(name=form.vars.tasters, bottle=bottle_id) else: for taster in form.vars.tasters: db.taster.insert(name=taster, bottle=bottle_id) response.flash = 'Wine and guest data are now registered' return dict(form=form, bottles = db(db.bottle).select(), \ tasters = db(db.taster).select()) -
注意,在我们执行db.bottle.insert之前,我们必须从form.vars中过滤字段,因为表单包含不属于该表的字段。
创建带有引用的多表表单
现在我们想要修改前面的例子,使得品酒者必须是系统中的注册用户,并且我们想要使用下拉框来选择他们。一种简单的方法是设置品酒者的最大数量(这里我们选择10)。
如何操作...
-
首先,我们需要修改模型,使得品酒者现在是一个多对多链接表(一个瓶子可以有多个品酒者,一个品酒者可以品尝多个瓶子):
db.define_table('bottle', Field('name'), Field('year', 'integer')) db.define_table('taster', Field('auth_user', db.auth_user), Field('bottle', db.bottle)) -
现在我们相应地更改操作:
def register_bottle(): tasters = range(10) form=SQLFORM.factory( db.bottle, *[Field('taster%i'%i, db.auth_user,label=T('Taster #%i'%i)) for i in tasters]) if form.accepts(request,session): bottle_id = \ db.bottle.insert(**db.bottle._filter_fields(form.vars)) for i in tasters: if 'taster%i'%i in form.vars: db.taster.insert(auth_user= form.vars['taster%i'%i],bottle=bottle_id) response.flash='Wine and guest data are now registered' return dict(form=form)
还有更多...
渲染此表单的一种天真方法是以下:
{{extend 'layout.html'}}
{{=form}}
但是,使用 JavaScript 可以使它更智能。这个想法是隐藏与品酒者相关的表单的所有行,只显示第一行,然后根据需要显示后续行。jQuery 是进行此类操作的优秀工具:
{{extend 'layout.html'}}
{{=form}}
<script>
var taster_rows = new Array();
for(var i=0; i<10; i++){
taster_rows[i] = new Array();
taster_rows[i][0] = '#no_table_taster'+i;
taster_rows[i][1] = '#no_table_taster'+(i+1)+'__row';
}
jQuery(function(){
for(var i=1; i<10; i++){
jQuery('#no_table_taster'+i+'__row').hide();
}
for(var i=0; i<9; i++){
jQuery('#no_table_taster'+i).change(
function(){
for(var i=0; i<10; i++){
if(taster_rows[i][0] == ("#" + $(this).attr("id"))){
jQuery(taster_rows[i][1]).slideDown();
}
}
});
}
}
);
</script>
它是如何工作的...
首先,我们隐藏所有行,除了taster0。然后我们注册js操作到事件。例如,当字段值改变时,比如taster2,我们使下一个字段taster3出现(i+1)。注意,如果taster3是一个字段名,那么#no_table_taster3是input/select标签的 ID,而#no_table_taster3__row是表格中行的 ID。这是一个 web2py 约定。"no_table"来自事实,即表单是由SQLFORM.factory生成的,并且不唯一地与数据库表相关联。
创建多表更新表单
我们现在想要在一个单独的表单中更新db.bottle表及其关联的db.tasters记录。这可以通过与之前食谱中解释的类似机制来完成。我们需要做更多的工作。
如何操作...
首先,我们将保留与前面例子相同的模型结构,但我们更改控制器操作:
def edit_bottle():
bottle_id = request.args(0)
bottle = db.bottle(bottle_id) or redirect(URL('error'))
bottle_tasters = db(db.taster.bottle==bottle_id).select()
tasters, actual_testers = range(10), len(bottle_tasters)
form=SQLFORM.factory(
Field('name', default=bottle.name),
Field('year', 'integer', default=bottle.year),
*[Field('taster%i'%i,db.auth_user, default=bottle_tasters[i].auth_user \ if i<actual_testers else '', label=T('Taster #%i'%i)) for
i in tasters])
if form.accepts(request,session):
bottle.update_record(**db.bottle._filter_fields(form.vars))
db(db.taster.bottle==bottle_id).delete()
for i in tasters:
if 'taster%i'%i in form.vars:
db.taster.insert(auth_user=
form.vars['taster%i'%i],bottle=bottle_id)
response.flash = 'Wine and guest data are now updated'
return dict(form=form)
它是如何工作的...
与之前的形式非常相似,但瓶体字段被显式传递给SQLFORM.factory,以便它们可以预先填充。tasters%i字段也预先填充了现有的品酒师。当表单提交时,相应的瓶体记录将被更新,过去的品酒师将被删除,并且插入新的瓶体和新的品酒师之间的关系。
还有更多...
总是还有更多。问题是现在隐藏空行的 JS 代码变得更加复杂。这是因为,在编辑自定义表单时,我们不希望隐藏有选中值的行。以下是一个可能的解决方案:
{{extend 'layout.html'}}
{{=form}}
<script>
var taster_rows = new Array();
for(var i=0; i<10; i++){
taster_rows[i] = new Array();
taster_rows[i][0] = '#no_table_taster'+i;
taster_rows[i][1] = '#no_table_taster'+(i+1)+'__row';
}
jQuery(function(){
for(var i=1; i<10; i++){
if(!jQuery('#no_table_taster'+i).val()){
jQuery('#no_table_taster'+i+'__row').hide();
}
}
for(var i=0; i<9; i++){
jQuery('#no_table_taster'+i).change(
function(){
for(var i=0; i<10; i++){
if(taster_rows[i][0] == ("#" + $(this).attr("id"))){
jQuery(taster_rows[i][1]).slideDown();
}
}
});
}
}
);
</script>
你能弄清楚它是做什么的吗?
星级评分小部件
在这个菜谱中,我们向您展示如何使用jquery星级评分插件,并将其与 web2py 集成。
准备工作
您需要从以下链接下载 jQuery 星级评分小部件:
orkans-tmp.22web.net/star_rating/index.html
将文件提取到新的static/stars文件夹下,以便stars/ui.stars.js、stars/ui.stars.css以及插件提供的必要图像都包含在内。
如何做到这一点...
-
创建一个名为
models/plugin_rating.py的模型文件,并在文件中编写以下内容:DEPENDENCIES = [ 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.9/jquery- ui.js', 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.9/themes/ui- darkness/jquery-ui.css', URL(c='static/stars',f='jquery.ui.stars.js'), URL(c='static/stars',f='jquery.ui.stars.css')] def rating_widget(f,v): from gluon.sqlhtml import OptionsWidget import uuid id = str(uuid.uuid4()) for path in DEPENDENCIES: response.files.append(path) return DIV(SPAN(_id="stars-cap"), DIV(OptionsWidget.widget(f,v),_id=id), SCRIPT("jQuery(function(){jQuery('#%s').stars({inputType: 'select'});});" % id)) -
然后,创建一个模型。例如:
db.define_table('song', Field('title'), Field('rating', 'integer')) -
将小部件设置为
rating_widget,如下所示:db.song.rating.requires = IS_IN_SET(range(0, 6)) db.song.rating.widget = rating_widget -
在上述两行或
rating_widget函数未定义之前,必须执行插件模型。 -
在这里重要的是,由星级评分表示的字段是一个具有
IS_IN_SET(range(0,6))的整数。
注意rating_plugin如何使用UUID来定义渲染小部件的DIV的id属性。这样,您可以使用rating插件拥有多个字段
第五章:添加 Ajax 效果
在本章中,我们将涵盖以下食谱:
-
使用
jquery.multiselect.js -
创建
select_or_add小部件 -
使用自动完成插件
-
创建下拉日期选择器
-
改进内置的
ajax函数 -
使用滑块表示数字
-
使用 jqGrid 和 web2py
-
使用 WebGrid 改进数据表
-
Ajax 化您的搜索功能
-
创建 sparklines
简介
在本章中,我们讨论了 jQuery 插件与 web2py 集成的示例。这些插件有助于使表单和表格更加交互友好,从而提高应用程序的可用性。特别是,我们提供了如何通过交互式添加选项按钮改进多选下拉列表、如何用滑块替换输入字段以及如何使用jqGrid和WebGrid显示表格数据的示例。
使用jquery.multiselect.js
<select multiple="true">..</select>的默认渲染非常丑陋且不直观,尤其是在您需要选择多个非连续选项时。这并不是 HTML 的缺陷,而是大多数浏览器设计不佳。无论如何,可以使用 JavaScript 覆盖多选select的呈现。在这里,我们将使用一个名为jquery.multiselect.js的 jQuery 插件。请注意,这个 jQuery 插件作为标准插件与 PluginWiki 一起提供,但我们假设您没有使用 PluginWiki。
准备工作
您需要从abeautifulsite.net/2008/04/jquery-multiselect下载jquery.muliselect.js,并将相应的文件放入static/js/jquery.multiselect.js和static/css/jquery.multiselect.css。
如何做...
-
在您的视图中,只需在
{{extend 'layout.html'}}:之前添加以下内容:{{ response.files.append('http://ajax.googleapis.com/ajax\ /libs/jqueryui/1.8.9/jquery-ui.js') response.files.append('http://ajax.googleapis.com/ajax\ /libs/jqueryui/1.8.9/themes/ui-darkness/jquery-ui.css') response.files.append(URL('static','js/jquery.multiSelect.js')) response.files.append(URL('static','css/jquery.\ multiSelect.css')) }} -
在
{{extend 'layout.html'}}:之后放置以下代码:<script> jQuery(document).ready(function(){jQuery('[multiple]'). multiSelect();}); </script>就这些了。你所有的多选
select都将被优雅地样式化。 -
考虑以下操作:
def index(): is_fruits = IS_IN_SET(['Apples','Oranges','Bananas','Kiwis','Lemons'], multiple=True) form = SQLFORM.factory(Field('fruits','list:string', requires=is_fruits)) if form.accepts(request,session): response.flash = 'Yummy!' return dict(form=form)可以使用以下视图尝试此操作:
{{ response.files.append('http://ajax.googleapis.com/ajax\ /libs/jqueryui/1.8.9/jquery-ui.js') response.files.append('http://ajax.googleapis.com/ajax\ /libs/jqueryui/1.8.9/themes/ui-darkness/jquery-ui.css') response.files.append(URL('static','js/jquery.multiSelect.js')) response.files.append(URL('static','css/jquery.\ multiSelect.css')) }} {{extend 'layout.html}} <script> jQuery(document).ready(function(){jQuery('[multiple]'). multiSelect();}); </script> {{=form}}这是它的截图:

创建 select_or_add 小部件
此小部件将在旁边创建一个带有添加按钮的对象,允许用户在不访问不同屏幕的情况下即时添加新类别等。它与IS_IN_DB一起工作,并使用 web2py 组件和 jQueryUI 对话框。
此小部件的灵感来自可以在以下链接中找到的OPTION_WITH_ADD_LINK切片:
web2pyslices.com/main/slices/take_slice/11
如何做...
-
将以下代码放入模型文件中。例如,
models/select_or_add_widget.py:class SelectOrAdd(object): def __init__(self, controller=None, function=None, form_title=None, button_text = None, dialog_width=450): if form_title == None: self.form_title = T('Add New') else: self.form_title = T(form_title) if button_text == None: self.button_text = T('Add') else: self.button_text = T(button_text) self.dialog_width = dialog_width self.controller = controller self.function = function def widget(self, field, value): #generate the standard widget for this field from gluon.sqlhtml import OptionsWidget select_widget = OptionsWidget.widget(field, value) #get the widget's id (need to know later on so can tell #receiving controller what to update) my_select_id = select_widget.attributes.get('_id', None) add_args = [my_select_id] #create a div that will load the specified controller via ajax form_loader_div = DIV(LOAD(c=self.controller, f=self.function, args=add_args,ajax=True), _id=my_select_id+"_dialog-form", _title=self.form_title) #generate the "add" button that will appear next the options #widget and open our dialog activator_button = A(T(self.button_text), _id=my_select_id+"_option_add_trigger") #create javascript for creating and opening the dialog js = 'jQuery( "#%s_dialog-form" ).dialog({autoOpen: false, show: "blind", hide: "explode", width: %s});' % (my_select_id, self.dialog_width) js += 'jQuery( "#%s_option_add_trigger" ).click(function() { jQuery( "#%s_dialog-form" ).dialog( "open" );return false;}); ' % (my_select_id, my_select_id) #decorate our activator button for good measure js += 'jQuery(function() { jQuery( "#%s_option_add_trigger" ).button({text: true, icons: { primary: "ui-icon-circle- plus"} }); });' % (my_select_id) jq_script=SCRIPT(js, _type="text/javascript") wrapper = DIV(_id=my_select_id+"_adder_wrapper") wrapper.components.extend([select_widget, form_loader_div, activator_button, jq_script]) return wrapper -
您可以使用以下方式将小部件分配给字段:
# Initialize the widget add_option = SelectOrAdd(form_title="Add a new something", controller="product", function="add_category", button_text = "Add New", dialog_width=500)此小部件接受以下参数:
-
form_title: string:这将作为 jQueryUI 对话框框的标题。默认值是添加新内容。 -
controller: string:这是将处理记录创建的控制器名称。 -
function: 字符串。这是将处理记录创建的函数的名称。它应该创建一个表单,接受它,并准备好发出 JavaScript 与小部件交互 - 请参阅步骤 4中的add_category。 -
button_text: 字符串。这是将出现在激活我们的表单对话框的按钮上的文本。默认值是添加。 -
dialog_width: 整数。这是对话框的期望宽度(以像素为单位)。默认值为450。
-
-
在
models/db.py中定义您的数据库表,如下所示:db.define_table('category', Field('name', 'string', notnull=True, unique=True), Field('description', 'text') ) db.define_table('product', Field('category_id', db.category, requires=IS_IN_DB(db, 'category.id', 'category.name')), Field('name', 'string', notnull=True), Field('description', 'text'), Field('price', 'decimal(10,2)', notnull=True) ) # assign widget to field db.product.category_id.widget = add_option.widget -
创建您的控制器函数:
#This is the main function, the one your users go to def create(): #Initialize the widget add_option = SelectOrAdd(form_title="Add new Product Category", controller="product", function="add_category", button_text = "Add New") #assign widget to field db.product.category_id.widget = add_option.widget form = SQLFORM(db.product) if form.accepts(request, session): response.flash = "New product created" elif form.errors: response.flash = "Please fix errors in form" else: response.flash = "Please fill in the form" #you need jQuery for the widget to work; include here or just #put it in your master layout.html response.files.append("http://ajax.googleapis.com/ajax/\ libs/jqueryui/1.8.9/jquery-ui.js") response.files.append("http://ajax.googleapis.com/ajax/\ libs/jqueryui/1.8.9/themes/smoothness/jquery-ui.css") return dict(message="Create your product", form = form) def add_category(): #this is the controller function that will appear in our dialog form = SQLFORM(db.category) if form.accepts(request): #Successfully added new item #do whatever else you may want #Then let the user know adding via our widget worked response.flash = T("Added") target = request.args[0] #close the widget's dialog box response.js = 'jQuery("#%s_dialog-form" ).dialog(\ "close" );' % target #update the options they can select their new category in the #main form response.js += \ """jQuery("#%s")\ .append("<option value='%s'>%s</option>");""" % \ (target, form.vars.id, form.vars.name) #and select the one they just added response.js += """jQuery("#%s").val("%s");""" % \ (target, form.vars.id) #finally, return a blank form in case for some reason they #wanted to add another option return form elif form.errors: # silly user, just send back the form and it'll still be in # our dialog box complete with error messages return form else: #hasn't been submitted yet, just give them the fresh blank #form return form这里是一个显示小部件操作的截图:
![如何操作...]()
-
点击添加新项按钮,对话框就会打开。(嗯,我无法正确输入我自己的小部件名称!)。
![如何操作...]()
-
点击提交,新的选项将被创建并在主表单中自动选中。

您可以从以下链接在 bitbucket 上获取源代码或示例应用程序:
bitbucket.org/bmeredyk/web2py-select_or_add_option-widget/src
使用自动完成插件
虽然 web2py 自带自动完成插件,但其行为有点像魔法,如果不适合您,您可能更喜欢使用 jQuery 插件进行自动完成。
准备中
从以下网站下载必要的文件:
bassistance.de/jquery-plugins/jquery-plugin-autocomplete/
将文件解压到static/autocomplete。确保您有以下文件:
-
static/autocomplete/jquery.autocomplete.js -
static/autocomplete/jquery.autocomplete.css
如何操作...
-
首先,在您的模型中定义以下小部件:
def autocomplete_widget(field,value): response.files.append(URL('static','autocomplete/jquery.\ autocomplete.js')) response.files.append(URL('static','autocomplete/jquery.\ autocomplete.css')) print response.files import uuid from gluon.serializers import json id = "autocomplete-" + str(uuid.uuid4()) wrapper = DIV(_id=id) inp = SQLFORM.widgets.string.widget(field,value) rows = field._db(field._table['id']>0). select(field,distinct=True) items = [str(t[field.name]) for t in rows] scr = SCRIPT("jQuery('#%s input').autocomplete({source: %s});" % \ (id, json(items))) wrapper.append(inp) wrapper.append(scr) return wrapper此小部件创建一个普通的
<input/>小部件 inp,随后是一个注册自动完成插件的脚本。它还将一个可能值列表传递给插件,这些值是通过字段的现有值获得的。 -
现在,在您的模型或控制器中,您只需将此小部件分配给任何字符串字段。例如:
db.define_table('person',Field('name')) db.person.name.widget = autocomplete_widget -
如果您想让小部件从不同的表/字段获取值,只需更改以下行:
rows = field._db(field._table['id']>0).select(field,distinct=True) items = [str(t[field.name]) for t in rows]将它们更改为以下内容:
rows = field._db(query).select(otherfield,distinct=True) items = [str(t[otherfield.name]) for t in rows]
还有更多...
这种方法的局限性在于,当小部件渲染并嵌入页面时,将获取所有可能的值。这种方法有两个局限性:
-
随着自动完成选项的增加,服务页面变得越来越慢。
-
它将您的全部数据暴露给访客
有一个解决方案。插件可以使用 Ajax 回调来获取数据。要使用 Ajax 调用远程获取项目,我们可以按以下方式修改小部件:
def autocomplete_widget(field,value):
import uuid
id = "autocomplete-" + str(uuid.uuid4())
callback_url = URL('get_items')
wrapper = DIV(_id=id)
inp = SQLFORM.widgets.string.widget(field,value)
scr = SCRIPT("jQuery('#%s input').
autocomplete('%s',{extraParams:{field:'%s',table:'%s'}});" % \
(id, callback_url,field.name,field._tablename))
wrapper.append(inp)
wrapper.append(scr)
return wrapper
现在您需要实现自己的callback_url。
def get_items():
MINCHARS = 2 # characters required to trigger response
MAXITEMS = 20 # numer of items in response
query = request.vars.q
fieldname = request.vars.field
tablename = request.vars.table
if len(query.strip()) > MINCHARS and fieldname and tablename:
field = db[tablename][fielfname]
rows = db(field.upper().startswith(qery)).
select(field,distinct=True,limitby=(0,MINITEMS))
items = [str(row[fieldname]) for row in rows]
else:
items = []
return '\n'.join(items)
这里是如何操作的示例:

创建下拉日期选择器
有时候,你可能不喜欢正常的弹出日历选择器,而想创建一个允许分别选择年、月和日的部件,使用下拉列表。这里我们提供了一个这样的部件。
如何操作...
-
在你的一个模型中编写以下部件:
def select_datewidget(field,value): MINYEAR = 2000 MAXYEAR = 2020 import datetime now = datetime.date.today() dtval = value or now.isoformat() year,month,day= str(dtval).split("-") dt = SQLFORM.widgets.string.widget(field,value) id = dt['_id'] dayid = id+'__day' monthid = id+'__month' yearid = id+'__year' wrapperid = id+'__wrapper' wrapper = DIV(_id=wrapperid) day = SELECT([OPTION(str(i).zfill(2)) for i in range(1,32)], value=day,_id=dayid) month = SELECT([OPTION(datetime.date(2008,i,1).strftime('%B'), _value=str(i).zfill(2)) for i in range(1,13)], value=month,_id=monthid) year = SELECT([OPTION(i) for i in range(MINYEAR,MAXYEAR)], value=year,_id=yearid) jqscr = SCRIPT(""" jQuery('#%s').hide(); var curval = jQuery('#%s').val(); if(curval) { var pieces = curval.split('-'); jQuery('#%s').val(pieces[0]); jQuery('#%s').val(pieces[1]); jQuery('#%s').val(pieces[2]); } jQuery('#%s select').change(function(e) { jQuery('#%s').val( jQuery('#%s').val()+'-'+jQuery('#%s').val()+'- '+jQuery('#%s').val()); }); """ % (id,id,yearid,monthid,dayid, wrapperid,id,yearid,monthid,dayid)) wrapper.components.extend([month,day,year,dt,jqscr]) return wrapper -
在你的控制器中创建一个测试表单,并将字段设置为使用该部件:
def index(): form = SQLFORM.factory( Field('posted','date',default=request.now, widget=select_datewidget)) if form.accepts(request,session): response.flash = "New record added" return dict(form=form)看起来是这样的:

改进内置的 ajax 函数
Web2py 附带一个static/js/web2py_ajax.js文件,该文件定义了一个 ajax 函数。它是jQuery.ajax的包装器,但提供了更简单的语法。然而,这个函数的设计是有意简约的。在这个菜谱中,我们向您展示如何重写它,以便在后台执行 Ajax 请求时显示旋转的图像。
如何操作...
-
首先,你需要一个旋转的图标。例如,从以下网站中选择一个:
www.freeiconsdownload.com/Free_Downloads.asp?id=585,并将其保存为static/images/loading.gif。 -
然后,编辑文件
static/js/web2py_ajax.js中的 ajax 函数,如下(对于较旧的 web2py 应用程序,此函数在views/web2py_ajax.html中):`function ajax(u,s,t) { /* app_loading_image contains the img html set in layout.html before including web2py_ajax.html */ jQuery("#"+t).html(app_loading_image); var query=""; for(i=0; i<s.length; i++) { if(i>0) query=query+"&"; query=query+encodeURIComponent(s[i])+"="+ encodeURIComponent(document.getElementById(s[i]).value); } // window.alert(loading_image); jQuery.ajax({type: "POST", url: u, data: query, success: function(msg) { if(t==':eval') eval(msg); else document.getElementById(t).innerHTML=msg; } }); };
使用滑块表示数字
jQuery UI 附带了一个方便的滑块,可以用来表示范围中的数值字段,而不是无聊的<input/>标签。
如何操作...
-
创建一个名为
models/plugin_slider.py的模型文件,并定义以下内容:def slider_widget(field,value): response.files.append("http://ajax.googleapis.com/ajax\ /libs/jqueryui/1.8.9/jquery-ui.js") response.files.append("http://ajax.googleapis.com/ajax\ /libs/jqueryui/1.8.9/themes/ui-darkness/jquery-ui.css") id = '%s_%s' % (field._tablename,field.name) wrapper = DIV(_id="slider_wrapper",_style="width: 200px;text-\ align:center;") wrapper.append(DIV(_id=id+'__slider')) wrapper.append(SPAN(INPUT(_id=id, _style="display: none;"), _id=id+'__value')) wrapper.append(SQLFORM.widgets.string.widget(field,value)) wrapper.append(SCRIPT(""" jQuery('#%(id)s__value').text('%(value)s'); jQuery('#%(id)s').val('%(value)s'); jQuery('#%(id)s').hide(); jQuery('#%(id)s__slider').slider({ value:'%(value)s', stop: function(event, ui){ jQuery('#%(id)s__value').text(ui.value); jQuery('#%(id)s').val(ui.value); }}); """ % dict(id=id, value=value))) return wrapper -
创建一个测试表,并将部件设置为我们的新滑块部件:
db.define_table("product", Field("quantity","integer", default=0)) -
然后,通过在控制器中创建一个表单来使用滑块:
def index(): db.product.quantity.widget=slider_widget form = SQLFORM(db.product) if form.accepts(request,session): response.flash = "Got it" inventory = db(db.product).select() return dict(form=form,inventory=inventory)![如何操作...]()
使用 jqGrid 和 web2py
jqGrid 是一个基于 jQuery 构建的 Ajax 启用 JavaScript 控件,它提供了一个表示和操作表格数据的解决方案。你可以把它看作是 web2py SQLTABLE辅助器的替代品。jqGrid 是一个客户端解决方案,它通过 Ajax 回调动态加载数据,从而提供分页、搜索弹出、行内编辑等功能。jqGrid 已集成到 PluginWiki 中,但在这里,我们将其作为一个独立的 web2py 程序来讨论,这些程序不使用插件。jqGrid 值得有一本书来介绍,但在这里我们只讨论其基本功能和最简单的集成。
准备工作
你将需要 jQuery(它随 web2py 一起提供)、jQuery.UI 以及一个或多个主题,你可以直接从 Google 获取,但你还需要 jqGrid,你可以从以下地方获取:
我们还假设我们有一个包含内容的表,你可以用随机数据预先填充:
from gluon.contrib.populate import populate
db.define_table('stuff',
Field('name'),
Field('quantity', 'integer'),
Field('price', 'double'))
if db(db.stuff).count() == 0:
populate(db.stuff, 50)
如何操作...
首先,你需要一个将显示 jqGrid 的辅助器,我们可以在一个模型中定义它。例如,models/plugin_qgrid.py:
def JQGRID(table,fieldname=None, fieldvalue=None, col_widths=[],
colnames=[], _id=None, fields=[],
col_width=80, width=700, height=300, dbname='db'):
# <styles> and <script> section
response.files.append('http://ajax.googleapis.com/ajax\
/libs/jqueryui/1.8.9/jquery-ui.js')
response.files.append('http://ajax.googleapis.com/ajax\
/libs/jqueryui/1.8.9/themes/ui-darkness/jquery-ui.css')
for f in ['jqgrid/ui.jqgrid.css',
'jqgrid/i18n/grid.locale-en.js',
'jqgrid/jquery.jqGrid.min.js']:
response.files.append(URL('static',f))
# end <style> and <script> section
from gluon.serializers import json
_id = _id or 'jqgrid_%s' % table._tablename
if not fields:
fields = [field.name for field in table if field.readable]
else:
fields = fields
if col_widths:
if isinstance(col_widths,(list,tuple)):
col_widths = [str(x) for x in col_widths]
if width=='auto':
width=sum([int(x) for x in col_widths])
elif not col_widths:
col_widths = [col_width for x in fields]
colnames = [(table[x].label or x) for x in fields]
colmodel = [{'name':x,'index':x, 'width':col_widths[i],
'sortable':True} \
for i,x in enumerate(fields)]
callback = URL('jqgrid',
vars=dict(dbname=dbname,
tablename=table._tablename,
columns=','.join(fields),
fieldname=fieldname or '',
fieldvalue=fieldvalue,
),
hmac_key=auth.settings.hmac_key,
salt=auth.user_id)
script="""
jQuery(function(){
jQuery("#%(id)s").jqGrid({
url:'%(callback)s',
datatype: "json",
colNames: %(colnames)s,
colModel:%(colmodel)s,
rowNum:10, rowList:[20,50,100],
pager: '#%(id)s_pager',
viewrecords: true,
height:%(height)s
});
jQuery("#%(id)s").jqGrid('navGrid','#%(id)s_pager',{
search:true,add:false,
edit:false,del:false
});
jQuery("#%(id)s").setGridWidth(%(width)s,false);
jQuery('select.ui-pg-selbox,input.ui-g-
input').css('width','50px');
});
""" % dict(callback=callback, colnames=json(colnames),
colmodel=json(colmodel),id=_id,
height=height,width=width)
return TAG'',
DIV(_id=_id+"_pager"),
SCRIPT(script))
我们可以这样在我们的控制中使用它:
@auth.requires_login()
def index():
return dict(mygrid = JQGRID(db.stuff))
这个函数简单地生成所有必需的 JavaScript,但不向它传递任何数据。相反,它传递一个回调函数 URL(jqgrid),该 URL 为安全起见进行了数字签名。我们需要实现这个回调。
我们可以在索引操作的同一控制器中定义回调:
def jqgrid():
from gluon.serializers import json
import cgi
hash_vars = 'dbname|tablename|columns|fieldname|
fieldvalue|user'.split('|')
if not URL.verify(request,hmac_key=auth.settings.hmac_key,
hash_vars=hash_vars,salt=auth.user_id):
raise HTTP(404)
dbname = request.vars.dbname or 'db'
tablename = request.vars.tablename or error()
columns = (request.vars.columns or error()).split(',')
rows=int(request.vars.rows or 25)
page=int(request.vars.page or 0)
sidx=request.vars.sidx or 'id'
sord=request.vars.sord or 'asc'
searchField=request.vars.searchField
searchString=request.vars.searchString
searchOper={'eq':lambda a,b: a==b,
'nq':lambda a,b: a!=b,
'gt':lambda a,b: a>b,
'ge':lambda a,b: a>=b,
'lt':lambda a,b: a<b,
'le':lambda a,b: a<=b,
'bw':lambda a,b: a.startswith(b),
'bn':lambda a,b: ~a.startswith(b),
'ew':lambda a,b: a.endswith(b),
'en':lambda a,b: ~a.endswith(b),
'cn':lambda a,b: a.contains(b),
'nc':lambda a,b: ~a.contains(b),
'in':lambda a,b: a.belongs(b.split()),
'ni':lambda a,b: ~a.belongs(b.split())}\
[request.vars.searchOper or 'eq']
table=globals()[dbname][tablename]
if request.vars.fieldname:
names = request.vars.fieldname.split('|')
values = request.vars.fieldvalue.split('|')
query = reduce(lambda a,b:a&b,
[table[names[i]]==values[i] for i in range(len(names))])
else:
query = table.id>0
dbset = table._db(query)
if searchField:
dbset=dbset(searchOper(table[searchField],searchString))
orderby = table[sidx]
if sord=='desc': orderby=~orderby
limitby=(rows*(page-1),rows*page)
fields = [table[f] for f in columns]
records = dbset.select(orderby=orderby,limitby=limitby,*fields)
nrecords = dbset.count()
items = {}
items['page']=page
items['total']=int((nrecords+(rows-1))/rows)
items['records']=nrecords
readable_fields=[f.name for f in fields if f.readable]
def f(value,fieldname):
r = table[fieldname].represent
if r: value=r(value)
try: return value.xml()
except: return cgi.escape(str(value))
items['rows']=[{'id':r.id,'cell':[f(r[x],x) for x in
readable_fields]} \
for r in records]
return json(items)
JQGRID 辅助工具和 jqgrid 动作都是预制的,非常类似于 PluginWiki 的 jgGrid 小部件,可能不需要任何修改。jqgrid 动作是由辅助工具生成的代码调用的。它检查 URL 是否正确签名(用户有权访问回调)或未签名,解析请求中的所有数据以确定用户想要什么,包括从 jqgrid 搜索弹出窗口构建查询,并通过 JSON 在数据上执行 select 和 return 操作。
注意,您可以在多个操作中使用多个 JQGRID(table),并且除了要显示的表之外,您不需要传递任何其他参数。但是,您可能希望向辅助工具传递额外的参数:
-
fieldname和fieldvalue属性用于根据table[fieldname]==fieldvalue预先筛选结果 -
col_widths是像素中的列宽列表 -
colnames是要替换field.name的列名列表 -
_id是网格的标签 ID -
fields是要显示的字段名列表 -
col_width=80是每列的默认宽度 -
width=700和height=300是网格的大小 -
dbname='db'是回调将使用的数据库的名称,如果您有多个数据库,或者您使用的是不是db的名称
使用 WebGrid 提高数据表
在这个菜谱中,我们将构建一个名为 WebGrid 的模块,您可以将它视为 web2py 的 SQLTABLE 的替代品。然而,它更智能:它支持分页、排序、编辑,并且易于使用和定制。它故意设计为不需要会话或 jQuery 插件。
准备工作
从 web2pyslices.com/main/static/share/webgrid.py 下载 webgrid.py,并将其存储在 modules/ 文件夹中。
您可以从 web2pyslices.com/main/static/share/web2py.app.webgrid.w2p 下载一个演示应用程序,但这对于 WebGrid 正常工作不是必需的。
我们将假设有一个具有 crud 定义的脚手架应用程序,以及以下代码:
db.define_table('stuff',
Field('name'),
Field('location'),
Field('quantity','integer'))
我们心中有一个简单的库存系统。
如何做到这一点...
我们将改变一下顺序。首先,我们将向您展示如何使用它。
-
将
webgrid.py模块添加到您的modules文件夹中(有关安装说明,请参阅 准备工作 部分)。在您的控制器中添加以下代码:def index(): import webgrid grid = webgrid.WebGrid(crud) grid.datasource = db(db.stuff.id>0) grid.pagesize = 10 return dict(grid=grid()) # notice the ()数据源可以是
Set、Rows、Table或Table列表。也支持连接。grid.datasource = db(db.stuff.id>0) # Set grid.datasource = db(db.stuff.id>0).select() # Rows grid.datasource = db.stuff # Table grid.datasource = [db.stuff,db.others] # list of Tables grid.datasource = db(db.stuff.id==db.other.thing) # joinWebGrid 的主要行组件包括
header、filter、datarow、pager、page_total和footer -
您可以使用
action_links链接到crud函数。只需告诉它crud在哪里公开:grid.crud_function = 'data' -
你可以开启或关闭
rows:grid.enabled_rows = ['header','filter', 'pager','totals','footer','add_links'] -
你可以控制
fields和field_headers:grid.fields = ['stuff.name','stuff.location','stuff.quantity'] grid.field_headers = ['Name','Location','Quantity'] -
你可以控制
action_links(指向crud操作的链接)和action_headers:grid.action_links = ['view','edit','delete'] grid.action_headers = ['view','edit','delete'] -
你可能需要修改
crud.settings.[action]_next,以便在完成操作后重定向到你的 WebGrid 页面:if request.controller == 'default' and request.function == 'data': if request.args: crud.settings[request.args(0)+'_next'] = URL('index') -
你可以为数值字段获取页面
总计:grid.totals = ['stuff.quantity'] -
你可以在列上设置
过滤器:grid.filters = ['stuff.name','stuff.created'] -
你可以修改
过滤器使用的查询(如果你的数据源是Rows对象,则不可用;使用rows.find):grid.filter_query = lambda f,v: f==v -
你可以控制哪些请求
变量可以覆盖grid设置:grid.allowed_vars = ['pagesize','pagenum','sortby','ascending','groupby','totals']当渲染单元格时,WebGrid 将使用字段表示函数,如果存在。如果你需要更多的控制,你可以完全覆盖渲染行的方式。
-
渲染每一行的函数可以被替换成你自己的
lambda或函数:grid.view_link = lambda row: ... grid.edit_link = lambda row: ... grid.delete_link = lambda row: ... grid.header = lambda fields: ... grid.datarow = lambda row: ... grid.footer = lambda fields: ... grid.pager = lambda pagecount: ... grid.page_total = lambda: -
这里有一些有用的变量,用于构建你自己的行:
grid.joined # tells you if your datasource is a join grid.css_prefix # used for css grid.tablenames grid.response # the datasource result grid.colnames # column names of datasource result grid.pagenum grid.pagecount grid.total # the count of datasource result例如,让我们自定义页脚:
grid.footer = lambda fields : TFOOT(TD("This is my footer" , _colspan=len(grid.action_links)+len(fields), _style="text-align:center;"), _class=grid.css_prefix + '-webgrid footer') -
你还可以自定义消息:
grid.messages.confirm_delete = 'Are you sure?' grid.messages.no_records = 'No records' grid.messages.add_link = '[add %s]' grid.messages.page_total = "Total:" -
你还可以使用
row_created事件在行创建时修改该行。让我们在表头中添加一个列:def on_row_created(row,rowtype,record): if rowtype=='header': row.components.append(TH(' ')) grid.row_created = on_row_created -
让我们将操作链接移到右侧:
def links_right(tablerow,rowtype,rowdata): if rowtype != 'pager': links = tablerow.components[:3] del tablerow.components[:3] tablerow.components.extend(links) grid.row_created = links_right![如何操作...]()
如果你在同一页面上使用多个网格,它们必须具有唯一的名称。
Ajax 化你的搜索功能
在这个菜谱中,我们描述了视频中展示的代码:
www.youtube.com/watch?v=jGuW43sdv6E
它与自动完成非常相似。它允许你在输入字段中输入代码,通过 Ajax 将文本发送到服务器,并显示服务器返回的结果。它可以用于执行实时搜索。它与自动完成不同,因为文本不一定来自一个表(它可以来自服务器端实现的更复杂的搜索条件),并且结果不用于填充输入字段。
如何操作...
-
我们需要从一个模型开始,在这个例子中,我们选择了这个模型:
db.define_table('country', Field('iso'), Field('name'), Field('printable_name'), Field('iso3'), Field('numcode')) -
我们用以下数据填充此模型:
if not db(db.country).count(): for (iso,name,printable_name,iso3,numcode) in [ ('UY','URUGUAY','Uruguay','URY','858'), ('UZ','UZBEKISTAN','Uzbekistan','UZB','860'), ('VU','VANUATU','Vanuatu','VUT','548'), ('VE','VENEZUELA','Venezuela','VEN','862'), ('VN','VIETNAM','Viet Nam','VNM','704'), ('VG','VIRGIN ISLANDS, BRITISH','Virgin Islands, British','VGB','092'), ('VI','VIRGIN ISLANDS, U.S.','Virgin Islands, U.s.','VIR','850'), ('EH','WESTERN SAHARA','Western Sahara','ESH','732'), ('YE','YEMEN','Yemen','YEM','887'), ('ZM','ZAMBIA','Zambia','ZMB','894'), ('ZW','ZIMBABWE','Zimbabwe','ZWE','716')]: db.country.insert(iso=iso,name=name,printable_name=printable_name, iso3=iso3,numcode=numcode) -
创建以下 CSS 文件
static/css/livesearch.css:#livesearchresults { background: #ffffff; padding: 5px 10px; max-height: 400px; overflow: auto; position: absolute; z-index: 99; border: 1px solid #A9A9A9; border-width: 0 1px 1px 1px; -webkit-box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.3); -moz-box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.3); -box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.3); } #livesearchresults a{ color:#666666; } input#livesearch { font-size:12px; color:#666666; background-color:#ffffff; padding-top:5px; width:200px; height:20px; border:1px solid #999999; } -
创建以下 JavaScript 文件
static/js/livesearch.js:function livesearch(value){ if(value != ""){ jQuery("#livesearchresults").show(); jQuery.post(livesearch_url, {keywords:value}, function(result){ jQuery("#livesearchresults").html(result); } ); } else{ jQuery("#livesearchresults").hide(); } } function updatelivesearch(value){ jQuery("#livesearch").val(value);jQuery("#livesearchresults"). hide(); } jQuery(function(){jQuery("#livesearchresults").hide();}); -
现在创建一个简单的控制器动作:
def index(): return dict() -
简单控制器动作关联到以下
views/default/index.html,它使用了在步骤 3和步骤 4中创建的 livesearch JS 和 CSS:<script type="text/javascript"> /* url definition for livesearch ajax call */ var livesearch_url = "{{=URL('ajaxlivesearch')}}"; </script> {{response.files.append(URL('static','css/livesearch.css'))}} {{response.files.append(URL('static','js/livesearch.js'))}} {{extend 'layout.html'}} <label for="livesearch">Search country:</label><br /> <input type="text" id="livesearch" name="country" autocomplete="off" onkeyup="livesearch(this.value);" /><br /> <div id="livesearchresults"></div> -
最后,在
index函数相同的控制器中,实现 Ajax 回调:def ajaxlivesearch(): keywords = request.vars.keywords print "Keywords: " + str(keywords) if keywords: query = reduce(lambda a,b:a&b, [db.country.printable_name.contains(k) for k in \ keywords.split()]) countries = db(query).select() items = [] for c in countries: items.append(DIV(A(c.printable_name, _href="#", _id="res%s"%c.iso, _onclick="updatelivesearch(jQuery('#res%s'). html())"%c.iso))) return DIV(*items)这就是它的样子:

创建 sparklines
Sparklines是小型图表,通常嵌入在文本中,用于总结时间序列或类似信息。jquery.sparklines插件提供了几种不同的图表样式和有用的显示选项。你可以将 sparklines 插件与jquery.timers插件结合使用,以显示实时变化的数据。这个菜谱展示了实现这一点的其中一种方法。
Sparkline 图表在需要直观比较大量相似数据系列的应用程序中非常有用。以下是一个链接,指向爱德华·图费尔的《美丽证据》一书中关于更多信息的一个章节:
www.edwardtufte.com/bboard/q-and-a-fetch-msg?msg_id=0001OR
我们将创建一个索引,显示 5 到 25 个条形图,展示随机数字,反向排序以模拟帕累托图。图表每秒更新一次,以从服务器获取的新数据。
显示效果如下:

此示例假设您可以使用单个 JSON 查询一次性获取所有 sparklines 的数据,并且您在视图渲染时知道要显示多少个图表。技巧是选择一个合适的方案来生成图形 ID,在这种情况下是["dynbar0", "dynbar1",....],并且使用从 JSON 服务函数返回的相同 ID 字符串作为字典的键。这使得使用 web2py 视图模板方法生成jquery.sparkline()调用以更新从服务函数返回的 sparklines 变得简单。
如何做到这一点...
-
首先,您需要下载以下内容:
-
plugins.jquery.com/project/sparklines, 进入 "static/js/jquery.sparkline.js" -
以及计时器,
plugins.jquery.com/project/timers,进入static/js/jquery.timers-1.2.js
-
-
然后,在您的
layout.html中,在包含web2py_ajax.html之前,添加以下内容:response.files.append(URL('static','js/jquery.sparkline.js')) response.files.append(URL('static','js/jquery.timers-1.2.js')) -
将以下操作添加到您的控制器中:
def index(): return dict(message="hello from sparkline.py", ngraphs=20, chartmin=0, chartmax=20) def call(): return service() @service.json def sparkdata(ngraphs,chartmin,chartmax): import random ngraphs = int(ngraphs) chartmin = int(chartmin) chartmax = int(chartmax) d = dict() for n in xrange(ngraphs): id = "dynbar" + str(n) ### data for bar graph. ### 9 random ints between chartmax and chartmin data = [random.choice(range(chartmin,chartmax))\ for i in xrange(9)] ### simulate a Pareto plot data.sort() data.reverse() d[id] = data return d -
然后,创建
views/default/index.html,如下所示:{{extend 'layout.html'}} {{ chartoptions = XML("{type:'bar',barColor:'green','chartRangeMin':'%d', 'chartRangeMax':'%d'}" % (chartmin,chartmax)) jsonurl = URL('call/json/sparkdata/\ %(ngraphs)d/%(chartmin)d/%(chartmax)d' % locals()) }} <script type="text/javascript"> jQuery(function() { jQuery(this).everyTime(1000,function(i) { jQuery.getJSON('{{=jsonurl}}', function(data) { {{for n in xrange(ngraphs):}} jQuery("#dynbar{{=n}}").sparkline(data.dynbar{{=n}}, {{ =chartoptions }} ); {{pass}} }); }); }); </script> <h1>This is the sparkline.html template</h1> {{for n in xrange(ngraphs):}} <p> Bar chart with dynamic data: <span id="dynbar{{=n}}" class="dynamicbar">Loading..</span> </p> {{pass}} {{=BEAUTIFY(response._vars)}}
第六章:使用第三方库
在本章中,我们将涵盖以下配方:
-
定制日志
-
聚合源
-
显示推文
-
使用 matplotlib 绘图
-
使用 RSS 小部件扩展 PluginWiki
简介
Python 的力量来自于可用的众多第三方库。本章的目标不是讨论这些第三方库的 API,因为这个任务将是巨大的。相反,目标是通过定制日志、检测可能的问题、在模型文件中创建自己的 API 以及将新接口打包为插件来展示如何正确地完成这项工作。
定制日志
Python 的日志功能强大且灵活,但实现起来可能很复杂。此外,web2py 中的日志记录引入了一组新的问题。这个配方提供了一个在 web2py 中有效日志记录的方法,利用 Python 的本地日志功能。
Python 的本地日志框架使用一个 logger 与 handler 的组合,其中一个或多个 logger 将日志记录到一个或多个 handler。日志框架使用单例模型来管理其 logger,因此以下代码行通过该名称返回一个全局Logger实例,仅在首次访问时实例化:
logging.getLogger('name')
默认情况下,Python 进程以单个 root,logger (name == ")开始,只有一个 handler 将日志记录到stdout。
如何做到...
在 web2py 中的日志记录涉及一些新问题,如下所述:
-
在应用级别配置和控制日志
-
只配置一次 logger
-
实现简单的日志记录语法
Python 的本地日志框架已经为每个进程维护了一个全局的命名 logger 集合。但在 web2py 中,由于应用在同一个进程中运行,logger 是跨应用共享的。如果我们想根据应用特定地配置和控制 logger,我们需要一个不同的解决方案。
创建特定于应用的 logger 的一个简单方法是在 logger 的名称中包含应用名称。
logging.getLogger(request.application)
这可以在模型文件中完成。现在,跨多个应用使用的相同代码将为每个应用返回不同的 logger。
我们希望在启动时只配置一次 logger。然而,当访问一个命名 logger 时,Python 没有提供检查 logger 是否已经存在的方法。
确保 logger 只配置一次的最简单方法,是检查它是否有任何 handler,如下所示:
def get_configured_logger(name):
logger = logging.getLogger(name)
if len(logger.handlers) == 0:
# This logger has no handlers, so we can assume
# it hasn't yet been configured.
# (Configure logger)
return logger
注意,如果loggername为空,你需要检索 Python 的 root logger。默认的 root logger 已经关联了一个 handler,所以你会检查 handler 的数量为1。root logger 不能被设置为特定于应用。
当然,我们不希望每次记录日志时都要调用get_configured_logger。相反,我们可以在模型中一次性进行全局赋值,并在整个应用程序中使用它。赋值将在每次你在控制器中使用记录器时执行,但实例化和配置只会在第一次访问时发生。
所以最后,只需将此代码放置在模型中:
import logging, logging.handlers
def get_configured_logger(name):
logger = logging.getLogger(name)
if (len(logger.handlers) == 0):
# This logger has no handlers, so we can assume
# it hasn't yet been configured
# (Configure logger)
pass
return logger
logger = get_configured_logger(request.application)
在以下示例中,在你的控制器中使用它:
logger.debug('debug message')
logger.warn('warning message')
logger.info('information message')
logger.error('error message')
更多...
我们可以用自定义的应用程序级记录器做什么?例如,我们可以重新编程 Google App Engine 上的记录,让消息进入数据存储表。以下是我们可以这样做的步骤:
import logging, logging.handlers
class GAEHandler(logging.Handler):
"""
Logging handler for GAE DataStore
"""
def emit(self, record):
from google.appengine.ext import db
class Log(db.Model):
name = db.StringProperty()
level = db.StringProperty()
module = db.StringProperty()
func_name = db.StringProperty()
line_no = db.IntegerProperty()
thread = db.IntegerProperty()
thread_name = db.StringProperty()
process = db.IntegerProperty()
message = db.StringProperty(multiline=True)
args = db.StringProperty(multiline=True)
date = db.DateTimeProperty(auto_now_add=True)
log = Log()
log.name = record.name
log.level = record.levelname
log.module = record.module
log.func_name = record.funcName
log.line_no = record.lineno
log.thread = record.thread
log.thread_name = record.threadName
log.process = record.process
log.message = record.msg
log.args = str(record.args)
log.put()
def get_configured_logger(name):
logger = logging.getLogger(name)
if len(logger.handlers) == 0:
if request.env.web2py_runtime_gae:
# Create GAEHandler
handler = GAEHandler()
else:
# Create RotatingFileHandler
import os
formatter = "%(asctime)s %(levelname)s " + \
"%(process)s %(thread)s "+ \
"%(funcName)s():%(lineno)d %(message)s"
handler = logging.handlers.RotatingFileHandler(
os.path.join(request.folder,'private/app.log'),
maxBytes=1024,backupCount=2)
handler.setFormatter(logging.Formatter(formatter))
handler.setLevel(logging.DEBUG)
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
logger.debug(name + ' logger created') # Test entry
else:
logger.debug(name + ' already exists') # Test entry
return logger
#### Assign application logger to a global var
logger = get_configured_logger(request.application)
你可以在以下网址了解更多关于这个主题的信息:
汇总源
在这个菜谱中,我们将使用feedparser和rss2构建一个 RSS 源聚合器。我们称之为Planet Web2py,因为它将基于字符串web2py过滤 rss 条目。
如何做到...
-
创建一个
models/db_feed.py,内容如下:db.define_table("feed", Field("name"), Field("author"), Field("email", requires=IS_EMAIL()), Field("url", requires=IS_URL(), comment="RSS/Atom feed"), Field("link", requires=IS_URL(), comment="Blog href"), Field("general", "boolean", comment="Many categories (needs filters)"), ) -
然后在
controllers/default.py中添加一个planet函数,通过使用feedparser:获取所有源来渲染一个基本页面def planet(): FILTER = 'web2py' import datetime import re import gluon.contrib.rss2 as rss2 import gluon.contrib.feedparser as feedparser # filter for general (not categorized) feeds regex = re.compile(FILTER,re.I) # select all feeds feeds = db(db.feed).select() entries = [] for feed in feeds: # fetch and parse feeds d = feedparser.parse(feed.url) for entry in d.entries: # filter feed entries if not feed.general or regex.search(entry.description): # extract entry attributes entries.append({ 'feed': {'author':feed.author, 'link':feed.link, 'url':feed.url, 'name':feed.name}, 'title': entry.title, 'link': entry.link, 'description': entry.description, 'author': hasattr(entry, 'author_detail') \ and entry.author_detail.name \ or feed.author, 'date': datetime.datetime(*entry.date_parsed[:6]) }) # sort entries by date, descending entries.sort(key=lambda x: x['date'],reverse=True) now = datetime.datetime.now() # aggregate rss2 feed with parsed entries rss = rss2.RSS2(title="Planet web2py", link = URL("planet").encode("utf8"), description = "planet author", lastBuildDate = now, items = [rss2.RSSItem( title = entry['title'], link = entry['link'], description = entry['description'], author = entry['author'], # guid = rss2.Guid('unknown'), pubDate = entry['date']) for entry in entries] ) # return new rss feed xml response.headers['Content-Type']='application/rss+xml' return rss2.dumps(rss)
在你能够使用这个函数之前,你需要在db.feed中添加一些源网址,例如,使用appadmin。
以下是一些关于 web2py 的示例 RSS 源:
更多...
可以在以下网址找到 web2py 示例 planet 的工作示例:
完整示例的完整源代码(planet-web2py)发布在 Google 代码项目中,可在以下网址找到:
code.google.com/p/planet-web2py/
该应用程序存储rss源条目,以加快聚合,并定期刷新源。
显示推文
在这个菜谱中,我们将展示如何使用simplejson显示最近的推文,并使用 web2py 包含的工具获取它。
如何做到...
-
首先,创建一个
models/0.py文件来存储基本配置,如下所示:TWITTER_HASH = "web2py" -
在
controllers/default.py中添加一个 Twitter 函数,通过使用获取工具获取所有推文并使用simplejson:解析它来渲染一个基本页面部分@cache(request.env.path_info,time_expire=60*15, cache_model=cache.r am) def twitter(): session.forget() session._unlock(response) import gluon.tools import gluon.contrib.simplejson as sj try: page = gluon.tools.fetch(' http://search.twitter.com/search. json?q=%%40%s' % TWITTER_HASH) data = sj.loads(page, encoding="utf-8")['results'] d = dict() for e in data: d[e["id"]] = e r = reversed(sorted(d)) return dict(tweets = [d[k] for k in r]) else: return 'disabled' except Exception, e: return DIV(T('Unable to download because:'),BR(),str(e)) -
在
views/default/twitter.load中创建一个用于 twitter 组件的视图,我们将渲染每条推文:<OL> {{ for t in tweets: }} <LI> {{ =DIV(H5(t["from_user_name"])) }} {{ =DIV(t["text"]) }} </LI> {{ pass }} </OL> -
然后,在
default/index.html中,添加使用 LOAD(jQuery)加载推文的部分:{{if TWITTER_HASH:}} <div class="box"> <h3>{{=T("%s Recent Tweets") % TWITTER_HASH}}</h3> <div id="tweets"> {{=LOAD('default','twitter. load',ajax=True)}}</div> </div>{{pass}}
更多内容...
您可以使用 CSS 样式来增强推文部分。创建一个 static/css/tweets.css 文件,包含以下代码:
/* Tweets */
#tweets ol {
margin: 1em 0;
}
#tweets ol li {
background: #d3e5ff;
list-style: none;
-moz-border-radius: 0.5em;
border-radius: 0.5em;
padding: 0.5em;
margin: 1em 0;
border: 1px solid #aaa;
}
#tweets .entry-date {
font-weight: bold;
display: block;
}
然后,将 CSS 文件添加到响应中:
def index():
response.files.append(URL("static","css/tweets.css"))
response.flash = T('You are successfully running web2py.')
return dict(message=T('Hello World'))
您可以使用以下属性进一步自定义此配方,这是该推文 API 为每条推文返回的:
-
iso_language_code -
to_user_name -
to_user_id_str -
profile_image_url_https -
from_user_id_str -
text -
from_user_name -
in_reply_to_status_id_str -
profile_image_url -
id' -
to_user -
source -
in_reply_to_status_id -
id_str' -
from_user -
from_user_id -
to_user_id -
geo -
created_at -
metadata
记住,在这个配方中,我们使用缓存来加速页面加载(15 分钟=60*15)。如果您需要更改它,请修改 @cache(…,time_expire=…)
使用 matplotlib 绘图
Matplotlib 是 Python 中最先进的绘图库。一些示例可以在以下 URL 中找到:
matplotlib.sourceforge.net/gallery.html
Matplotlib 可以用于以下两种模型:
-
PyLab(一个 Matlab 兼容模式)
-
更多的 Pythonic API
大多数文档使用 PyLab,这是一个问题,因为 PyLab 共享全局状态,并且与 Web 应用程序不兼容。我们需要使用更 Pythonic 的 API。
如何做到这一点...
Matplotlib 有许多后端可以用于在 GUI 中打印或打印到文件。
为了在 Web 应用中使用 matplotlib,我们需要指导它实时生成图形,将其打印到内存映射文件中,并将文件内容流式传输到页面访问者。
在这里,我们展示了一个实用函数来绘制以下形式的数据集:
name = [(x0,y0),(x1,y1),...(xn,yn)]
-
创建一个
models/matplotlib.py文件,包含以下代码:from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas from matplotlib.figure import Figure import cStringIO def myplot(title='title',xlab='x',ylab='y',mode='plot', data={'xxx':[(0,0),(1,1),(1,2),(3,3)], 'yyy':[(0,0,.2,.2),(2,1,0.2,0.2),(2,2,0.2,0.2), (3,3,0.2,0.3)]}): fig=Figure() fig.set_facecolor('white') ax=fig.add_subplot(111) if title: ax.set_title(title) if xlab: ax.set_xlabel(xlab) if ylab: ax.set_ylabel(ylab) legend=[] keys=sorted(data) for key in keys: stream = data[key] (x,y)=([],[]) for point in stream: x.append(point[0]) y.append(point[1]) if mode=='plot': ell=ax.plot(x, y) legend.append((ell,key)) if mode=='hist': ell=ax.hist(y,20) if legend: ax.legend([x for (x,y) in legend], [y for (x,y) in legend], 'upper right', shadow=True) canvas=FigureCanvas(fig) stream=cStringIO.StringIO() canvas.print_png(stream) return stream.getvalue() -
您现在可以尝试它,在您的控制器中使用以下操作:
def test_images(): return HTML(BODY( IMG(_src=URL('a_plot')), IMG(_src=URL('a_histogram')))) def a_plot(): response.headers['Content-Type']='image/png' return myplot(data={'data':[(0,0),(1,1),(2,4),(3,9),(4,16)]}) def a_histogram(): response.headers['Content-Type']='image/png' return myplot(data={'data':[(0,0),(1,1),(2,4),(3,9),(4,16)]}, mode='hist')-
http://.../test_images -
http://.../a_plot.png -
http://.../a_histogram.png
-
它是如何工作的...
当您访问 test_images 时,它会生成一个包含图形的 HTML:
<img src="img/a_plot.png"/>
<img src="img/a_histogram.png"/>
每个这些 URL 都调用 models/matplotlib.py 中的 myplot 函数。绘图函数生成一个包含一个子图的图形(一组 X-Y 轴)。然后在该子图 ax 上绘制(当 mode="plot" 时连接点,当 mode="hist" 时绘制直方图),并将图形打印到一个名为 stream 的内存映射画布上。然后它从流中读取二进制数据并返回。
更多内容...
在示例中,关键函数是 ax.plot 和 ax.hist,它们在子图的轴上绘制。您现在可以通过复制提供的 myplot 函数,重命名它,并用其他函数替换 ax.plot 或 ax.hist(例如散点图、误差线等)来创建更多的绘图函数。现在,您应该可以直接从 matplotlib 文档中找到。
使用 RSS 小部件扩展 PluginWiki
PluginWiki是 web2py 插件中最复杂的。它添加了许多功能;特别是,它为您的应用程序添加了一个 CMS,并定义了可以嵌入 CMS 页面以及您自己的视图的小部件。此插件可以扩展,这里我们向您展示如何添加一个新的小部件。
有关插件-wiki 的更多信息,请参阅:
web2py.com/examples/default/download
如何操作...
-
创建一个名为
models/plugin_wiki_rss.py的文件,并将以下代码添加到其中:class PluginWikiWidgets(PluginWikiWidgets): @staticmethod def aggregator(feed, max_entries=5): import gluon.contrib.feedparser as feedparser d = feedparser.parse(feed) title = d.channel.title link = d.channel.link description = d.channel.description div = DIV(A(B(title[0], _href=link[0]))) created_on = request.now for entry in d.entries[0:max_entries]: div.append(A(entry.title,' - ', entry.updated, _href=entry.link)) div.append(DIV(description)) return div -
现在,您可以使用以下语法将此小部件包含在 PluginWiki CMS 页面上:
name:aggregator feed:http://rss.cbc.ca/lineup/topstories.xml max_entries:4您也可以使用以下语法将其包含在任何 web2py 页面上:
{{=plugin_wiki.widget('aggregator',max_entries=4, feed='http://rss.cbc.ca/lineup/topstories.xml')}}
还有更多...
web2py 用户博格丹通过使用随 PluginWiki 一起提供的 jQuery UI 对此插件进行了一些修改,使其更加流畅。以下是改进后的插件:
class PluginWikiWidgets(PluginWikiWidgets):
@staticmethod
def aggregator(feeds, max_entries=5):
import gluon.contrib.feedparser as feedparser
lfeeds = feeds.split(",")
strg='''
<script>
var divDia = document.createElement("div");
divDia.id ="dialog";
document.body.appendChild(divDia);
var jQuerydialog=jQuery("#dialog").dialog({
autoOpen: false,
draggable: false,
resizable: false,
width: 500
});
</script>
'''
for feed in lfeeds:
d = feedparser.parse(feed)
title=d.channel.title
link = d.channel.link
description = d.channel.description
created_on = request.now
strg+='<a class="feed_title" href="%s">%s</a>' % \
(link[0],title[0])
for entry in d.entries[0:max_entries]:
strg+='''
<div class="feed_entry">
<a rel="%(description)s" href="%(link)s">
%(title)s - %(updated)s</a>
<script>
jQuery("a").mouseover(function () {
var msg = jQuery(this).attr("rel");
if (msg) {
jQuerydialog[0].innerHTML = msg;
jQuerydialog.dialog("open");
jQuery(".ui-dialog-titlebar").hide();
}
}).mousemove(function(event) {
jQuerydialog.dialog("option", "position", {
my: "left top",
at: "right bottom",
of: event,
offset: "10 10"
});
}).mouseout(function(){
jQuerydialog.dialog("close");
});
</script></div>''' % entry
return XML(strg)
此脚本的修改版本不使用辅助工具,而是使用原始 HTML 以提高速度,对 CSS 友好,并使用对话框弹出窗口来输入详细信息。
第七章. 网络服务
本章我们将涵盖以下菜谱:
-
使用 jQuery 消费 web2py JSON 服务
-
消费 JSON-RPC 服务
-
从 JavaScript 发起 JSON-RPC
-
使用 pyamf 从 Flex 发起 amf3 RPC 调用
-
web2py 中的 PayPal 集成
-
PayPal 网络支付标准
-
获取 Flickr 照片
-
通过Amazon Web Services (AWS)使用 Boto 发送电子邮件
-
使用 mapscript 制作 GIS 地图
-
Google 群组和 Google 代码源代码阅读器
-
创建 SOAP 网络服务
简介
本章不涉及创建网络服务(该主题在官方 web2py 手册中有讨论);而是关于如何使用网络服务。最常用的网络服务使用协议,如 JSON、JSON-RPC、XML、XMLRPC 和/或 SOAP。web2py 支持所有这些协议,但集成可能相当复杂。在这里,我们提供了与 Flex、Paypal、Flickr 和 GIS 集成的示例。
使用 jQuery 消费 web2py JSON 服务
这是一个从服务器检索 JSON 数据并使用 jQuery 消费它的简单示例。
如何做到这一点...
从 web2py 返回 JSON 的方式有很多,但在这里我们考虑的是 JSON 服务的案例,例如:
def consumer():
return dict()
@service.json
def get_days():
return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday",
"Friday", "Saturday"]
def call():
return service()
在这里,函数consumer实际上并没有做什么;它只是返回一个空字典以渲染视图,该视图将使用服务。get_days定义了服务,函数调用暴露了所有已注册的服务。get_days不需要在控制器中,也可以在模型中。call始终在default.py脚手架控制器中。
现在,我们为消费者操作创建一个视图:
{{extend 'layout.html'}}
<div id="target"></div>
<script>
jQuery.getJSON("{{=URL('call',args=['json','get_days'])}}",
function(msg){
jQuery.each(msg, function(){ jQuery("#target").
append(this + "<br />"); } )
});
</script>
它是如何工作的...
jQuery.getJSON的第一个参数是以下服务的 URL:
http://127.0.0.1:8000/app/default/call/json/get_days
这始终遵循以下模式:http://<domain>/<app>/<controller>/call/<type>/<service>
URL 位于{{...}}之间,因为它在服务器端解析,而其他所有内容都在客户端执行。
jQuery.getJSON的第二个参数是一个回调,它将传递 JSON 响应。在我们的例子中,回调遍历响应中的每个项目(字符串形式的周天列表),并将每个字符串,后跟一个<br/>,追加到<div id="target">中。
更多内容...
如果你启用了通用 URL,你可以将json服务实现为一个常规操作。
response.generic_pattern = ['get_days.json']
def get_days():
return ["Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"]
在这种情况下,你不需要使用call操作,你可以将消费者操作的视图重写如下:
{{extend 'layout.html'}}
<div id="target"></div>
<script>
jQuery.getJSON(
"{{=URL('get_days.json')}}",
function(msg){
jQuery.each(
msg,
function(){
jQuery("#target").append(this + "<br />");
}
);
}
);
</script>
这样做的话,URL 会更短。那么,为什么使用@service.json而不是后一种方法呢?有两个原因。第一个原因是,在前一种情况下,你可以使用相应的装饰器同时暴露相同的函数,用于 JSON-RPC、XMLRPC、SOAP 和 AMF 服务,而在后一种情况下,这会更复杂。第二个原因是,使用@service.json,GET 变量会自动解析并作为变量传递给服务函数。例如:
@service.json
def concat(a,b):
return a+b
这可以等价地用以下方式调用:
http://127.0.0.1:8000/app/default/call/json/concat?a=hello&b=world
http://127.0.0.1:8000/app/default/call/json/concat/hello/world
http://127.0.0.1:8000/app/default/call/json/concat/hello?b=world
消费 JSON-RPC 服务
虽然,之前我们考虑的是 JSON 服务的情况,但现在我们感兴趣的是 JSON-RPC 服务。这更复杂,因为变量(request 和 response)有更严格的格式,由协议规定。
准备工作
我们可以在纯 web2py 中创建 JSON-RPC 服务,但更有可能的是我们会从不同的 Python 程序中消费它。为此,我们将假设一个标准的 jsonrpc 库,它可以在以下 URL 中找到:
github.com/bmjames/python-jsonrpc
你可以使用以下命令安装它:
easy_install jsonrpc
如何做...
-
首先,我们需要创建
service。我们将考虑之前使用的相同示例,但改变其装饰器:from gluon.tools import Service service = Service(globals()) @service.jsonrpc def concat(a,b): return a+b def call(): return service() -
现在,要调用它,我们需要一个来自独立(非 web2py)Python 程序的 JSON-RPC 客户端库:
from jsonrpc.proxy import JSONRPCProxy proxy = JSONRPCProxy( 'http://127.0.0.1:8000',path='/app/default/call/jsonrpc') print proxy.call('concat','hello','world')
更多...
现在还有其他的 JSON-RPC 库,例如 json-rpc.org/wiki/python-json-rpc,它使用的语法更接近 xmlrpclib 的语法:
from jsonrpc import ServerProxy
proxy = ServerProxy(
'http://127.0.0.1:8000/app/default/call/jsonrpc')
print proxy.concat('hello','world')
注意,在这种情况下,方法名变成了一个属性。这两个库不兼容,但名称相同。确保你知道你正在使用哪一个。
web2py 在 gluon/contrib/simplejsonrpc.py 中包含了它自己的 JSON-RPC 客户端库,并且它的 API 与之前的示例兼容:
def test_concat():
from gluon.contrib.simplejsonrpc import ServerProxy
proxy = ServerProxy(
'http://127.0.0.1:8000/%s/default/call/jsonrpc' %
request.application)
return proxy.concat('hello','world')
JSON-RPC 从 JavaScript
有很多理由你想在 Web 应用程序中,客户端和服务器之间使用 JSON-RPC 作为传输协议。这特别有用于创建丰富的客户端界面,因为 JSON-RPC 比 XML-RPC 快,因为它更简洁,并且更容易被 JavaScript 代码解析。JSON-RPC 比单纯的 JSON 更好,因为它是一个 RPC 协议,这意味着它会为你处理错误传播。
在这个配方中,我们提供了一个如何做到这一点的示例。
你可以在以下 URL 上的文章中阅读更多关于此的信息,该文章由 Luke Kenneth Casson Leighton 撰写,他是出色的 Pyjamas 库 的作者:
www.advogato.org/article/993.html
这个配方是基于这里发布的代码:
otomotion.org/BasicJSONRPC/static/BasicJSONRPC-application.zip
准备工作
这个配方是基于 json-xml-rpc 库,它可以在以下位置找到:
code.google.com/p/json-xml-rpc
这是一个用于连接到 web2py 的动作的 RPC JavaScript 客户端实现,它使用其本地的 JSON-RPC 支持。
这不是一个完美的方法,但它提供了服务器和客户端之间的一定程度的解耦,使得我愿意忽略其小缺陷。这篇启发性的文章,由 Luke Kenneth Casson Leighton 撰写,更详细地介绍了这种方法(见 Full-blown JavaScript-led Development 部分)。这也是 GWT (code.google.com/webtoolkit/) 和 PyJamas (pyjs.org/) 等框架所使用的方法。
如何做到这一点...
-
我们将创建两个控制器和一个视图。第一个控制器将简单地加载视图中定义的丰富客户端界面。第二个控制器定义 JSON-RPC 方法。没有真正的理由不使用单个控制器来完成这两个目的,但将两个功能保持在不同文件中是一种更好的设计。
第一个控制器可以是
default.py,我们可以使用通常的简单操作:def index(); return dict()在
view views/default/index.html中,我们将简单地添加以下代码:{{ response.files.append(URL('static','js/jquery.js')) response.files.append(URL('static','js/rpc.js')) response.files.append(URL('static','js/BasicJSONRPC.js')) }} {{extend 'layout.html'}}BasicJSONRPC.py控制器除了对视图的引用外,没有其他内容。def index(): response.view = "BasicJSONRPC.html" return dict() def BasicJSONRPC(): response.view = "BasicJSONRPC.html" return dict()BasicJSONRPCData.py控制器是实际与生活交汇的地方。我们将从简单开始。import math from gluon.tools import Service service = Service(globals()) def call(): return service() @service.jsonrpc def systemListMethods(): #Could probably be rendered dynamically return ["SmallTest"]; @service.jsonrpc def SmallTest(a, b): return a + bsystemListMethods动作是json-xml-rpc库所必需的。默认情况下,该库实际上调用system.ListMethods,这在 Python 中无法支持。因此,我们在 RPC 库内部的调用中移除了点号。Python 函数只需返回一个包含所有可能调用方法的字符串数组。 -
现在我们已经准备好了控制器,我们可以继续到客户端部分。访问 RPC 方法的 URL 大概如下:
http://localhost/Application/Controller/call/jsonrpc -
使用此 URL 和
json-xml-rpc库,我们创建一个 JavaScriptDataController对象,我们将使用它进行所有未来的过程调用。var ConnectionCreationTime = null; var DataController = null; var Connected = false; function InitDataConnection() { Connected = false; // replace with the correct service url var url = http://localhost/Application/Controller/call/jsonrpc // var url = GetConnectionURL(); try { // Here we connect to the server and build // the service object (important) DataController = new rpc.ServiceProxy(url); Connected = true; } catch(err) { Log("Connection Error: " + err.message); Connected = false; } var now = new Date(); ConnectionCreated = now; } -
默认情况下,
json-xml-rpc库为异步调用创建DataController。由于你不想在请求期间阻塞你的 JavaScript,异步调用是期望的行为。然而,如果你想要快速测试你的远程方法,你可以从 Firebug 控制台中运行以下 JavaScript 代码:http://getfirebug.com InitDataConnection(); rpc.setAsynchronous(DataController,false); DataController.SmallTest(1,2);-
json-xml-rpc文档位于code.google.com/p/json-xml-rpc/wiki/DocumentationForJavaScript,其中详细说明了如何运行异步调用。function RunSmallTest() { if(Connected == false) Log("Cannot RunSmallTest unless connected"); else { var a = GetAValue(); var b = GetBValue(); Log("Calling remote method SmallTest using values a=" + a + " and b=" + b); DataController.SmallTest({params:[a,b], onSuccess:function(sum){ Log("SmallTest returned " + sum); }, onException:function(errorObj){ Log("SmallTest failed: " + errorObj.message); }, onComplete:function(responseObj){ Log("Call to SmallTest Complete"); } }); Log("Asynchronous call sent"); } } -
你的 Python 函数可以返回字典和数组,正如我们的
BiggerTest函数所展示的那样:@service.jsonrpc def BiggerTest(a, b): results = dict() results["originalValues"] = [a,b] results["sum"] = a + b results["difference"] = a - b results["product"] = a * b results["quotient"] = float(a)/b results["power"] = math.pow(a,b) return results注意
不要忘记更新
systemListMethods函数以包含任何新函数。
-
-
在这一步,你应该能够使用 JavaScript(在 Firebug 控制台中同步调用)测试远程调用并查看结果:
>>> InitDataConnection(); POST http://127.0.0.1:8000/BasicJSONRPC/BasicJSONRPCData/call/ jsonrpc 200 OK 20ms rpc.js (line 368) >>> rpc.setAsynchronous(DataController,false); >>> var results = DataController.BiggerTest(17,25); POST http://127.0.0.1:8000/BasicJSONRPC/BasicJSONRPCData/call/ jsonrpc 200 OK 20ms rpc.js (line 368) >>> results.originalValues [17, 25] >>> results.originalValues[1] 25 >>> results.sum 42 >>> results.difference -8 >>> results.quotient 0.68 -
认证也有效,因为每次请求都会发送 cookie,因此 web2py 能够解析 JSON-RPC 调用的会话 ID cookie。可以通过确保调用函数(而不是单个服务函数;这很重要)来向远程函数添加安全要求:
@auth.requires_login() def call(): return service() -
如果你还在主
BasicJSONRPC.py控制器上设置了@auth.requires_login,那么当用户首次加载页面时,他们将登录,并且所有后续的 RPC 调用都将得到正确认证。然而,这个问题与超时有关。如果用户让页面空闲直到超时发生,他们仍然可以触发对服务器的 RPC 调用。认证将失败,并且默认的 web2py 值auth.settings.login_url, /default/user/login将被作为视图调用。问题在于,由于视图不是一个有效的 JSON-RPC 消息,json-xml-rpc库将丢弃它并失败。你可以捕获错误,但很难识别它。我找到的最简单解决方案,并且我希望其他人能找到更好的解决方案,是将auth.settings.login_url的值设置为 RPC 控制器中的一个操作,该操作返回的只是一个简单的字符串。 -
在
db.py中设置:auth.settings.login_url = URL("BasicJSONRPC", 'Login') -
Login是一个非 JSON-RPC 操作(因为我们不希望它需要认证),它返回一个容易识别的字符串:def Login(): return "Not logged in" -
我们可以随后通过在每次 RPC 调用失败时运行检查来从客户端检测认证失败。在异步调用的
onException处理程序(见RunSmallTest)中,替换以下代码以处理认证:onException:function(errorObj){ if(errorObj.message.toLowerCase().indexOf( "badly formed json string: not logged in") >= 0) PromptForAuthentication(); else Log("SmallTest failed: " + errorObj.message); }这种方法的一个明显缺陷是我们失去了对常规 HTML 视图非常有用的登录视图。因此,虽然认证对 RPC 调用有效,但它破坏了 HTML 视图的认证。
-
我们现在可以简化我们的调用。
虽然无法真正简化 json-xml-rpc 库用于异步调用的语法,但对于仅仅获取或更新客户端数据对象的调用,它确实可以在某种程度上自动化许多部分。如果你试图以一致的方式处理错误和认证,这特别有用。我们可以使用以下客户端包装函数来执行异步调用:
function LoadDataObject(objectName,params, responseObject,errorObject) { Log("Loading data object \"" + objectName + "\"") eval("" + objectName + " = \"Loading\""); eval(objectName +"Ready = false"); if(responseObject === undefined) { if(Connected != true) { Log("Not connected, connecting..."); InitDataConnection(); } var listUndefined = eval("DataController." + objectName + " !== undefined") if(Connected == true && listUndefined == true) { var paramsString = ""; for(var i in params) { paramsString += "params[" + i + "],"; } //Removing trailing coma paramsString = paramsString.substring(0, (paramsString.length - 1)); eval( "DataController." + objectName + "({params:[" + paramsString + "], onSuccess:function(response){LoadDataObject(\"" + objectName + "\",[" + paramsString + "],response)}, onException:function(error){ Log(\"Error detected\"); LoadDataObject(\"" + objectName + "\",[" + paramsString + "],null, error);}, onComplete:function(responseObj){ Log(\"Finished loading " + objectName + "\");} });" ); } else { eval(objectName + " = \"Could not connect\""); eval(objectName + "Ready = false"); Log("Could not connect. Either server error " + "or calling non existing method (" + objectName + ")"); } } else { if(errorObject === undefined) { eval(objectName + " = responseObject"); eval(objectName +"Ready = true"); } else { Log("Failed to Load Data Object " + objectName + ": " + errorObject.message) eval(objectName + " = errorObject"); eval(objectName + "Ready = false"); } } }这个函数可以用于任意数量的数据对象。要求如下:
-
定义一个与 RPC 函数同名的
data object变量(例如:UserList) -
定义另一个变量,其名称后跟
Ready(例如:UserListReady) -
调用包装函数,通过传递一个字符串形式的 RPC 操作名称和一个包含任何所需参数值的数组(例如:
LoadDataObject("UserList", ["admins",false]))
-
在调用过程中,ready变量将被设置为false,而data object变量将被设置为字符串Loading。如果发生错误,ready变量将保持false,而data object变量将被设置为错误对象。如果需要,你可以轮询这两个变量。
还有更多...
json-xml-rpc库是一个单独的 JavaScript 文件,可以通过从以下 Google 托管代码站点下载rpc-client-JavaScript ZIP 文件来获取:
code.google.com/p/json-xml-rpc/downloads/list
它有优秀的文档,位于以下 URL:
code.google.com/p/json-xml-rpc/wiki/DocumentationForJavaScript
然而,他们的代码中有一个 bug。在修订版 36中,我们必须将第 422 行更改为第 424:
//Handle errors returned by the server
if(response.error !== undefined){
var err = new Error(response.error.message);
到以下
//Handle errors returned by the server
if(response.error && response.error !== undefined){
var err = new Error(response.error.message);
我们还必须在system.ListMethods的调用中删除第 151 行和第 154 行中的点,以便支持 Python 中的systemListMethods函数。
使用 pyamf 从 Flex 调用 amf3 RPC
与《官方 web2py 书籍》中的示例不同,在这个菜谱中,我们向您展示如何与mxml Flex 应用程序通信,而不是与 Flash 通信。
准备工作
首先,你必须安装pyamf并将其对 web2py 可见(web2py 最初不包含 pyamf)。为此,访问以下 URL 的 pyamf 下载页面,并获取最新稳定版本的 ZIP 文件:
www.pyamf.com/community/download.html
解压并根据INSTALL.txt中的说明进行安装。我建议使用以下命令,以避免可能的问题:
python setup.py install --disable-ext
这将在 Python 的安装文件夹中放置一个.egg包(例如PyAMF-0.5.1-py2.6.egg),位于\Lib\site-packages下(例如,C:\Python26\Lib\site-packages)。.egg基本上是一个 ZIP 存档(如.jar到 Java),所以打开它并提取pyamf文件夹。转到 web2py 安装文件夹,找到library.zip存档。将pyamf添加到这个存档中。就是这样!现在,web2py 将透明地运行pyamf。
如何做到这一点...
-
Python 代码:假设你正在开发一个名为
app的应用程序,web2py 服务器运行在本机(127.0.0.1:8000)。添加一个名为rpc.py的新控制器,并将以下代码添加到控制器中:from gluon.tools import Service service = Service(globals()) def call(): session.forget() return service() @service.amfrpc3("mydomain") def test(): return "Test!!!"注意,mydomain 非常重要。你可以使用不同的域名,但必须保持一致。不要忘记它!
-
Flex mxml/AS3 代码:现在,创建一个新的Flex应用程序,并用以下代码替换其内容:
<?xml version="1.0" encoding="utf-8"?> <mx:Application layout="absolute"> <mx:Script> <![CDATA[ import mx.rpc.events.FaultEvent; import mx.rpc.events.ResultEvent; import mx.controls.Alert; private function resultHandler( event:ResultEvent):void { trace(event.result.toString()); } private function faultHandler( event:FaultEvent):void { trace(event.fault.message); } ]]> </mx:Script> <mx:RemoteObject id="amfService" endpoint="http://127.0.0.1:8000/app/rpc/call/amfrpc3" destination="mydomain" showBusyCursor="true"> <mx:method name="test" result="resultHandler(event)" fault="faultHandler(event)"/> </mx:RemoteObject> <mx:Button x="250" y="150" label="Fire" click="amfService.test();"/> </mx:Application> -
code_xml:请注意RemoteObject. Endpoint的定义是一个服务 URL。它不包括 RPC 方法名称,该名称应在mx:method对象的name属性中指定。/call/amfrpc3是一个标准 URL 后缀,不应更改。指定目标属性很重要,它是@service.amfrpc3(...)装饰器中控制器中出现的相同 ID。 -
设置
crossdomain.xml:注意,为了让 Flex 能够从不同的域调用 RPC 服务,需要在那个域的服务器顶级目录下暴露一个适当的crossdomain.xml文件。例如:http://mydomain.com:8000/crossdomain.xml要做到这一点,在应用程序的
static/文件夹内创建crossdomain.xml文件(web2py 不支持公共资源,因此我们将进行一些路由),并添加适当的访问策略。例如,完全访问(出于安全原因不推荐):<?xml version="1.0"?> <!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd"> <cross-domain-policy> <allow-access-from domain="*" /> </cross-domain-policy>现在转到 web2py 安装文件夹,创建一个名为 routes.py 的文件,并包含以下内容:
routes_in = (('/crossdomain.xml', '/app/static/crossdomain.xml'),)此文件指示 web2py 服务器将 crossdomain.xml 中的所有请求重定向到应用程序静态资源的位置。不要忘记关闭并运行服务器进程,以便它重新加载路由信息。
Web2py 中的 PayPal 集成
这个食谱旨在介绍 web2py 中的 Paypal 集成。它绝对没有涵盖所有可能的 PayPal 集成,主要关注 PayPal 命名的 标准集成。给出的示例在撰写本文时已被验证,但它们只能作为起点,而不是参考。为此,请使用 PayPal 和 web2py 的文档。
注意
PayPal 提供了不同层次的集成,这取决于你需要做什么,可能更适合你的需求。在开始编程之前,了解 PayPal 提供的基本集成概念是很重要的,这样你可以提前规划最适合你需求的内容。
话虽如此,在进一步讨论之前,让我尝试给你一个不同层次的大致概念,以便更好地理解这个食谱涵盖的小领域。然而,这是一个大多数小型和中型项目可能会遇到的领域。
在大体上,使用 PayPal 可以实现三个层次的集成:
-
快速结账: 在 PayPal 的卖家账户中,您可以创建与您可能销售的每个项目相关的按钮信息(名称、描述、项目编号和定价)。您可以以这种方式定义多达 1000 个不同的按钮或项目。之后,只需将按钮设置在 HTML 中,与应用程序一起使用即可。关于 web2py,只需将 PayPal 为每个按钮创建的代码复制到您的产品
db中的文本字段,然后在需要时将其显示在屏幕上,这实际上非常简单。使用这种方法,可以选择不同的购买体验,包括直接结账或购物车管理(由 PayPal 管理),这可以让您在 PayPal 的结账屏幕中添加或删除项目。我不喜欢这种方法,除非您要销售非常非常少的商品代码,因为这可能会使您在 PayPal 上维护文章变得痛苦。如果您销售少量服务或具有小价格组合的商品,这可能会非常有价值,因为您不需要从编程角度做太多工作,而且设置起来非常简单。 -
标准集成: 这是我们将在本文中介绍的内容。它基本上允许您管理自己的产品数据库等,并在支付时将所有数据发送到 PayPal,以便整个结账过程在 PayPal 上管理。交易完成后,您可以选择(根据您在 PayPal 卖家账户中的个人资料配置)是否将客户重定向回您的域名(您可以设置一个默认 URL 返回,或者每次发送结账数据时动态发送该 URL,但此功能需要在您的卖家账户中激活)。以下两点需要在此处提及,我认为它们是标准集成的一部分,尽管它们不是使您的网站基本功能正常工作的必需品:
-
支付数据传输 (PDT): 这将是一个将客户送回您域名的流程,让您能够捕获交易数据(来自 PayPal 的支付确认数据),并在您自己的域名中的确认屏幕上显示,同时可以显示您可能想要显示的任何其他信息,或者将客户重定向以继续购物。这并不完全安全,因为没有什么是保证客户会被重定向的;这种情况可能发生,因为在某些情况下,PayPal 不会执行重定向,而是强制客户点击一个额外的按钮返回您的域名,以便给客户加入 PayPal 的机会。当客户使用信用卡支付而不是使用他的 PayPal 账户时,这种情况就会发生。
-
即时支付通知 (IPN): 这是一个消息服务,它连接到您的域名,以发送在 PayPal 上处理的每笔交易的信息。它会在您确认接收(或四天未确认)之前不断发送消息。这是收集 PayPal 上所有交易数据的最佳方式,并触发您可能有的任何内部流程。通常,您会在此时进行产品的发货。
-
-
详细集成: 在这里,我实际上是在汇总许多其他方法和 API,我将不会详细说明;其中一些用于非常特定的用途。我想特别提到的唯一方法是名称值对 (NVP),因为它提供了一个非常简单的编程接口,您可以使用它进行非常详细的过程控制,管理所有数据以及从您的域名到所有交易流程。使用 NVP,例如,您可以在您的域名中捕获与支付相关的所有数据,然后仅在这一点上,将所有信息发送到 PayPal 进行处理(与处理结账相反,这是我们之前所做的事情)。您可以在
web2py.com/appliances/default/show/28找到一个很好的实现示例,或者访问主网页,在免费应用程序下找到它,PayPalEngine,由Matt Sellers开发。然而,您应该检查 PayPal 的详细文档,因为该过程涉及许多步骤,以确保交易的最大安全性。
因此,基本上,在快速结账过程中,PayPal 管理您的购物车(和主数据)、结账流程,当然,还有支付。使用标准集成,PayPal 管理结账和支付,而使用更详细的集成,您可以使其仅管理支付。
如何操作...
在继续之前,有关与 PayPal 集成的所有技术文档可以在以下位置找到:
cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/library_documentation
如果此链接发生变化,可以通过点击以下文档链接找到该链接:
接下来,关于如何使用标准集成,您应该做的第一件事是为自己创建一个沙盒账户。您可以在https://developer.paypal.com/上完成此操作。创建自己的账户,登录后,创建至少两个测试账户:一个seller和一个buyer。有一个很好的指南,称为PP 沙盒用户指南,您可以在之前提供的文档链接中找到,或者在一个 HTML 版本中查看cms.paypal.com/us/cgi-bin/? cmd=_render-content&content_ID=developer/howto_testing_sandbox。如何设置您的账户并开始运行的所有信息都在那里描述。
一旦设置并运行,您将拥有您的卖家 ID 和电子邮件(您可以使用它们中的任何一个在下面的代码中向 PayPal 进行身份验证,尽管我更喜欢 ID,以避免可能的垃圾邮件)。
好的,那么现在,我们已经可以创建一个结账按钮,该按钮将带我们的客户访问包含所有购物车数据的 PayPal 网站。在继续之前,您可以在之前提供的文档链接下找到与此点相关的所有文档,在网站支付标准集成指南中,或者直接在 HTML 格式中查看:
cms.paypal.com/us/cgi-bin/? cmd=_render-content&content_ID=developer/howto_html_wp_standard_overview
检查有关第三方购物车的信息。无论如何,创建发送所有信息的按钮实际上非常简单。您只需要在结账页面视图中添加以下代码:
<form action="https://www.sandbox.paypal.com/cgi-bin/webscr"
method="post">
<!-- Select the correct button depending on country etc.
If you can do it with pre-generated buttons (with prices included
etc)
then so much the better for security -->
<input type="hidden" name="business" value="{{=paypal_id}}" />
<input type="image" src=
"https://www.sandbox.paypal.com/es_XC/i/btn/btn_buynowCC_LG.gif"
border="0" name="submit" alt="PayPal - The safer, easier way to pay
online!">
<img alt="" border="0" src=
"https://www.sandbox.paypal.com/es_XC/i/scr/pixel.gif" width="1"
height="1">
<form action="http://www.sandbox.paypal.com/cgi-bin/webscr"
method="post" />
<input type="hidden" name="cmd" value="_cart" />
<input type="hidden" name="upload" value="1" />
<input type="hidden" name="charset" value="utf-8">
<input type="hidden" name="currency_code" value="EUR" />
<input type="hidden" name="display" value="1"/>
<input type="hidden" name="shopping_url"
value="http://www.micropolixshop.com/giftlist/default/glist"/>
<!-- Not really necessary, only if want to allow
continue Shopping -->
<input type="hidden" name="notify_url" value=
"http://www.micropolixshop.com/giftlist/default/ipn_handler"/>
<!-- Or leave blank and setup default url at paypal -->
<input type="hidden" name="return"
value="http://www.micropolixshop.com/giftlist/default/confirm"/>
<!-- Or leave blank and setup default url at paypal -->
<input type="hidden" name="custom" value="{{=session.event_code}}"/>
{{k=1}}
{{for id,product in products.items():}}
<input type="hidden" name="item_number_{{=k}}"
value="{{=product.ext_code}}"/>
<input type="hidden" name="item_name_{{=k}}"
value="{{=product.name}}"/>
<input type="hidden" name="quantity_{{=k}}"
value="{{=session.cart[str(id)]}}"/>
<input type="hidden" name="discount_rate_{{=k}}" value="15"/>
<!-- ie, wants a 15% on all articles always -->
<input type="hidden" name="tax_{{=k}}"
value="{{=product.price*product.tax_rate}}"/>
<input type="hidden" name="amount_{{=k}}"
value="{{=product.price}}"/>
{{k+=1}}
{{pass}}
</form>
关于列表lst:CheckoutButton:的一些评论
-
在所有情况下,要从沙盒环境迁移到生产环境,只需将使用的 URL 从
www.sandbox.paypal.com更改为www.paypal.com即可。 -
您可以使用卖家账户中的“创建新按钮”功能来创建按钮,然后重用该代码。这将让您选择要使用的语言和按钮类型。这样,您将获得用于您的 PayPal 按钮的正确图片链接。
-
字段
cmd的value _cart非常重要。阅读文档以查看此字段的可能值,具体取决于您想做什么。在这个例子中,我假设了一个购物车场景。 -
如果您设置了卖家账户配置文件,则可以省略字段
shopping_url, notify_url和return。如果在这里设置,则它将优先于在您的卖家账户中设置的默认值。 -
字段
custom我认为相当重要,因为它是少数几个允许您引入不向客户显示的数据的字段之一,这可能允许您跟踪任何额外信息。它是按交易(而不是按项目)计算的。在这种情况下,我选择使用内部事件代码来跟踪与某个事件(如特殊的promotion或任何其他)相关的所有购买。 -
如您所见,我创建了一个循环,包含所有购物车项目以进行结账,通过传递包含所有产品数据的字典。我在会话中拥有已购买商品的信息。它们按照 PayPal 规则命名和编号。
-
关于折扣,尽管您为每个商品设置了折扣,但 PayPal 只显示折扣总额。我不知道这是否在 Pro 版本中有所不同。
如需更多信息,您应查阅之前提到的文档,其中包含您可用的所有字段列表(包括运费等)。
结账确认/支付数据传输:一旦客户通过 PayPal 完成支付,如果账户中已设置并他是 PayPal 用户(否则他需要点击按钮返回您的网站),他将被自动重定向到您的网站。本节向您展示如何设置您的应用程序,以便它将从 PayPal 接收支付数据确认,并向客户显示确认信息。
您可以在此处阅读有关此主题的详细文档:
cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/howto_html_paymentdatatransfer
在这里,您可以详细了解如何设置,以便您知道从哪里获取您的令牌,这是您需要用来向 PayPal 确认并获取数据的。在任何情况下,请参考以下表示基本 PDT 交易流程图的图(来自 PayPal 文档),以便给您一个详细的过程流程视图:

在lst:generic-def列表中,我包含了一些我在设置界面时使用的通用函数。Connection类定义是我在网上冲浪时找到的一个通用连接示例的修改版本,但我真的不记得具体在哪里找到了。我包含的add_to_cart、remove_from_cart、empty_cart和checkout作为如何设置购物车的示例,这些是从EStore中提取的,可以在www.web2py.com/appliances/default/show/24找到。
再次强调,这里的不同方法被过度简化了,试图用几行文字解释不同的可能性:
# db.py file
#####################################################################
# Global Variables definition
#####################################################################
domain='www.sandbox.paypal.com'
protocol='https://'
user=None
passwd=None
realm=None
headers = {'Content-Type':'application/x-www-form-urlencoded'}
# This token should also be set in a table so that the seller can set
#it up
# dynamically and not through the code. Same goes for the PAGINATE.
paypal_token="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
PAGINATE = 20
#####################################################################
# default.py file
#####################################################################
# coding: utf8
import datetime
import string
if not session.cart: session.cart, session.balance={},0
app=request.application
#### Setup PayPal login email (seller id) in the session
#### I store paypal_id in a table
session.paypal_id=myorg.paypal_id
import urllib2, urllib
import datetime
class Connection:
def __init__(self, base_url, username, password, realm = None,
header = {}):
self.base_url = base_url
self.username = username
self.password = password
self.realm = realm
self.header = header
def request(self, resource, data = None, args = None):
path = resource
if args:
path += "?" + (args)
# create a password manager
password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
if self.username and self.password:
# Add the username and password.
password_mgr.add_password(self.realm, self.base_url,
self.username, self.password)
handler = urllib2.HTTPBasicAuthHandler(password_mgr)
# create "opener" (OpenerDirector instance)
opener = urllib2.build_opener(handler)
# Install the opener.
# Now all calls to urllib2.urlopen use our opener.
urllib2.install_opener(opener)
#Create a Request
req=urllib2.Request(self.base_url + path, data, self.header)
# use the opener to fetch a URL
error = ''
try:
ret=opener.open(req)
except urllib2.HTTPError, e:
ret = e
error = 'urllib2.HTTPError'
except urllib2.URLError, e:
ret = e
error = 'urllib2.URLError'
return ret, error
def add_to_cart():
"""
Add data into the session.cart dictionary
Session.cart is a dictionary with id product_id and value = quantity
Session.balance is a value with the total of the transaction.
After updating values, redirect to checkout
"""
pid=request.args[0]
product=db(db.product.id==pid).select()[0]
product.update_record(clicked=product.clicked+1)
try: qty=session.cart[pid]+1
except: qty=1
session.cart[pid]=qty
session.balance+=product.price
redirect(URL('checkout'))
def remove_from_cart():
"""
allow add to cart
"""
pid = request.args[0]
product=db(db.product.id==pid).select()[0]
if session.cart.has_key(pid):
session.balance-=product.price
session.cart[pid]-=1
if not session.cart[pid]: del session.cart[pid]
redirect(URL('checkout'))
def empty_cart():
"""
allow add to cart
"""
session.cart, session.balance={},0
redirect(URL('checkout'))
def checkout():
"""
Checkout
"""
pids = session.cart.keys()
cart={}
products={}
for pid in pids:
products[pid]=db(db.product.id==pid).select()[0]
return dict(products=products,paypal_id=session.paypal_id)
最后,在lst:confirm列表中确认,将处理从 PayPal 发送的信息,按照之前基本 PDT 交易流程图中的四个步骤进行,步骤 2、3、4 和 5。
def confirm():
"""
This is set so as to capture the transaction data from PayPal
It captures the transaction ID from the HTTP GET that PayPal
sends.
And using the token from vendor profile PDT, it does a form post.
The data from the http get comes as vars Name Value Pairs.
"""
if request.vars.has_key('tx'):
trans = request.vars.get('tx')
# Establish connection.
conn = Connection(base_url=protocol+domain, username=user,
password = passwd, realm = realm, header = headers)
data = "cmd=_notify-synch&tx="+trans+"&at="+paypal_token
resp,error=conn.request('/cgi-bin/webscr', data)
data={}
if error=='':
respu = resp.read()
respuesta = respu.splitlines()
data['status']=respuesta[0]
if respuesta[0]=='SUCCESS':
for r in respuesta[1:]:
key,val = r.split('=')
data[key]=val
msg=''
if data.has_key('memo'): msg=data['memo']
form = FORM("Quiere dejar un mensaje con los regalos?",
INPUT(_name=T('message'),_type="text",_value=msg),
INPUT(_type="submit"))
if form.accepts(request,session):
email=data['payer_email'].replace('%40','@')
id = db.gift_msg.insert(buyer=data['payer_email'],
transact=trans,msg=form.vars.message)
response.flash=T('Your message will be passed on to the
recipient')
redirect(URL('index'))
return dict(data=data,form=form)
return dict(data=data)
else:
data['status']='FAIL'
else:
redirect(URL('index'))
return dict(trans=trans)
仅为了完整性,我添加了一个非常基础的confirm.html示例,您可以在lst:confirmhtml列表中看到。
{{extend 'layout.html'}}
{{if data['status'] == 'SUCCESS':}}
<p><h3>{{=T('Your order has been received.')}}</h3></p>
<hr>
<b>{{=T('Details')}}</b><br>
<li>
{{=T('Name:')}} {{=data['first_name']}} {{=data['last_name']}}
</li>
<li>'
{{=T('Purchases for event:')}}: {{=data['transaction_subject']}}
</li>
<li>
{{=T('Amount')}}: {{=data['mc_currency']}} {{=data['mc_gross']}}
</li>
<hr>
{{=form}}
{{else:}}
{{=T('No confirmation received from PayPal. This can be due to a
number of reasons; please check your email to see if the
transaction was successful.')}}
{{pass}}
{{=T('Your transaction has finished, you should receive an email of
your purchase.')}}<br>
{{=T('If you have an account at PayPal, you can check your
transaction details at')}}
<a href='https://www.paypal.es'>www.paypal.es</a>
即时支付通知(IPN):如前所述,不能完全信任 PDT 过程来接收所有交易的信息,因为可能会发生许多事情。因此,如果你需要对你的销售信息进行额外的处理,或者如果你想保留实际处理的销售的本地区域数据库,你需要实现一个额外的过程。
这是通过 IPN 完成的。你可以在之前给出的文档站点 URL 中找到所有相关文档。你需要在你的卖家账户中启用 IPN 功能,并给出一个默认 URL 来接收这些消息,这个 URL 应该等于你处理它们的视图。在这个例子中,它将是:www.yourdomain.com/yourapp/default/ipn_handler>。
该过程与 PDT 的过程非常相似;甚至变量都是相同的。主要区别在于 IPN 是从 PayPal 发送的,直到你确认它们。这个功能的视图default/ipn_handler.html完全可以留空。我还包括了一个用于记录 PayPal 消息的表定义。
无论如何,你可以在列表lst:ipnhandler中找到一个如何设置的示例:
#### At models/db.py
#####################################################################
db.define_table('ipn_msgs',
Field('trans_id',label=T('transaction id')),
Field('timestamp','datetime',label=T('timestamp')),
Field('type',label=T('type')),
Field('msg','text',label=T('message')),
Field('processed','boolean',label=T('processed')),
Field('total','double',label=T('total')),
Field('fee','double',label=T('fee')),
Field('currency',length=3,label=T('currency')),
Field('security_msg',label=T('security message'))
)
#### At controllers/default.py
#####################################################################
def ipn_handler():
"""
Manages the ipn connection with PayPal
Ask PayPal to confirm this payment, return status and detail strings
"""
parameters = None
parameters = request.vars
if parameters:
parameters['cmd'] = '_notify-validate'
params = urllib.urlencode(parameters)
conn = Connection(base_url=protocol+domain, username=user,
password = passwd, realm = realm, header = headers)
resp,error =conn.request('/cgi-bin/webscr', params)
timestamp=datetime.datetime.now()
# We are going to log all messages confirmed by PayPal.
if error =='':
ipn_msg_id = db.ipn_msgs.insert(trans_id=parameters['txn_id'],
timestamp=timestamp,type=resp.read(),msg=params,
total=parameters['mc_gross'],fee=parameters['mc_fee'],
currency=parameters['mc_currency'])
# But only interested in processing messages that have payment
#status completed and are VERIFIED by PayPal.
if parameters['payment_status']=='Completed':
process_ipn(ipn_msg_id,parameters)
唯一缺少的是处理接收到的信息,并检查错误或可能的欺诈尝试。你可以在列表lst:processipn中看到一个示例函数。尽管这可能是每个项目都会相当大的变化,但我希望它可能对你作为一个粗略的指南有所帮助。
def process_ipn(ipn_msg_id,param):
"""
We process the parameters sent from IPN PayPal, to correctly
store the confirmed sales in the database.
param -- request.vars from IPN message from PayPal
"""
# Check if transaction_id has already been processed.
query1 = db.ipn_msgs.trans_id==param['txn_id']
query2 = db.ipn_msgs.processed == True
rows = db(query1 & query2).select()
if not rows:
trans = param['txn_id']
payer_email = param['payer_email']
n_items = int(param['num_cart_items'])
pay_date = param['payment_date']
total = param['mc_gross']
curr = param['mc_currency']
event_code = param['custom']
if param.has_key('memo'): memo=param['memo']
event_id = db(db.event.code==event_code).select(db.event.id)
if not event_id:
db.ipn_msgs[ipn_msg_id]=dict(security_msg=T('Event does not
exist'))
else:
error=False
for i in range(1,n_items+1):
product_code = param['item_number'+str(i)]
qtty = param['quantity'+str(i)]
line_total = float(param['mc_gross_'+str(i)]) +
float(param['mc_tax'+str(i)])
product=db(db.product.ext_code==product_code).
select(db.product.id)
if not product:
db.ipn_msgs[ipn_msg_id]=dict(security_msg=T('Product code
does not exist'))
error=True
else:
db.glist.insert(event=event_id[0],product=product[0],
buyer=payer_email,transact=trans,
purchase_date=pay_date,quantity_sold=qtty,
price=line_total,observations=memo)
if not error: db.ipn_msgs[ipn_msg_id]=dict(processed=True)
希望这个部分能帮助你使用 web2py 设置 PayPal 网站,或者至少帮助你理解设置一个网站背后的基本概念,以及你拥有的不同可能性。
PayPal 网络支付标准
这个配方展示了使用加密请求和 IPN 实现 PayPal 网络支付标准的示例,以确保流程的安全性。请注意,在这个配方中,使用了 web2py 版本 1.77.3。希望它仍然在最新的 web2py 版本中工作。
如何做...
-
为了实现我们与 PayPal 的集成,我首先开始编写生成加密表单提交到 PayPal 的代码,用于我们所有的购物车操作。如果你这样做,并且配置 PayPal 只接受签名请求,那么用户就不能篡改你的表单并更改商品的价格。为此,我在我们的系统上安装了M2Crypto模块,并创建了一个模块,用于对 PayPal 表单进行签名。请注意,这不能在 Google App Engine 上运行,因为 M2Crypto 不支持 GAE。
我还没有找到在 App Engine 上运行的替代品,所以你不能在那个环境中使用这个 PayPal 支付配方。
加密模块(
crypt.py)使用证书对数据进行签名,然后加密它,如下面的代码所示:from M2Crypto import BIO, SMIME, X509, EVP def paypal_encrypt(attributes, sitesettings): """ Takes a list of attributes for working with PayPal (in our case adding to the shopping cart), and encrypts them for secure transmission of item details and prices. @type attributes: dictionary @param attributes: a dictionary of the PayPal request attributes. An example attribute set is: >>> attributes = {"cert_id":sitesettings.paypal_cert_id, "cmd":"_cart", "business":sitesettings.cart_business, "add":"1", "custom":auth.user.id, "item_name":"song 1 test", "item_number":"song-1", "amount":"0.99", "currency_code":"USD", "shopping_url":'http://'+\ Storage(globals()).request.env.http_host+\ URL(args=request.args), "return":'http://'+\ Storage(globals()).request.env.http_host+\ URL('account', 'downloads'), } @type sitesettings: SQLStorage @param sitesettings: The settings stored in the database. this method requires I{tenthrow_private_key}, I{tenthrow_public_cert}, and I{paypal_public_cert} to function @rtype: string @return: encrypted attribute string """ plaintext = '' for key, value in attributes.items(): plaintext += u'%s=%s\n' % (key, value) plaintext = plaintext.encode('utf-8') # Instantiate an SMIME object. s = SMIME.SMIME() # Load signer's key and cert. Sign the buffer. s.pkey = EVP.load_key_string(sitesettings.tenthrow_private_key) s.x509 = X509.load_cert_string( sitesettings.tenthrow_public_cert) #s.load_key_bio(BIO.openfile(settings.MY_KEYPAIR), # BIO.openfile(settings.MY_CERT)) p7 = s.sign(BIO.MemoryBuffer(plaintext), flags=SMIME.PKCS7_BINARY) # Load target cert to encrypt the signed message to. #x509 = X509.load_cert_bio(BIO.openfile(settings.PAYPAL_CERT)) x509 = X509.load_cert_string(sitesettings.paypal_public_cert) sk = X509.X509_Stack() sk.push(x509) s.set_x509_stack(sk) # Set cipher: 3-key triple-DES in CBC mode. s.set_cipher(SMIME.Cipher('des_ede3_cbc')) # Create a temporary buffer. tmp = BIO.MemoryBuffer() # Write the signed message into the temporary buffer. p7.write_der(tmp) # Encrypt the temporary buffer. p7 = s.encrypt(tmp, flags=SMIME.PKCS7_BINARY) # Output p7 in mail-friendly format. out = BIO.MemoryBuffer() p7.write(out) return out.read() -
然后,我们在视图中构建表单,并加密它们:
{{from applications.tenthrow.modules.crypt import * }} {{ attributes = {"cert_id":sitesettings.paypal_cert_id, "cmd":"_cart", "business":sitesettings.cart_business, "add":"1", "custom":auth.user.id, "item_name":artist_name + ": " + song['name'], "item_number":"song-"+str(song['cue_point_id']), "amount":song['cost'], "currency_code":"USD", "shopping_url":full_url('http',r=request,args=request.args), "return":full_url('https', r=request, c='account', \ f='alldownloads'), } encattrs = paypal_encrypt(attributes, sitesettings) }} <form target="_self" action="{{=sitesettings.cart_url}}" method="post" name="song{{=song['cue_point_id']}}"> <!-- Identify your business so that you can collect the payments. --> <input type="hidden" name="cmd" value="_s-xclick" class="unform"/> <input type="hidden" name="encrypted" value="{{=encattrs}}" class="unform"/> <a onclick="document.song{{=song['cue_point_id']}}.submit()" class="trBtn"> <img src="img/trIconDL.png')}}" alt="Download {{=(song['name'])}}" class="original"/> <img src="img/trIconDL_Hover.png')}}" alt="Download {{=(song['name'])}}" class="hover"/> </a> <img alt="" border="0" width="1" height="1" src="img/pixel.gif" class="unform"/> </form>注意,上述代码调用了一个名为 full_url()的方法,其定义如下:
def full_url(scheme="http", a=None, c=None, f=None, r=None, args=[], vars={}, anchor='', path = None ): """ Create a fully qualified URL. The URL will use the same host that the request was made from, but will use the specified scheme. Calls C{gluon.html.URL()} to construct the relative path to the host. if <scheme>_port is set in the settings table, append the port to the domain of the created URL @param scheme: scheme to use for the fully-qualified URL. (default to 'http') @param a: application (default to current if r is given) @param c: controller (default to current if r is given) @param f: function (default to current if r is given) @param r: request @param args: any arguments (optional) @param vars: any variables (optional) @param anchor: anchorname, without # (optional) @param path: the relative path to use. if used overrides a,c,f,args, and vars (optional) """ port = '' if sitesettings.has_key(scheme+"_port") and sitesettings[scheme+"_port"]: port = ":" + sitesettings[scheme+"_port"] if scheme == 'https' and sitesettings.has_key("https_scheme"): scheme = sitesettings.https_scheme url = scheme +'://' + \ r.env.http_host.split(':')[0] + port if path: url = url + path else: url = url+URL(a=a, c=c, f=f, r=r, args=args, vars=vars, anchor=anchor) return url -
然后,我需要能够处理我们的 PayPal IPN 响应。以下代码正是这样做的。你会看到我只处理购买请求。我还保留了特定于我们数据库的代码,关于如何编码产品 ID,然后使用该产品 ID 在我们的数据库中创建记录。根据我们数据库中这些购买记录的存在,我允许用户下载他们购买的内容。因此,用户必须在 IPN 消息处理完毕后才能下载他们的购买内容。这通常在他们提交订单后的 5 到 30 秒内完成。大多数情况下,消息在 PayPal 将用户重定向回我们的网站之前就已经被接收和处理。我们的
paypal.py控制器将有一个处理即时支付通知的函数,如以下代码部分所述(注意我们在模块目录中有openanything。访问diveintopython.org/获取最新版本):from applications.app.modules.openanything import * def ipn(): """ This controller processes Instant Payment Notifications from PayPal. It will verify messages, and process completed cart transaction messages only. all other messages are ignored for now. For each item purchased in the cart, the song_purchases table will be updated with the purchased item information, allowing the user to download the item. logs are written to /tmp/ipnresp.txt the PayPal IPN documentation is available at: https://cms.paypal.com/cms_content/US/en_US/files/developer/ IPNGuide.pdf """ """ sample PayPal IPN call: last_name=Smith& txn_id=597202352& receiver_email=seller%40paypalsandbox.com& payment_status=Completed&tax=2.02& mc_gross1=12.34& payer_status=verified& residence_country=US& invoice=abc1234& item_name1=something& txn_type=cart& item_number1=201& quantity1=1& payment_date=16%3A52%3A59+Jul.+20%2C+2009+PDT& first_name=John& mc_shipping=3.02& charset=windows-1252& custom=3& notify_version=2.4& test_ipn=1& receiver_id=TESTSELLERID1& business=seller%40paypalsandbox.com& mc_handling1=1.67& payer_id=TESTBUYERID01& verify_sign=AFcWxV21C7fd0v3bYYYRCpSSRl31AtrKNnsnrW3-8M8R- P38QFsqBaQM& mc_handling=2.06& mc_fee=0.44& mc_currency=USD& payer_email=buyer%40paypalsandbox.com& payment_type=instant& mc_gross=15.34& mc_shipping1=1.02 """ #@todo: come up with better logging mechanism logfile = "/tmp/ipnresp.txt" verifyurl = "https://www.paypal.com/cgi-bin/webscr" if request.vars.test_ipn != None and request.vars.test_ipn == '1': verifyurl = "https://www.sandbox.paypal.com/cgi-bin/webscr" params = dict(request.vars) params['cmd'] = '_notify-validate' resp = fetch(verifyurl, post_data=params) #the message was not verified, fail if resp['data'] != "VERIFIED": #@todo: figure out how to fail f = open(logfile, "a") f.write("Message not verified:\n") f.write(repr(params) + "\n\n") f.close() return None ... if request.vars.txn_type != "cart": #for now ignore non-cart transaction messages f = open(logfile, "a") f.write("Not a cart message:\n") f.write(repr(params) + "\n\n") f.close() return None ... if request.vars.payment_status != 'Completed': #ignore pending transactions f = open(logfile, "a") f.write("Ignore pending transaction:\n") f.write(repr(params) + "\n\n") f.close() return None ... #check id not recorded if len(db(db.song_purchases.transaction_id==request. vars.txn_id).select())>0: #transaction already recorded f = open(logfile, "a") f.write("Ignoring recorded transaction:\n") f.write(repr(params) + "\n\n") f.close() return None #record transaction num_items = 1 if request.vars.num_cart_items != None: num_items = request.vars.num_cart_items for i in range(1, int(num_items)+1): #i coded our item_number to be a tag and an ID. the ID is # a key to a table in our database. tag, id = request.vars['item_number'+str(i)].split("-") if tag == "song": db.song_purchases.insert(auth_user=request.vars.custom, cue_point=id, transaction_id=request.vars.txn_id, date=request.vars.payment_date.replace('.', '')) elif tag == "song_media": db.song_purchases.insert(auth_user=request.vars.custom, song_media=id, transaction_id=request.vars.txn_id, date=request.vars.payment_date.replace('.', '')) elif tag == "concert": db.concert_purchases.insert(auth_user=request.vars.custom, playlist=id, transaction_id=request.vars.txn_id, date=request.vars.payment_date.replace('.', '')) else: #@TODO: this is an error, what should we do here? f = open(logfile, "a") f.write("Ignoring bad item number: " + \ request.vars['item_number'+str(i)] + "\n") f.write(repr(params) + "\n\n") f.close() f = open(logfile, "a") f.write("Processed message:\n") f.write(repr(params) + "\n\n") f.close() return None
就这些了,朋友们!
获取 Flickr 照片
这个菜谱可以用来获取通过照片集 ID 过滤的 Flickr 照片列表。
准备工作
首先,你需要生成一个APIKEY,你可以在 Flickr 开发者页面上完成:
www.flickr.com/services/api/misc.api_keys.html
之后,你需要创建一个函数来获取 Flickr API。通常,这会在模型中创建,但你也可以在模块中完成。
如何操作...
-
在你的模型文件中的任何一个创建一个函数。我们将创建一个名为
models/plugin_flickr.py的函数,如下所示:def plugin_flickr(key, photoset=None, per_page=15, page=1): from urllib2 import urlopen from xml.dom.minidom import parse as domparse apiurl = 'http://api.flickr.com/services/rest/?method=flickr. photosets.getPhotos&api_key=%(apikey)s&photoset_id= %(photoset)s&privacy_filter=1&per_page=%(per_page)s&page= %(page)s&extras=url_t,url_m,url_o,url_sq' dom = domparse(urlopen(apiurl % dict(photoset=photoset, per_page=per_page, page=page, apikey=key))) photos = [] for node in dom.getElementsByTagName('photo'): photos.append({ 'id':node.getAttribute('id'), 'title':node.getAttribute('title'), 'thumb':node.getAttribute('url_t'), 'medio':node.getAttribute('url_m'), 'original':node.getAttribute('url_o'), 'square':node.getAttribute('url_sq'), }) return photos -
现在,你可以在任何控制器或视图中调用该函数。例如,在一个控制器操作中,如下所示:
def testflickr(): photos = plugin_flickr( key='YOUR_API_KEY', photoset='THE_PHOTOSET_ID', per_page=15, page=1) return dict(photos=photos) -
在关联的
views/default/testflickr.html中,你可以添加以下内容:{{extend 'layout.html'}} {{for photo in photos:}} {{=IMG(_src=photo['square'])}} {{pass}}最终产品将类似于以下截图所示:

通过亚马逊网络服务(AWS)使用 Boto 发送电子邮件
亚马逊简单电子邮件服务是一种无需操作自己的邮件服务器即可发送电子邮件的好方法。此代码利用了Boto库,它是 AWS 的 Python 接口。
准备工作
-
首先,你需要在
aws.amazon.com注册 AWS。 -
然后,在
aws.amazon.com/ses/启用简单电子邮件服务。 -
你需要从
aws-portal.amazon.com/gp/aws/developer/account/index.html获取你的亚马逊AWS-KEY和AWS-SECRET-KEY。 -
最后,你需要在你的
web2py/site-packages文件夹中安装 Boto,或者在任何路径下,以便 web2py 可以找到并导入它。你可以在 GitHub 上找到 Boto:github.com/boto/boto。
在您获得亚马逊邮件服务器的生产访问权限之前,您必须预先注册您想要使用的每个发送者和接收者的电子邮件地址(最多 100 个)。这对于开发和测试来说是可行的,但在生产环境中当然是不行的。要注册电子邮件地址,请执行以下代码,将 AWS-KEY 和 AWS-SECRET-KEY 替换为您自己的密钥,并将 myemail@address.com 替换为您想要注册的电子邮件地址。
从 web2py shell 或任何其他 Python shell 中,运行以下命令:
from boto.ses.connection import SESConnection
def verify_email_address():
conn = SESConnection('', '')
m = conn.verify_email_address('myemail@address.com')
如何操作...
假设一切已按之前所述安装和配置,发送电子邮件很容易:
def test_send_emails():
aws_key = 'YOUR_AWS_KEY'
aws_secret_key = 'YOUR_SECRET_KEY'
from boto.ses.connection import SESConnection
conn = SESConnection(aws_key, aws_secret_key)
return conn.send_email(source='myemail@address.com',
subject='Subject',
body='Body.',
to_addresses='recipient@email.com',
cc_addresses=None,
bcc_addresses=None,
format='text',
reply_addresses=None,
return_path=None)
使用 mapscript 制作 GIS 图表
MapServer 是一个开源平台,用于将空间数据和交互式地图应用程序发布到网络。最初于 1990 年代中期在明尼苏达大学开发,MapServer 采用 MIT 风格许可发布,并在所有主要平台上运行。
这个菜谱将向您展示如何使用 MapServer 网络服务通过 mapscript 库发布地理参考地图。
准备工作
首先,您需要从以下地址安装 mapscript:
pypi.python.org/pypi/mapscript/5.4.2.1.
您可以通过输入以下命令来完成:
easy_install mapscript
我们还将假设您在应用程序文件夹中有一个名为 private/test2.map 的地图。一个 .map 文件看起来像是一个 ascii 文件,它描述了一个地图(坐标、类型、带标签的点等),如下所示:
MAP
NAME "sample"
EXTENT -180 -90 180 90 # Geographic
SIZE 800 400
IMAGECOLOR 128 128 255
END # MAP
您可以在此处了解更多关于地图文件的信息:
如何操作...
GIS 地图通过 WXS 服务公开。在这里,我们向您展示一个简单的操作,该操作提供了一种服务,用于发布存储在文件 private/test2.map 中的地图:
def wxs():
import mapscript
import os
path_map = os.path.join(request.folder, 'private', request.args(0))
if not request.vars:
return ''
req = mapscript.OWSRequest()
for v in request.vars:
req.setParameter(v, request.vars[v])
map = mapscript.mapObj(path_map)
mapscript.msIO_installStdoutToBuffer()
map.OWSDispatch(req)
content_type = mapscript.msIO_stripStdoutBufferContentType()
content = mapscript.msIO_getStdoutBufferBytes()
response.header = "Content-Type","%s; charset=utf-8"%content_type
return content
此服务可以通过 QGis (www.qgis.org/) 或任何其他 Web MapService 客户端 (en.wikipedia.org/wiki/Web_Map_Service),或 Web Feature Service 客户端 (en.wikipedia.org/wiki/Web_Feature_Service) 来消费。
传递给 QGIS 的 URL 是:
http://localhost:8000/mapas/default/wxs/test2.map
在这里,test2.map 指向我们的地图文件(存储在文件 private/test2.map 中,由之前描述的 wxs 函数提供服务)。
Google 群组和 Google 代码源代码订阅者
在这个菜谱中,我们将实现一个简单的源代码阅读器,使用 RSS 从 Google 群组和 Google 代码检索消息。
如何操作...
我们将创建一个名为 models/plugin_feedreader.py 的文件,其中包含以下代码:
def plugin_feedreader(name, source='google-group'):
"""parse group feeds"""
from gluon.contrib import feedparser
if source=='google-group':
URL = "http://groups.google.com/group/%(name)s/
feed/rss_v2_0_msgs.xml"
elif source=='google-code':
URL = "http://code.google.com/feeds/p/%(name)s/hgchanges/basic"
else:
URL = source
url = URL % dict(name=name)
g = feedparser.parse(url)
html = UL(*[LI(A(entry['title'],_href=entry['link']))\
for entry in g['entries'][0:5]])
return XML(html)
现在,在任何控制器中,您都可以嵌入最新的 Google 群组信息:
{{=plugin_feedreader('web2py', source='google-group')}}
或者阅读最新的 Google 代码源代码更新:
{{=plugin_feedreader('web2py', source='google-code')}}
创建 SOAP 网络服务
简单对象访问协议 (SOAP) 是一种基于 XML 的复杂进程间通信标准,用于网络服务实现。它广泛用于遗留应用程序(尤其是 JAVA 和 .NET 语言),并支持类型声明和 Web 服务定义文件 (WSDL) 。
web2py 已经支持一种通用的基础设施,以简单的方式公开网络服务,使用 Service 工具(rss、json、jsonrpc、xmlrpc、jsonrpc、amfrpc 和 amfrpc3)。
包含在 gluon/contribs(自版本 #1.82.1 以来)中的 PySimpleSOAP 库旨在添加 SOAP 支持,扩展当前的理念。
如何操作...
使用 @service.soap 装饰函数来提供操作服务,声明以下内容:
-
公开的操作方法(按惯例使用驼峰式命名)
-
返回类型
-
参数类型
类型使用字典声明,将参数/结果名称映射到标准的 Python 转换函数(str、int、float、bool 等)。
例如,创建一个应用程序(例如 webservices),在一个控制器(sample.py)中添加以下代码:
from gluon.tools import Service
service = Service(globals())
@service.xmlrpc
@service.soap('AddStrings',returns={'AddResult':str},
args={'a':str, 'b':str})
@service.soap('AddIntegers',returns={'AddResult':int},
args={'a':int, 'b':int})
def add(a,b):
"Add two values"
return a+b
@service.xmlrpc
@service.soap('SubIntegers',returns={'SubResult':int},
args={'a':int, 'b':int})
def sub(a,b):
"Substract two values"
return a-b
def call():
return service()
此外,web2py 可以动态生成帮助网页(操作列表、xml 消息示例)和 WSDL XML:
-
操作帮助(对于
SubIntegers方法,在这种情况下):127.0.0.1:8000/webservices/sample/call/soap?op=SubIntegers
样本操作列表页面:
Welcome to Web2Py SOAP webservice gateway
The following operations are available
See WSDL for webservice description
AddIntegers: Add two values
SubIntegers: Substract two values
AddStrings: Add two values
Notes: WSDL is linked to URL retriving the full xml. Each operation
is linked to its help page.
样本操作帮助页面:
AddIntegers
Add two values
Location: http://127.0.0.1:8000//webservices/sample/call/soap
Namespace: http://127.0.0.1:8000/webservices/sample/soap
SoapAction?: -N/A by now-
Sample SOAP XML Request Message:
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope
>
<soap:Body>
<AddIntegers
>
<a>
<!--integer-->
</a>
<b>
<!--integer-->
</b>
</AddIntegers>
</soap:Body>
</soap:Envelope>
Sample SOAP XML Response Message:
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope
>
<soap:Body>
<AddIntegersResponse
>
<AddResult>
<!--integer-->
</AddResult>
</AddIntegersResponse>
</soap:Body>
</soap:Envelope>
您可以使用此库测试 web2py 公开的网络服务:
def test_soap_sub():
from gluon.contrib.pysimplesoap.client import SoapClient, SoapFault
# create a SOAP client
client = SoapClient(wsdl="http://localhost:8000/webservices/
sample/call/soap?WSDL")
# call SOAP method
response = client.SubIntegers(a=3,b=2)
try:
result = response['SubResult']
except SoapFault:
result = None
return dict(xml_request=client.xml_request,
xml_response=client.xml_response,
result=result)
还有更多...
pysimplesoap 包含在 web2py 的最新版本中,因为它正在积极维护。您可以经常检查版本以查找增强功能,以扩展此食谱。
虽然有几个 Python SOAP 库,但这个库被设计得尽可能简单,并且完全与 web2py 集成。
更多信息、支持的功能和平台,请查看以下链接:
code.google.com/p/pysimplesoap/wiki/Web2Py
注意
要查看最新 web2py 版本的帮助页面,您应该在示例中创建一个视图,sample/call.html,在新版本的 web2py 中,出于安全原因,默认不公开通用视图
{{extend 'layout.html'}}
{{for tag in body:}}
{{=tag}}
{{pass}}
第八章. 认证和授权
在本章中,我们将介绍以下菜谱:
-
自定义认证
-
在登录失败时使用 CAPTCHA
-
使用 pyGravatar 为用户个人资料页面获取头像
-
多用户和教师模式
-
使用 OAuth 2.0 通过 Facebook 进行认证
简介
几乎每个应用程序都需要能够认证用户并设置权限。web2py 提供了一个广泛且可定制的基于角色的访问控制机制。在本章中,我们向您展示如何通过向用户表添加字段、在多次登录失败后添加 CAPTCHA 安全性以及如何创建 全球认可的头像(Gravatars——代表用户的图标)。我们还讨论了 web2py 的 teacher 模式,允许学生共享一个 web2py 实例来开发和部署他们的应用程序。最后,我们提供了一个与 OAuth 2.0 集成的示例,这是最新联邦认证协议之一。Web2py 还支持 ofthe 协议,如 CAS、OpenID、OAuth 1.0、LDAP、PAM、X509 以及更多。但是,一旦你学会了其中一个,使用官方文档学习其他协议应该很容易。
自定义认证
有两种方式可以自定义认证。旧的方式是从头开始定义自定义的 db.auth_user 表。新的方式是让 web2py 定义 auth 表,但列出 web2py 应该包含在表中的额外字段。在这里,我们将回顾后一种方法。
具体来说,我们将假设每个用户还必须有一个用户名、电话号码和地址。
如何做到这一点...
在 db.py 模型中,替换以下行:
auth.define_tables()
用以下代码替换它:
auth.settings.extra_fields['auth_user'] = [
Field('phone_number',requires=IS_MATCH('\d{3}\-\d{3}\-\d{4}')),
Field('address','text')]
auth.define_tables(username=True)
它是如何工作的...
auth.settings.extra_fields 是一个额外字段的字典。key 是要添加额外字段的 auth 表的名称。value 是额外字段列表。请注意,我们为 auth_user 添加了两个额外字段(电话号码和 address),但没有添加 username。
username 必须以特殊方式处理,因为它涉及到认证过程,该过程通常基于 email 字段。通过将用户名参数传递到下一行,我们告诉 web2py 我们想要 username 字段,并希望用它来登录而不是 email 字段。
auth.define_tables(username=True)
用户名也将被设置为唯一。
还有更多...
在某些情况下,注册可能发生在正常的注册表单之外(例如,当使用 Janrain 或管理员注册用户时)。然而,你可能需要在用户首次登录后强制他们完成注册。这可以通过设置默认为 False 的虚拟隐藏额外字段 complete_registration 来完成,并在他们更新个人资料时将其设置为 True:
auth.settings.extra_fields['auth_user'] = [
Field('phone_number',requires=IS_MATCH('\d{3}\-\d{3}\-\d{4}'),
comment = "i.e. 123-123-1234"),
Field('address','text'),
Field('complete_registration',default=False,update=True,
writable=False, readable=False)]
uth.define_tables(username=True)
然后,我们可能希望强制新用户在登录时完成他们的注册。在 db.py 中,我们可以添加以下代码:
if auth.user and not auth.user.complete_registration:
if not (request.controller,request.function) == ('default','user'):
redirect(URL('default','user/profile'))
这将强制新用户编辑他们的个人资料。
在登录失败时使用 CAPTCHA
web2py 内置了ReCaptcha支持(www.google.com/recaptcha),但它通常是开启或关闭。开启它是有用的,因为它可以防止对应用程序表单的暴力攻击,但对于普通用户来说可能会很烦人。在这里,我们提出了一种解决方案,一个插件,在固定数量的登录失败后条件性地开启 ReCaptcha。
如何操作...
你需要做的只是创建一个新的models/plugin_conditionalrecaptcha.py文件,其中包含以下代码,你的工作就完成了:
MAX_LOGIN_FAILURES = 3
# You must request the ReCaptcha keys
# in order to use this feature
RECAPTCHA_PUBLIC_KEY = ''
RECAPTCHA_PRIVATE_KEY = ''
def _():
from gluon.tools import Recaptcha
key = 'login_from:%s' % request.env.remote_addr
num_login_attempts = cache.ram(key,lambda:0,None)
if num_login_attempts >= MAX_LOGIN_FAILURES:
auth.settings.login_captcha = Recaptcha(
request,RECAPTCHA_PUBLIC_KEY,RECAPTCHA_PRIVATE_KEY)
def login_attempt(form,key=key,n=num_login_attempts+1):
cache.ram(key,lambda n=n:n,0)
def login_success(form,key=key):
cache.ram(key,lambda:0,0)
auth.settings.login_onvalidation.append(login_attempt)
auth.settings.login_onaccept.append(login_success)
_()
还有更多...
你可以通过传递参数到 JavaScript 中来自定义 ReCaptcha 的外观。如果你使用的是默认用户控制器来公开auth登录表单,你可以简单地编辑user.html视图,并添加以下代码:
<script>
var RecaptchaOptions = {
theme : 'clean',
tabindex : 2
};
</script>
在以下行之前添加它:
{{=form}}
完整的 ReCaptcha 客户端 API 可以在以下 URL 查看:
recaptcha.net/apidocs/captcha/client.html
使用 pyGravatar 为用户个人资料页面获取头像
首先,从以下 URL 下载pyGravatar:
bitbucket.org/gridaphobe/pygravatar/src
将gravatar.py放入applications/yourapp/modules。如果你愿意,可以使用以下命令:
pip install pyGravatar
在你的任何模型源文件中,你必须导入 Gravatar 库才能使用它,如下所示示例:
from gravatar import Gravatar
如果你使用的是 scaffold 应用程序,编辑default/user.html视图,如下所示:
{{extend 'layout.html'}}
<h3>{{=T( request.args(0).replace('_',' ').capitalize() )}}</h3>
<div id="web2py_user_form">
{{if 'profile' in request.args:}}
<img src="img/{{=Gravatar(auth.user.email).thumb}}" />
{{pass}}
现在,你的个人资料页面将看起来像以下截图:

现在,在任何页面中,如果你想显示用户头像,你只需使用以下代码:
<img src='{{=Gravatar(auth.user.email).thumb}}' />
<img src='{{=Gravatar('email@domain.com').thumb}}' />
你可以更进一步,从en.gravatar.com/获取用户个人资料简介。将以下代码添加到default/user.html。
{extend 'layout.html'}}
<h2>{{=T( request.args(0).replace('_',' ').capitalize() )}}</h2>
<div id="web2py_user_form">
{{if 'profile' in request.args:}}
{{user = Gravatar(auth.user.email)}}
<img src="img/{{=user.thumb}}" />
<blockquote style='width:300px;'>
{{try:}}
{{=user.profile['aboutMe']}}
{{except Exception:}}
No profile information
{{pass}}
</blockquote>
{{pass}}
然后,你会得到以下内容:

你也可以在default/user.html视图中获取用户在 Gravatar 上注册的额外服务:
{extend 'layout.html'}}
<div id="web2py_user_form">
{{if 'profile' in request.args:}}
{{user = Gravatar(auth.user.email)}}
<img src="img/{{=user.thumb}}" />
<blockquote style='width:300px;'>
{{try:}}
{{=user.profile['aboutMe']}}
{{services = user.profile.get('accounts', {})}}
{{=UL(*[LI(A(service['shortname'], _href=service['url'])) for
service in services])}}
{{except Exception:}}
No profile information
{{pass}}
</blockquote>
{{pass}}
然后,你将在你的网页中看到额外的信息(关于我和注册服务的 URL):

告诉你的用户使用与你的应用程序中相同的电子邮件地址在en.gravatar.com/注册。
多用户和教师模式
自 1.92 版本以来,您可以将 web2py 设置为 mult-iuser 或 teaching 模式。系统上安装了一个 web2py 实例,一个账户是 admin(或 teacher),其他账户是 students。学生只能看到他们自己的应用程序。它的工作原理如下:管理员页面已更改,现在包含登录和注册标题。在此模式下,第一个登录的用户将获得 teacher 角色。后续注册将成为 students,在 teacher 批准后。在以下配方中,我假设在 127.0.0.1 的 8000 端口上本地运行 web2py。teacher 和 students 需要一个SSL 加密的 web2py 实例。有关更多详细信息,请参阅第十一章,其他技巧和窍门。
注意,在 multi-user 模式下,没有安全机制来防止管理员之间的干扰。
如何操作...
-
在文件夹中安装 web2py;比如说
web2py_mu。 -
在
admin/appadmin/0.py中设置MULTI_USER_MODE = True。 -
以通常的方式启动 web2py,并点击管理界面的链接。现在您可以看到修改后的管理登录界面。
点击
register创建教师账户。现在您进入管理应用程序。 -
点击
logout,然后点击register创建第一个student账户(您可以让学生们这样做;提供给他们链接)。学生注册后,其状态为待批准。批准学生。
-
使用以下网址进入 web2py 的
appadmin,即管理应用程序:http://127.0.0.1:8000/admin/appadmin。 -
点击
auth_user表。您现在可以看到教师和学生的账户。对于每个已批准的学生:
-
点击其 ID(最左侧列)
-
在
registration_key字段中删除单词待批准。
-
如果可用,您还可以通过使用 CSV 文件(将进行扩展)导入学生名单。
还有更多...
对于刚开始学习 Python 和 webapp 的学生来说,从一个最小应用程序开始可能有所帮助。这个简单的设置将不包括 Ajax,并且仅涵盖最小模板功能。
备注
在右侧,我们需要一个额外的选项来加载一个基于文件 minimal.w2p 的最小应用程序。
appadmin 的组件对于初学者来说并不相关,且令人望而生畏,默认情况下,配置选项 BASIC_STUDENT 为 False 可以有所帮助。教师可以将其打开,并在稍后关闭。当 False 时,这些文件可以隐藏起来,包括管理屏幕、向导和其他高级选项。
使用 OAuth 2.0 通过 Facebook 进行认证
以下配方将展示如何构建一个简单的应用程序,该应用程序使用 Facebook 的 OAuth 2.0 认证服务进行用户认证。
OAuth 2.0 是 OAuth 1.0a 协议的演进。您可以在以下网址找到该协议的详细描述:
不深入细节,协议的主要对象是允许网站(提供者)向其他网站(消费者)提供受信任的身份验证凭据。这在范围上与已经由 web2py 实现的 CAS 身份验证系统非常相似。
目前,web2py 仅作为消费者实现 OAuth 2.0。但这已经足够允许任何 web2py 应用程序对 OAuth 2.0 服务提供者进行身份验证。
我们将展示如何实现一个小型应用程序,该程序使用 Facebook 作为 OAuth 2.0 提供者,因为它是最深入测试过的提供者。
准备工作
在开始之前,你需要在 Facebook 上注册一个应用程序:

你必须小心使用与应用程序相同的精确 URL(包括 TCP 端口)。
现在,建议你使用 Facebook Graph API Python 库,以编程方式访问REST接口。这个配方使用了这个库。你可以从这里下载它:https://github.com/facebook/python-sdk。将其复制到你应用程序的*modules*目录中。
如果你想要使用 web2py 附带的 JSON 引擎,将文件开头的import代码更改为以下样子(其他语句不需要):
# for web2py
from gluon.contrib import simplejson
_parse_json = lambda s: simplejson.loads(s)
如何操作...
-
创建一个包含注册页面中显示的App Id和App Secret的文件。将其保存在你应用程序的
modules目录中,文件名为fbappauth.py:CLIENT_ID="xxxxxxxxxxxxxxxx" CLIENT_SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -
现在,你只需要更改模型中的
auth代码。你可以将它放在与脚手架应用程序一起提供的常规db.py模型中。######################################### ## use fb auth ## for facebook "graphbook" application ######################################### import sys, os from fbappauth import CLIENT_ID,CLIENT_SECRET from facebook import GraphAPI, GraphAPIError from gluon.contrib.login_methods.oauth20_account import OAuthAccount class FaceBookAccount(OAuthAccount): """OAuth impl for FaceBook""" AUTH_URL="https://graph.facebook.com/oauth/authorize" TOKEN_URL="https://graph.facebook.com/oauth/access_token" def __init__(self, g): OAuthAccount.__init__(self, g, CLIENT_ID, CLIENT_SECRET, self.AUTH_URL, self.TOKEN_URL, scope='user_photos,friends_photos') self.graph = None def get_user(self): '''Returns the user using the Graph API.''' if not self.accessToken(): return None if not self.graph: self.graph = GraphAPI((self.accessToken())) user = None try: user = self.graph.get_object("me") except GraphAPIError, e: self.session.token = None self.graph = None if user: return dict(first_name = user['first_name'], last_name = user['last_name'], username = user['id']) auth.settings.actions_disabled = ['register','change_password', 'request_reset_password','profile'] auth.settings.login_form=FaceBookAccount(globals()) auth.settings.login_next=URL(f='index')正如你所见,这个类将
gluon.contrib.login_methods.oauth20_account.py中的通用OAuthAccount类进行了特殊化,以便它可以与 Facebook 身份验证服务器一起工作。
以下行定义了在身份验证服务器确认用户身份后用户将到达的位置。将其更改为你需要的内容。
auth.settings.login_next=URL(f='index')
还有更多...
通常,你无法在最终部署应用程序的公共服务器上测试你的应用程序。通常你可以使用localhost或127.0.0.1作为主机进行测试。
在常见情况下,使用localhost作为主机名与你的应用程序不兼容。在/etc/hosts(在先前注册的示例应用程序中)中添加适当的条目。
# fb app testing setup
127.0.0.1 bozzatest.example.com
但是,请注意,在两种情况下,你都需要使用端口80以避免问题。
# python web2py.py -p 80 -a <secret>
这通常在启动 web2py 或使用系统支持的功能时需要管理员权限。
第九章。路由食谱
在本章中,我们将介绍以下食谱:
-
使用
routes.py创建更干净的 URL -
创建一个简单的路由器
-
添加 URL 前缀
-
将应用程序与域名关联
-
省略应用程序名称
-
从 URL 中删除应用程序名称和控制器
-
在 URL 中将下划线替换为连字符
-
映射
favicons.ico和robots.txt -
使用 URL 指定语言
简介
在其核心,web2py 包含一个将 URL 映射到函数调用的分发器。这种映射称为路由,并且可以进行配置。这可能是有必要的,为了缩短 URL,或者将 web2py 应用程序作为现有应用程序的替代品部署,而不希望破坏旧的外部链接。web2py 随带两个路由器,即双向路由配置。旧的那个使用正则表达式匹配传入的 URL 并将其映射到应用/控制器/函数。而新风格的路由器则采用更全面的方法。
使用 routes.py 创建更干净的 URL
在 web2py 中,默认情况下,传入的 URL 被解释为 http://domain.com/application/controller/function/arg1/arg2?var1=val1&var2=val2。
即,URL 的前三个元素被解释为 web2py 应用程序名称、控制器名称和函数名称,剩余的路径元素保存在 request.args(一个 列表)中,查询字符串保存在 request.vars(一个 字典)中。
如果传入的 URL 路径元素少于三个,则使用默认值填充缺失的元素:/init/default/index,或者如果没有名为 init 的应用程序,则使用 welcome 应用程序填充:/welcome/default/index。web2py 的 URL() 函数从其组成部分创建 URL 路径(默认情况下,没有方案或域名):应用程序、控制器、函数、参数列表和变量字典。结果通常用于网页中的 href 链接,以及重定向函数的参数。
作为其路由逻辑的一部分,web2py 还支持 URL 重写,其中配置文件 routes.py 指定 URL() 重写它生成的 URL 的规则,以及 web2py 解释传入的 URL 的规则。有两种独立的重写机制,这取决于在 routes.py 中配置的是哪一个。
一个使用正则表达式模式匹配来重写 URL 字符串,而另一个使用路由参数字典来控制重写。我们分别称它们为 基于模式的路由器 和 基于参数的路由器(有时它们分别被称为旧路由器和新路由器,但这些术语描述性不强,我们将避免使用它们)。
以下部分提供了一个旧路由器的使用示例。本章的其余部分提供了一个新路由器的使用示例。
准备工作
通常,Web URL 的结构如下 http://host/app/controller/function/args。
现在想象一个应用程序,其中每个用户都有自己的主页。例如:http://host/app/default/home/johndoe,其中 home 是渲染页面的动作,而 johndoe 是 request.args(0),它告诉 web2py 我们正在寻找哪个用户。虽然这是可能的,但拥有如下外观的 URL 会更好:
http://host/johndoe/home。
这可以通过使用 web2py 的基于模式的路由机制来实现。
我们将假设以下名为 pages 的最小化应用程序。
在 models/db.py 中添加以下代码:
db = DAL('sqlite://storage.sqlite')
from gluon.tools import *
auth = Auth(db)
auth.settings.extra_fields = [Field('html','text'),Field('css','te
xt')]
auth.define_tables(username=True)
将以下代码和常规脚手架文件添加到 controllers/default.py 中:
def index():
return locals()
def user():
return dict(form=auth())
def home():
return db.auth_user(username=request.args(0)).html
def css():
response.headers['content-type']='text/css'
return db.auth_user(username=request.args(0)).css
如何实现...
我们通过在主 web2py 文件夹中创建/编辑 routes.py 来实现以下规则:
routes_in = (
# make sure you do not break admin
('/admin','/admin'),
('/admin/$anything','/admin/$anything'),
# make sure you do not break appadmin
('/$app/appadmin','/$app/appadmin'),
('/$app/appadmin/$anything','/$app/appadmin/$anything'),
# map the specific urls for this the "pages" app
('/$username/home','/pages/default/home/$username'),
('/$username/css','/pages/default/css/$username'),
# leave everything else unchanged
)
routes_out = (
# make sure you do not break admin
('/admin','/admin'),
('/admin/$anything','/admin/$anything'),
# make sure you do not break appadmin
('/$app/appadmin','/$app/appadmin'),
('/$app/appadmin/$anything','/$app/appadmin/$anything'),
# map the specific urls for this the "pages" app
('/pages/default/home/$username','/$username/home'),
('/pages/default/css/$username','/$username/css'),
# leave everything else unchanged
)
注意,$app 是正则表达式 (? P<app>\w+) 的快捷方式,它将匹配不包含斜杠的任何内容。$username 是 (? P<username>\w+) 的快捷方式。同样,您可以使用其他变量。$anything 是特殊的,因为它对应着不同的正则表达式,(? P<app>.*);即,它将匹配直到 URL 结尾的任何内容。
代码的关键部分如下:
routes_in=(
...
('/$username/home','/pages/default/home/$username'),
...
)
routes_out=(
...
('/pages/default/home/$username','/$username/home'),
...
)
这些映射了 home 的请求。然后我们对 css 动作做同样的处理。其余的代码实际上不是必需的,但它确保您不会意外地破坏 admin 和 appadmin URL。
创建一个简单的路由器
本章的此部分和下一部分将处理新的基于参数的路由器,它通常更容易配置,并且有效地处理大多数常见的重写任务。如果可能,请尝试使用基于参数的路由器,但如果您需要更多控制特殊 URL 重写任务,请查看基于模式的路由器。
使用基于参数的路由器的起点是将文件 router.example.py 复制到 web2py 的 base 目录中的 routes.py。(routes.example.py 文件对于基于模式的路由器也具有相同的作用。)该 example 文件包含其各自路由系统的基本文档;更多文档可在 web2py 书籍的在线版本中找到,第四章核心:URL 重写和错误路由。
每当 routes.py 发生更改时,您必须重新启动 web2py,或者如果管理员应用程序可用,加载以下 URL,以便新的配置生效:
http://yourdomain.com/admin/default/reload_routes
注意
示例路由文件包含一组 Python doctests。当您更改路由配置时,请向 routes.py 中的 doctests 添加或编辑,以检查您的配置是否符合预期。
我们想要解决的第一个问题是,在可能的情况下,我们想要从可见 URL 中消除默认应用程序和控制器。
如何实现...
-
将
router.example.py复制到主 web2py 文件夹中的routes.py,并按以下方式编辑。找到routers字典:routers = dict( # base router BASE = dict( default_application = 'welcome', ), ) -
将
default_application从welcome更改为你的应用程序名称。如果你的默认控制器和函数没有命名为default和index,请指定这些默认值:routers = dict( # base router BASE = dict( default_application = 'myapp', default_controller = 'mycontroller', default_function = 'myfunction', ), )
添加 URL 前缀
通常,当你在一个生产服务器上运行 web2py 时,相同的 URL 可能被多个应用程序或服务共享,你需要添加一个额外的PATH_INFO前缀来识别 web2py 服务。例如:
example.com/web2py/app/default/index
在这里,web2py/标识 web2py 服务,而php/标识一个 php 服务,映射是由网络服务执行的。你可能想从PATH_INFO中消除额外的web2py/。
如何操作...
当你指定path_prefix时,它被添加到由URL()生成的所有 URL 之前,并从所有传入的 URL 中移除。例如,如果你想你的外部 URL 看起来像http://example.com/web2py/app/default/index,你可以这样做:
routers = dict(
# base router
BASE = dict(
default_application = 'myapp',
path_prefix = 'web2py',
),
)
将应用程序与域名关联
通常,你想将特定的域名与特定的 web2py 应用程序关联起来,以便将指定域名指向的传入 URL 路由到适当的应用程序,而无需在 URL 中包含应用程序名称。再次强调,参数化路由器非常有用。
如何操作...
使用基于参数的路由器的域名功能:
routers = dict(
BASE = dict(
domains = {
"domain1.com" : "app1",
"www.domain1.com" : "app1",
"domain2.com" : "app2",
},
exclusive_domain = True,
),
# app1 = dict(...),
# app2 = dict(...),
)
在此示例中,domain1.com和domain2.com由同一个物理主机提供服务。配置指定了将domain1.com(在这种情况下,其子域名www)的 URL 路由到app1,将domain2.com的 URL 路由到app2。如果exclusive_domain(可选)设置为True,那么来自除domain2.com(以及类似地对于app1)之外的域的请求尝试使用 URL 生成指向app2的 URL 将失败,除非它们明确提供主机名到 URL。
注意,你也可以使用以下方式,进一步配置两个应用的路径:
app1 = dict(...),
app2 = dict(...),
省略应用程序名称
如果你正在使用参数化路由器,你可能想从静态文件的可视 URL 中省略默认应用程序名称。
如何操作...
这很简单;你只需按照以下方式打开map_static标志:
routers = dict(
# base router
BASE = dict(
default_application = 'myapp',
map_static = True,
),
)
或者,如果你正在使用特定应用程序的路由字典,为每个应用程序(例如以下示例中的myapp)打开map_static标志:
routers = dict(
# base router
BASE = dict(
default_application = 'myapp',
),
myapp = dict(
map_static = True,
),
)
从 URL 中移除应用程序名称和控制器
有时候,你想使用参数化路由器的 URL 解析,但又不想重写可见的 URL。再次强调,你可以使用参数化路由器,但请禁用 URL 重写。
如何操作...
在routes.py中找到路由器的dict,如下所示:
routers = dict(
# base router
BASE = dict(
default_application = 'welcome',
),
)
找到它后,将其更改为以下内容:
routers = dict(
# base router
BASE = dict(
applications = None,
controllers = None,
),
)
它是如何工作的...
将 applications 和 controllers 设置为 None(函数和 languages 默认设置为 None),告诉参数路由器不要省略可见 URL 中的相应部分。web2py 的默认 URL 解析比许多应用可能需要的更严格,因为它假设 URL 组件可能用于文件名。参数路由器更紧密地遵循 HTTP URL RFCs,这使得它对需要更多异国情调字符在它们的参数或查询字符串中的应用程序更友好。本食谱中的 null 路由器启用此解析,而实际上不重写 URL。
在 URL 中将下划线替换为破折号
URL 中的下划线可能看起来很丑,当 URL 被下划线时,它们可能很难看到,就像它们通常在网页上那样。破折号是一个更视觉上吸引人的替代品,但你不能在函数名中使用破折号,因为它们还必须是合法的 Python 标识符。你可以使用参数路由器,将破折号映射为 _!
参数路由器的 map_hyphen 标志将应用、控制器和函数名称中的下划线转换为可见 URL 中的破折号,并在接收到 URL 时将其转换回下划线。Args, vars(查询字符串)和可能的语言选择器不受影响,因为破折号在这些字段中是允许的。因此,以下 URL:
http://some_controller/some_function
将显示为以下内容:
http://some-controller/some-function
虽然内部控制器和函数名称保留了它们的下划线。
如何做到这一点...
打开 map_hyphen 标志。在路由器指令中添加以下代码:
routers = dict(
# base router
BASE = dict(
default_application = 'myapp',
),
myapp = dict(
map_hyphen = True,
),
)
映射 favicon.ico 和 robots.txt
一些特殊文件,如 robots.txt 和 favicon.ico,作为 URL 的根路径直接访问。因此,它们必须从 root 文件夹映射到应用的 static 文件夹中。
如何做到这一点...
默认情况下,基于参数的路由器将 root_static 设置如下:
routers = dict(
# base router
BASE = dict(
default_application = 'myapp',
root_static = ['favicon.ico', 'robots.txt']
),
)
这指定了要服务的文件来自默认应用的静态目录。
使用 URL 指定语言
第二章中的“使用 cookies 设置语言”食谱描述了如何将用户语言偏好保存到 cookie 中。在这个食谱中,我们描述了如何做类似的事情——将用户语言偏好存储在 URL 中。这种方法的一个优点是,然后可以保存包含语言偏好的链接。
如何做到这一点...
参数路由器支持 URL 中的可选 language 字段,作为应用名称之后的字段:
domain.com/app/lang/controller/function
语言字段遵循常规省略规则:如果使用默认语言,参数路由器将省略语言标识符,如果省略不会造成歧义。
基于 URL 的语言处理通常会在特定应用的参数路由器中指定,设置default_language和languages如下:
routers = dict(
# base router
BASE = dict(
default_application = app,
),
app = dict(
default_language = 'en',
languages = ['en', 'it', 'pt', 'pt-br'],
),
)
要使用URL()指定出站 URL 的语言,将request.lang设置为支持的任何一种语言。对于入站请求,request.lang将被设置为入站 URL 指定的语言。与语言在 cookie 中的设置类似,在使用翻译之前,使用T.force强制在模型文件中使用所需的翻译。例如,在你的模型中,你可以执行以下操作:
T.force(request.lang)
第十章:报告菜谱
在本章中,我们将介绍以下菜谱:
-
创建 PDF 报告
-
创建 PDF 列表
-
创建 PDF 标签、徽章和发票
简介
在 web2py 中生成 PDF 报告有许多方法。一种方法是用ReportLab,这是 Python 中用于生成 PDF 的顶尖库。另一种方法是将LaTeX生成,并将其转换为 PDF。这可能是生成 PDF 最强大的方法,web2py 通过在其contrib文件夹中打包markmin2latex和markmin2pdf来帮助你。然而,这两种方法都需要掌握第三方库和语法。本章中描述了第三种方法:使用 web2py 打包的 pyfpdf 库直接将 HTML 转换为 PDF。
创建 PDF 报告
谁不需要生成 PDF 报告、发票、账单?web2py 自带了pyfpdf库,它可以提供 HTML 视图到 PDF 的转换,并可用于此目的。pyfpdf仍处于初级阶段,缺乏一些高级功能,例如在reportlab中可以找到的功能,但对于普通用户来说已经足够了。
你可以通过使用 web2py HTML 辅助工具,混合页眉、徽标、图表、文本和表格,制作一份看起来专业的商业报告。以下是一个例子:

这种方法的主要优势是,相同的报告可以以 HTML 视图的形式呈现,或者可以以 PDF 格式下载,只需付出最小的努力。
如何做到这一点...
这里,我们提供了一个生成示例报告的控制器示例,然后讨论其语法和 API:
import os
def report():
response.title = "web2py sample report"
# include a chart from google chart
url = "http://chart.apis.google.com/chart?cht=p3&chd=t:60,
40&chs=500x200&chl=Hello|World&.png"
chart = IMG(_src=url, _width="250",_height="100")
# create a small table with some data:
rows = [THEAD(TR(TH("Key",_width="70%"),
TH("Value",_width="30%"))),
TBODY(TR(TD("Hello"),TD("60")),
TR(TD("World"),TD("40")))]
table = TABLE(*rows, _border="0", _align="center", _width="50%")
if request.extension=="pdf":
from gluon.contrib.pyfpdf import FPDF, HTMLMixin
# create a custom class with the required functionalities
class MyFPDF(FPDF, HTMLMixin):
def header(self):
"hook to draw custom page header (logo and title)"
# remember to copy logo_pb.png to static/images (and remove
#alpha channel)
logo=os.path.join(request.folder,"static","images",
"logo_pb.png")
self.image(logo,10,8,33)
self.set_font('Arial','B',15)
self.cell(65) # padding
self.cell(60,10,response.title,1,0,'C')
self.ln(20)
def footer(self):
"hook to draw custom page footer (printing page numbers)"
self.set_y(-15)
self.set_font('Arial','I',8)
txt = 'Page %s of %s' % (self.page_no(), self.alias_nb_pages())
self.cell(0,10,txt,0,0,'C')
pdf=MyFPDF()
# create a page and serialize/render HTML objects
pdf.add_page()
pdf.write_html(table.xml())
pdf.write_html(CENTER(chart).xml())
# prepare PDF to download:
response.headers['Content-Type']='application/pdf'
return pdf.output(dest='S')
# else normal html view:
return dict(chart=chart, table=table)
它是如何工作的...
关键在于创建和序列化pdf对象的行:
if request.extension=='pdf':
...
pdf=MyFPDF()
...
return pdf.output(dest='S')
pdf对象可以解析原始 HTML 并将其转换为 PDF。在这里,MyFPDF通过定义自己的页眉和页脚扩展了FPDF。
以下行在将使用辅助工具创建的 HTML 组件序列化为 PDF 中起着关键作用:
pdf.write_html(table.xml())
pdf.write_html(CENTER(chart).xml())
内部,PyFPDF使用 Python HTMLParser的基本 HTML 渲染器。它读取 HTML 代码,并将其转换为 PDF 指令。尽管它只支持基本的渲染,但它可以很容易地扩展或与其他 PDF 原语混合。
此外,只要使用简单且受支持的标签,你还可以使用 web2py 最新版本中包含的default.pdf视图渲染基本的 HTML。
查看以下 URLs 中的PyFPDF维基文档以获取更多信息及示例:
创建 PDF 列表
作为前一个菜谱的后续,我们可以以非常 Pythonic 的方式创建漂亮的表格,这些表格可以自动扩展到多个页面,带有页眉/页脚、列/行高亮等功能:
你可以在pyfpdf.googlecode.com/files/listing.pdf中看到一个示例。
如何做到这一点...
这里有一个或多或少可以自说的例子:
def listing():
response.title = "web2py sample listing"
# define header and footers:
head = THEAD(TR(TH("Header 1",_width="50%"),
TH("Header 2",_width="30%"),
TH("Header 3",_width="20%"),
_bgcolor="#A0A0A0"))
foot = TFOOT(TR(TH("Footer 1",_width="50%"),
TH("Footer 2",_width="30%"),
TH("Footer 3",_width="20%"),
_bgcolor="#E0E0E0"))
# create several rows:
rows = []
for i in range(1000):
col = i % 2 and "#F0F0F0" or "#FFFFFF"
rows.append(TR(TD("Row %s" %i),
TD("something", _align="center"),
TD("%s" % i, _align="right"),
_bgcolor=col))
# make the table object
body = TBODY(*rows)
table = TABLE(*[head,foot, body],
_border="1", _align="center", _width="100%")
if request.extension=="pdf":
from gluon.contrib.pyfpdf import FPDF, HTMLMixin
# define our FPDF class (move to modules if it is reused
class MyFPDF(FPDF, HTMLMixin):
def header(self):
self.set_font('Arial','B',15)
self.cell(0,10, response.title ,1,0,'C')
self.ln(20)
def footer(self):
self.set_y(-15)
self.set_font('Arial','I',8)
txt = 'Page %s of %s' % (self.page_no(), self.alias_nb_pages())
self.cell(0,10,txt,0,0,'C')
pdf=MyFPDF()
# first page:
pdf.add_page()
pdf.write_html(table.xml())
response.headers['Content-Type']='application/pdf'
return pdf.output(dest='S')
# else return normal html view:
return dict(table=table)
创建 PDF 标签、徽章和发票
此配方展示了如何使用 pyfpdf 库制作简单的会议徽章和发票,但可以轻松地适应打印标签(Avery 或其他格式),以及其他文档。
如何做...
-
首先,您必须定义两个表格来保存模板和将用于设计 PDF 的元素。
-
创建一个模型,例如
models/plugin_fpdf_templates.py,并将其中的以下代码添加到其中:def _(): PAPER_FORMATS = ["A4","legal","letter"] ELEMENT_TYPES = {'T':'Text', 'L':'Line', 'I':'Image', 'B':'Box', 'BC':'BarCode'} FONTS = ['Arial', 'Courier', 'Helvetica', 'Times-Roman', 'Symbol','ZapfDingbats'] ALIGNS = {'L':'Left', 'R':'Right', 'C':'Center', 'J':'Justified'} NE = IS_NOT_EMPTY() db.define_table("pdf_template", Field("pdf_template_id","id"), Field("title"), Field("format", requires=IS_IN_SET(PAPER_FORMATS)), format = '%(title)s') db.define_table("pdf_element", Field("pdf_template_id", db.pdf_template), Field("name", requires=NE), Field("type", length=2, requires=IS_IN_SET(ELEMENT_TYPES)), Field("x1", "double", requires=NE), Field("y1", "double", requires=NE), Field("x2", "double", requires=NE), Field("y2", "double", requires=NE), Field("font", default="Arial", requires=IS_IN_SET(FONTS)), Field("size", "double", default="10", requires=NE), Field("bold", "boolean"), Field("italic", "boolean"), Field("underline", "boolean"), Field("foreground", "integer", default=0x000000, comment="Color text"), Field("background", "integer", default=0xFFFFFF, comment="Fill color"), Field("align", "string", length=1, default="L", requires=IS_IN_SET(ALIGNS)), Field("text", "text", comment="Default text"), Field("priority", "integer", default=0, comment="Z-Order")) _() -
然后,在控制器
badges.py中添加一些函数来创建初始的基础标签/徽章。根据您的标签格式轻松复制徽章,然后,最终基于一些用户数据(即speakers)生成 PDF:# coding: utf8 import os, os.path from gluon.contrib.pyfpdf import Template def create_label(): pdf_template_id = db.pdf_template.insert(title="sample badge", format="A4") # configure optional background image and insert his element path_to_image = os.path.join(request.folder, 'static','42.png') if path_to_image: db.pdf_element.insert(pdf_template_id=pdf_template_id, name='background', type='I', x1=0.0, y1=0.0, x2=85.23, y2=54.75, font='Arial', size=10.0, bold=False, italic=False, underline=False, foreground=0, background=16777215, align='L', text=path_to_image, priority=-1) # insert name, company_name, number and attendee type elements: db.pdf_element.insert(pdf_template_id=pdf_template_id, name='name', type='T', x1=4.0, y1=25.0, x2=62.0, y2=30.0, font='Arial', size=12.0, bold=True, italic=False, underline=False, foreground=0, background=16777215, align='L', text='', priority=0) db.pdf_element.insert(pdf_template_id=pdf_template_id, name='company_name', type='T', x1=4.0, y1=30.0, x2=50.0, y2=34.0, font='Arial', size=10.0, bold=False, italic=False, underline=False, foreground=0, background=16777215, align='L', text='', priority=0) db.pdf_element.insert(pdf_template_id=pdf_template_id, name='no', type='T', x1=4.0, y1=34.0, x2=80.0, y2=38.0, font='Arial', size=10.0, bold=False, italic=False, underline=False, foreground=0, background=16777215, align='R', text='', priority=0) db.pdf_element.insert(pdf_template_id=pdf_template_id, name='attendee_type', type='T', x1=4.0, y1=38.0, x2=50.0, y2=42.0, font='Arial', size=10.0, bold=False, italic=False, underline=False, foreground=0, background=16777215, align='L', text='', priority=0) return dict(pdf_template_id=pdf_template_id) def copy_labels(): # read base label/badge elements from db base_pdf_template_id = 1 elements = db(db.pdf_element.pdf_template_id==\ base_pdf_template_id).select(orderby=db.pdf_element.priority) # set up initial offset and width and height: x0, y0 = 10, 10 dx, dy = 85.5, 55 # create new template to hold several labels/badges: rows, cols = 5, 2 pdf_template_id = db.pdf_template.insert(title="sample badge\ %s rows %s cols" % (rows, cols), format="A4") # copy the base elements: k = 0 for i in range(rows): for j in range(cols): k += 1 for e in elements: e = dict(element) e['name'] = "%s%02d" % (e['name'], k) e['pdf_template_id'] = pdf_template_id e['x1'] = e['x1'] + x0 + dx*j e['x2'] = e['x2'] + x0 + dx*j e['y1'] = e['y1'] + y0 + dy*i e['y2'] = e['y2'] + y0 + dy*i del e['update_record'] del e['delete_record'] del e['id'] db.pdf_element.insert(**e) return {'new_pdf_template_id': pdf_template_id} def speakers_badges(): # set template to use from the db: pdf_template_id = 2 # query registered users and generate speaker labels speakers = db(db.auth_user.id>0).select(orderby= db.auth_user.last_name|db.auth_user.first_name) company_name = "web2conf" attendee_type = "Speaker" # read elements from db elements = db(db.pdf_element.pdf_template_id== pdf_template_id).select(orderby=db.pdf_element.priority) f = Template(format="A4", elements = elements, title="Speaker Badges", author="web2conf", subject="", keywords="") # calculate pages: label_count = len(speakers) max_labels_per_page = 5*2 pages = label_count / (max_labels_per_page - 1) if label_count % (max_labels_per_page - 1): pages = pages + 1 # fill placeholders for each page for page in range(1, pages+1): f.add_page() k = 0 li = 0 for speaker in speakers: k = k + 1 if k > page * (max_labels_per_page ): break if k > (page - 1) * (max_labels_per_page ): li += 1 #f['item_quantity%02d' % li] = it['qty'] f['name%02d' % li] = unicode("%s %s" % (speaker.first_name, speaker.last_name), "utf8") f['company_name%02d' % li] = unicode("%s %s" % \ (company_name, ""), "utf8") f['attendee_type%02d' % li] = attendee_type ##f['no%02d' % li] = li response.headers['Content-Type']='application/pdf' return f.render('badge.pdf', dest='S')要检查此示例:
-
执行
create_label,并记录创建的pdf_template_id的值 -
将
copy_labels设置为base_pdf_template_id中的值,然后执行它 -
将
speaker_badges设置为pdf_template_id,然后执行它该函数应生成包含您应用程序注册用户标签(徽章)的 PDF。
样本徽章具有以下背景图像:
![如何做...]()
-
-
然后,它在其上写入文本,填充演讲者姓名、地址等。您可以使用类似的方法制作出席证书和类似的多份报告。
对于更复杂的示例,请参阅以下发票控制器(您需要导入发票设计;查看
pyfpdf应用程序示例以获取完整示例):# coding: utf8 from gluon.contrib.pyfpdf import Template import os.path import random from decimal import Decimal def invoice(): # set sample invoice pdf_template_id: invoice_template_id = 3 # generate sample invoice (according to Argentina's regulations) # read elements from db elements = db(db.pdf_element.pdf_template_id== invoice_template_id).select(orderby=db.pdf_element.priority) f = Template(format="A4", elements = elements, title="Sample Invoice", author="Sample Company", subject="Sample Customer", keywords="Electronic TAX Invoice") # create some random invoice line items and detail data detail = "Lorem ipsum dolor sit amet, consectetur. " * 5 items = [] for i in range(1, 30): ds = "Sample product %s" % i qty = random.randint(1,10) price = round(random.random()*100,3) code = "%s%s%02d" % (chr(random.randint(65,90)), chr(random.randint(65,90)),i) items.append(dict(code=code, unit='u', qty=qty, price=price, amount=qty*price, ds="%s: %s" % (i,ds))) # divide and count lines lines = 0 li_items = [] for it in items: qty = it['qty'] code = it['code'] unit = it['unit'] for ds in f.split_multicell(it['ds'], 'item_description01'): # add item description line (without price nor amount) li_items.append(dict(code=code, ds=ds, qty=qty, unit=unit, price=None, amount=None)) # clean qty and code (show only at first) unit = qty = code = None # set last item line price and amount li_items[-1].update(amount = it['amount'], price = it['price']) # split detail into each line description obs="\n<U>Detail:</U>\n\n" + detail for ds in f.split_multicell(obs, 'item_description01'): li_items.append(dict(code=code, ds=ds, qty=qty, unit=unit, price=None, amount=None)) # calculate pages: lines = len(li_items) max_lines_per_page = 24 pages = lines / (max_lines_per_page - 1) if lines % (max_lines_per_page - 1): pages = pages + 1 # fill placeholders for each page for page in range(1, pages+1): f.add_page() f['page'] = 'Page %s of %s' % (page, pages) if pages>1 and page<pages: s = 'Continues on page %s' % (page+1) else: s = '' f['item_description%02d' % (max_lines_per_page+1)] = s f["company_name"] = "Sample Company" f["company_logo"] = os.path.join(request.folder,"static", "images","logo_pb.png") f["company_header1"] = "Some Address - somewhere -" f["company_header2"] = "http://www.example.com" f["company_footer1"] = "Tax Code ..." f["company_footer2"] = "Tax/VAT ID ..." f['number'] = '0001-00001234' f['issue_date'] = '2010-09-10' f['due_date'] = '2099-09-10' f['customer_name'] = "Sample Client" f['customer_address'] = "Siempreviva 1234" # print line item... li = 0 k = 0 total = Decimal("0.00") for it in li_items: k = k + 1 if k > page * (max_lines_per_page - 1): break if it['amount']: total += Decimal("%.6f" % it['amount']) if k > (page - 1) * (max_lines_per_page - 1): li += 1 if it['qty'] is not None: f['item_quantity%02d' % li] = it['qty'] if it['code'] is not None: f['item_code%02d' % li] = it['code'] if it['unit'] is not None: f['item_unit%02d' % li] = it['unit'] f['item_description%02d' % li] = it['ds'] if it['price'] is not None: f['item_price%02d' % li] = "%0.3f" % it['price'] if it['amount'] is not None: f['item_amount%02d' % li] = "%0.2f" % it['amount'] # last page? print totals: if pages == page: f['net'] = "%0.2f" % (total/Decimal("1.21")) f['vat'] = "%0.2f" % (total*(1-1/Decimal("1.21"))) f['total_label'] = 'Total:' else: f['total_label'] = 'SubTotal:' f['total'] = "%0.2f" % total response.headers['Content-Type']='application/pdf' return f.render('invoice.pdf', dest='S')这里是输出示例:

它是如何工作的...
PDF 模板是预定义文档(如发票、税务表格等),其中每个元素(文本、线条、条形码等)都有一个固定的位置 (x1, y1, x2 和 y2),样式(字体、大小等),以及默认文本。
这些元素可以作为占位符使用,因此程序可以更改默认文本以填充文档。
此外,元素也可以定义在 CSV 文件或数据库中,因此用户可以轻松地根据其打印需求调整表格。模板用作 dict,使用以下属性设置其项的值:
-
name:这是占位符标识 -
type: T表示文本,L表示线条,I表示图像,B表示框,BC表示条形码 -
x1, y1, x2和y2:这些是左上角和右下角的坐标(以毫米为单位)。 -
font:这可以取以下值——Arial,Courier, Helvetica, Times, Symbol, ZapfDingbats -
size:这是文本大小(以点为单位),即 10 -
bold, italic和underline:这是文本样式(非空以启用) -
foreground, background:这些是文本和填充颜色,即0xFFFFFF -
align:这些是文本对齐方式,其中L表示左对齐,R表示右对齐,C表示居中对齐 -
text:这是可以在运行时替换的默认字符串 -
priority:这指定了Z-Order
元素可以手动定义(只需传递一个dict),或者可以从 CSV 表格(使用parse_csv)中读取,或者存储在数据库中,如本例中所示,使用pdf_element表。
还有更多...
这是一个基本的示例,用于展示使用填空 PDF 模板生成徽章的过程,但它也可以用来制作任何定制的重复设计。
此外,还有一个可视化设计器,可以拖放元素,图形调整它们的属性,并轻松测试它们。
有关更多信息,请参阅以下 URL 上的 PyFPDF wiki 文档:
第十一章。其他技巧和窍门
在本章中,我们将介绍以下食谱:
-
使用 PDB 和嵌入的 web2py 调试器
-
使用 Eclipse 和 PyDev 进行调试
-
使用 shell 脚本更新 web2py
-
创建一个简单的页面统计插件
-
无需图像或 JavaScript 来圆角
-
设置
cache.disk配额 -
使用
cron检查 web2py 是否正在运行 -
构建 Mercurial 插件
-
构建 pingback 插件
-
为移动浏览器更改视图
-
使用数据库队列进行后台处理
-
如何有效地使用模板块
-
使用 web2py 和 wxPython 创建独立应用程序
简介
本章包含不适合任何其他章节的食谱,但典型 web2py 用户认为它们很重要。一个例子是使用 Eclipse 与 web2py 一起使用。后者是一个非常流行的 Java 集成开发环境,与 Python 工作得很好,但与 web2py 一起使用时存在一些怪癖,在这里,我们向您展示如何通过适当的配置克服这些怪癖。其他例子包括如何开发适合移动设备的应用程序,以及如何开发使用 wxPython GUI 的独立应用程序。
使用 PDB 和嵌入的 web2py 调试器
web2py 在 admin 应用程序中内置了交互式(网页浏览器)调试功能,类似于 shell,但直接向 PDB(Python 调试器)发出命令。
虽然这不是一个功能齐全的视觉调试器,但可以用于程序性地设置断点,然后进入并执行变量和堆栈检查,程序上下文中的任意代码执行,指令跳转和其他操作。
使用此调试器是可选的,它旨在供高级用户使用(应谨慎使用,或者你可以阻止 web2py 服务器)。默认情况下,它不会被导入,并且不会修改 web2py 的正常操作。
实现可以增强和扩展以进行其他类型的 COMET-like 通信(使用 AJAX 从服务器向客户端推送数据),以及通用长运行进程。
如何做到这一点...
PDB 是 Python 调试器,包含在标准库中。
-
你可以通过编写以下内容来启动调试器:
import pdb; pdb.set_trace()例如,让我们调试欢迎默认索引控制器:
def index(): import pdb; pdb.set_trace() message = T('Hello World') return dict(message=message) -
然后,当你打开索引页面:
http://127.0.0.1:8000/welcome/default/index,(PDB) 提示将在你启动 web2py 的控制台中出现:$ python web2py.py -a a web2py Web Framework Created by Massimo Di Pierro, Copyright 2007-2011 Version 1.99.0 (2011-09-15 19:47:18) Database drivers available: SQLite3, pymysql, PostgreSQL Starting hardcron... please visit: http://127.0.0.1:8000 use "kill -SIGTERM 16614" to shutdown the web2py server > /home/reingart/web2py/applications/welcome/controllers/default. py(20)index() -> message = T('Hello World') (Pdb) -
调试器指出,我们在
welcome/controllers/default.py的 第 20 行 处停止。在此点,可以发出任何Pdb命令。最有用的命令如下:-
help:此命令打印可用命令的列表 -
where:此命令打印当前的堆栈跟踪 -
list [first[, last]]:此命令列出源代码(在第一行和最后一行之间) -
p expression:此命令评估表达式并打印结果 -
! statement:此命令执行一个 Python 语句 -
step: step in:此命令执行当前行,进入函数 -
next: step next:这个命令执行当前行,不进入函数 -
return: step return:这个命令继续执行直到函数退出 -
continue:这个命令继续执行,并且仅在断点处停止 -
jump lineno:这个命令改变将要执行的下一行 -
break filename:lineno:这个命令设置一个断点 -
quit:这个命令从调试器退出(终止当前程序)命令可以通过只输入第一个字母来发出;例如,看看以下示例会话:
(Pdb) n > /home/reingart/web2py/applications/welcome/controllers/default.py(21)index() -> return dict(message=message) (Pdb) p message <lazyT 'Hello World'> (Pdb) !message="hello web2py recipe!" (Pdb) w > /home/reingart/web2py/applications/welcome/controllers/default.py(21)index() -> return dict(message=message) (Pdb) c -
-
命令是
n(next,执行行),p(打印消息变量),!message=(将其值更改为hello web2py recipe!),w(查看当前的堆栈跟踪),以及continue(退出调试器)。问题在于,如果你没有直接访问控制台(例如,如果 web2py 在 apache 内运行,pdb 将无法工作),则无法使用这种技术。
如果没有控制台可用,可以使用嵌入的 web2py 调试器。唯一的区别是,不是调用 pdb,而是使用 gluon.debug,它运行一个定制的 PDB 版本,通过浏览器中的 web2py 交互式 shell 来运行。
-
在前面的示例中,将
pdb.set_trace()替换为gluon.debug.stop_trace,并在return函数之前添加gluon.debug.stop_trace()以将控制权交还给 web2py:def index(): gluon.debug.set_trace() message = T('Hello World') gluon.debug.stop_trace() return dict(message=message) -
然后,当你打开索引页面
http://127.0.0.1:8000/welcome/default/index时,浏览器将阻塞,直到你进入调试页面(包含在管理界面中):http://127.0.0.1:8000/admin/debug。 -
在调试页面上,你可以发出之前列出的任何 PDB 命令,并像在本地控制台一样与你的程序交互。
以下图像显示了最后一个会话,但这次是在 web2py 调试器内部:

它是如何工作的...
web2py 调试器定义了一个从 Queue.Queue 继承的 Pipe 类,用于线程间通信,用作 PDB 的标准输入和输出,以与用户交互。
在线类似壳的界面使用 ajax 回调来接收用户命令,将它们发送到调试器,并打印结果,就像用户直接在控制台中使用 PDB 一样。
当调用 gluon.debug.set_trace()(即在调试应用的控制器中)时,自定义的 web2py PDB 实例会被运行,然后输入和输出会被重定向并排队,直到其他线程打开队列并与它通信(通常,管理员调试应用是从另一个浏览器窗口调用的)。
同时,在调试过程中,PDB 执行所有工作,而 web2py 只负责重定向输入和输出消息。
当调用 gluon.debug.stop_trace() 时,线程发送 void 数据(None 值)到监视线程,以表示调试已完成。
如介绍中所述,此功能旨在为中级和高级用户设计,因为如果未调用stop_trace,或者调试控制器未刷新,那么内部通信队列可能会阻塞 web2py 服务器(应实现超时以避免死锁)。
被调试的页面将在调试结束前被阻塞,这与通过控制台使用pdb相同。调试控制器将在达到第一个断点(set_trace)前被阻塞。
更多详细信息,请参阅 web2py 源文件中的gluon/debug.py和applications/admin/controllers/debug.py。
还有更多...
PDB 是一个功能齐全的调试器,支持条件断点和高级命令。完整的文档可以在以下 URL 找到:
docs.python.org/library/pdb.html
PDB 源自 BDB 模块(Python 调试框架),可用于扩展此技术以添加更多功能,实现轻量级远程调试器(它是一个不需要控制台交互的基本调试器,因此可以使用其他用户界面)。
此外,Pipe类是与长时间运行进程交互的示例,在类似 COMET 的场景中可能很有用,可以将数据从服务器推送到浏览器,而不需要保持连接打开(使用标准 Web 服务器和 AJAX)。
结合这两种技术,开发了一个新的调试器(QDB),使得能够远程调试 web2py 应用程序(即使在生产环境中)。在接下来的段落中,将展示一个示例用例。更多信息请参阅以下内容:
code.google.com/p/rad2py/wiki/QdbRemotePythonDebugger
要使用 qdb,你必须下载qdb.py(见前一个链接),并将其放置在gluon.contrib目录中(它将被包含在 web2py 的后续版本中)。
然后,在你的控制器中导入它并调用set_trace以开始调试,如下例所示:
def index():
response.flash = T('Welcome to web2py')
import gluon.contrib.qdb as qdb
qdb.set_trace()
return dict(message='Hello World')
当你打开你的控制器并且达到set_trace时,qdb 将监听远程连接以附加并开始调试器交互。你可以通过以下方式执行 qdb 模块(python qdb.py)来启动调试会话:
C:\rad2py\ide2py>python qdb.py
qdb debugger fronted: waiting for connection to ('localhost', 6000)
> C:\web2py\applications\welcome\controllers/default.py(19)
-> return dict(message=T('Hello World'))
(Cmd) p response.flash
Welcome to web2py!
> C:\web2py\applications\welcome\controllers/default.py(19)
-> return dict(message=T('Hello World'))
(Cmd) c
你可以与 PDB 相同的命令进行交互,即单步执行、打印值、继续等。
注意,web2py(后端调试器)和 qdb 前端调试器是不同的进程,因此你可以调试甚至是一个守护进程 Web 服务器,例如 Apache。此外,在qdb.py源文件中,你可以更改地址/端口和密码以连接到互联网上的远程服务器。
web2py 将在 2.0 版本中包含 qdb 和基于 Web 的用户界面调试器(用于开发环境)。
对于一个功能齐全的 web2py IDE(适用于开发或生产环境),包括基于此方法的视觉调试器,请参阅以下内容:
使用 Eclipse 和 PyDev 进行调试
Eclipse是一个开源的可扩展开发平台和应用框架,旨在构建、部署和管理整个软件生命周期的软件。它在 Java 世界中非常受欢迎。PyDev是 Eclipse 的 Python 扩展,允许将 Eclipse 用作 Python(以及 web2py)的 IDE,因此,在这里,我们向您展示如何设置 web2py 以与这些工具良好地协同工作。
准备工作
-
下载最新的 Eclipse IDE (
www.eclipse.org/downloads/),并将其解压到您选择的文件夹中。 -
通过在文件夹中运行
eclipse.exe来启动 Eclipse。注意,Eclipse 没有安装,但你必须安装 Java 运行时(http://java.com/en)。 -
通过点击帮助 | 安装新软件,并输入以下网址,然后点击添加按钮来安装 PyDev:
-
选择所有选项并点击**下一步**。
-
应该会提示你接受许可协议。继续通过向导,当它询问你是否想要重启时,点击**否**。
-
为你的操作系统安装正确的 mercurial 版本:
-
返回到帮助 | 安装新软件,并输入以下网址:
-
继续通过向导,当它要求你重启时,点击**是**。
-
通过转到文件 | 新建 | 项目 | Mercurial | 使用 Mercurial 克隆 Mercurial 仓库来在 Eclipse 中创建一个新项目,并输入以下网址:
-
在克隆目录名称字段中输入
web2py。 -
通过转到窗口 | 首选项 | PyDev | 解释器来设置解释器,并选择你的 Python 二进制文件的路径:

就这些了!你可以通过在项目树中找到 web2py.py 并右键单击选择调试作为 | Python 运行来开始调试。你也可以通过从相同菜单中选择调试配置来传递参数给 web2py.py。
还有更多...
而不是从 mercurial 存储库安装 web2py,你可以让 PyDev 指向现有的 web2py 安装(它必须是一个源安装,而不是 web2py 二进制文件)。在这种情况下,只需转到文件 | 新建 | PyDev,并指定你的 web2py 安装目录:

使用 shell 脚本更新 web2py
web2py 管理员界面提供了一个升级按钮,它会下载最新的 web2py,并将其解压到旧版本之上(除了欢迎、管理员和示例之外,它不会覆盖应用程序)。这是可以的,但它会带来一些潜在的问题:
-
管理员可能已被禁用
-
你可能希望一次性更新多个安装,并且更愿意通过编程方式来做。
-
你可能想要存档之前的版本,以防需要回滚
我们在这个菜谱中提供的脚本仅适用于解决 Linux 和 Mac 上的这些问题。
如何操作...
-
将文件移动到
web2py文件夹下:cd /path/to/web2py -
确保你是拥有 web2py 文件夹的用户,或者你至少有
write权限。将以下脚本保存到文件中(例如:update_web2py.sh),并使其可执行:chmod +x update_web2py.sh -
然后,运行它:
# update-web2py.sh
# 2009-12-16
#
# install in web2py/.. or web2py/ or web2py/scripts as update-
# web2py.sh
# make executable: chmod +x web2py.sh
#
# save a snapshot of current web2py/ as web2py/../web2py-version.
# zip
# download the current stable version of web2py
# unzip downloaded version over web2py/
TARGET=web2py
if [ ! -d $TARGET ]; then
# in case we're in web2py/
if [ -f ../$TARGET/VERSION ]; then
cd ..
# in case we're in web2py/scripts
elif [ -f ../../$TARGET/VERSION ]; then
cd ../..
fi
fi
read a VERSION c < $TARGET/VERSION
SAVE=$TARGET-$VERSION
URL=http://www.web2py.com/examples/static/web2py_src.zip
ZIP=`basename $URL`
SAVED=""
#### Save a zip archive of the current version,
#### but don't overwrite a previous save of the same version.
###
if [ -f $SAVE.zip ]; then
echo "Remove or rename $SAVE.zip first" >&2
exit 1
fi
if [ -d $TARGET ]; then
echo -n ">>Save old version: " >&2
cat $TARGET/VERSION >&2
zip -q -r $SAVE.zip $TARGET
SAVED=$SAVE.zip
fi
###
#### Download the new version.
###
echo ">>Download latest web2py release:" >&2
curl -O $URL
###
#### Unzip into web2py/
###
unzip -q -o $ZIP
rm $ZIP
echo -n ">>New version: " >&2
cat $TARGET/VERSION >&2
if [ "$SAVED" != "" ]; then
echo ">>Old version saved as $SAVED"
fi
还有更多...
是的,还有更多。当升级 web2py 时,欢迎应用程序也会升级,它可能包含新的 appadmin、新的布局和新的 JavaScript 库。你可能还想升级你的应用程序。你可以手动进行,但必须小心,因为根据你的应用程序如何工作,这可能会破坏它们。对于名为 app 的应用程序,你可以使用以下命令升级 appadmin:
cp applications/welcome/controllers/appadmin.py applications/app/\
controllers
cp applications/welcome/views/appadmin.py applications/app/views
你可以使用以下命令升级通用视图:
cp applications/welcome/views/generic.* applications/app/views
你可以使用以下命令升级 web2py_ajax:
cp applications/welcome/views/web2py_ajax.html applications/app/views
cp applications/welcome/static/js/web2py_ajax.js applications/app/
static/\js
最后,你可以使用以下命令升级所有静态文件:
cp -r applications/welcome/static/* applications/app/static/
你可能需要更加选择性地操作。首先备份,并小心行事。
创建一个简单的页面统计插件
在这个菜谱中,我们将向您展示如何创建一个插件,以分层格式显示页面统计信息。
如何操作...
首先,创建一个名为 models/plugin_stats.py 的文件,其中包含以下代码:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
def _(db, # reference to DAL obj. page_key, # string to id page
page_subkey='', # string to is subpages
initial_hits=0, # hits initial value
tablename="plugin_stats" # table where to store data
):
from gluon.storage import Storage
table = db.define_table(tablename,
Field('page_key'),
Field('page_subkey'),
Field('hits', 'integer'))
record = table(page_key=page_key,page_subkey=page_subkey)
if record:
new_hits = record.hits + 1
record.update_record(hits=new_hits)
hits = new_hits
else:
table.insert(page_key=page_key,
page_subkey=page_subkey,
hits=initial_hits)
hits = initial_hits
hs = table.hits.sum()
total = db(table.page_key==page_key).select(hs).first()(hs)
widget = SPAN('Hits:',hits,'/',total)
return Storage(dict(hits=hits,total=total,widget=widget))
plugin_stats = _(db,
page_key=request.env.path_info,
page_subkey=request.query_string)
如果你想要将结果显示给访客,请将以下内容添加到 views/layout.html:
{{=plugin_stats.widget}}
它是如何工作的...
plugin 文件是一个模型文件,在每次请求时都会执行。它调用以下查询,定义一个存储点击次数的表,每条记录由 page_key (request.env.path_info) 和 page_subkey (request.query_string) 标识。
plugin_stats = _(db,
page_key=request.env.path_info,
page_subkey=request.query_string)
如果不存在具有此键和子键的记录,则创建它。如果存在,则检索它,并将字段 hits 的值增加一。函数 _ 有一个奇怪的名字,但并没有什么特别之处。你可以选择不同的名字;我们只是不希望污染命名空间,因为这个函数只需要使用一次。该函数返回一个分配给 plugin_stats 的 Storage 对象,其中包含以下内容:
-
hits:这是与当前page_key和page_subkey对应的点击次数 -
total:这是与当前页面相同的page_key但不同子键的点击次数总和 -
widget:这是一个显示点击次数的 span,以及total,它可以嵌入到视图中
还有更多...
注意,你可以选择将以下行更改为其他内容,并使用不同的变量来对页面进行分组以进行计数:
page_key=request.env.path_info
page_subkey=request.query_string
无需图像或 JavaScript 的圆角
现代浏览器支持 CSS 指令来圆角。它们包括以下内容:
-
WebKit(Safari,Chrome)
-
Gecko(Firefox)
-
欧珀(需要重大修改)
准备工作
我们假设你有一个包含以下 HTML 代码的视图,并且你想要圆角 box 类:
<div class="box">
test
</div>
如何操作...
为了看到效果,我们还需要更改背景颜色。例如,在style文件中,为默认布局在static/styles/base.css中添加以下代码:
.box {
-moz-border-radius: 5px; /* for Firefox */
-webkit-border-radius: 5px; /* for Safari and Chrome */
background-color: yellow;
}
第一行-moz-border-radius: 5px仅被 Firefox 解释,其他浏览器忽略。第二行仅被 Safari 和 Chrome 解释。
还有更多...
那么,关于 Opera 呢?Opera 没有 CSS 指令来设置圆角,但你可以按照以下方式修改之前的 CSS,让 web2py 生成一个动态图像,用作背景,并具有所需的颜色和圆角:
.box {
-moz-border-radius: 5px; /* for Firefox */
-webkit-border-radius: 5px; /* for Safari and Chrome */
background-color: yellow;
background-image: url("../images/border_radius?r=4&fg=249,249,249&
bg=235,232,230"); /*
for opera */
}
为了达到这个目的,创建一个controllers/images.py文件,并将以下代码添加到其中:
def border_radius():
import re
radius = int(request.vars.r or 5)
color = request.vars.fg or 'rbg(249,249,249)'
if re.match('\d{3},\d{3},\d{3}',color):
color = 'rgb(%s)' % color
bg = request.vars.bg or 'rgb(235,232,230)'
if re.match('\d{3},\d{3},\d{3}',bg):
bg = 'rgb(%s)'%bg
import gluon.contenttype
response.headers['Content-Type']= 'image/svg+xml;charset=utf-8'
return '''<?xml version="1.0" ?><svg
><rect fill="%s" x="0" y="0"
width="100%%" height="100%%" /><rect ill="%s" x="0" y="0"
width="100%%" height="100%%" rx="%spx"
/></svg>'''%(bg,color,radius)
此代码将动态生成一个 SVG 图像。
参考:home.e-tjenesten.org/~ato/2009/08/border-radius-opera。
设置 cache.disk 配额
这个配方是关于 web2py 在 Linux 上使用 RAM 内存进行磁盘缓存(使用tmpfs)。
cache.disk是一种流行的缓存机制,允许多个共享文件系统的 web2py 安装共享缓存。它不如memcache高效,因为在对共享文件系统进行写入时可能会成为瓶颈;尽管如此,这仍然是某些用户的一个选项。如果你使用cache.disk,你可能想通过设置配额来限制写入缓存的数据量。这可以通过创建一个临时内存映射文件系统来实现,同时还能提高性能。
如何操作...
主要思想是使用cache.disk与tmpfs。
-
首先,你需要以
root身份登录并执行以下命令:mount -t tmpfs tmpfs $folder_path -o rw,size=$size-
这里:
$folder_path是你挂载 RAM 片段的文件夹路径$size是你想要分配的内存量(M- 兆字节)例如:
mkdir /var/tmp/myquery mount -t tmpfs tmpfs /var/tmp/myquery -o rw,size=200M -
-
你刚刚分配了 200 MB 的 RAM。现在我们得在 web2py 应用程序中将其映射。只需在你的模型中写下以下内容:
from gluon.cache import CacheOnDisk cache.disk = CacheOnDisk(request, folder='/the/memory/mapped/folder')因此,在我们的情况下:
cache.disk = CacheOnDisk(request, folder='/var/tmp/myquery') -
现在,当你使用:
db(...).select(cache=(cache.disk,3600)....)或者以下内容:
@cache(request.env.path_info, time_expire=5, cache_model=cache. disk) def cache_controller_on_disk(): import time t = time.ctime() return dict(time=t, link=A('click to reload', _href=request.url))你可以为每个查询/控制器等缓存的 ram 空间设置配额,并且每个都可以有不同的尺寸设置。
使用 cron 检查 web2py 是否正在运行
如果你在一台 UNIX 机器上,你可能想监控 web2py 是否正在运行。针对此问题的生产级解决方案是使用Monit:mmonit.com/monit/documentation/。
它可以监控你的进程,记录问题,还可以自动为你重启它们。在这里,我们提供一个简单的 DIY 解决方案,遵循 web2py 的极简精神。
如何操作...
-
我们将创建文件
/root/bin/web2pytest.sh,以检查 web2py 是否正在运行,如果未运行则启动 web2py。#! /bin/bash # written by Ivo Maintz export myusername=mdipierro export port=8000 export web2py_path=/home/mdipierro/web2py if ! ` netcat -z localhost $port ` then pgrep -flu $myusername web2py | cut -d -f1 | xargs kill > /\ dev/null 2>&1 chown $myusername: /var/log/web2py.log su $myusername -c 'cd $web2py_path && ./web2py.py -p $port -a \ password 2>&1 >> /var/log/web2py.log' sleep 3 if ` netcat -z localhost $port ` then echo "web2py was restarted" else echo "web2py could not be started!" fi fi -
现在使用
shell命令编辑crontab:crontab -e -
添加一条
crontab行,指示crontab守护进程每三分钟运行我们的脚本:
*/3 * * * * /root/bin/web2pytest.sh > /dev/null
- 注意,你可能需要编辑脚本的前几行来设置正确的用户名、端口和想要监控/重启的 web2py 路径。
构建 Mercurial 插件
web2py 的 admin 支持Mercurial进行版本控制,但能否通过 HTTP 进行拉取和推送更改?
在这个菜谱中,我们介绍了一个由单个文件组成的 web2py 插件。它包装了 Mercurial 的hgwebdir wsgi应用,并允许用户从网页浏览器或hg客户端与 web2py 应用程序的 mercurial 仓库进行交互。
这对于以下两个原因来说很有趣:
-
一方面,如果您使用 mercurial 对您的应用程序进行版本控制,此插件允许您将仓库在线共享给其他人。
-
在另一方面,这是一个如何从 web2py 调用第三方 WSGI 应用的绝佳例子。
准备工作
这要求您从源运行 web2py,并且您已安装 mercurial。您可以使用以下命令安装 mercurial:
easy_install mercurial
此插件仅在已安装 mercurial 的 Python 发行版上才能工作。您可以将 mercurial 打包到 web2py 应用程序本身中,但我们不建议这样做。如果您不是 mercurial 的常规用户,使用此插件几乎没有意义。
如何做到这一点...
创建此插件所需的所有操作只是创建一个新的控制器,"plugin_mercurial.py":
""" plugin_mercurial.py
Author: Hans Christian v. Stockhausen <hc at vst.io>
Date: 2010-12-09
"""
from mercurial import hgweb
def index():
""" Controller to wrap hgweb
You can access this endpoint either from a browser in which case
the hgweb interface is displayed or from the mercurial client.
hg clone http://localhost:8000/app/plugin_mercurial/index app
"""
# HACK - hgweb expects the wsgi version to be reported in a tuple
wsgi_version = request.wsgi.environ['wsgi.version']
request.wsgi.environ['wsgi.version'] = (wsgi_version, 0)
# map this controller's URL to the repository location and #instantiate app
config = {URL():'applications/'+request.application}
wsgi_app = hgweb.hgwebdir(config)
# invoke wsgi app and return results via web2py API
# http://web2py.com/book/default/chapter/04#WSGI
items = wsgi_app(request.wsgi.environ, request.wsgi.start_response)
for item in items:
response.write(item, escape=False)
return response.body.getvalue()
这里是来自 shell 的示例报告视图:

这里是plugin_above:的视图:

您还可以将代码推送到仓库。要能够推送代码到仓库,您需要编辑/创建文件application/<app>/.hg/hgrc,并添加以下条目,例如:
[web]
allow_push = *
push_ssl = False
显然,这仅推荐在受信任的环境中使用。另外,请参阅www.selenic.com/mercurial/hgrc.5.html#web中的hgrc文档。
hgwebdir WSGI 应用可以公开多个仓库,尽管对于特定于 web2py 应用程序的插件来说,这可能不是您想要的。如果您确实想要这样,尝试调整传递给hgwebdir构造函数的config变量。例如,您可以通过request.args[0]传递要访问的仓库名称。URL 会更长,因此您可能需要在routes.py中设置一些规则。
config = {
'app/plugin_mercurial/index/repo1':'path/to/repo1',
'app/plugin_mercurial/index/repo2':'path/to/repo2',
'app/plugin_mercurial/index/repo3':'path/to/repo3'
}
构建 pingback 插件
Pingbacks 允许博客文章和其他资源,如照片,自动通知彼此的回链。此插件公开了一个装饰器来启用 pingback 的控制器函数,以及一个 pingback 客户端来通知例如Wordpress博客,我们链接到了它。
Pingback是一个标准协议,其 1.0 版本在以下 URL 中描述:
www.hixie.ch/specs/pingback/pingback
plugin_pingback 由一个单独的模块文件组成。
如何做到这一点...
首先,创建一个module/plugin_pingback.py文件,包含以下代码:
#!/usr/bin/env python
# coding: utf8
#
# Author: Hans Christian v. Stockhausen <hc at vst.io>
# Date: 2010-12-19
# License: MIT
#
# TODO
# - Check entity expansion requirements (e.g. <) as per Pingback # spec page 7
# - make try-except-finally in PingbackClient.ping robust
import httplib
import logging
import urllib2
import xmlrpclib
from gluon.html import URL
__author__ = 'H.C. v. Stockhausen <hc at vst.io>'
__version__ = '0.1.1'
from gluon import *
# we2py specific constants
TABLE_PINGBACKS = 'plugin_pingback_pingbacks'
# Pingback protocol faults
FAULT_GENERIC = 0
FAULT_UNKNOWN_SOURCE = 16
FAULT_NO_BACKLINK = 17
FAULT_UNKNOWN_TARGET = 32
FAULT_INVALID_TARGET = 33
FAULT_ALREADY_REGISTERED = 48
FAULT_ACCESS_DENIED = 49
FAULT_UPSTREAM_ERROR = 50
def define_table_if_not_done(db):
if not TABLE_PINGBACKS in db.tables:
db.define_table(TABLE_PINGBACKS,
Field('source', notnull=True),
Field('target', notnull=True),
Field('direction', notnull=True,
requires=IS_IN_SET(('inbound', 'outbound'))),
Field('status'), # only relevant for outbound pingbacks
Field('datetime', 'datetime', default=current.request.now))
class PingbackServerError(Exception):
pass
class PingbackClientError(Exception):
pass
class PingbackServer(object):
" Handles incomming pingbacks from other sites. "
def __init__(self, db, request, callback=None):
self.db = db
self.request = request
self.callback = callback
define_table_if_not_done(db)
def __call__(self):
"""
Invoked instead of the decorated function if the request is a
pingback request from some external site.
"""
try:
self._process_request()
except PingbackServerError, e:
resp = str(e.message)
else:
resp = 'Pingback registered'
return xmlrpclib.dumps((resp,))
def _process_request(self):
" Decode xmlrpc pingback request and process it "
(self.source, self.target), method = xmlrpclib.loads(
self.request.body.read())
if method != 'pingback.ping':
raise PingbackServerError(FAULT_GENERIC)
self._check_duplicates()
self._check_target()
self._check_source()
if self.callback:
self.callback(self.source, self.target, self.html)
self._store_pingback()
def _check_duplicates(self):
" Check db whether the pingback request was previously processed "
db = self.db
table = db[TABLE_PINGBACKS]
query = (table.source==self.source) & (table.target==self.target)
if db(query).select():
raise PingbackServerError(FAULT_ALREADY_REGISTERED)
def _check_target(self):
" Check that the target URI exists and supports pingbacks "
try:
page = urllib2.urlopen(self.target)
except:
raise PingbackServerError(FAULT_UNKNOWN_TARGET)
if not page.info().has_key('X-Pingback'):
raise PingbackServerError(FAULT_INVALID_TARGET)
def _check_source(self):
" Check that the source URI exists and contains the target link "
try:
page = urllib2.urlopen(self.source)
except:
raise PingbackServerError(FAULT_UNKNOWN_SOURCE)
html = self.html = page.read()
target = self.target
try:
import BeautifulSoup2
soup = BeautifulSoup.BeautifulSoup(html)
exists = any([a.get('href')==target for a in soup.findAll('a')])
except ImportError:
import re
logging.warn('plugin_pingback: Could not import BeautifulSoup,' \
' using re instead (higher risk of pingback spam).')
pattern = r'<a.+href=[\'"]?%s[\'"]?.*>' % target
exists = re.search(pattern, html) != None
if not exists:
raise PingbackServerError(FAULT_NO_BACKLINK)
def _store_pingback(self):
" Companion method for _check_duplicates to suppress duplicates. "
self.db[TABLE_PINGBACKS].insert(
source=self.source,
target=self.target,
direction='inbound')
class PingbackClient(object):
" Notifies other sites about backlinks. "
def __init__(self, db, source, targets, commit):
self.db = db
self.source = source
self.targets = targets
self.commit = commit
define_table_if_not_done(db)
def ping(self):
status = 'FIXME'
db = self.db
session = current.session
response = current.response
table = db[TABLE_PINGBACKS]
targets = self.targets
if isinstance(targets, str):
targets = [targets]
for target in targets:
query = (table.source==self.source) & (table.target==target)
if not db(query).select(): # check for duplicates
id_ = table.insert(
source=self.source,
target=target,
direction='outbound')
if self.commit:
db.commit()
try:
server_url = self._get_pingback_server(target)
except PingbackClientError, e:
status = e.message
else:
try:
session.forget()
session._unlock(response)
server = xmlrpclib.ServerProxy(server_url)
status = server.pingback.ping(self.source, target)
except xmlrpclib.Fault, e:
status = e
finally:
db(table.id==id_).update(status=status)
def _get_pingback_server(self, target):
" Try to find the target's pingback xmlrpc server address "
# first try to find the pingback server in the HTTP header
try:
host, path = urllib2.splithost(urllib2.splittype(target)[1])
conn = httplib.HTTPConnection(host)
conn.request('HEAD', path)
res = conn.getresponse()
server = dict(res.getheaders()).get('x-pingback')
except Exception, e:
raise PingbackClientError(e.message)
# next try the header with urllib in case of redirects
if not server:
page = urllib2.urlopen(target)
server = page.info().get('X-Pingback')
# next search page body for link element
if not server:
import re
html = page.read()
# pattern as per Pingback 1.0 specification, page 7
pattern = r'<link rel="pingback" href=(P<url>[^"])" ?/?>'
match = re.search(pattern, html)
if match:
server = match.groupdict()['url']
if not server:
raise PingbackClientError('No pingback server found.')
return server
def listen(db, callback=None):
"""
Decorator for page controller functions that want to support
pingbacks.
The optional callback parameter is a function with the following
signature.
callback(source_uri, target_uri, source_html)
"""
request = current.request
response = current.response
def pingback_request_decorator(_):
return PingbackServer(db, request, callback)
def standard_request_decorator(controller):
def wrapper():
" Add X-Pingback HTTP Header to decorated function's response "
url_base = '%(wsgi_url_scheme)s://%(http_host)s' % request.env
url_path = URL(args=['x-pingback'])
response.headers['X-Pingback'] = url_base + url_path
return controller()
return wrapper
if request.args(0) in ('x-pingback', 'x_pingback'):
return pingback_request_decorator
else:
return standard_request_decorator
def ping(db, source, targets, commit=True):
" Notify other sites of backlink "
client = PingbackClient(db, source, targets, commit)
client.ping()
下面是如何使用它的方法:
-
导入模块
-
使用
listen装饰应该接收 pingback 的动作 -
使用
ping修改应该发送 pingback 的动作
这里有一个具体的例子,我们假设有一个简单的博客系统:
import plugin_pingback as pingback
def on_pingback(source_url, target_url, source_html):
import logging
logging.info('Got a pingback')
# ...
@pingback.listen(db,on_pingback)
def viewpost():
" Show post and comments "
# ...
return locals()
def addpost():
" Admin function to add new post "
pingback.ping(globals(),
source=new_post_url,
targets=[linked_to_post_url_A, linked_to_post_url_B]
)
# ...
return locals()
它是如何工作的...
plugin_pingback.py模块提供了plugin_pingback插件的核心理念。
PingbackServer类处理传入的 pingback。PingbackClient类用于通知外部网站关于反向链接。在你的代码中,你不需要直接使用这些类。相反,使用模块函数listen和ping。
listen是一个用于你想要 pingback 启用控制器函数的装饰器。在底层,它使用PingbackServer。这个装饰器接受db作为其第一个参数,并可选地接受第二个callback参数。callback签名是函数名(source、target或html),其中source是 pingback 源 URI,target是目标 URI,html是源页面内容。
ping用于使用PingbackClient通知外部网站关于反向链接。
第一个参数是,对于listen,db对象,第二个是源页面 URI,第三个是字符串或目标 URI 的列表,最后是commit参数(默认为True)。在这个点上,可能需要进行DB commit,因为包含 ping 的控制函数可能正在生成源页面。如果源页面没有提交,目标页面的 pingback 系统将无法找到它,因此拒绝 pingback 请求。
为移动浏览器更改视图
如果你的 Web 应用程序是从移动设备(如手机)访问的,那么很可能是访问者正在使用小屏幕和有限的带宽来访问你的网站。你可能想检测这一点,并为这些访问者提供页面的轻量级版本。轻量级的含义取决于上下文,但在这里我们假设你只是想为这些访问者更改默认布局。
web2py 提供了两个 API,允许你完成这项操作。
-
你可以检测客户端是否正在使用移动设备:
if request.user_agent().is_mobile: ... -
你可以要求 web2py 将默认视图
*.html替换为*.mobile.html,对于任何使用@mobilize装饰器的操作。from gluon.contrib.user_agent_parser import mobilize @mobilize def index(): return dict()
在这个菜谱中,我们将向你展示如何手动完成这项操作,使用第三方库:mobile.sniffer和mywurlf,而不是使用内置的 web2py API。
准备工作
这个片段使用库mobile.sniffer和pywurfl来解析 HTTP 请求中的USER_AGENT头。我们将创建一个返回True/False的单个函数。
你可以使用以下命令安装它们:
easy_install mobile.sniffer
easy_install pywurfl
如何操作...
我们将创建我们的函数,例如,如果我们有这个请求example.com/app/controller/function,常规视图将在views/controller/function.html中,而移动视图将在views/controller/function.mobile.html中。如果它不存在,它将回退到常规视图。
这可以通过以下函数实现,您可以将它放在任何模型文件中,例如models/plugin_detect_mobile.py。
# coding: utf8
import os
def plugin_detect_mobile(switch_view=True):
from mobile.sniffer.detect import detect_mobile_browser
if detect_mobile_browser(request.env.http_user_agent):
if switch_view:
view = '%(controller)s/%(function)s.mobile.%(extension)s' %
request
if os.path.exists(os.path.join(request.folder, 'views',view)):
response.view = view
return True
return False
plugin_detect_mobile()
使用数据库队列进行后台处理
让我们考虑一个非常典型的需要用户注册的应用程序。在用户提交注册表单后,应用程序会发送一封确认电子邮件,要求用户验证注册过程。然而,问题在于用户不会立即收到下一页的响应,因为他们必须等待应用程序连接到 SMTP 邮件服务器,发送消息,保存一些数据库结果,然后最终返回下一视图。另一个可能的病理情况可以是;假设这个相同的应用程序提供了一个仪表板,允许用户下载 PDF 报告或 OpenOffice Calc格式的数据。为了辩论,这个过程通常需要五到十分钟来生成 PDF 或电子表格。显然,让用户等待服务器处理这些数据是没有意义的,因为他们将无法执行任何其他操作。
而不是实际执行这些可能需要较长时间运行的操作,应用程序只需在数据库中注册一个请求来执行所述操作。由cron执行的后台进程可以读取这些请求,然后继续处理它们。
对于用户注册,只需提供一个名为emails_to_send的数据库表;这将导致一个每分钟运行一次的后台进程,并发送单次会话中的所有电子邮件。进行注册的用户将受益于更快的注册速度,而我们的应用程序则受益于只需要为多封电子邮件进行一次 SMTP 连接。
对于报告生成,用户可以提交请求以获取相关文件。他们可能会访问应用程序上的下载页面,该页面显示了已请求的文件的处理情况。同样,一个后台进程可以加载所有报告请求,将它们处理成输出文件,并将结果保存到数据库中。用户可以重新访问下载页面,并能够下载处理后的文件。用户可以在等待报告完成的同时继续执行其他任务。
如何做到这一点...
对于这个示例,我们将使用用户报告请求。这将是一个牙科网站,其中存储着客户信息。办公室职员希望了解客户按邮编的人口分布情况,以帮助确定在哪里发送他们的新广告活动最为合适。让我们假设这是一个非常大的牙科诊所,拥有超过 10 万客户。这份报告可能需要一些时间。
为了做到这一点,我们需要以下表:
db.define_table('clients',
Field('name'),
Field('zipcode'),
Field('address'))
db.define_table('reports',
Field('report_type'),
Field('report_file_loc'),
Field('status'),
Field('submitted_on', 'datetime', default=request.now),
Field('completed_on', 'datetime', default=None))
当用户导航到reports页面时,他们会看到可以下载的可能报告的选项。以下是一个报告请求的控制器函数示例:
def request_report():
report_type = request.vars.report_type
# make sure its a valid report
if report_type not in ['zipcode_breakdown', 'name_breakdown']:
raise HTTP(404)
# add the request to the database to process
report_id = db.reports.insert(report_type=report_type,
status='pending')
# return something to uniquely identify this report in case
# this request was made from Ajax.
return dict(report_id=report_id)
现在是处理所有报告请求的脚本。
def process_reports():
from collections import defaultdict
reports_to_process = db(db.reports.status == 'pending').select()
# set selected reports to processing so they do not get picked up
# a second time if the cron process happens to execute again while
# this one is still executing.
for report in reports_to_process:
report.update_record(status='processing')
db.commit()
for report in reports_to_process:
if report.report_type == 'zipcode_breakdown':
# get all zipcodes
zipcodes = db(db.clients.zipcode != None).select()
# if the key does not exist, create it with a value of 0
zipcode_counts = defaultdict(int)
for zip in zipcodes:
zipcode_counts[zip] += 1
# black box function left up to the developer to implement
# just assume it returns the filename of the report it created.
filename = make_pdf_report(zipcode_counts)
report.update_record(status='done',
completed_on=datetime.datetime.now(),
report_file_loc=filename)
# commit record so it reflects into the database immediately.
db.commit()
process_reports()
现在我们有了生成报告的代码,它需要一个执行的方式。让我们将此函数的调用添加到web2py cron/crontab文件中。
* * * * * root *applications/dentist_app/cron/process_reports.py
现在,当用户请求页面时,他们要么会看到报告正在处理,要么会看到一个下载生成报告的链接。
还有更多...
在这个菜谱中,我们使用了Poor-Man's Queue的例子来将任务调度到后台进程。然而,这种方法可以扩展到一定数量的用户,但在某个时候,可以使用外部消息队列来进一步加快速度。
自从版本 1.99.1 以来,web2py 包括其自己的内置调度器和调度 API。它在最新版的官方 web2py 手册中有记录,但您也可以在以下链接中了解更多:
www.web2py.com/examples/static/epydoc/web2py.gluon.scheduler-module.html
有一个插件将 celery 集成到 web2py 中:
code.google.com/p/web2py-celery/
前者使用数据库访问来分配任务,而后者通过 celery 使用RabbitMQ来实现企业消息队列服务器。
如何有效地使用模板块
如您可能已经知道,web2py 模板系统非常灵活,提供了模板继承、包含以及一个最近新出现(且文档不足)的功能,称为块。
块是子模板可以覆盖其父模板的某些部分,并用它们自己的内容替换或扩展内容的一种方式。
例如,一个典型的布局模板包括几个可以根据用户当前所在页面进行覆盖的位置。例如,标题栏、导航的部分、可能是一个页面标题或关键词。
在这个例子中,我们将考虑一个典型的企业应用,该应用在每个页面上都有自定义 JavaScript 来处理仅限于该页面的元素;解决这个问题的方法将为块的使用生成一个基本模式。
如何操作...
首先,让我们处理使用块的基本模式,因为这也解决了我们示例应用中需要在 HTML 页面<head>元素中放置额外 JavaScript 块的问题。
考虑以下layout.html文件:
<!doctype html>
<head>
<title>{{block title}}My Web2py App{{end}}</title>
<script type="text/javascript" src={{=URL(c="static/js",
f="jquery.js")}}></script>
{{block head}}{{end}}
</head>
<body>
<h1>{{block body_title}}My Web2py App{{end}}</h1>
<div id="main_content">
{{block main_content}}
<p>Page has not been defined</p>
{{end}}
</div>
</body>
以及以下detail.html文件:
{{extend "layout.html"}}
{{block title}}Analysis Drilldown - {{super}}{{end}}
{{block head}}
<script>
$(document).ready(function() {
$('#drill_table').sort();
});
</script>
{{end}}
{{block main_content}}
<table id="drill_table">
<tr>
<td>ABC</td>
<td>123</td>
</tr>
<tr>
<td>EFG</td>
<td>456</td>
</tr>
</table>
{{end}}
这将渲染以下输出文件:
<!doctype html>
<head>
<title>Analysis Drilldown - My Web2py App</title>
<script type="text/javascript" src="img/jquery.js"></script>
<script>
$(document).ready(function() {
$('#drill_table').sort();
});
</script>
</head>
<body>
<h1>My Web2py App</h1>
<div id="main_content">
<table id="drill_table">
<tr>
<td>ABC</td>
<td>123</td>
</tr>
<tr>
<td>EFG</td>
<td>456</td>
</tr>
</table>
</div>
</body>
还有更多...
注意在覆盖标题块时使用{{super}}。{{super}}将覆盖其父块的 HTML 输出,并将其插入到该位置。因此,在这个例子中,页面标题可以保留全局站点标题,但将这个独特的页面名称插入到标题中。
另一点需要注意的是,当一个块在子模板中没有定义时,它仍然会渲染。由于没有为body_title块定义,它仍然渲染了My web2py App。
此外,块弃用了旧 web2py {{include}} 助手的需求,因为子模板可以定义一个表示页面主要内容位置的块。这是在其他流行的模板语言中广泛使用的设计模式。
使用 web2py 和 wxPython 创建独立应用程序
web2py 可以用来创建不需要浏览器或网络服务器的桌面可视化应用程序。这在需要独立应用程序(即不需要网络服务器安装)时非常有用,而且这种方法还可以简化用户界面编程,无需高级 JavaScript 或 CSS 要求,直接访问用户的机器操作系统和库。
此配方展示了如何使用 模型 和 助手 创建一个示例表单,使用 wxPython GUI 工具包将基本人员信息存储到数据库中,代码行数少于 100 行,遵循 web2py 的最佳实践。
准备工作
首先,你需要一个有效的 Python 和 web2py 安装,然后从( www.wxpython.org/download.php)下载并安装 wxPython。
其次,你需要 gui2py,这是一个小型库,用于管理表单,连接 web2py 和 wx(http://code.google.com/p/gui2py/downloads/list)。
你也可以使用 Mercurial 从项目仓库中提取源代码:
hg clone https://codegoogle.com/p/gui2py/.
如何做...
在此基本配方中,我们将介绍以下步骤:
-
导入 wxPython、gui2py 和 web2py。
-
创建一个包含多个字段和验证器的示例
Person表。 -
创建 wxPython GUI 对象(应用程序、主框架窗口和 HTML 浏览器)。
-
为
Person表创建一个 web2py SQL 表单。 -
定义事件处理器以处理用户输入(验证和插入行)。
-
连接事件处理器,显示窗口,并开始与用户交互。
以下是一个完整的示例,源代码具有自解释性。将其输入并保存为常规 Python 脚本,例如,在你的主目录中作为 my_gui2py_app.py:
#!/usr/bin/python
# -*- coding: latin-1 -*-
import sys
# import wxPython:
import wx
# import gui2py support -wxHTML FORM handling- (change the path!)
sys.path.append(r"/home/reingart/gui2py")
from gui2py.form import EVT_FORM_SUBMIT
# import web2py (change the path!)
sys.path.append(r"/home/reingart/web2py")
from gluon.dal import DAL, Field
from gluon.sqlhtml import SQLFORM
from gluon.html import INPUT, FORM, TABLE, TR, TD
from gluon.validators import IS_NOT_EMPTY, IS_EXPR, IS_NOT_IN_DB,
IS_IN_SET
from gluon.storage import Storage
# create DAL connection (and create DB if not exists)
db=DAL('sqlite://guitest.sqlite',folder=None)
# define a table 'person' (create/aster as necessary)
person = db.define_table('person',
Field('name','string', length=100),
Field('sex','string', length=1),
Field('active','boolean', comment="check!"),
Field('bio','text', comment="resume (CV)"),
)
# set sample validator (do not allow empty nor duplicate names)
db.person.name.requires = [IS_NOT_EMPTY(),
IS_NOT_IN_DB(db, 'person.name')]
db.person.sex.requires = IS_IN_SET({'M': 'Male', 'F': 'Female'})
# create the wxPython GUI application instance:
app = wx.App(False)
# create a testing frame (wx "window"):
f = wx.Frame(None, title="web2py/gui2py sample app")
# create the web2py FORM based on person table
form = SQLFORM(db.person)
# create the HTML "browser" window:
html = wx.html.HtmlWindow(f, style= wx.html.HW_DEFAULT_STYLE |
wx.TAB_TRAVERSAL)
# convert the web2py FORM to XML and display it
html.SetPage(form.xml())
def on_form_submit(evt):
"Handle submit button user action"
global form
print "Submitting to %s via %s with args %s"% (evt.form.action,
evt.form.method, evt.args)
if form.accepts(evt.args, formname=None, keepvalues=False, dbio=False):
print "accepted!"
# insert the record in the table (if dbio=True this is done by web2py):
db.person.insert(name=form.vars.name,
sex=form.vars.sex,
active=form.vars.active,
bio=form.vars.bio,
)
# don't forget to commit, we aren't inside a web2py controller!
db.commit()
elif form.errors:
print "errors", form.errors
# refresh the form (show web2py errors)
html.SetPage(form.xml())
# connect the FORM event with the HTML browser
html.Bind(EVT_FORM_SUBMIT, on_form_submit)
# show the main window
f.Show()
# start the wx main-loop to interact with the user
app.MainLoop()
记得将 /home/reingart/web2py /home/reingart/gui2py 更改为你的 web2py 和 gui2py 安装路径。
保存文件后,运行它:
python my_gui2py_app.py
你应该看到一个准备接收数据的程序窗口,并对其进行测试!它应该像常规 web2py 应用程序一样工作:

它是如何工作的...
此配方使用基本的 wxPython 对象,在这种情况下,是 wx.HTML 控制器(你可以看到原始的 form_example.zip,它是 gui2py 的基础):
wx.HTML 基本上是 wxPython 浏览器,它可以显示简单的 HTML 标记(主要用于显示帮助页面、报告和进行简单的打印)。它可以扩展以渲染自定义 HTML 标签(表单、INPUT、TEXTAREA 等),模拟正常浏览器。
首先,程序应导入所需的库,定义模型,并创建一个wx应用程序和一个基本窗口(在wx世界中是一个Frame)。一旦在主窗口中创建了wx.HTML控件,事件处理程序应连接到wx,以告知如何响应用户操作。事件处理程序接收已解析的表单数据,执行标准表单验证并使用 DAL(类似于 web2py 控制器)插入行数据。最后,这是一个 GUI 应用程序,因此必须调用MainLoop。它将永远运行,等待用户事件,并调用适当的事件处理程序。
主要优势在于wx.HTML消除了对 JavaScript 引擎的需求,因此事件可以直接在 Python 中编程,并且确保了在wxPython运行的不同平台上获得相同的结果,无需处理 HTML 兼容性问题。
由于代码是一个标准的 Python 程序,您可以直接在用户机器上访问高级功能,例如打开文件或套接字连接,或使用库与摄像头、USB 设备或旧式硬件交互。
此外,这种方法允许您重用您的 web2py 知识(数据访问层 DAL、模型、辅助工具、内置验证等),从而加快独立可视化 GUI 应用程序的开发速度,遵循 Web 开发的最佳实践。
还有更多...
此配方可以通过添加更多高级 wxPython 控件进一步扩展,例如wx.ListCtrl或wx.Grid,从而能够制作具有电子表格功能的响应式完整功能应用程序,自定义单元格编辑器,虚拟行以浏览大量记录等。
此外,wx.AUI(高级用户界面)允许构建具有停靠工具栏和面板、视觉样式等的现代外观的应用程序。
您可以查看更多





















。在我们的 web2py 应用程序文件夹中,我们最终得到以下文件结构:




浙公网安备 33010602011771号