在Desk页面中使用Vue

使用 Vue 为你的 Desk 应用添加页面,可以让你兼得两者的便利:

  1. 你可以使用 Vue 的响应式和组合式特性
  2. 你可以使用 Frappe JS API

这一切无需手动配置 Vue 应用即可实现。你可以按照
以下步骤进行设置。

创建一个页面

导航到 Page DocType(位于 /app/page),然后创建一个新的 Page 条目。
系统会在你选择的模块中为你的页面创建新文件。

检查创建的文件

在你应用目录下的模块文件夹中,你会找到一个新文件夹,里面包含三个
文件:

└── app
    └── app
        └── module
            └── page
                └── test_vue
                    ├── __init__.py
                    ├── test_vue.json
                    └── test_vue.js

其中,在新创建的 JavaScript 文件中,你会看到 make_app_page 被调用:

frappe.pages['test-vue'].on_page_load = function (wrapper) {
  var page = frappe.ui.make_app_page({
    parent: wrapper,
    title: 'None',
    single_column: true,
  });
};

这用于创建一个新页面。更多信息请参阅 Page API。

当你访问 /app/test-vue 时,这个文件会被加载,你可以访问该
路径,届时会显示一个标题为“None”的空页面。

创建 Vue 文件

要使用 Vue,你的 .vue 源文件需要被转译和打包。Frappe
框架开箱即用,已内置此功能。

我们现在将创建两个文件:一个 .vue 文件,以及一个用于加载你的 Vue
代码的 .js 文件。你可以将这些文件添加到应用的 public/js 文件夹下:

└── app
    └── app
        └── public
            └── test_vue
                ├── TestVue.vue
                └── test_vue.bundle.js

首先,我们将创建 .vue 文件 TestVue.vue。这是一些用于测试设置的简单 Vue 代码:

<script setup>
import { ref } from 'vue';

const message = ref('Hello, World');
</script>
<template>

<h1>{{ message }}</h1>
</template>

其次,创建用于加载 Vue 代码的 .js 文件:

import { createApp } from 'vue';
import TestVue from './TestVue.vue';

// A simple function to mount your Vue app
function setup_vue(wrapper) {
  const app = createApp(TestVue);
  app.mount(wrapper.get(0));
  return app;
}

// We'll call this function from the generated test_vue.js file
frappe.ui.setup_vue = setup_vue;
export default setup_vue;

引入打包文件

在生成的 test_vue.js 文件中,添加以下代码以加载 Vue 应用:

frappe.pages['test-vue'].on_page_load = function (wrapper) {
  const page = frappe.ui.make_app_page({
    parent: wrapper,
    title: 'Test Vue',
    single_column: true,
  });

  // hot reload when in developer mode
  if (frappe.boot.developer_mode) {
    frappe.hot_update ??= frappe.hot_update;
    frappe.hot_update.push(() => load_vue(wrapper));
  }
};
frappe.pages['test-vue'].on_page_show = (wrapper) => load_vue(wrapper);

// Simple callback function to load Vue in the page
async function load_vue(wrapper) {
  const $parent = $(wrapper).find('.layout-main-section');
  $parent.empty();

  // Require the bundle and mount the Vue app
  await frappe.require('test_vue.bundle.js');
  frappe.test_vue_app = frappe.ui.setup_vue($parent);
}

这会调用 load_vue 函数,该函数会 requires 打包文件,并在页面加载或显示时挂载
Vue 应用。

构建或运行开发服务器

要构建你的代码,你可以调用:

bench build

注意:这会为你 bench 上的所有应用构建前端代码。

要运行开发服务器,你可以导航到 frappe 应用(即 bench/apps/frappe)的文件夹并运行:

npm run build -- --apps app --watch

这会监听你应用中的任何更改并重新构建代码,这应该会
在你的浏览器中触发热重载。

如果你一切操作正确,你应该会看到你的页面显示 Vue 应用的消息:

制作图表

Frappe Charts 可以让你为单个或多个离散数据集绘制简单的折线图、柱状图或百分比图。你还可以设置特殊的检查点值和汇总统计数据。想了解更多,请查看文档 https://frappe.github.io/charts/。

链接字段格式化器

如果某个实体同时维护了编码和名称(例如员工可能有员工编码和员工姓名),而我们希望在链接字段中同时显示 ID 和名称,就可以创建一个格式化器。

示例:

