然后在 **任务(Task)** 的客户端模块的 :doc:`on_page_loaded ` 事件处理程序中添加如下代码:
.. code-block:: js
if (task.change_password.can_view()) {
$("#menu-right #pass a").click(function(e) {
e.preventDefault();
task.change_password.open({open_empty: true});
task.change_password.append_record();
});
}
else {
$("#menu-right #pass a").hide();
}
这段代码会检查用户是否有权限查看该数据项,如果有则打开空数据集并创建编辑表单,否则隐藏该菜单项。
在 “修改密码(Change password)” 客户端模块中添加如下代码:
.. code-block:: js
function on_edit_form_created(item) {
item.edit_form.find("#ok-btn")
.off('click.task')
.on('click', function() {
change_password(item);
});
item.edit_form.find("#cancel-btn")
.off('click.task')
.on('click', function() {
item.close_edit_form();
});
}
function change_password(item) {
item.post();
item.server('change_password', [item.old_password.value, item.new_password.value], function(res) {
if (res) {
item.warning('Password has been changed. The application will be reloaded.',
function() {
task.logout();
location.reload();
});
}
else {
item.alert_error("Can't change the password.");
item.edit();
}
});
}
function on_field_changed(field, lookup_item) {
var item = field.owner;
if (field.field_name === 'old_password') {
item.server('check_old_password', [field.value], function(error) {
if (error) {
item.alert_error(error);
}
});
}
}
function on_edit_form_close_query(item) {
return true;
}
在这里我们重写了 **确定(OK)** 和 **取消(Cancel)** 按钮的点击事件。默认情况下,这些事件在任务的客户端模块中定义,用于保存记录更改和取消编辑。在 ``on_edit_form_close_query`` 事件处理程序中返回 true,这样任务客户端模块中定义的 “是/否/取消” 对话框就不会弹出。
``on_field_changed`` 事件处理程序会检查旧密码是否正确。它和 ``change_password`` 函数会向服务器发送请求,调用数据项服务器模块中定义的函数:
.. code-block:: py
def change_password(item, old_password, new_password):
user_id = item.session['user_info']['user_id']
users = item.task.users.copy(handlers=False)
users.set_where(id=user_id)
users.open()
same_password = item.task.check_password_hash(users.password_hash.value, old_password)
if users.rec_count== 1 and same_password:
users.edit()
users.password_hash.value = item.task.generate_password_hash(new_password)
users.post()
users.apply()
return True
else:
return False
def check_old_password(item, old_password):
user_id = item.session['user_info']['user_id']
users = item.task.users.copy(handlers=False)
users.set_where(id=user_id)
users.open()
same_password = item.task.check_password_hash(users.password_hash.value, old_password)
if users.rec_count == 1 and same_password:
return
else:
return 'Invalid password'
:doc:`session ` 属性用于获取当前用户的 ``id``。
密码修改后,客户端会自动刷新。
===========================================
如何通过自定义用户表进行认证
===========================================
默认情况下,所有的用户信息都存储在 admin.sqlite 数据库中的一个表中。该表的结构是固定的,无法更改。
本节介绍如何使用自定义用户表中的数据进行用户认证。
首先,创建一个分组 **认证(Authentication)** ,选中它并添加一个名为 **用户(Users)** 的实体项,包含如下字段:
.. image:: https://jampy-docs.readthedocs.io/projects/V7/zh-cn/latest/_images/users_fields_jampy.png
:align: center
:alt: users_fields_jampy.png
我们不会在表中直接存储用户密码,而是在界面中使用该字段。实际存储的是加盐后的密码哈希值,保存在 password_hash 字段中。
我们还创建了名为 “角色(Roles)” 的 :doc:`查找列表 ` ,并在 “角色 “(Roles)” 字段定义中使用。
我们为其添加了与 :doc:`角色 ` 表中相同的角色(id 和名称)。后续需要保持这些角色的同步。
.. image:: https://jampy-docs.readthedocs.io/projects/V7/zh-cn/latest/_images/roles_lookup_list_jampy.png
:align: center
:alt: roles_lookup_list_jampy.png
在 :doc:`角色 ` 中,需要设置只有负责该项的用户才能查看 **用户(Users)** 实体项。
我们在 :doc:`查看表单对话框 ` 和 :doc:`编辑表单对话框 ` 的字段列表中移除了 password_hash 字段。
在 **用户(Users)** 服务端模块中,定义如下 :doc:`on_apply ` 事件处理程序:
.. code-block:: py
def on_apply(item, delta, params, connection):
for d in delta:
if not (d.rec_deleted() or d.rec_modified() and d.login.value == d.login.old_value):
users = d.task.users.copy(handlers=False)
users.set_where(login=d.login.value)
users.open(fields=['login'])
if users.rec_count:
raise Exception('There is a user with this login - %s' % d.login.value)
if d.password.value:
d.edit();
d.password_hash.value = delta.task.generate_password_hash(d.password.value)
d.password.value = None
d.post();
在该事件处理程序中,我们检查是否有相同登录名的用户,如果有则抛出异常,否则用 task 的 :doc:`generate_password_hash ` 方法生成哈希,并将密码字段设为 None。
在客户端模块中,定义如下 on_field_get_text 事件处理程序。它会将密码显示为 “**********” :
.. code-block:: js
function on_field_get_text(field) {
var item = field.owner;
if (field.field_name === 'password') {
if (item.id.value || field.value) {
return '**********';
}
}
}
最后,在 **任务(Task)** 的服务端模块中定义 :doc:`on_login ` 事件处理程序:
.. code-block:: py
def on_login(task, form_data, info):
users = task.users.copy(handlers=False)
users.set_where(login=form_data['login'])
users.open()
if users.rec_count == 1:
if task.check_password_hash(users.password_hash.value, form_data['password']):
return {
'user_id': users.id.value,
'user_name': users.name.value,
'role_id': users.role.value,
'role_name': users.role.display_text
}
现在需要在 **用户(Users)** 中添加一个有权限管理用户的管理员。之后即可在项目 :doc:`参数 ` 中启用安全模式(Safe mode)。
===============================
如何创建用户注册表单
===============================
本部分内容适用于 Jam.py V5。如需 V7 相关内容,请访问:
https://groups.google.com/g/jam-py/c/DwALkbBsFcw/m/MujcOlgYAAAJ
或在此下载示例:
https://drive.google.com/file/d/1Gty1bC2I3srFo9XeQ0dXdAdnYeJbwoeB/view?usp=drive_link
本主题假设你已根据前一节创建了 :doc:`用户 ` 实体项。
现在我们创建一个 *register.html* 文件。
它包含一个注册表单:
.. code-block:: html
以及一段 JavaScript 代码:
.. code-block:: js
$(document).ready(function(){
function register(name, login, password) {
$.ajax({
url: "ext/register",
type: "POST",
contentType: "application/json;charset=utf-8",
data: JSON.stringify([name, login, password]),
success: function(response, textStatus, jQxhr) {
if (response.result.data) {
show_alert(response.result.data);
}
else {
$("div.alert-success").show();
setTimeout(
function() {
window.location.href = "index.html";
},
1000
);
}
},
error: function(jqXhr, textStatus, errorThrown) {
console.log(errorThrown);
}
});
}
function show_alert(message) {
$("div.alert-error")
.text(message)
.show();
}
$('input').focus(function() {
$("div.alert").hide();
});
$("#register-btn").click(function() {
var name = $("#name").val(),
login = $("#login").val(),
password1 = $("#password1").val(),
password2 = $("#password2").val();
if (!name) {
show_alert('Name is not specified');
}
else if (!login) {
show_alert('Login is not specified');
}
else if (!password1) {
show_alert('Password is not specified');
}
else if (password1 !== password2) {
show_alert('Passwords do not match');
}
else {
register(name, login, password1)
}
})
})
当用户点击 **确定(OK)** 按钮时,JavaScript 会向服务器发送 url 为 “ext/register ” 的 ajax post 请求,参数为 “ name, login, password ” 。
当服务器收到以 “ext/” 开头的请求时,会触发 :doc:`on_ext_request ` 事件。
**任务(Task)** 的服务端模块包含如下 ``on_ext_request`` 事件处理程序:
.. code-block:: py
def on_ext_request(task, request, params):
reqs = request.split('/')
if reqs[2] == 'register':
name, login, password = params
users = task.users.copy(handlers=False)
users.set_where(login=login)
users.open()
if users.rec_count:
return 'Existing login, please use different login'
users.append()
users.name.value = name
users.login.value = login
users.password_hash.value = task.generate_password_hash(password)
users.role.value = 2
users.post()
users.apply()
它会检查 url 是否包含 “register” ,然后判断是否有相同登录名的用户,如果没有则注册新用户。
参见
----------
:doc:`on_ext_request `
==========================================
认证
==========================================
在 Jam.py 的代码仓库中有一个 “身份验证(Authentication)” 项目。
:download:`auth.zip <../../_static/auth.zip>`
这个项目演示了本节的前三个主题。 您可以将其下载,创建一个新项目,
然后 :doc:`导入 ` 这个文件。
.. toctree::
:maxdepth: 1
how_to_authenticate_from_custom_users_table
how_to_create_registration_form
how_give_user_ability_to_change_password
==================================================
在 AWS 上部署 Jam.py 的逐步指南
==================================================
本文改编自
https://devops.profitbricks.com/tutorials/install-and-configure-mod_wsgi-on-ubuntu-1604-1/
希望对大家有所帮助。
* 创建一个 AWS 账户并登录
* 进入 EC2,创建一个实例(本例中使用 Ubuntu 16.04 t2.micro)
* 按提示下载私钥
* 使用 Puttygen 将 pem 转换为 ppk (参加: https://stackoverflow.com/questions/3190667/convert-pem-to-ppk-file-format)
* 从 AWS 控制台获取 EC2 实例的公共 DNS
* 使用 Putty SSH 登录到 EC2 实例(指向公共 DNS 和您的 ppk)
* 用户名为 ubuntu
* 更新软件包信息库:
.. code-block:: console
sudo apt-get update
* 安装 pip:
.. code-block:: console
sudo apt-get install python3-pip
* 安装 jam.py:
.. code-block:: console
sudo pip3 install jam.py
* 安装 Apache:
.. code-block:: console
sudo apt-get install apache2 apache2-utils libexpat1 ssl-cert
* 安装 mod-wsgi:
.. code-block:: console
sudo apt-get install libapache2-mod-wsgi-py3
* 重启 Apache:
.. code-block:: console
sudo /etc/init.d/apache2 restart
* 切换到此处:
.. code-block:: console
cd /var/www/html/
* 创建目录:
.. code-block:: console
sudo mkdir [appname]
* 切换到此处:
.. code-block:: console
cd [appname]
* 创建应用:
.. code-block:: console
sudo jam-project.py
* 检查应用是否存在:
.. code-block:: console
ls
* 创建配置:
.. code-block:: console
sudo nano /etc/apache2/conf-available/wsgi.conf
* 粘贴以下内容:
.. code-block:: apache
WSGIScriptAlias / /var/www/html/[appname]/wsgi.py
WSGIPythonPath /var/www/html/[appname]
Require all granted
Alias /static/ /var/www/html/[appname]/static/
Require all granted
* 退出并保存
* 给 apache 文件权限:
.. code-block:: console
sudo chmod 777 /var/www/html/[appname]
* 给 apache 所有权:
.. code-block:: console
sudo chown -R www-data:www-data /var/www
* 启用 wsgi:
.. code-block:: console
sudo a2enconf wsgi
* 重启 apache:
.. code-block:: console
sudo /etc/init.d/apache2 restart
* 在 AWS 上创建安全组,允许您在端口 80 上连接 HTTP
* 将实例分配到安全组
* 测试
* 如果不工作,检查错误日志以了解情况:
.. code-block:: console
nano /var/log/apache2/error.log
*本文最初由 Simon Cox 发布于*
https://groups.google.com/forum/#!msg/jam-py/Zv5JfkLRFy4/22tolZ-hAQAJ
=======================================
如何在 PythonAnywhere 上部署项目
=======================================
* 使用 pip 安装 Jam.py。为此,请打开一个新的 **Bash** 命令行并运行:
.. code-block:: console
mkvirtualenv --python=/usr/bin/python3.13 my-virtualenv # use whichever python version you prefer
pip install jam.py-v7
上面的 **mkvirtualenv** 命令可能显示如下:
.. code-block:: console
created virtual environment CPython3.10.5.final.0-64 in 8486ms
creator CPython3Posix(dest=/home/username/.virtualenvs/my-virtualenv, ...)
**dest** 对应的路径将用于 **Virtualenv** 部分,这对于每个用户,各不相同。
* 将你的项目文件夹压缩为一个 zip 文件,然后在 **文件(Files)** 选项卡将其上传并解压:
假设你有一个 *username* 的用户,你的项目就会位于 */home/username/project_folder* 目录下。
* 打开 **Web** 选项卡,添加一个新的 web 应用程序。在 **代码(Code)** 部分指定:
* 源代码(Source code): */home/username/project_folder*
* 工作目录(Working directory): */home/username/project_folder*
* WSGI 配置文件: 打开 */var/www/username_pythonanywhere_com_wsgi.py* 文件,
删除已有内容,并只添加以下代码:
.. code-block:: py
import os
import sys
path = '/home/username/project_folder'
if path not in sys.path:
sys.path.append(path)
from jam.wsgi import create_application
application = create_application(path)
在 **静态文件(Static files)** 部分,指定 Jam.py 的静态文件的访问位置和路径,例如:
* URL: */static/*
* 目录(Directiory): */home/username/project_folder/static*
在 **Virtualenv** 部分,指定使用上面 **mkvirtualenv** 命令创建的虚拟环境路径。例如:
* /home/username/.virtualenvs/my-virtualenv/
在 **强制 HTTPS(Force HTTPS)** 部分,启用 HTTPS。
* 在 **重启(Reload)** 部分,重启服务器。
* 要进行调试,请在 **日志(Logs)** 部分查看日志内容。
================================================================
如何在 Linux Apache HTTP 服务器上部署 Jam-py 应用?
================================================================
抱歉,因为我对 MS 服务器完全不了解,
因此,这里基本说的是直接部署到云服务器上,它们通常开放 22、80 和 443 端口。
前提是拥有 DNS 服务器名称的签名证书(即,下面的 YOUR_SERVER DNS )。
也可以使用自签名证书等,这里不涵盖这些内容。
此外,还需要安装 Python 和授予 sudo 访问权限(或 Linux 的 root 权限)。
应用处于只读模式。您可以访问 admin.html 页面,但无法更改任何内容。
我在 Google Cloud 服务器上进行了一些调整,这是一个微型 Ubuntu 实例,
使用 apt-get 进行普通的 apache2 安装。
* 为 Apache 安装 wsgi 模块 :
.. code-block:: console
apt-get install libapache2-mod-wsgi
* 为 apache 启用 ssl 和 wsgi 模块 :
.. code-block:: console
a2enmod ssl wsgi
* 为 jam-py 应用创建自定义配置文件,例如 /etc/apache2/sites-available/test.conf(仍在完善中):
.. code-block:: apache
ServerName YOUR_SERVER
ServerAlias
ServerAdmin YOUR_EMAIL
ErrorLog ${APACHE_LOG_DIR}/test-error-sec.log
CustomLog ${APACHE_LOG_DIR}/test-access-sec.log combined
#below is for cx_Oracle
SetEnv LD_LIBRARY_PATH /u01/app/oracle/product/11.2.0/xe/lib
SetEnv ORACLE_SID XE
SetEnv ORACLE_HOME /u01/app/oracle/product/11.2.0/xe
#finish cx_Oracle
DocumentRoot /var/www/html/simpleassets
SSLEngine on
SSLCertificateFile "/etc/ssl/private/your.crt"
SSLCertificateKeyFile "/etc/ssl/private/your.key"
SSLCertificateChainFile "/etc/ssl/private/your_chain.crt"
SSLCACertificateFile "/etc/ssl/private/your_CA.crt"
WSGIDaemonProcess web user=www-data group=www-data processes=1 threads=5
WSGIScriptAlias / /var/www/html/simpleassets/wsgi.py
Options +ExecCGI
SetHandler wsgi-script
AddHandler wsgi-script .py
Order deny,allow
Allow from all
Require all granted
Order deny,allow
Allow from all
# comment the following for ubuntu <13
Require all granted
# comment the following for ubuntu < 13
Require all granted
上面的文件使用带有 your.key 的签名证书 your.crt ,以及从 CA 获取的 CA 和链文件。
请查看网络上关于证书和 DNS 的资源。
您需要获取并将这些文件复制到 /etc/ssl/private 文件夹中。将 YOUR_xyz 更改为您的自己的内容。
/var/www/html 是 Ubuntu 用于提供网页的默认文件夹。
* 像往常一样安装 jam-py
我创建了 /var/www/html/simpleassets 文件夹,在那里解压了 jam-py SimpleAssets 项目。
按照这里解释的步骤来部署这些:
简单来说,导出您的项目,保存好 zip 文件,并将其复制到您的网络托管服务器的所需文件夹中。
同时复制 admin.sqlite 和您的数据库(假设您使用的是 sqlite3 数据库)。
如果使用其他数据库,例如 mysql,您需要导出/导入数据库。
* 启用 test.conf (上面没有扩展名的文件名):
.. code-block:: console
a2ensite test; systemctl restart apache2
就是这样。目前,我保留了 80 端口不变,jam-py 只在 https 端口上运行。
要调试问题,我会从 SeLinux 或 apparmor 开始。 在 Ubuntu 上,这可能会有所帮助:
.. code-block:: console
sudo /etc/init.d/apparmor stop
现在,问题是如何在一个 https 服务器上运行两个 jam-py 实例?
这个问题的一个可能答案是 DNS。您可以将您的 DNS
设置为例如 second_instance.YOUR_SERVER 名称(上面的实际例子是 jam2.research...)。
因此,上面的 test.conf 文件几乎相同,只是 YOUR_SERVER 被称为 second_instance.YOUR_SERVER。
/etc/apache2/sites-available/test3.conf 文件内容如下 :
.. code-block:: apache
ServerName second_instance.YOUR_SERVER
ServerAlias
ServerAdmin YOUR_EMAIL
ErrorLog ${APACHE_LOG_DIR}/test3-error-sec.log
CustomLog ${APACHE_LOG_DIR}/test3-access-sec.log combined
#below is for cx_Oracle
SetEnv LD_LIBRARY_PATH /u01/app/oracle/product/11.2.0/xe/lib
SetEnv ORACLE_SID XE
SetEnv ORACLE_HOME /u01/app/oracle/product/11.2.0/xe
#finish cx_Oracle
DocumentRoot /var/www/html/simpleassets3
SSLEngine on
SSLCertificateFile "/etc/ssl/private/your.crt"
SSLCertificateKeyFile "/etc/ssl/private/your.key"
SSLCertificateChainFile "/etc/ssl/private/your_chain.crt"
SSLCACertificateFile "/etc/ssl/private/your_CA.crt"
WSGIDaemonProcess assets3 user=www-data group=www-data processes=1 threads=5
WSGIScriptAlias / /var/www/html/simpleassets3/wsgi.py
Options +ExecCGI
SetHandler wsgi-script
AddHandler wsgi-script .py
Order deny,allow
Allow from all
Require all granted
Order deny,allow
Allow from all
# comment the following for ubuntu <13
Require all granted
# comment the following for ubuntu < 13
Require all granted
Jam-py 应用的 second_instance 现在位于例如 /var/www/html/simpleassets3 中,
并且 WSGIDaemonProcess 已调整为新的守护进程,称为 assets3。其他一切几乎相同。
这是可能的,因为 SSL 证书是 *(星号或通配符)证书,使您能够在一个 DNS 域上运行多个服务。
*本文最初由 Dražen Babić 发布于* https://github.com/jam-py/jam-py/issues/35
==================================================================
Nginx 配合 Gunicorn 或 uvicorn 的使用,如何实现?
==================================================================
Green Unicorn (gunicorn) 是一个 HTTP/WSGI 服务器,
设计用于为快速客户端或响应较慢的应用程序提供服务。
也就是说,它通常在缓冲前端服务器(如 nginx 或 lighttpd)的后面运行。
默认情况下, ``gunicorn`` 会监听 127.0.0.1。
切换到 jam 应用文件夹,或使用(例如,在脚本、cron 作业中)如下代码 :
.. code-block:: console
python /usr/bin/gunicorn --chdir /path/to/jam/App wsgi
或从 /path/to/jam/App 目录运行:
.. code-block:: console
gunicorn wsgi
[2018-04-13 15:01:44 +0000] [8650] [INFO] Starting gunicorn 19.4.5
[2018-04-13 15:01:44 +0000] [8650] [INFO] Listening at: http://127.0.0.1:8000 (8650)
[2018-04-13 15:01:44 +0000] [8650] [INFO] Using worker: sync
[2018-04-13 15:01:44 +0000] [8654] [INFO] Booting worker with pid: 8654
.
.
要在所有接口和端口 8081 上启动 jam.python :
.. code-block:: console
gunicorn -b 0.0.0.0:8081 wsgi
[2018-04-13 15:03:34 +0000] [8680] [INFO] Starting gunicorn 19.4.5
[2018-04-13 15:03:34 +0000] [8680] [INFO] Listening at: http://0.0.0.0:8081 (8680)
[2018-04-13 15:03:34 +0000] [8680] [INFO] Using worker: sync
[2018-04-13 15:03:34 +0000] [8684] [INFO] Booting worker with pid: 8684
.
.
如果需要,可以使用 --workers=5 启动 5 个工作进程。
对于 ``uvicorn`` ,我们需要修改 wsgi.py 文件并安装 ``asgiref`` 。
我们将其命名为 asgi.py 文件,内容如下 :
.. code-block:: py
from jam.wsgi import create_application
from asgiref.wsgi import WsgiToAsgi
application = WsgiToAsgi(create_application(__file__))
要在 ``localhost`` 和端口 8000 上启动 jam.py :
.. code-block:: console
uvicorn asgi:application
INFO: Started server process [16576]
INFO: Waiting for application startup.
INFO: ASGI 'lifespan' protocol appears unsupported.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
Nginx 配置:
在 /etc/nginx/sites-enabled/default (Linux Mint) 中注释掉默认的 location 部分::
.. code-block:: nginx
#location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
# try_files $uri $uri/ =404;
# }
并添加 :
.. code-block:: nginx
# Proxy connections to the application servers
# app_servers
location / {
proxy_pass http://app_servers;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
在 /etc/nginx/nginx.conf 中添加 127.0.0.1:8081,如果这是你的 ``Gunicorn`` 或 ``uvicorn``
服务器地址和端口:
.. code-block:: nginx
# Configuration containing list of application servers
upstream app_servers {
server 127.0.0.1:8081;
}
这也使得在不同端口上运行不同的应用服务器成为可能 :
.. code-block:: console
Client Request ----> Nginx (Reverse-Proxy)
|
/|\
| | `-> App. Server I. 127.0.0.1:8081
| `--> App. Server II. 127.0.0.1:8082
`----> App. Server III. 127.0.0.1:8083
重启 ``nginx`` 即可!
恭喜! 我们现在可以测试 ``Nginx`` 与 Jam.py 的配合使用了。
现在,关于 ``certificates``:
在 /etc/nginx/sites-enabled/jam 中,我们可以添加如下配置,
将所有 HTTP 请求重定向到 HTTPS 并转发到 8001 端口(或根据上述设置的任何其他端口):
.. code-block:: nginx
server {
listen 80;
server_name YOUR_SERVER;
access_log off;
location /static/ {
alias /path/to/jam/App/static/;
}
location / {
proxy_pass http://127.0.0.1:8001;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Real-IP $remote_addr;
add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"';
}
return 301 https://$server_name$request_uri;
}
server {
listen 443;
server_name YOUR_SERVER_FQDN;
access_log off;
location /static/ {
alias /path/to/jam/App/static/;
}
location = /favicon.ico {
alias /path/to/jam/App/favicon.ico;
}
ssl on;
ssl_certificate /etc/nginx/ssl/YOUR_SERVER.crt;
ssl_certificate_key /etc/nginx/ssl/YOUR_SERVER.key;
add_header Strict-Transport-Security "max-age=31536000";
location / {
client_max_body_size 10M;
proxy_pass http://127.0.0.1:8001;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Real-IP $remote_addr;
add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"';
}
就是这样!
恭喜!我们现在可以在 HTTPS 端口上测试 Nginx 与 Jam.py 的配合使用了!
*本文最初由 Dražen Babić 发布于* https://github.com/jam-py/jam-py/issues/67
=============
如何部署
=============
.. toctree::
:maxdepth: 1
how_to_deploy_project_on_pythonanywhere
a_step-by-step_guide_to_deploy_a_jam_py_on_the_aws
how_to_deploy_to_linux_apache
how_to_do_with_gunicorn
=================================
CSV 文件的导入与导出
=================================
首先,在数据项的客户端模块中,我们创建两个按钮,点击后会执行相应的导入/导出函数:
.. code-block:: js
function on_view_form_created(item) {
var csv_import_btn = item.add_view_button('Import csv file'),
csv_export_btn = item.add_view_button('Export csv file');
csv_import_btn.click(function() { csv_import(item) });
csv_export_btn.click(function() { csv_export(item) });
}
function csv_export(item) {
item.server('export_scv', function(file_name, error) {
if (error) {
item.alert_error(error);
}
else {
var url = [location.protocol, '//', location.host, location.pathname].join('');
url += 'static/files/' + file_name;
window.open(encodeURI(url));
}
});
}
function csv_import(item) {
task.upload('static/files', {accept: '.csv', callback: function(file_name) {
item.server('import_scv', [file_name], function(error) {
if (error) {
item.warning(error);
}
item.refresh_page(true);
});
}});
}
这些函数会调用服务器模块中定义的相应函数。在该模块中我们使用了 Python 的 csv 模块。
导出时不会包含系统字段——主键字段和删除标志字段。
下面是用 Pyton 3 实现的代码:
.. code-block:: py
import os
import csv
def export_scv(item):
copy = item.copy()
copy.open()
file_name = item.item_name + '.csv'
path = os.path.join(item.task.work_dir, 'static', 'files', file_name)
with open(path, 'w', encoding='utf-8') as csvfile:
fieldnames = []
for field in copy.fields:
if not field.system_field():
fieldnames.append(field.field_name)
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for c in copy:
dic = {}
for field in copy.fields:
if not field.system_field():
dic[field.field_name] = field.text
writer.writerow(dic)
return file_name
def import_scv(item, file_name):
copy = item.copy()
path = os.path.join(item.task.work_dir, 'static', 'files', file_name)
with open(path, 'r', encoding='utf-8') as csvfile:
copy.open(open_empty=True)
reader = csv.DictReader(csvfile)
for row in reader:
print(row)
copy.append()
for field in copy.fields:
if not field.system_field():
field.text = row[field.field_name]
copy.post()
copy.apply()
用 Python 2 实现的代码如下:
.. code-block:: py
import os
import csv
def export_scv2(item):
copy = item.copy()
copy.open()
file_name = item.item_name + '.csv'
path = os.path.join(item.task.work_dir, 'static', 'files', file_name)
with open(path, 'wb') as csvfile:
fieldnames = []
for field in copy.fields:
if not field.system_field():
fieldnames.append(field.field_name.encode('utf8'))
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for c in copy:
dic = {}
for field in copy.fields:
if not field.system_field():
dic[field.field_name.encode('utf8')] = field.text.encode('utf8')
writer.writerow(dic)
return file_name
def import_scv2(item, file_name):
copy = item.copy()
path = os.path.join(item.task.work_dir, 'static', 'files', file_name)
with open(path, 'rb') as csvfile:
item.task.execute('delete from %s' % item.table_name)
copy.open(open_empty=True)
reader = csv.DictReader(csvfile)
for row in reader:
print(row)
copy.append()
for field in copy.fields:
if not field.system_field():
field.text = row[field.field_name.encode('utf8')].decode('utf8')
copy.post()
copy.apply()
==================================================
如何编写具有全局作用域的函数 ?
==================================================
在实体元素的服务端或客户端模块中定义的每个函数,都会成为该元素的一个属性。
因此,借助 :doc:`任务树 ` ,
你可以在项目的任意模块中,访问在客户端或服务端模块内声明的任意函数。
例如,若我们在 **客户(Customers)** 的客户端模块中声明了一个函数 ``some_func`` ,
就可以在项目的任意模块中调用它。需要注意的是, **task** 在客户端中是一个全局变量。
.. code-block:: js
task.customers.some_func()
在服务端中, **task** 不是全局变量,但触发/调用它的实体项会被传入至每个由
:doc:`server ` 方法调用的事件处理程序和函数中。
因此,如果 ``some_func`` 函数是在 Customers 的服务端模块中声明的,
在函数或事件处理程序中就可以按如下方式调用:
.. code-block:: js
def on_apply(item, delta, params):
item.task.customers.some_func()
需要注意,事件处理程序本质就是函数,同样也可以由其他模块调用。
==========================================
如何批量修改选中记录的字段值
==========================================
本例将演示如何将 “曲目(Tracks)” 主数据中选中记录的 “Media Type”(媒体类型)字段批量设置为相同的值。
.. image:: https://jampy-docs.readthedocs.io/projects/V7/zh-cn/latest/_images/set_media_type_jampy.png
:align: center
:alt: set_media_type_jampy.png
首先,需要将 :doc:`table_options ` 的 multiselect 属性设置为 true,这样在 “曲目(Tracks)” 表格的最左侧会显示复选框,用户可以选择多条记录。然后在 “曲目(Tracks)” 客户端模块的 :doc:`on_view_form_created ` 事件处理程序中,创建一个 **设置媒体类型(Set media type)** 按钮。
.. code-block:: js
function on_view_form_created(item) {
item.table_options.multiselect = true;
item.add_view_button('Set media type').click(function() {
set_media_type(item);
});
}
当点击该按钮时,会执行模块中定义的 ``set_media_type`` 函数。
在该函数中,我们创建了 “曲目(Tracks)” 实体项的一个副本。调用 :doc:`copy ` 方法时,传入的 handlers 选项值为 false,表示副本不会继承应用构建器中表单对话框的设置,以及该实体项客户端模块中定义的所有函数和事件。
接着分析 :doc:`selections ` 属性,该属性是一个数组,包含用户选中的记录的主键字段值。
然后,通过调用 :doc:`open ` 方法并传入 open_empty 选项,
初始化副本的数据集。我们还设置 fields 选项,使数据集只包含 media_type 字段,
并将该字段的 required 属性设为 true。
最后,在调用 :doc:`append_record ` 方法前,
动态分配 :doc:`on_edit_form_created ` 事件处理程序,
以修改 **确定(OK)** 按钮的点击事件(该事件原本定义在任务的客户端模块中)。
在新的点击事件处理程序中,首先调用 :doc:`post ` 方法,
检查媒体类型值是否已设置。如果抛出异常,则调用 :doc:`edit ` 方法,允许用户对其进行重新设置。
.. code-block:: js
function set_media_type(item) {
var copy = item.copy({handlers: false}),
selections = item.selections;
if (selections.length > 1000) {
item.alert('Too many records selected.');
}
else if (selections.length || item.rec_count) {
if (selections.length === 0) {
selections = [item.id.value];
}
copy.open({fields: ['media_type'], open_empty: true});
copy.edit_options.title = 'Set media type to ' + selections.length +
' record(s)';
copy.edit_options.history_button = false;
copy.media_type.required = true;
copy.on_edit_form_created = function(c) {
c.edit_form.find('#ok-btn').off('click.task').on('click', function() {
try {
c.post();
item.server('set_media_type', [c.media_type.value, selections],
function(res, error) {
if (error) {
item.alert_error(error);
}
if (res) {
item.selections = [];
item.refresh_page(true);
c.cancel_edit();
item.alert(selections.length + '
record(s) have been modified.');
}
}
);
}
finally {
c.edit();
}
});
};
copy.append_record();
}
}
当用户点击 **确定(OK)** 按钮时,实体项的 :doc:`server ` 方法会在服务器端执行 ``set_media_type`` 函数,从而批量修改选中记录的字段值。
服务器端修改完成后,在客户端会取消选中状态,刷新页面数据,调用 :doc:`cancel_edit ` 方法取消编辑状态,并提示用户操作的结果。
.. code-block:: py
def set_media_type(item, media_type, selections):
copy = item.copy()
copy.set_where(id__in=selections)
copy.open(fields=['id', 'media_type'])
for c in copy:
c.edit()
c.media_type.value = media_type
c.post()
c.apply()
return True
=============================
如何给表单添加按钮
=============================
为编辑/视图表单添加按钮的最简单方法是使用对应的
:doc:`add_edit_button ` /
:doc:`add_view_button ` 方法。
你可以在
:doc:`on_edit_form_created ` /
:doc:`on_view_form_created `
事件处理程序中调用这些添加按钮的方法。
例如,客户(Customers) 实体项使用这些代码,在客户端模块为一个视图表单添加了按钮:
.. code-block:: js
function on_view_form_created(item) {
item.table_options.multiselect = false;
if (!item.lookup_field) {
var print_btn = item.add_view_button('Print', {image: 'bi bi-printer'}),
email_btn = item.add_view_button('Send email', {image: 'bi bi-mailbox'});
email_btn.click(function() { send_email() });
print_btn.click(function() { print(item) });
item.table_options.multiselect = true;
}
}
在代码中,检查了实体的 :doc:`lookup_field ` 属性是否存在,然后(创建视图表单不是为了为查找字段选择值时)定义了两个按钮,
并为它们的 JQuery 点击事件分配了模块中的 ``send_email`` 和 ``print`` 函数。
================================================
如何在后台执行计算
================================================
你可以在任务的服务端模块中使用如下代码,在 Web 应用中每隔 3 分钟(可通过 interval 设置修改)启动一个后台线程执行一些计算:
.. code-block:: py
import threading
import time
import traceback
def background(task):
interval = 3 * 60
time.sleep(interval)
while True:
if not time:
return
with task.lock('background'):
try:
print('background')
# some code to execute in background for example:
# tracks = task.tracks.copy()
# tracks.open()
# for t in tracks:
# t.edit()
# t.sold.value = #some value
# t.post()
# tracks.apply()
except Exception as e:
traceback.print_exc()
time.sleep(interval)
def on_created(task):
bg = threading.Thread(target=background, args=(task,))
bg.daemon = True
bg.start()
.. note::
当多个 Web 应用以并行进程运行时,background 函数会在每个进程中执行。
为防止该函数被同时执行,我们使用了 task 的 lock 方法。
.. note::
Jam.py V7 引入了 *计算字段(calculated field)* 。现在可以在主/明细(Master/Detail)场景下,使用服务器端函数(SUM、COUNT、MIN、MAX、AVG)对其他表字段进行汇总。用户可以根据需要,将服务器端的计算代码替换为计算字段。
===================================================
如何更改表单中元素的样式和属性
===================================================
使用 jQuery ,能够访问表单上的任意 DOM元素。
在下面的示例中,在实体项的客户端模块的 ``on_edit_form_created`` 事件处理程序中,
我们找到 **确定(OK)** 按钮,将其隐藏,
并将编辑表单中 **取消(Cancel)** 按钮的文本更改为 “关闭(Close)” :
.. code-block:: js
function on_edit_form_created(item) {
item.edit_form.find("#ok-btn").hide();
item.edit_form.find("#cancel-btn").text('Close');
}
当应用程序创建输入控件(input)时,它会向每个输入控件(input)添加一个类(class),
该类的名称是相应字段的 :doc:`field_name ` 属性值。
因此,使用 jQuery `选择器 `_ ,
我们就能向下面那样找到 customer 字段对应的输入控件(input)
(我们使用 “customer” 样式类选择在编辑表单里的输入控件元素):
.. code-block:: js
item.edit_form.find("input.customer")
找到表单的元素后,就能使用 jQuery 方法对其更改。
由于字段的输入框是在 :doc:`on_edit_form_created `
事件触发后,由 :doc:`create_inputs `
方法自动创建的(可参考任务客户端模块中的 ``on_edit_form_created`` 事件处理器),
因此你必须编写 :doc:`on_edit_form_shown `
事件处理器来修改输入框样式和属性。
示例代码 ::
.. code-block:: js
function on_edit_form_shown(item) {
item.edit_form.find('input.name').css('color', 'red');
item.edit_form.find('input.name').css('font-size', '24px');
item.edit_form.find('input.tracks_sold').width(20);
item.edit_form.find('input.genre').parent().width('40%');
item.edit_form.find('input.composer').prop('type', 'password');
}
将像下面一样修改表单的输入框:
.. image:: https://jampy-docs.readthedocs.io/projects/V7/zh-cn/latest/_images/form_elements_style_jampy.png
:align: center
:alt: form_elements_style_jampy.png
请注意:若你需要修改带有前置按钮或后置按钮的输入框(日期、日期时间、查询字段的输入框)的宽度,请直接设置该输入框父元素的宽度:
.. code-block:: js
item.edit_form.find('input.album').parent().width('50%');
更改 DOM 元素样式的另一种方法是使用 CSS。在应用程序构建器中选择任务节点后,“project.css” 按钮将出现在右侧窗格中。单击它,打开位于项目文件夹中的 *project.css* 文件。
您可以使用在它里面输入定义项目中 DOM 元素样式的 CSS 。
项目中创建的每个实体项的表单都有 css 类,使开发人员能够识别定位表单。
每个表单都有一个类来标识它的类型:“view-form”、“edit-form”、“filter-form” 或 “param-form” 。
例如,以下代码将隐藏表单底部按钮中的图像:
.. code-block:: css
.view-form .form-footer .btn i {
display: none;
}
更多编辑表单的例子:
.. code-block:: css
.edit-form #ok-btn {
font-weight: bold;
background-color: lightblue;
}
.edit-form.invoices input.total {
color: red;
}
同样,每个表单有个样式类,其名称是对应实体项的 :doc:`item_name ` 属性值。
The following code will remove images in the buttons only in the **Invoices**
view form:
下面的代码将只隐藏 **发票(Invoices)** 表单中按钮的图像
.. code-block:: css
.view-form.invoices .form-footer .btn i {
display: none;
}
你可以更改表格的显示方式。由 :doc:`create_table ` 方法创建的表格,有一个 “dbtable” 样式和一个以实体项的 :doc:`item_name ` 属性值命名的样式。表格的每列也有一个样式,其名称是对应字段的 :doc:`field_name ` 属性值。
示例中,以下代码将以粗体显示 **Invoices** 表**Customer** 列中单元格的内容:
.. code-block:: css
.dbtable.invoices td.customer {
font-weight: bold;
}
另一种改变字段列显示方式的方法是编写 :doc:`on_field_get_html ` 事件处理程序。
例如:
.. code-block:: js
function on_field_get_html(field) {
if (field.field_name === 'total') {
if (field.value > 10) {
return '' + field.display_text + '';
}
}
}
===========================
如何创建自定义菜单
===========================
要创建一个自定义菜单,你必须在 *任务* 的客户端模块中为 *任务* 的 :doc:`create_menu ` 方法指定 custom_menu 选项的值。
====================================================
在表单之间导航并保留数据的方法
====================================================
本指南说明在创建新记录时,如何在表单之间导航并在表单间保持数据完整性。
该模式包括:
- 一个包含按钮以打开空表单(表单2)的视图。
- 表单2,带有用于返回到表单 1 的导航按钮。
- 表单1,作为接收来自表单 2 数据的目标表单。
1. 设置初始视图
========================================
向视图中添加一个 **New** 按钮,用于打开表单 2 :
.. code:: js
// ===== VIEW CONFIGURATION =====
function on_view_form_created(item) {
item.view_form.find('#new-btn')
.text('New')
.off('click.task')
.on('click', function() {
openForm2();
});
item.refresh_page(true);
}
function openForm2() {
task.f2.open({open_empty: true});
task.f2.append_record();
}
2. 配置表单 2(中间表单)
========================================
为表单 2 设置一个 **下一表单(Next Form)**(下一表单)按钮,用于导航回表单1:
.. code:: js
// ===== FORM 2 CONFIGURATION =====
function on_edit_form_created(item) {
item.edit_form.find('#ok-btn')
.text('Next Form')
.off('click.task')
.on('click', function() {
item.close_edit_form();
setTimeout(function() {
openForm1(item);
}, 300);
});
}
function openForm1(item) {
task.f1.open({open_empty: true});
task.f1.append_record();
}
function on_edit_form_close_query(item) {
return true;
}
3. 配置表单1(目标表单)
========================================
将表单 2 的数据映射到表单 1 字段:
.. code:: js
// ===== FORM 1 CONFIGURATION =====
function on_edit_form_created(item) {
var title = 'First Form value: ';
if (item.is_new()) {
// 将数据从表单2传到表单1
item.f1t1.value = task.f2.f2t1.value;
if (item.f1t1.value) {
title += item.f1t1.value + ' value typed';
}
item.edit_options.title = title;
} else {
title = item.f1t1.value;
item.edit_options.title = title;
}
}
需要记住的关键点
========================================
``open_empty:`` true:确保表单以无预加载数据的状态打开
``append_record():`` 向表单添加一个新的空记录
``setTimeout():`` 在打开下一个表单之前给当前表单适当的关闭时间
``on_edit_form_close_query:`` 返回 true 可绕过未保存更改的警告
字段映射参考
========================================
+--------------------+-----------------+-----------------------------------------------------------+
| 来源 | 目标 | 说明 |
+====================+=================+===========================================================+
| task.f2.f2t1.value | item.f1t1.value | 将表单 2 字段 f2t1 的数据传到表单 1 字段 f1t1 |
+--------------------+-----------------+-----------------------------------------------------------+
另见
========
:doc:`on_edit_form_close_query `
:doc:`表单窗体 `
:doc:`create_edit_form `
:doc:`edit_form `
=====================================================
如何使用其他数据库中的数据
=====================================================
你可以使用其他数据库中的数据。
首先,你需要指定表名和字段信息。可以按照以下方式操作:
* 在任务树中选择 “项目(Project)” 节点,点击页面右侧的 **数据库(Database)** 按钮。
* 设置数据库为手动模式,并填写数据库连接的属性。
* 按照 :doc:`集成已有数据库 ` 的说明导入表信息。
* 在任务树中再次选择 “项目(Project)” 节点,点击 **数据库(Database)** 按钮,恢复之前的设置。
然后,在新数据项的 ``服务端模块(Server module)`` 中添加代码,实现对数据库中表的数据读写。
下面是 MySQL 数据库(自增主键字段)的代码示例:
.. code-block:: py
import MySQLdb
from jam.db import mysql
def on_open(item, params):
connection = item.task.create_connection_ex(mysql, database='demo', \
user='root', password='111', host='localhost', encoding='UTF8')
try:
sql = item.get_select_query(params, mysql)
rows = item.task.select(sql, connection, mysql)
finally:
connection.close()
return rows, ''
def on_apply(item, delta, params):
connection = item.task.create_connection_ex(mysql, database='demo', \
user='root', password='111', host='localhost', encoding='UTF8')
try:
sql = delta.apply_sql(params, mysql)
result = item.task.execute(sql, None, connection, mysql)
finally:
connection.close()
return result
如果数据库使用生成器(generators)获取主键值(如 Firebird),则必须为新记录指定主键:
.. code-block:: py
import fdb
from jam.db import firebird
def on_open(item, params):
connection = item.task.create_connection_ex(firebird, database='demo.fdb', \
user='SYSDBA', password='masterkey', encoding='UTF8')
try:
sql = item.get_select_query(params, firebird)
rows = item.task.select(sql, connection, firebird)
finally:
connection.close()
return rows, ''
def get_id(table_name, connection):
cursor = connection.cursor()
cursor.execute('SELECT NEXT VALUE FOR "%s" FROM RDB$DATABASE' % (table_name + '_SEQ'))
r = cursor.fetchall()
return r[0][0]
def on_apply(item, delta, params):
connection = item.task.create_connection_ex(firebird, database='demo.fdb', \
user='SYSDBA', password='masterkey', encoding='UTF8')
for d in delta:
if not d.id.value:
d.edit()
d.id.value = get_id(item.table_name, connection)
for detail in d.details:
for r in detail:
if not r.id.value:
r.edit()
r.id.value = get_id(r.table_name, connection)
r.post()
d.post()
try:
sql = delta.apply_sql(params, firebird)
result = item.task.execute(sql, None, connection, firebird)
finally:
connection.close()
return result
你也可以在任务的 ``on_open`` 和 ``on_apply`` 事件中处理。下面是任务客户端模块的代码示例:
.. code-block:: py
import MySQLdb
from jam.db import mysql
def on_open(item, params):
if item.item_name in ['table1', 'table2']: # or
#if item.table_name in ['table1', 'table2']:
connection = item.task.create_connection_ex(mysql, database='demo', \
user='root', password='111', host='localhost', encoding='UTF8')
try:
sql = item.get_select_query(params, mysql)
rows = item.task.select(sql, connection, mysql)
finally:
connection.close()
return rows, ''
def on_apply(item, delta, params):
if item.item_name in ['table1', 'table2']:
connection = item.task.create_connection_ex(mysql, database='demo', \
user='root', password='111', host='localhost', encoding='UTF8')
try:
sql = delta.apply_sql(params, mysql)
result = item.task.execute(sql, None, connection, mysql)
finally:
connection.close()
return result
MSSQL 示例:
.. code-block:: py
import pymssql
from jam.db import mssql
def on_open(item, params):
connection = item.task.create_connection_ex(mssql, database='jam7', \
user='sa', password='password', server='127.0.0.1', port='1433')
try:
sql = item.get_select_query(params, mssql)
rows = item.task.select(sql, connection, mssql)
finally:
connection.close()
return rows, ''
def on_apply(item, delta, params):
connection = item.task.create_connection_ex(mssql, database='jam7', \
user='sa', password='password', server='127.0.0.1', port='1433')
try:
sql = delta.apply_sql(params, mssql)
result = item.task.execute(sql, None, connection, mssql)
finally:
connection.close()
return result
.. note::
不要为这些表设置 History 属性为 True,否则会报错。历史表在项目中所有数据库只能有一个。
你可以尝试在其他数据库中创建历史表,并为其编写 ``on_open`` 和 ``on_apply`` 事件处理程序。
.. note::
上述方法未在 Jam.py V7 下测试。
更简单的方案是使用数据库同义词(synonyms)。
===============================================
支持在明细表中嵌套明细表吗?
===============================================
支持,你可以在明细表中再嵌套明细表。
假设我们有三个对象 —— “投票(Polls)”、“问题(Questions)”端和 “答案(Answers)”端。
“答案(Answers)” 是 “问题(Questions)” 的明细表,我们将 “问题(Questions)” 作为 “投票(Polls)” 的明细表。
一种实现方式是在 “Questions” 表中添加一个整型字段 “poll” ,
并在 “投票(Polls)” 的客户端模块中添加如下代码:
.. code-block:: js
function on_edit_form_created(item) {
item.edit_options.form_header = false;
var q = task.questions.copy();
q.set_where({pool: item.id.value});
q.view(item.edit_form.find('.edit-detail'));
q.view_options.form_header = false;
q.on_view_form_created = function(quest) {
quest.paginate = false;
quest.view_options.form_header = false;
};
q.on_before_append = function(quest) {
if (!item.id.value) {
quest.alert_error('Poll is not specified.');
quest.abort();
}
};
q.on_before_post = function(quest) {
q.pool.value = item.id.value;
};
}
function on_field_changed(field, lookup_item) {
var item = field.owner;
item.apply();
item.edit();
}
function on_before_delete(item) {
var q = task.questions.copy();
q.set_where({id: item.id.value});
q.open();
while (!q.eof()) {
q.delete();
}
q.apply();
}
.. image:: https://jampy-docs.readthedocs.io/projects/V7/zh-cn/latest/_images/details_jampy.png
:align: center
:alt: details_jampy.png
======================================
如何从客户端执行 Python 代码
======================================
虽然在操作系统层面通过 ``Popen`` 命令执行任意 Python 脚本是可行的,
但我们首先演示 **服务端模块 [F8]** 代码的使用方法。
你可以调用 :doc:`server ` 方法向服务端发送请求,
以执行在实体项的服务端模块中定义的函数。
在下方示例中,我们创建了一个 jQuery 对象类型的 ``btn`` 按钮,随后通过其点击事件绑定一个函数,
该函数会调用实体项的 :doc:`server ` 方法,
运行在实体项的服务端模块中定义的 ``calculate`` 函数。
客户端模块中的代码如下:
.. code-block:: js
function on_view_form_created(item) {
var btn = item.add_view_button('Calculate', {type: 'primary'});
btn.click(function() {
item.server('calculate', [1, 2, 3], function(result, error) {
if (error) {
item.alert_error(error);
}
else {
console.log(result);
}
})
});
}
在服务端中的代码如下:
.. code-block:: py
def calculate(item, a, b, c):
return a + b + c
要执行操作系统脚本,我们可以使用带有 ``Popen`` 和类似于上面的按钮的服务器端模块代码:
.. code-block:: py
build = Popen([make, 'html'] , cwd=build_path, stderr=STDOUT,stdout = PIPE, shell=shell)
result, err = build.communicate()
result = result.decode("utf-8")
要查看构建结果,我们可以使用带有按钮的 JavaScript 模态表单来显示它:
.. code-block:: js
item.edit_form.find("#build-info-btn").hide().click(function() {
show_build_info(item);
});
function show_build_info(item) {
var i = 0,
color,
html = '
',
info = item.build_result.split('\n');
for (i = 0; i < info.length; i++) {
color = '#333333';
if (build_problems(item, info[i])) {
color = 'red';
}
html += '' + info[i] + ' ';
}
html += '