语言解析

在这里,让我们来看看 Frappe 中语言是如何解析的,以及你如何在你的
Frappe 应用或脚本中使用它们。

你会话的语言取决于 frappe.lang 的值。它的解析顺序如下:

  1. 表单字典 > _lang
  2. Cookie > preferred_language [仅限访客用户]
  3. 请求头 > Accept-Language [仅限访客用户]
  4. 用户文档 > language
  5. 系统设置 > language

表单字典:_lang

表单字典的 _lang 参数具有最高优先级。设置此参数将
更新给定请求中的所有可翻译组件。Frappe 在某些地方使用这种机制
来处理邮件模板和打印视图。

虽然在每个请求中传递 ?_lang=ru 可能不太实际。如果你
想要持久但临时的语言设置,你可以在 cookies 中设置
preferred_language 键。Frappe 利用这一点来实现网站
语言切换器。此方法可用于根据客户端来持久化语言设置。

仅对访客用户生效。对已登录用户忽略。

请求头:Accept-Language

另一种相对更简洁、更标准的管理语言的方法是使用
Accept-Language 请求头。如果前两种方法都未设置,Frappe 会开始
解析此请求头的值,其中包含客户端可接受语言的有序集合。你可以查看 Mozilla 文档
以获取更多相关信息。

仅对访客用户生效。对已登录用户忽略。

用户与系统设置

用户文档中有一个 language 字段,用于设置该用户的会话语言。
此设置会跨设备、客户端持久保存。这允许特定
用户以他们选择的语言查看网站和 Desk。例如,
如果一个用户在“法语”网站上将其语言设置为“俄语”,当他们
登录时,网站会自动翻译成俄语。

系统设置中的 language 字段用于设置整个
网站的语言。它的优先级最低,是所有会话的备用语言。

请求生命周期

Web 应用的用户可以访问不同的 URL,例如 /about/posts/api/resources。每个请求都根据以下请求类型进行处理。

  1. /api 开头的 API 请求由 REST API 处理器处理。
  2. 文件下载,如备份文件 (/backups)、公共文件 (/files) 和私有文件 (/private/files),会单独处理,以响应可下载的文件。
  3. 网页请求,如 /about/posts,由网站路由器处理。本页将进一步说明。

详细了解 API 请求和静态文件。

请求预处理

在触发路由规则之前,会发生一些操作。这些操作包括预处理请求、初始化记录器和速率限制器。

路径解析器

一旦请求从 app.py 到达网站路由器,它就会通过路径解析器进行处理。

路径解析器执行以下操作:

重定向解析

路径解析器尝试为传入的请求路径解析任何可能的重定向。路径解析器从 website_redirects 钩子获取重定向规则,并从网站设置中获取路由重定向。

路由解析

如果传入的请求路径没有重定向,路径解析器会尝试解析路由,以根据 website_routing_rules 钩子的规则以及启用了 has_web_view 的 DocType 文档中设置的动态路由来获取最终端点。

渲染器选择

一旦获得最终端点,它会传递给所有可用的页面渲染器,以检查哪个页面渲染器可以渲染给定的路径。第一个对 can_render 请求返回 True 的页面渲染器将用于渲染该路径。

页面渲染器

页面渲染器负责为给定的端点渲染页面或响应页面。页面渲染器使用 Python 类实现。一个页面渲染器类需要有两个方法,即 can_renderrender

路径解析器调用 can_render 来检查某个渲染器实例是否可以渲染特定路径。
一旦某个渲染器从 can_render 返回 True,该渲染器类就负责渲染该路径。

页面渲染器类示例

from frappe.website.page_renderers.base_renderer import BaseRenderer

class PageRenderer(BaseRenderer):
 def can_render(self):
 return True

 def render(self):
 response_html = "Response"
 return self.build_response(response_html)

以下是处理所有通用类型网页的标准页面渲染器。

StaticPage

使用 StaticPage,您可以从站点上安装的任何应用的 www 文件夹中提供 PDF、图片等文件。任何是以下类型 htmlmdjsxmlcsstxtpy 的文件都被视为静态文件。
提供静态文件的首选方式是将它们添加到您的 frappe 应用的 public 文件夹中。这样,它们将由 NGINX 直接提供,利用压缩和缓存,同时减少延迟。

TemplatePage

TemplatePage 在所有应用中查找 www 文件夹,如果它是 HTML 或 Markdown 文件,则返回该文件;如果它是一个文件夹,则返回该文件夹中的 index.htmlindex.md 文件。

WebformPage

如果请求路径与任何可用的 Web 表单路由匹配,WebformPage 会尝试在 Web 表单列表中渲染 Web 表单。

DocumentPage

如果 DocType 的 /templates 文件夹中有可用的文档模板,DocumentPage 会尝试渲染该模板。模板文件名应与 DocType 名称相同。例如:如果您想为 User 文档类型添加文档模板,则 User DocType 的 templates 文件夹中应有 user.html。文件夹结构将类似于 doctype/user/templates/user.html

ListPage

如果 DocType 的 /templates 文件夹中有列表模板,ListPage 将渲染它。请查看 Blog Post 模板文件夹以获取实现参考。

PrintPage

PrintPage 渲染文档的打印视图。它使用标准打印格式,除非通过 default_print_format 为 DocType 设置了不同的打印格式。

NotFoundPage

NotFoundPage 渲染标准的未找到页面,并响应 404 状态码。

NotPermittedPage

NotPermittedPage 渲染标准的权限拒绝页面,并带有 403 状态码。

添加自定义页面渲染器

如果您有其他标准页面渲染器未处理的需求,可以通过 page_renderer [钩子] 添加自定义页面渲染器。

# in hooks.py of your custom app

page_renderer = "path.to.your.custom_page_renderer.CustomPage"

页面渲染器类需要有两个方法,即 can_renderrender