frappe.form.link_formatters['Employee'] = function(value, doc) {
    if(doc.employee_name && doc.employee_name !== value) {
        return value + ': ' + doc.employee_name;
    } else {
        return value;
    }
}

注意事项:

  1. 主键(name)和描述性名称(例如 employee_name)都必须存在于文档中。描述性名称字段可以隐藏
  2. 这需要在文档加载之前加载,并且可以复用于所有表单。你也可以将它添加到 build.json 中

脚本 API

在 Frappe 框架的服务器脚本、打印格式和脚本报表中可以调用的受限命令列表

注意: 这仅适用于应用内脚本。如果您需要更多功能,您必须创建一个“应用程序”并在 Python 模块中编写事件处理程序

Python 模块

以下 Python 模块可用

json

Python 标准模块 json

格式化

_(翻译)

翻译一个字符串

示例:_("This is translatable")

frappe.format

根据数据类型格式化一个值

示例:frappe.format_value(value, dict(fieldtype='Currency'))

frappe.date_format

格式化为默认日期格式

frappe.format_date

返回日期如“2019年9月1日”

会话

frappe.form_dict

表单/请求参数

示例:请求参数 /page?name="test" 可以通过 frappe.form_dict.name 访问

frappe.request

请求对象

frappe.response

响应对象

frappe.session.user

当前用户

frappe.session.csrf_token

当前会话的 CSRF 令牌

frappe.user

当前用户

frappe.get_fullname

返回当前用户的全名

frappe.get_gravatar

从 frappe.utils.get_gravatar_url 获取用户显示图像

frappe.full_name

当前用户的全名

文档(ORM)

文档访问和编辑

frappe.get_meta

获取元数据对象

frappe.new_doc

创建一个新的文档记录。

frappe.get_doc

获取文档。您还可以保存或执行文档公开的任何方法。

示例:frappe.get_doc("User", frappe.session.user)

frappe.get_last_doc

获取特定 DocType 的最后一条文档,可带有一组给定的过滤器。默认为最后创建的。

示例:frappe.get_last_doc("User", filters={"name": ("like", "%@apple.com")})

frappe.get_cached_doc

获取文档(或缓存的)

frappe.get_mapped_doc

frappe.rename_doc

frappe.delete_doc

frappe.get_system_settings

获取系统默认设置

数据库

数据库访问 API

frappe.db.get_list

获取由当前用户权限过滤的记录列表

示例:frappe.db.get_list("Customer") 将返回客户列表

frappe.db.get_all

获取所有记录的列表

frappe.db.sql

运行一个 SELECT 查询

示例:frappe.db.sql("select name from Customer where name like 'm%'")

frappe.db.get_value

从记录中获取一个值

示例:frappe.db.get_value("User", frappe.session.user, "first_name")

frappe.db.get_single_value

从单类型文档中获取值

示例:frappe.db.get_single_value("System Settings", "default_currency")

frappe.db.get_default

获取默认值

frappe.db.escape

清理数据库查询的值以防止 SQL 注入

frappe.db.set_value

设置一个值

frappe.db.exists

检查文档是否存在。如果存在则返回文档的 name,否则返回 None。

frappe.db.commit

允许用户在服务器脚本(如自定义调度器脚本)中进行显式提交。
注意:这在任何 DocType 事件服务器脚本中都不起作用

frappe.db.rollback

允许用户通过服务器脚本回滚更改。
注意:这在任何 DocType 事件服务器脚本中都不起作用

查询构建器

frappe.qb

用于运行 SELECT 查询的查询构建器 API。

示例:frappe.qb.from_("Task").select("*").run()

实用工具

实用方法和函数

run_script

运行一个服务器脚本(返回值在 frappe.flags 中)

frappe.msgprint

在服务器端作为响应的一部分显示一个模态框。

示例:frappe.msgprint("Hello")

frappe.get_hooks

获取应用程序钩子

frappe.utils

frappe.utils 中的方法

frappe.render_template

渲染一个 Jinja 模板

frappe.get_url

通过 frappe.utils.get_url 获取当前站点的 URL

socketio_port

socketio 的端口

style.border_color

返回 ‘#d1d8dd’

guess_mimetype

返回 mimetypes.guess_type

html2text

将 HTML 编码为文本(markdown)

dev_server

如果处于开发者模式则为 True

frappe.log_error

生成带有回溯的错误日志

FrappeClient

使用 requests 会话连接到 Frappe 站点

API

从 Frappe 发起外部 API 调用。

frappe.make_get_request

发起一个 GET 请求。

示例:frappe.make_get_request('https://example.com')

frappe.make_post_request

发起一个 POST 请求。

示例:frappe.make_post_request('https://example.com', data={'username: 'test'})

frappe.make_put_request

发起一个 PUT 请求。

示例:frappe.make_put_request('https://example.com', headers={'Auth': 'Bearer xyz'})

邮件

frappe.sendmail

发送一封邮件。请参阅文档

示例:frappe.sendmail(recipients=['[email protected]'], sender='[email protected]', subject='My Subject', message='<p>Hello</p>')

服务器脚本

注意:从版本 15 开始,服务器脚本默认被禁用,以提高共享 bench 的安全性。

你可以使用以下命令在你的 bench 上启用它们。

bench set-config -g server_script_enabled 1

如果你托管在 Frappe Cloud 上,你需要创建一个私有 bench 才能启用服务器脚本。公共共享 bench 不允许使用服务器脚本。

服务器脚本让你可以动态定义一个 Python 脚本,在文档事件或 API 触发时在服务器上执行。

如何创建服务器脚本


要创建服务器脚本

  1. 如果你的站点托管在 frappe cloud 上,这里有一篇帮助文章说明了启用服务器脚本的步骤。如果是自托管账户,请在你的站点的 site_config.json 中将 server_script_enabled 设置为 true。
  2. 要添加/编辑服务器脚本,请确保你的角色是系统管理员。
  3. 在 awesomebar 中输入“新建服务器脚本”并按回车,创建一个新的服务器脚本文档。
  4. 设置服务器脚本的类型(文档事件 / API)。
  5. 设置文档类型和事件名称,或方法名称、脚本并保存。
  6. 功能

2.1 启用服务器脚本

服务器脚本必须通过 site_config.json 启用

bench --site site1.local set-config server_script_enabled true

2.2 文档事件

对于要通过文档事件调用的脚本,你必须设置参考文档类型和事件名称来定义触发器

  • 插入前
  • 插入后
  • 验证前
  • 保存前
  • 保存后
  • 提交前
  • 提交后
  • 取消前
  • 取消后
  • 删除前
  • 删除后
  • 保存前(已提交文档)
  • 保存后(已提交文档)
  • 打印前

2.3 API 脚本

通过使用脚本类型 "API",可以即时创建 API 端点。端点的名称取决于API 方法字段。所有使用服务器脚本创建的 API 都会自动加上 /api/method 前缀。

例如,一个API 方法为 "delete-note" 的脚本可以通过 /api/method/delete-note 访问。使用 Frappe 的前端请求库,你可以在客户端脚本中使用 frappe.call("delete-note")。

可以通过为创建的 API 勾选允许访客来启用访客访问。响应可以通过 frappe.response["message"] 对象设置。

API 服务器脚本还支持基于 IP 的速率限制,你可以通过勾选“启用速率限制”并指定在给定时间窗口内可以进行多少次调用来启用它。

2.3 安全性

Frappe 框架使用 RestrictedPython 库来限制对服务器脚本可用方法的访问。只有下面列出的安全方法在服务器脚本中可用。

有关允许的方法,请参阅脚本 API

2.4 将服务器脚本用作库

你可以通过在脚本中设置 frappe.flags 值,将服务器脚本用作内部方法。

2.5 比较更改

你可以使用“比较版本”按钮来对比服务器脚本的两个版本。

示例


3.1 在更改前更改属性的值

脚本类型:保存前

if "test" in doc.description:
    doc.status = 'Closed'

3.2 自定义验证

脚本类型:“保存前”

if "validate" in doc.description:
    raise frappe.ValidationError

3.3. 自动创建待办事项

脚本类型:“保存后”

if doc.allocted_to:
    frappe.get_doc(dict(
        doctype = 'ToDo',
        owner = doc.allocated_to,
        description = doc.subject
    )).insert()

3.4 API

  • 脚本类型:API
  • 方法名称:test_method
frappe.response['message'] = "hello"

请求:/api/method/test_method

3.5 内部库

版本 13 中的新功能

从一个 script_1 脚本调用另一个 script_2

脚本 1:

frappe.flags.my_key = 'my value'

脚本 2:

my_key = run_script('script_1').get('my_key')

系统控制台

系统控制台可帮助您基于脚本 API 运行 Python 命令进行调试
。仅允许具有系统管理员角色的用户使用。

要访问系统控制台,请在搜索栏中搜索它。

API

脚本 API
公开的方法可以通过系统控制台访问

记录响应

要打印响应,请使用 log 方法。

客户端脚本

客户端脚本让你可以动态定义一个自定义表单脚本,该脚本会在用户的浏览器上执行。如果你选择使用非标准工具或库,请务必在不同浏览器上进行测试,以确保在你的用户群体中具备兼容性。

在版本 13 中,自定义脚本已重命名为客户端脚本

  1. 如何创建客户端脚本

创建客户端脚本的方法

  1. 要添加/编辑客户端脚本,请确保你的角色是系统管理员。

  2. 在搜索栏中输入“新建客户端脚本”并按回车,创建一个新的客户端脚本文档。

  3. 选择你想要自定义表单的文档类型。

  4. 使用预设模板更新脚本并保存。

  5. 前往你已自定义的文档类型,查看更改效果。

  6. 功能特性


与服务器脚本的诸多限制相比,客户端脚本中一切皆可自由使用。这是因为前端 API 在设计上就是安全的。你可以使用所有 JavaScript API,以及 Frappe 的 API 和你可能已用于自定义 Desk 的任何其他 JS 或 CSS 库。

你通过客户端脚本添加的验证只会在通过浏览器访问的标准表单视图中生效。如果你希望这些验证也能通过 API 或系统控制台访问时生效,请使用服务器脚本。

  1. 示例

3.1 自定义验证

在通过 Frappe 标准表单视图创建或更新文档时,添加额外的表单验证。

// additional validation on Task dates
frappe.ui.form.on('Task', 'validate', function(frm) {
    if (frm.doc.from_date < get_today()) {
        msgprint('You can not select past date in From Date');
    }
});

3.2 在字段更新时获取值

在更改 customer 字段的值时获取 local_tax_no。

cur_frm.add_fetch('customer', 'local_tax_no', 'local_tax_no');

3.3 自定义字段属性

在创建文档后将字段 ibsn 设为只读。

// make a field read-only after saving
frappe.ui.form.on('Task',  {
    refresh: function(frm) {
        frm.set_df_property('ibsn', 'read_only', !frm.is_new());
    }
});

3.4 自定义字段属性

在保存时计算销售发票表单上的佣金。

// calculate sales incentive
frappe.ui.form.on('Sales Invoice',  {
    validate: function(frm) {
        // calculate incentives for each person on the deal
        total_incentive = 0
        $.each(frm.doc.sales_team,  function(i,  d) {
            // calculate incentive
            let incentive_percent = 2;
            if(frm.doc.base_grand_total > 400) incentive_percent = 4;
            // actual incentive
            d.incentives = flt(frm.doc.base_grand_total) * incentive_percent / 100;
            total_incentive += flt(d.incentives)
        });
        frm.doc.total_incentive = total_incentive;
    }
});

3.5 筛选邮件收件人

你可以通过客户端脚本限制可用的邮件地址。每个表单都可以指定哪些电子邮件地址应该可供选择。这可以减少用户的认知负担,并避免将邮件发送给错误的收件人。

CommunicationComposer(邮件对话框)会检查是否存在为“收件人”、“抄送”和“密送”字段提供筛选条件的客户端脚本。例如,如果某个报价单只应显示与该报价单收件人关联的电子邮件地址,我们可以添加以下脚本:

frappe.ui.form.on("Quotation", {
    get_email_recipient_filters: function (frm, field) {
        // field can be "recipients", "cc" or "bcc"
        if (field === "recipients") {
            return [
                [
                    "Dynamic Link",
                    "link_doctype",
                    "=",
                    frm.doc.quotation_to
                ],
                [
                    "Dynamic Link",
                    "link_name",
                    "=",
                    frm.doc.party_name
                ],
            ];
        }
    },
    // ...
};

这些筛选条件引用的是联系人文档类型及其链接子表。

即使有筛选条件,仍然可以粘贴任意收件人。

3.6 设置默认邮件收件人

在诸如销售订单 -> 交货单 -> 销售发票这样的标准流程中,邮件收件人通常是预先定义好的。例如,这可以由客户经理提前完成。

你可以通过客户端脚本设置默认邮件收件人。每个表单都可以通过编程方式提供自己的默认收件人。

CommunicationComposer(邮件对话框)会检查是否存在提供默认收件人的客户端脚本。例如,如果你的报价单文档类型包含一个名为密送("custom_bcc")的字段,你可以添加以下客户端脚本,将其用作邮件对话框中的默认密送地址:

frappe.ui.form.on("Quotation", {
    get_email_recipients: function (frm, field) {
        // field can be "recipients", "cc" or "bcc"
        if (field === "bcc") {
            return [frm.doc.custom_bcc]
        }
    },
    // ...
};

Frappe API 的界面测试

你可以编写集成测试,也可以直接使用 QUnit 在 JavaScript 中编写测试

QUnit 帮助你使用 QUnit 框架和原生 frappe API 编写 UI 测试。你可能已经猜到了,这是一种更快的测试编写方式。

测试运行器

要编写基于 QUnit 的测试,请将测试添加到应用程序的 tests/ui 文件夹中。测试文件必须以 test_ 开头,并以 .js 扩展名结尾。

要运行你的文件,你可以使用测试运行器。测试运行器提供了一个用户界面,用于加载所有 QUnit 测试并在浏览器中运行它们。

在 CI 中,所有 QUnit 测试都由测试运行器使用 frappe/tests/test_test_runner.py 运行

运行测试

要运行基于测试运行器的测试,请使用 run-ui-tests bench 命令,并传入你想运行的文件名。

bench run-ui-tests –test frappe/tests/ui/test_list.js

这会将文件名传递给 test_test_runner.py,它会在浏览器中加载所需的 JS 并执行测试

调试测试

要调试测试,你可以从 UI 中的测试运行器打开它,并手动运行以查看它究竟在哪里失败。

测试顺序

在 Frappe 中,UI 测试按固定顺序运行以确保依赖关系。

测试运行的顺序将在 tests/ui/tests.txt
文件中。

运行所有 UI 测试

要一起运行应用程序的所有 UI 测试,请运行

bench run-ui-tests –app [app_name]

这将逐一运行 tests/ui 文件夹中的所有文件。

QUnit 测试示例

以下是 QUnit 中 To Do 测试的示例

QUnit.test(“Test quick entry”, function(assert) {
assert.expect(2);
let done = assert.async();
let random_text = frappe.utils.get_random(10);

frappe.run_serially([
() => frappe.set_route(‘List’, ‘ToDo’),
() => frappe.new_doc(‘ToDo’),
() => frappe.quick_entry.dialog.set_value(‘description’, random_text),
() => frappe.quick_entry.insert(),
(doc) => {
assert.ok(doc && !doc.__islocal);
return frappe.set_route(‘Form’, ‘ToDo’, doc.name);
},
() => assert.ok(cur_frm.doc.description.includes(random_text)),

// Delete the created ToDo
() => frappe.tests.click_page_head_item(‘Menu’),
() => frappe.tests.click_dropdown_item(‘Delete’),
() => frappe.tests.click_page_head_item(‘Yes’),

() => done()
]);
});

使用 Promise 编写对测试友好的代码

Promise 是编写对测试友好的代码的好方法。如果你的方法调用了异步调用(ajax),那么你应该返回一个 Promise 对象。在编写测试时,如果你遇到一个不返回 Promise 对象的函数,你应该更新代码以返回一个 Promise 对象。

界面集成测试

你可以使用 Cypress 来编写集成测试。它是一个基于 NodeJS 的全栈测试框架,不依赖 Selenium。

要编写集成测试,请在 cypress/integration 目录中创建一个 .js 文件。

示例

以下是一个用于检查待办事项插入的集成测试示例

context('ToDo', () => {
 before(() => {
 cy.login('Administrator', 'admin');
 cy.visit('/desk');
 });

 it('creates a new todo', () => {
 cy.visit('/app/todo/new-todo-1');
 cy.fill_field('description', 'this is a test todo', 'Text Editor').blur();
 cy.get('.page-title').should('contain', 'Not Saved');
 cy.get('.primary-action').click();
 cy.visit('/desk#List/ToDo');
 cy.location('hash').should('eq', '/app/todo');
 cy.get('.list-row').should('contain', 'this is a test todo');
 });
});

在本地运行 Cypress

Cypress 使用你系统上安装的任何基于 Chromium 的浏览器来运行测试。每个应用都有自己的 Cypress 测试套件。
例如,要运行 frappe 应用的测试,请运行以下命令

cd ~/frappe-bench/apps/frappe
yarn cypress:open

这将打开 Cypress Electron 界面,你可以在其中手动运行任何测试,或运行所有测试。