路径解析器调用 can_render 来检查某个渲染器实例是否可以渲染特定路径。
一旦某个渲染器从 can_render 返回 True,该渲染器类就负责渲染该路径。

注意: 自定义页面渲染器具有优先权,其 can_render 方法将在标准页面渲染器之前被调用。

示例:


from frappe.website.utils import build_response
from frappe.website.page_renderers.base_renderer import BaseRenderer

class CustomPage(BaseRenderer):
 def can_render(self):
 return True

 def render(self):
 response_html = "Custom Response"
 return self.build_response(response_html)

注意:您还可以扩展标准页面渲染器,以覆盖或使用某些标准功能。

金贾API

这些是 frappe 提供的白名单方法,可用于 Jinja
模板中。

frappe.format

frappe.format(value, df, doc)

将原始值(存储在数据库中)格式化为用户可读的格式。
例如,将 2019-09-08 转换为 08-09-2019

用法

{{ frappe.format('2019-09-08', {'fieldtype': 'Date'}) }}

09-08-2019

frappe.format_date

frappe.format_date(date_string)

将日期格式化为人类可读的长格式。

用法

{{ frappe.format_date('2019-09-08') }}

September 8, 2019

frappe.get_url

frappe.get_url()

返回站点 URL

用法

{{ frappe.get_url() }}

https://frappe.io

frappe.get_doc

frappe.get_doc(doctype, name)

根据名称返回文档。

用法


 {% set doc = frappe.get_doc('Task', 'TASK00002') %}
 {{ doc.title }} - {{ doc.status }}

 Buy Eggs - Open

frappe.get_all

frappe.get_all(doctype, filters, fields, order_by, start, page_length, pluck)

返回某个 DocType 的所有记录列表。如果未提供 name 参数,则仅返回文档的 fields

签名

frappe.get_all(doctype, filters, fields, order_by, start, page_length)

用法


 {% set tasks = frappe.get_all('Task', filters={'status': 'Open'}, fields=['title', 'due_date'], order_by='due_date asc') %}
 {% for task in tasks %}

### {{ task.title }}

Due Date: {{ frappe.format_date(task.due_date) }}

 {% endfor %}

### Redesign Website

Due Date: September 8, 2019

### Add meta tags on websites

Due Date: September 22, 2019

frappe.get_list

frappe.get_list(doctype, filters, fields, order_by, start, page_length)

frappe.get_all 类似,但会根据当前会话用户的权限过滤记录。

frappe.db.get_value

frappe.db.get_value(doctype, name, fieldname)

从文档中返回单个字段值(或值列表)。

用法



 {% set company_abbreviation = frappe.db.get_value('Company', 'TennisMart', 'abbr') %}
 {{ company_abbreviation }}

 {% set title, description = frappe.db.get_value('Task', 'TASK00002', ['title', 'description']) %}
 ### {{ title }}

{{ description }}

TM

frappe.db.get_single_value

frappe.db.get_single_value(doctype, fieldname)

从单个 DocType 返回字段值。

用法



 {% set timezone = frappe.db.get_single_value('System Settings', 'time_zone') %}
 {{ timezone }}

 Asia/Kolkata

frappe.get_system_settings

frappe.get_system_settings(fieldname)

从系统设置中返回字段值。

用法


 {% if frappe.get_system_settings('country') == 'India' %}
 Pay via Razorpay
 {% else %}
 Pay via PayPal
 {% endif %}

Pay via Razorpay

frappe.get_meta

frappe.get_meta(doctype)

返回文档类型的元数据。其中包含字段、标题字段、
图片字段等信息。

用法



 {% set meta = frappe.get_meta('Task') %}
 Task has {{ meta.fields | len }} fields.
 {% if meta.get_field('status') %}
 It also has a Status field.
 {% endif %}

 Task has 18 fields. It also has a Status field.

frappe.get_fullname

frappe.get_fullname(user_email)

返回传入用户邮箱的完整姓名。如果未传入用户,则假定为
当前登录用户。

用法


The fullname of [email protected] is {{ frappe.get_fullname('[email protected]') }}
The current logged in user is {{ frappe.get_fullname() }}

The fullname of [email protected] is Faris Ansari
The current logged in user is John Doe

frappe.render_template

frappe.render_template(template_name, context)

使用上下文渲染 Jinja 模板字符串或文件。

用法



 {{ frappe.render_template('templates/includes/footer/footer.html', {}) }}

{{ frappe.render_template('{{ foo }}', {'foo': 'bar'}) }}

bar

frappe._

frappe._(string)_(string)

用法


{{ _('This string should get translated') }}

इस तार का अनुवाद होना चाहिए

frappe.session.user

返回当前会话用户

frappe.session.csrf_token

返回当前会话的 CSRF 令牌

frappe.form_dict

如果模板在 Web 请求中求值,则 frappe.form_dict
查询参数的字典,否则为 None

frappe.lang

翻译函数当前使用的语言。两位小写字母代码。

数据库接口

版本 16 起,get_listget_all 有一些破坏性变更。

frappe.db.get_list

frappe.db.get_list(doctype, filters, or_filters, fields, order_by, group_by, start, page_length, run)

  • 也可用别名 frappe.get_list 调用

doctype 表中返回记录列表。这是 SELECT 查询的 ORM 封装。它还会为当前会话用户应用记录的用户权限。如果未提供 fields 关键字参数,则仅返回文档名称。默认情况下,此方法返回一个 dict 列表,但您可以通过提供 pluck 关键字参数来提取特定字段:

frappe.db.get_list('Employee')

# output
[{'name': 'HR-EMP-00008'},
 {'name': 'HR-EMP-00006'},
 {'name': 'HR-EMP-00010'},
 {'name': 'HR-EMP-00005'}
]

# with pluck
frappe.db.get_list('Employee', pluck='name')

# output
['HR-EMP-00008',
 'HR-EMP-00006',
 'HR-EMP-00010',
 'HR-EMP-00005'
]

组合过滤器和其他参数:

frappe.db.get_list('Task',
    filters={
        'status': 'Open'
    },
    fields=['subject', 'date'],
    order_by='date desc',
    start=10,
    page_length=20,
    as_list=True
)

# output
(('Update Branding and Design', '2019-09-04'),
('Missing Documentation', '2019-09-02'),
('Fundraiser for Foundation', '2019-09-03'))

# Tasks with date after 2019-09-08
frappe.db.get_list('Task', filters={
    'date': ['>', '2019-09-08']
})

# Tasks with date between 2020-04-01 and 2021-03-31 (both inclusive)
frappe.db.get_list('Task', filters=[[
    'date', 'between', ['2020-04-01', '2021-03-31']
]])

# Tasks with subject that contains "test"
frappe.db.get_list('Task', filters={
    'subject': ['like', '%test%']
})

# Count number of tasks grouped by status
frappe.db.get_list('Task',
    fields=['count(name) as count', 'status'],
    group_by='status'
)

# For version 16 +
frappe.db.get_list('Task',
    fields=[{'COUNT': 'name', 'as': 'count'}, 'status'],
    group_by='status'
)

# output
[{'count': 1, 'status': 'Working'},
 {'count': 2, 'status': 'Overdue'},
 {'count': 2, 'status': 'Open'},
 {'count': 1, 'status': 'Filed'},
 {'count': 20, 'status': 'Completed'},
 {'count': 1, 'status': 'Cancelled'}]

在版本 16 之前,设置 run=False 会返回 SQL 查询而不是执行它。从版本 16 开始,您将获得一个查询构建器对象。您可以像以前一样调用 .get_sql() 来获取 SQL 查询,否则您可以在运行之前按需修改查询(query.run())。

frappe.db.get_all

frappe.db.get_all(doctype, filters, or_filters, fields, order_by, group_by, start, page_length, run)

  • 也可用别名 frappe.get_all 调用

frappe.db.get_list 相同,但会获取所有记录而不应用权限。

frappe.db.get_value

frappe.db.get_value(doctype, name, fieldname)frappe.db.get_value(doctype, filters, fieldname)

  • 也可用别名 frappe.get_valuefrappe.db.get_values 调用

返回文档的字段值或值列表。

# single value
subject = frappe.db.get_value('Task', 'TASK00002', 'subject')

# multiple values
subject, description = frappe.db.get_value('Task', 'TASK00002', ['subject', 'description'])

# as dict
task_dict = frappe.db.get_value('Task', 'TASK00002', ['subject', 'description'], as_dict=1)
task_dict.subject
task_dict.description

# with filters, will return the first record that matches filters
subject, description = frappe.db.get_value('Task', {'status': 'Open'}, ['subject', 'description'])

frappe.db.getsinglevalue

frappe.db.get_single_value(doctype, fieldname)

从单个 DocType 返回字段值。

timezone = frappe.db.get_single_value('System Settings', 'timezone')

frappe.db.set_value

frappe.db.set_value(doctype, name, fieldname, value)

  • 也可用别名 frappe.db.update 调用

在数据库中设置字段值,不会调用 ORM 触发器,但会更新修改时间戳(除非指定不更新)。

# update a field value
frappe.db.set_value('Task', 'TASK00002', 'subject', 'New Subject')

# update multiple values
frappe.db.set_value('Task', 'TASK00002', {
    'subject': 'New Subject',
    'description': 'New Description'
})

# update without updating the `modified` timestamp
frappe.db.set_value('Task', 'TASK00002', 'subject', 'New Subject', update_modified=False)

此方法不会调用 validateon_update 等 ORM 触发器。使用此方法更新隐藏字段,或当您清楚自己在做什么时使用。

frappe.db.exists

frappe.db.exists(doctype, name)

如果文档记录存在,则返回 true。

传入 doctype 和 docname:

frappe.db.exists("User", "[email protected]", cache=True)

传入包含 "doctype" 键的过滤器字典:

frappe.db.exists({"doctype": "User", "full_name": "Jane Doe"})

传入 doctype 和过滤器字典:

frappe.db.exists("User", {"full_name": "Jane Doe"})

frappe.db.count

frappe.db.count(doctype, filters)

返回给定 doctypefilters 的记录数。

# total number of Task records
frappe.db.count('Task')

# total number of Open tasks
frappe.db.count('Task', {'status': 'Open'})

frappe.db.delete

frappe.db.delete(doctype, filters)

删除与 filters 匹配的 doctype 记录。这会执行 DML 命令,这意味着它可以回滚。如果未指定过滤器,则会删除该 doctype 的所有记录。

frappe.db.delete("Route History", {
    "modified": ("<=", last_record_to_keep[0].modified),
    "user": user
})

frappe.db.delete("Error Log")
frappe.db.delete("__Test Table")

您可以传入 doctype 名称或内部表名。按照惯例,Frappe 中的内部表以 __ 为前缀。API 遵循此约定。上述命令在表 tabError Log__Test Table 上运行无条件的 DELETE 查询。

frappe.db.truncate

frappe.db.truncate(doctype)

截断数据库中的表。这会执行 DDL 命令 TRUNCATE TABLE,在执行语句之前会触发提交。此操作无法回滚。您可能希望使用它来定期清理日志表。

frappe.db.truncate("Error Log")
frappe.db.truncate("__Test Table")

上述命令在表 tabError Log__Test Table 上运行 TRUNCATE 查询。

frappe.db.commit

frappe.db.commit()

提交当前事务。调用 SQL COMMIT

在大多数情况下,您无需手动提交。请参阅下面的 Frappe 数据库事务模型。

frappe.db.savepoint

frappe.db.savepoint(save_point)

创建一个命名保存点,您可以稍后回滚到该点。

frappe.db.rollback

frappe.db.rollback()

回滚当前事务。调用 SQL ROLLBACK

如果在类型为 POSTPUT 的 Web 请求期间抛出异常,Frappe 将自动运行 frappe.db.rollback()。如果您需要在事务中提前回滚,请使用此方法。

frappe.db.rollback(save_point="save_point_name")

回滚到特定保存点,而不是回滚整个事务。此回滚不会撤销对文件系统以及任何其他回滚监视器所做的更改。

frappe.db.sql

frappe.db.sql(query, values, as_dict)

执行任意 SQL 查询。这对于包含连接语句的复杂服务端报表、调整数据库以适应新功能等可能很有用。

示例:

values = {'company': 'Frappe Technologies Inc'}
data = frappe.db.sql("""
 SELECT
 acc.account_number
 gl.debit
 gl.credit
 FROM `tabGL Entry` gl
 LEFT JOIN `tabAccount` acc
 ON gl.account = acc.name
 WHERE gl.company = %(company)s
""", values=values, as_dict=0)

避免使用此方法,因为它会绕过验证和完整性检查。如果可能,最好使用 frappe.get_doc、frappe.db.get_list 等。

frappe.db.multisql

frappe.db.multisql({'mariadb': mariadb_query, 'postgres': postgres_query})

为任何受支持的数据库引擎执行合适的 SQL 语句。

frappe.db.rename_table

frappe.db.rename_table(old_name, new_name)

执行查询以更改表名。直接指定 DocType 或内部表名来重命名表。

示例:

frappe.db.rename_table("__internal_cache", "__temporary_cache")
frappe.db.rename_table("todo", "ToDo")

仅当您了解其影响时才应使用第二个示例。

不要使用此方法重命名 DocType 表。请改用 frappe.rename_doc 来实现。

frappe.db.describe

frappe.db.describe(doctype)

返回指定 DocType 的表结构描述元组。

frappe.db.changecolumntype

frappe.db.change_column_type(doctype, column, new_type)

更改指定 DocType 的列类型。

frappe.db.add_index

frappe.db.add_index(doctype, fields, index_name)

为指定字段的 DocType 创建索引。

注意:如果需要对 TEXT 或 BLOB 字段创建索引,必须指定固定长度。

示例:

frappe.db.add_index("Notes", ["id(10)", "content(500)"], index_name)

frappe.db.add_unique

frappe.db.add_unique(doctype, fields, constraint_name=None)

为指定字段的 DocType 创建唯一约束。

示例:

frappe.db.add_unique("DoctypeName",["field1","field2"])

frappe.db.bulk_update

frappe.db.bulk_update(doctype, doc_updates, *, chunk_size=100, modified=None, modified_by=None, update_modified=True, debug=False)

使用带有 CASE 表达式的单个 SQL UPDATE 语句,批量更新多个文档。

示例:

frappe.db.bulk_update(
    "Task",
    {
        "TASK-0001": {"status": "Closed", "description": "Completed by QA"},
        "TASK-0002": {"status": "Open", "description": "Pending assignment"},
    },
    chunk_size=200,
    modified_by="[email protected]",
    update_modified=True,
    debug=True,
)

doc_updates 格式:

{
    "DOC-0001": {"field1": "value1", "field2": "value2"},
    "DOC-0002": {"field1": "valueA", "field2": "valueB"},
}

注意事项:

  • 直接更新数据库;不会触发文档事件或验证。
  • 未指定的字段保持不变。

数据库事务钩子

注意:此 API 在 v15 版本中引入。

Frappe 提供了钩子,用于在发出提交(commit)或回滚(rollback)等事务命令之前或之后运行回调。这些钩子适用于以下场景:

  • 如果事务被回滚,则回滚数据库之外所做的更改。
  • 仅在事务提交后,才将数据库之外的更改刷新。

这些钩子包括:

  • frappe.db.before_commit.add(func: Callable)
  • frappe.db.after_commit.add(func: Callable)
  • frappe.db.before_rollback.add(func: Callable)
  • frappe.db.after_rollback.add(func: Callable)

用法示例:

def create_file(self):
    self.write_file()
    # This ensures rollback if DB transaction is rolledback
    frappe.db.after_rollback.add(self.rollback_file)

def rollback_file(self):
    self.delete_file()

数据库事务模型

Frappe 的数据库抽象层默认实现了一个合理的事务模型。因此,在大多数情况下,您无需手动处理 SQL 事务。该模型的简要描述如下:

Web 请求

  • 在执行 POSTPUT 时,如果对数据库进行了任何写入操作,这些操作将在请求成功结束时提交。
  • 使用 frappe.call 进行的 AJAX 调用默认是 POST,除非更改设置。
  • GET 请求不会触发隐式提交。
  • 请求处理期间发生的任何未捕获异常都将导致事务回滚。

后台/定时任务

  • 将函数作为后台或定时任务调用时,成功完成后将提交事务。
  • 任何未捕获异常都将导致事务回滚。

补丁(Patches)

  • 补丁的 execute 函数成功完成后,将自动提交事务。
  • 任何未捕获异常都将导致事务回滚。

单元测试

  • 运行一个测试模块后,事务将被提交。测试模块指的是任何 Python 测试文件,例如 test_core.py
  • 所有测试完成后,事务也会被提交。
  • 任何未捕获异常都将退出测试运行器,因此不会提交事务。

注意:如果您在任何地方捕获了异常,数据库抽象层将无法感知到错误的发生,因此您需要负责正确回滚事务。

文档接口

文档是 DocType 的一个实例。它派生自 frappe.model.Document 类,并代表数据库表中的一条记录。

frappe.get_doc

frappe.get_doc(doctype, name)

返回由 doctypename 标识的记录对应的文档对象。如果未找到文档,则会引发 DoesNotExistError 异常。如果 doctype 是单例 DocType,则不需要 name

# get an existing document
doc = frappe.get_doc('Task', 'TASK00002')
doc.title = 'Test'
doc.save()

# get a single doctype
doc = frappe.get_doc('System Settings')
doc.timezone # Asia/Kolkata

frappe.get_doc(dict)

返回一个内存中新的文档对象,该对象在数据库中尚不存在。

# create a new document
doc = frappe.get_doc({
    'doctype': 'Task',
    'title': 'New Task'
})
doc.insert()

frappe.get_doc(doctype={document_type}, key1 = value1, key2 = value2, ...)

返回一个内存中新的文档对象,该对象在数据库中尚不存在。

# create new object with keyword arguments
user = frappe.get_doc(doctype='User', email_id='[email protected]')
user.insert()

frappe.get_last_doc

frappe.get_last_doc(doctype, filters, order_by)

返回在指定 doctype 下创建的最后一个文档对象。

# get the last Task created
task = frappe.get_last_doc('Task')

您还可以指定过滤器来优化结果。例如,您可以通过添加过滤器来检索最后一个已取消的任务。

# get the last available Cancelled Task
task = frappe.get_last_doc('Task', filters={"status": "Cancelled"})

默认情况下,order_by 参数设置为 creation desc,但可以覆盖此值以使用其他可以达到相同目的的非标准字段。例如,您在 任务 DocType 下有一个字段 timestamp,它记录的是任务被批准或标记为有效的时间,而不是创建时间。

# get the last Task created based on a non-standard field
task = frappe.get_last_doc('Task', filters={"Status": "Cancelled"}, order_by="timestamp desc")

或者,您也可以完全反其道而行之,作为玩笑将其更改为“creation asc”来检索第一个文档。

frappe.get_cached_doc

类似于 frappe.get_doc,但会先查询缓存中的文档,然后再访问数据库。

frappe.new_doc

frappe.new_doc(doctype)

创建新文档的另一种方式。

# create a new document
doc = frappe.new_doc('Task')
doc.title = 'New Task 2'
doc.insert()

frappe.delete_doc

frappe.delete_doc(doctype, name)

从数据库中删除该记录及其子记录。同时也会删除与之关联的其他文档,如通信、评论等。

frappe.delete_doc('Task', 'TASK00002')

frappe.rename_doc

frappe.rename_doc(doctype, old_name, new_name, merge=False)

将文档的 name(主键)从 old_name 重命名为 new_name。如果 mergeTrue 且存在 new_name 的记录,则会将该记录与其合并。

frappe.rename_doc('Task', 'TASK00002', 'TASK00003')

只有在 DocType 表单中设置了 允许重命名 时,重命名操作才会生效。

frappe.get_meta

frappe.get_meta(doctype)

返回 doctype 的元信息。这也会应用自定义字段和属性设置器。

meta = frappe.get_meta('Task')
meta.has_field('status') # True
meta.get_custom_fields() # [field1, field2, ..]

要获取 DocType 的原始文档(不含自定义字段和属性设置器),请使用 frappe.get_doc('DocType', doctype_name)

frappe.only_for

frappe.only_for(roles, message=False)

如果当前用户没有任何允许的角色,则引发 frappe.PermissionError 异常。

如果当前用户是 Administrator,则跳过权限检查。

# restrict action to System Manager role
frappe.only_for("System Manager")

您也可以允许多个角色:

# allow multiple roles
frappe.only_for(["System Manager", "Accounts Manager"])

frappe.get_docs

frappe.get_docs(doctype, filters, *, chunk_size=1000, limit=None, limit_start=0, order_by="creation asc", as_iterator=False)

返回文档对象列表。使用 as_iterator=True 分块获取记录,以便更好地管理内存。

# Fetch specific documents with child tables
tasks = frappe.get_docs('Task', filters={'status': 'Open'}, limit=10)

for task in tasks:
    task.status = "Closed"
    task.save()

# Efficiently iterate through large datasets
leads = frappe.get_docs('Lead', as_iterator=True, chunk_size=500)

for lead in leads:
    lead.process_lead() # Custom controller method

文档方法

本节列出了 doc 对象上可用的常用方法。

doc.insert

此方法将新文档插入数据库表。它会检查用户权限,如果控制器中编写了 before_insertvalidateon_updateafter_insert 方法,则会执行这些方法。

它有一些“逃生舱”机制,可用于跳过下面说明的某些检查。

doc.insert(
    ignore_permissions=True, # ignore write permissions during insert
    ignore_links=True, # ignore Link validation in the document
    ignore_if_duplicate=True, # dont insert if DuplicateEntryError is thrown
    ignore_mandatory=True # insert even if mandatory fields are not set
)

doc.save

此方法保存对现有文档的更改。它会检查用户权限,并在更新前执行 validate,在更新值后执行 on_update

doc.save(
    ignore_permissions=True, # ignore write permissions during insert
    ignore_version=True # do not create a version record
)

doc.delete

从数据库表中删除文档记录。此方法是 frappe.delete_doc 的别名。

doc.delete()

doc.get_doc_before_save

返回更改前的文档版本。您可以使用它来比较自上次版本以来发生了什么变化。

old_doc = doc.get_doc_before_save()
if old_doc.price != doc.price:
    # price changed
    pass

doc.has_value_changed

如果给定字段的值在保存前后发生了变化,则返回 True。

price_changed = doc.has_value_changed("price")

if price_changed:
    pass

doc.reload

将从数据库获取最新值并更新文档状态。

当您在处理文档时,可能代码的其他部分会直接更新数据库中某个字段的值。在这种情况下,您可以使用此方法重新加载文档。

doc.reload()

doc.check_permission

如果当前用户没有所提供权限类型的权限,则抛出异常。

doc.check_permission(permtype='write') # throws if no write permission

doc.get_title

根据 title_field 或名为 titlename 的字段获取文档标题。

title = doc.get_title()

doc.notify_update

发布实时事件以指示文档已被修改。客户端事件处理程序通过更新表单来响应此事件。

doc.notify_update()

doc.db_set

直接在数据库中设置文档的字段值,并更新修改时间戳。

此方法不会触发控制器验证,应谨慎使用。

# updates value in database, updates the modified timestamp
doc.db_set('price', 2300)

# updates value in database, will trigger doc.notify_update()
doc.db_set('price', 2300, notify=True)

# updates value in database, will also run frappe.db.commit()
doc.db_set('price', 2300, commit=True)

# updates value in database, does not update the modified timestamp
doc.db_set('price', 2300, update_modified=False)

doc.append

向子表追加一个新项目。

doc.append("childtable", {
    "child_table_field": "value",
    "child_table_int_field": 0,
    ...
})

doc.get_url

返回此文档的 Desk URL。例如:/app/task/TASK00002

url = doc.get_url()

doc.add_comment

向此文档添加评论。评论将显示在表单视图的时间线中。

# add a simple comment
doc.add_comment('Comment', text='Test Comment')

# add a comment of type Edit
doc.add_comment('Edit', 'Values changed')

# add a comment of type Shared
doc.add_comment("Shared", "{0} shared this document with everyone".format(user))

doc.add_seen

将给定/当前用户添加到已查看此文档的用户列表中。这将更新表中的 _seen 列。该列以 JSON 数组形式存储。

# add john to list of seen
doc.add_seen('[email protected]')

# add session user to list of seen
doc.add_seen()

此功能仅在 DocType 中启用了 跟踪已查看 时才有效。

doc.add_viewed

当用户查看文档(即打开表单)时,添加一条查看日志。

# add a view log by john
doc.add_viewed('[email protected]')

# add a view log by session user
doc.add_viewed()

此功能仅在 DocType 中启用了 跟踪查看 时才有效。

doc.add_tag

向文档添加标签。标签通常用于筛选和分组文档。

# add tags
doc.add_tag('developer')
doc.add_tag('frontend')

doc.get_tags

返回与特定文档关联的标签列表。

# get all tags
doc.get_tags()

doc.run_method

如果控制器中定义了方法,则运行该方法;如果定义了钩子,也会触发钩子。

doc.run_method('validate')

doc.queue_action

在后台运行控制器方法。如果该方法具有内部函数,例如 _submit 对应 submit,则将调用该内部函数。

doc.queue_action('send_emails', emails=email_list, message='Howdy')

doc.get_children()

仅适用于树形 DocType(继承自 NestedSet)。

返回一个生成器,为每个子记录生成一个 NestedSet 实例。

for child_doc in doc.get_children():
    print(child_doc.name)

它也可以递归应用:

for child_doc in doc.get_children():
    print(child_doc.name)
    for grandchild_doc in child_doc.get_children():
        print(grandchild_doc.name)

doc.get_parent()

仅适用于树形 DocType(继承自 NestedSet)。

返回父记录的 NestedSet 实例。

parent_doc = doc.get_parent()
grandparent_doc = parent_doc.get_parent()

doc.db_insert()

将文档序列化并插入数据库。警告:此操作会绕过所有验证以及插入前后可能需要运行的控制器方法。如有疑问,请改用 doc.insert()

doc = frappe.get_doc(doctype="Controller", data="")
doc.db_insert()

doc.db_update()

将文档序列化并更新到数据库。警告:此操作会绕过所有验证以及更新前后可能需要运行的控制器方法。如有疑问,请改用 doc.save()

doc = frappe.get_last_doc("User")
doc.last_active = now()
doc.db_update()

后台作业

Frappe 内置了一套在后台运行任务的系统。它通过使用 schedule 包和一个简单的长时间运行的无限 while 循环来实现。

你可以使用 frappe.enqueue 方法将一个 Python 方法加入队列以在后台运行:

def long_running_job(param1, param2):
    # expensive tasks
    pass

# directly pass the function
frappe.enqueue(long_running_job, queue='short', param1='A', param2='B')

# or pass the full module path as string
frappe.enqueue('app.module.folder.long_running_job', queue='short', param1='A', param2='B')

以下是你传递给 enqueue 的所有可能参数:

frappe.enqueue(
    method, # python function or a module path as string
    queue="default", # one of short, default, long
    timeout=None, # pass timeout manually
    is_async=True, # if this is True, method is run in worker
    now=False, # if this is True, method is run directly (not in a worker) 
    job_name=None, # specify a job name
    enqueue_after_commit=False, # enqueue the job after the database commit is done at the end of the request
    at_front=False, # put the job at the front of the queue
    **kwargs, # kwargs are passed to the method as arguments
)

你也可以使用 frappe.enqueue_doc 将一个文档(Document)方法加入队列:

frappe.enqueue_doc(
    doctype,
    name,
    "do_something", # name of the controller method
    queue="long",
    timeout=4000,
    param="value"
)

队列

框架默认配置了 3 个队列:shortdefaultlong。每个队列都有一个默认超时时间,如下所示:

  • short:300 秒
  • default:300 秒
  • long:1500 秒

你也可以向 enqueue 方法传递自定义的超时时间。

自定义队列

你可以通过在 [common_site_config.json](https://frappeframework.com/docs/v14/user/en/basics/site_config#common-site-config) 中进行配置来添加自定义队列:

{
    ...
    "workers": {
        "myqueue": {
            "timeout": 5000, # queue timeout
            "background_workers": 4, # number of workers for this queue
        }   
    }
}

工作进程

默认情况下,Frappe 会设置 3 种工作进程类型来消费各个队列中的任务。默认配置如下所示:

bench worker --queue short
bench worker --queue default
bench worker --queue long

在生产环境中,这 3 个工作进程会被复制到配置数量的后台工作进程中,以处理更高的工作负载。

注意:这种将工作进程映射到单个队列的方式只是一种约定,并非必须遵循。

多队列消费

你可以通过指定一个逗号分隔的队列名称字符串,来让工作进程消费多个队列。

示例:如果你想要合并 short 和 default 工作进程,并且只使用两种类型的工作进程而不是默认配置,你可以像这样修改你的工作进程配置:

bench worker --queue short,default
bench worker --queue long

注意:这里展示的示例是针对 Procfile 格式的,但它们也很容易应用于 supervisor 或 systemd 配置。

使用 --burst 的突发模式

bench worker --queue short --burst

该命令会生成一个临时工作进程,该进程将开始消费 short 队列,并在队列清空后退出。如果你定期需要更多的工作进程,你可以使用操作系统的 crontab 在特定时间设置突发工作进程。

调度器事件

你可以使用调度器事件,通过 scheduler_events 钩子在后台定期运行任务。

app/hooks.py

scheduler_events = {
    "hourly": [
        # will run hourly
        "app.scheduled_tasks.update_database_usage"
    ],
}

app/scheduled_tasks.py

def update_database_usage():
    pass

在 hooks.py 中更改任何计划事件后,你需要运行 bench migrate 才能使更改生效。

可用事件

  • hourlydailyweeklymonthly

这些事件将分别每小时、每天、每周和每月触发一次。

  • hourly_longdaily_longweekly_longmonthly_long

与上述相同,但这些任务在 long 工作进程中运行,适用于长时间运行的任务。

  • all

all 事件每 4 分钟触发一次。这可以通过 common_site_config.json 中的 scheduler_interval 键进行配置。

  • cron

一个有效的 cron 字符串,可以被 croniter 解析。

使用示例:

scheduler_events = {
    "daily": [
        "app.scheduled_tasks.manage_recurring_invoices"
    ],
    "daily_long": [
        "app.scheduled_tasks.take_backups_daily"
    ],
    "cron": {
        "15 18 * * *": [
            "app.scheduled_tasks.delete_all_barcodes_for_users"
        ],
        "*/6 * * * *": [
            "app.scheduled_tasks.collect_error_snapshots"
        ],
        "annual": [
            "app.scheduled_tasks.collect_error_snapshots"
        ]
    }
}

可配置的调度器事件

在需要用户可配置触发间隔的场景下,创建一个 Scheduler Event 记录,并针对该记录创建一个 Scheduled Job Type 条目。这不需要 scheduler_event 钩子。

示例:

# Create `Scheduler Event` record
sch_eve = frappe.new_doc("Scheduler Event")
sch_eve.scheduled_against = "Process Payment Reconciliation"
sch_eve.save()

# Create `Scheduled Job Type`
job = frappe.new_doc("Scheduled Job Type")
job.frequency = "Cron"
job.scheduler_event = sch_eve.name
job.cron_format = "0/5 * * * *"     # runs every five minutes
job.save()

Scheduled Job Type 的触发间隔可以在之后修改,并且会在 bench 迁移时保持不变。

由调度器触发的任务由 管理员 用户运行。这也意味着,除非另有指定,否则你通过计划任务创建的任何文档都将归 管理员 用户所有。

实时(socket.io)

Frappe 内置了一个基于 socket.io 的实时事件 API。由于 socket.io 需要 Node 服务器来运行,我们在主 Web 服务器之外并行运行一个 Node 进程。

客户端 API (JavaScript)

frappe.realtime.on

要在客户端(浏览器)监听实时事件,可以使用 frappe.realtime.on 方法:

frappe.realtime.on('event_name', (data) => {
    console.log(data)
})

frappe.realtime.off

停止监听您已订阅的事件:

frappe.realtime.off('event_name')

服务端 API (Python)

frappe.publish_realtime

要从服务端发布实时事件,可以使用 frappe.publish_realtime 方法:

frappe.publish_realtime('event_name', data={'key': 'value'})

frappe.publish_progress

您可以使用此方法在对话框中显示进度条:

frappe.publish_progress(25, title='Some title', description='Some description')

自定义事件处理器 (Python)

注意:此功能仅在夜间版中可用。此功能被视为实验性功能。

您可以通过在自定义应用中创建 your_app/your_app/realtime/handlers.py 文件来实现自定义的实时事件处理器。其语法与 API 白名单非常相似。

from frappe.realtime import realtime

@realtime.on(
  "project_subscribe",
  frappe_context=False,   # open a Frappe context (DB + session) for the handler body
  allow_guest=False,      # if False, the event is dropped when socket.user == "Guest"
)

默认值:frappe_context=Falseallow_guest=False

示例

import frappe
from frappe.realtime import Socket, realtime

@realtime.on("project_subscribe")
def project_subscribe(socket: Socket, project: str) -> None:
    if socket.has_permission("Project", project):
        socket.join(f"project:{project}")

第一个参数始终是输入的 Socket。其余参数是客户端发送的负载,按位置传递。事件名称 "project_subscribe" 是浏览器通过 frappe.realtime.emit(...) 发出的。

使用 frappe_context=True 的工作示例

使用 frappe_context=True 时,完整的 ORM 在处理器主体中可用 — frappe.get_docfrappe.local.sitefrappe.session.user 都会被填充,并反映已认证的 socket 用户。

此示例处理器加载一个文档,检查权限,并将结果发送回客户端:

@realtime.on("test_get_doc", frappe_context=True)
def test_get_doc(socket: Socket, doctype: str, docname: str) -> None:
    import frappe
    try:
        doc = frappe.get_doc(doctype, docname)
        doc.check_permission()
        socket.emit(
            "test_get_doc_result",
            {
                "ok": True,
                "site": frappe.local.site,
                "user": frappe.session.user,
                "doctype": doc.doctype,
                "name": doc.name,
                "modified": str(doc.get("modified")),
            },
        )
    except Exception as e:
        socket.emit("test_get_doc_result", {"ok": False, "error": f"{type(e).__name__}: {e}"})

从浏览器端:

frappe.realtime.on("test_get_doc_result", (r) => console.log(r));
frappe.realtime.emit("test_get_doc", "User", "Administrator");

请参阅“权限检查”部分,了解其成本以及何时应优先使用基于 HTTP 的廉价权限检查。

仅当处理器所属的应用安装在连接站点上时,该处理器才会运行。所属应用会自动从导入中检测出来。

您可能需要重启 socketio 服务器才能看到代码更改生效。有关编写自定义事件处理器的更多信息,请参阅 Socket.IO 文档。

Socket 对象

只读身份信息,在连接时填充:

socket.site            # the site this socket is on
socket.user            # "Guest" for anonymous
socket.user_type       # e.g. "System User"
socket.installed_apps  # list of installed apps

房间、发送和瞬态状态:

socket.join(room)                        # add this socket to a room
socket.leave(room)                       # remove it
socket.emit(event, data=None, room=None) # emit to a room, or to this client if room is None
socket.get(key, default=None)            # read transient per-socket state
socket.set(key, value)                   # persist transient per-socket state (cleared on disconnect)

权限检查

有两种方法可以执行 doctype 权限检查。

A. HTTP(默认,推荐)socket.has_permission(doctype, name) 向 Web 进程发起请求,与核心处理器的行为完全一致。实时进程中不建立数据库连接。成本低。除非有特殊原因,否则请使用此方法。

if socket.has_permission("Project", project):
    socket.join(f"project:{project}")

B. 进程内(frappe_context=True:为处理器主体打开一个完整的 Frappe 上下文,以便您可以直接调用 frappe.has_permission(...)、查询数据库等。代价:每个此类事件都会执行 frappe.init -> connect -> set_user -> commit/rollback -> destroy,并强制在实时进程中建立数据库连接。请谨慎使用。

@realtime.on("project_subscribe", frappe_context=True)
def project_subscribe(socket: Socket, project: str) -> None:
    if frappe.has_permission("Project", doc=project, ptype="read"):
        socket.join(f"project:{project}")

完整参考:++https://github.com/frappe/frappe/blob/develop/frappe/realtime/README.md++

自定义事件处理器 (NodeJS)

从 v16 版本开始可用。 被视为实验性功能。这是之前的实现,将在新版本中逐步淘汰。

Socket.IO 服务器路径;新应用应优先使用上述 Python 处理器。

您可以通过在应用的 realtime 文件夹中创建 handlers.js 文件来实现自定义的实时事件处理器。

此文件需要有一个单一的导出 — 一个在 socket 实例上设置事件处理器的函数。例如,对于一个名为“chat”的应用:

// bench/apps/chat/realtime/handlers.js

function chat_app_handlers(socket) {
    socket.on("hello_chat", () => {
        console.log("hello world!");
    });
}

module.exports = chat_app_handlers;

使用 frappe.realtime.emit("hello_chat") 从客户端代码触发此事件。您可能需要重启 socketio 服务器才能看到代码更改生效。有关编写自定义事件处理器的更多信息,请参阅 Socket.IO 文档。

向客户端推送事件

您不能直接从 Web 进程发送事件 — 您需要通过 Redis 发布,然后实时服务器将其桥接到已连接的 socket。您发布到的房间字符串必须与您的处理器 join 到的房间匹配。

from frappe.realtime import publish_to_room
publish_to_room("project:PROJ-0001", "project_updated", {"status": "Open"})

其他命名的辅助函数,每个都是对 publish_realtime 的轻量封装。

publish_to_user(user, event, message=None)
publish_to_doc(doctype, docname, event, message=None)
publish_to_doctype(doctype, event, message=None)
publish_task_progress(task_id, message=None)
publish_to_website(event, message=None)
publish_to_all(event, message=None)
publish_to_room(room, event, message=None)

完整参考:++https://github.com/frappe/frappe/blob/develop/frappe/realtime/README.md++

自定义客户端

如果你正在开发一个不使用 Desk 界面的 SPA 或移动应用,你可以编写自定义客户端来连接 socket.io 服务器。请参考官方 Socket.IO 客户端文档。

自定义客户端示例:

  • gameplan/frontend/src/socket.js
  • frappe/socketio_client.js

自定义客户端中的授权

有两种方式可以对 socket.io 服务器的连接进行身份验证:

  • Cookies — 在类似浏览器的环境中,连接会自动发送 cookies,socketio 服务器会使用它们进行身份验证。
  • 授权头 — 如果 cookies 不可用(例如移动应用),请像 API 请求一样使用 Authorization 头。请参阅 REST API 身份验证文档和 Socket.IO extraHeaders。

实现说明

  • 实时服务器使用 socket.io 服务器,用 node.js 编写,位于 /realtime 目录中。
  • 实时客户端是 socket.io 客户端库的封装,位于 public/js/frappe/socketio_client.js 中。
  • Python 进程通过 Redis 发布-订阅通道将事件发布到 node 服务器。实时服务器订阅 Redis 通道,并重新发布给所有已订阅的客户端。
  • 实时服务器是多租户的:所有站点流量都按站点名称进行命名空间隔离。命名空间会动态创建为 /{sitename},其中 sitename 是站点在 sites 目录中的文件夹名称(或 frappe.local.site)。
  • 实时服务器使用主 Frappe Web 服务器来验证连接。SID cookie 或授权头会传递给客户端,并用于确保连接是有效用户,且可以根据权限订阅 DocTypes/文档。

可用房间

房间 访问权限
all 所有系统用户默认连接
website 任何用户(包括访客)均可访问
user:{username} 按用户分配的房间。无需权限检查即可加入
doctype:{doctype} 按 DocType 分配的房间。只有具有 DocType 权限的用户才能加入;打开列表/表单视图时自动订阅
doc:{doctype}/{name} 按文档分配的房间。只有具有文档权限的用户才能加入;打开表单视图时自动订阅