SQLite 搜索

SQLite 搜索是 Frappe 应用程序的一个全文搜索框架,利用 SQLite 的 FTS5(全文搜索)引擎提供高级搜索功能。它提供了拼写纠正、基于时间的时效性评分、自定义排名、权限感知过滤和可扩展的评分管道等功能。

目录

  • 快速开始

  • 工作原理

  • 配置

  • 功能与自定义

  • API 参考

快速开始

1. 创建搜索类

通过继承 SQLiteSearch 来创建搜索实现:


# my_app/search.py

from frappe.search.sqlite_search import SQLiteSearch

class MyAppSearch(SQLiteSearch):

    # Database file name

    INDEX_NAME = "my_app_search.db"

    # Define the search schema

    INDEX_SCHEMA = {

        "metadata_fields": ["project", "owner", "status"],

        "tokenizer": "unicode61 remove_diacritics 2 tokenchars '-_'",

    }

    # Define which doctypes to index and their field mappings

    INDEXABLE_DOCTYPES = {

        "Task": {

            "fields": ["name", {"title": "subject"}, {"content": "description"}, "modified", "project", "owner", "status"],

        },

        "Issue": {

            "fields": ["name", "title", "description", {"modified": "last_updated"}, "project", "owner"],

            "filters": {"status": ("!=", "Closed")},  # Only index non-closed issues

        },

    }

    def get_search_filters(self):

        """Return permission filters for current user"""

        # Get projects accessible to current user

        accessible_projects = frappe.get_all(

            "Project",

            filters={"owner": frappe.session.user},

            pluck="name"

        )

        if not accessible_projects:

            return {"project": []}  # No access

        return {"project": accessible_projects}

2. 注册搜索类

将你的搜索类添加到 hooks.py 文件中:


# my_app/hooks.py

sqlite_search = ['my_app.search.MyAppSearch']

3. 创建 API 端点

创建一个白名单方法来暴露搜索功能:


# my_app/api.py

import frappe

from my_app.search import MyAppSearch

@frappe.whitelist()

def search(query, filters=None):

    search = MyAppSearch()

    result = search.search(query, filters=filters)

    return result

4. 构建索引

通过编程方式或控制台构建搜索索引:


from my_app.search import MyAppSearch

search = MyAppSearch()

search.build_index()

工作原理

1. 索引过程

完整索引构建

当你调用 build_index() 时,框架会执行一次完整的索引重建:

  1. 数据库准备:创建一个临时的 SQLite 数据库,并根据你的模式配置 FTS5 表

  2. 文档收集:使用配置的字段映射和过滤器查询所有指定的 DocType

  3. 文档处理:对于每个文档:

    • 根据 INDEXABLE_DOCTYPES 配置提取和映射字段

    • 使用 BeautifulSoup 清理 HTML 内容以提取纯文本

    • 如果覆盖了 prepare_document(),则应用自定义的文档准备逻辑

    • 验证必填字段(标题、内容)是否存在

  4. 批量插入:将处理后的文档分批插入 FTS5 索引以提高性能

  5. 词汇表构建:从所有索引文本中构建拼写纠正字典

  6. 原子替换:以原子方式用新数据库替换现有索引数据库

单个文档索引

使用 index_doc()remove_doc() 进行实时更新:

  1. 单个文档处理:使用相同的字段映射逻辑检索并处理一个文档

  2. 增量更新:通过插入、更新或删除特定文档来更新现有的 FTS5 索引

  3. 词汇表更新:使用文档中的新术语更新拼写字典

2. 搜索过程

当用户使用 search() 执行搜索时,框架会执行以下步骤:

  1. 权限过滤:调用 get_search_filters() 来确定当前用户可以访问哪些文档

  2. 查询预处理

    • 验证搜索查询不为空

    • 将用户提供的过滤器与权限过滤器合并

  3. 拼写纠正

    • 对照词汇字典分析查询词条

    • 使用三元组相似度来为拼写错误的单词提供纠正建议

    • 使用纠正后的词条扩展原始查询

  4. FTS5 查询执行

    • 构建一个兼容 FTS5 的查询字符串

    • 对 SQLite 数据库执行全文搜索

    • 应用元数据过滤器(状态、所有者、项目等)

    • 检索带有 BM25 分数的原始结果

  5. 结果处理

    • 自定义评分:应用评分管道来计算最终的相关性分数

      • 基础 BM25 分数处理

      • 标题匹配加权(精确匹配和部分匹配)

      • 基于文档年龄的时效性加权

      • 自定义评分函数(特定 DocType、基于优先级等)

    • 排名:按最终分数对结果进行排序并分配排名位置

    • 内容格式化:生成内容摘要并高亮匹配词条

配置

INDEX_SCHEMA

定义搜索索引的结构:


INDEX_SCHEMA = {

    # Text fields that will be searchable (defaults to ["title", "content"])

    "text_fields": ["title", "content"],

    # Metadata fields stored alongside text content for filtering

    "metadata_fields": ["project", "owner", "status", "priority"],

    # FTS5 tokenizer configuration

    "tokenizer": "unicode61 remove_diacritics 2 tokenchars '-_@.'"

}

INDEXABLE_DOCTYPES

指定要索引的 DocType 以及如何映射其字段:


INDEXABLE_DOCTYPES = {

    "Task": {

        # Field mapping

        "fields": [

            "name",

            {"title": "subject"},        # Maps subject field to title

            {"content": "description"},  # Maps description field to content

            {"modified": "creation"},    # Use creation instead of modified for recency boost

            "project",

            "owner"

        ],

        # Optional filters to limit which records are indexed

        "filters": {

            "status": ("!=", "Cancelled"),

            "docstatus": ("!=", 2)

        }

    }

}

字段映射规则

  • 字符串字段:直接映射 "field_name"

  • 别名字段:字典映射 {"schema_field": "doctype_field"}

  • 必填字段titlecontent 字段必须存在或显式映射(例如 {"title": "subject"}

  • 自动添加的字段doctypename 会自动包含

  • 修改字段:如果在任何文档类型配置中使用,则会自动添加。用于近期加权 – 如果您想使用不同的时间戳字段(如 creationlast_updated),请使用 {"modified": "creation"} 将其映射到 modified

功能与自定义

权限过滤

实现 get_search_filters() 来控制访问权限:


def get_search_filters(self):

    """Return filters based on user permissions"""

    user = frappe.session.user

    if user == "Administrator":

        return {}  # No restrictions

    # Example: User can only see their own and public documents

    return {

        "owner": user,

        "status": ["Active", "Published"]

    }

自定义评分

创建自定义评分函数以影响搜索相关性:


class MyAppSearch(SQLiteSearch):

    ...

    @SQLiteSearch.scoring_function

    def *get*priority_boost(self, row, query, query_words):

        """Boost high-priority items"""

        priority = row.get("priority", "Medium")

        if priority == "High":

            return 1.5

        if priority == "Medium":

            return 1.1

        return 1.0

近期加权

框架使用 modified 字段自动提供基于时间的近期加权:


# The modified field is used for calculating document age

# Recent documents get higher scores:

# - Last 24 hours: 1.8x boost

# - Last 7 days: 1.5x boost

# - Last 30 days: 1.2x boost

# - Last 90 days: 1.1x boost

# - Older documents: gradually decreasing boost

# If your doctype uses a different timestamp field, map it to modified:

INDEXABLE_DOCTYPES = {

    "GP Discussion": {

        "fields": ["name", "title", "content", {"modified": "last_post_at"}, "project"],

    },

    "Article": {

        "fields": ["name", "title", "content", {"modified": "published_date"}, "category"],

    }

}

文档准备

覆盖 prepare_document() 以进行自定义文档处理:


def prepare_document(self, doc):

    """Custom document preparation"""

    document = super().prepare_document(doc)

    if not document:

        return None

    # Add computed fields

    if doc.doctype == "Task":

        # Combine multiple fields into content

        content_parts = [

            doc.description or "",

            doc.notes or "",

            "\n".join([comment.content for comment in doc.get("comments", [])])

        ]

        document["content"] = "\n".join(filter(None, content_parts))

        # set fields that might be stored in another table

        document["category"] = get_category_for_task(doc)

    return document

拼写纠正

框架内置了使用三元组相似度的拼写纠正功能:


# Spelling correction happens automatically

search_result = search.search("projetc managment")  # Will find "project management"

# Access correction information

print(search_result["summary"]["corrected_words"])

# Output: {"projetc": "project", "managment": "management"}

内容处理

HTML 内容会自动使用 BeautifulSoup 进行清理和处理:


# Complex HTML content like this:

html_content = """

<div class="article">

<h1>API Documentation</h1>

<p>Learn how to integrate with our <a href="/api">REST API</a>.</p>

    <img src="/images/api-flow.png" alt="API workflow diagram">

<ul>

<li><strong>Authentication:</strong> Use <code>Bearer tokens

  • Rate limiting: 1000 requests/hour
  • See our code examples for details.
    Method POST
    analytics.track('page_view'); .hidden { display: none; }
    """ # Is automatically converted to clean, searchable plain text: """ API Documentation Learn how to integrate with our REST API. Authentication: Use Bearer tokens Rate limiting: 1000 requests/hour See our code examples for details. Method POST """ # The cleaning process: # 1. Removes all HTML tags (
    ,

    , , , etc.) # 2. Strips out scripts, styles, and non-content elements # 3. Extracts link text while removing href URLs # 4. Normalizes whitespace and line breaks
    
    results = search.search("project update", title_only=True)
    

    高级过滤

    
    accessible_projects = ['PROJ001', 'PROJ002', ...]
    
    filters = {
    
        "project": accessible_projects,     # Multiple values (IN clause)
    
        "owner": current_user,              # Single value (= clause)
    
    }
    
    results = search.search("bug fix", filters=filters)
    

    自动索引处理

    当您注册搜索类时,框架会自动处理索引的构建和维护:

    
    # hooks.py
    
    sqlite_search = ['my_app.search.MyAppSearch']
    

    框架自动完成的工作:

    1. 迁移后索引构建:运行 bench migrate 后自动构建搜索索引

    2. 定期索引验证:每 15 分钟检查一次索引是否存在,如果缺失则重新构建

    3. 实时文档更新:在文档生命周期事件(插入、更新、删除)中自动调用 index_doc()remove_doc(),适用于您在 INDEXABLE_DOCTYPES 中定义的所有文档类型

    手动索引处理

    如果您希望手动控制索引的生命周期,可以通过不在 sqlite_search 钩子中注册搜索类来退出自动索引处理。

    
    from my_app.search import MyAppSearch
    
    def build_index_in_background():
    
        """Manually trigger background index building"""
    
        search = MyAppSearch()
    
        if search.is_search_enabled() and not search.index_exists():
    
            frappe.enqueue("my_app.search.build_index", queue="long")
    
    # hooks.py
    
    scheduler_events = {
    
        # Custom scheduler (if you want different timing)
    
        "daily": ["my_app.search.build_index_if_not_exists"],
    
    }
    

    API 参考

    search(query, title_only=False, filters=None)

    返回格式化结果的主要搜索方法。

    参数:

    • query(字符串):搜索查询文本

    • title_only(布尔值):仅在标题字段中搜索

    • filters(字典):要应用的附加过滤器

    返回值:

    
    {
    
        "results": [
    
            {
    
                "doctype": "Task",
    
                "name": "TASK-001",
    
                "title": "Fix login bug",
    
                "content": "User cannot login after password reset...",
    
                "score": 0.85,
    
                "original_rank": 3, # original bm25 rank
    
                "rank": 1, # modified rank after custom scoring pipeline
    
                # ... other metadata fields
    
            }
    
        ],
    
        "summary": {
    
            "duration": 0.023,
    
            "total_matches": 15,
    
            "returned_matches": 15,
    
            "corrected_words": {"loggin": "login"},
    
            "corrected_query": "Fix login bug",
    
            "title_only": False,
    
            "filtered_matches": 15,
    
            "applied_filters": {"status": ["Open"]}
    
        }
    
    }
    

    build_index()

    从头构建完整的搜索索引。

    index_doc(doctype, docname)

    索引单个文档。

    remove_doc(doctype, docname)

    从索引中移除单个文档。

    is_search_enabled()

    检查搜索是否已启用(覆盖以添加禁用逻辑)。

    index_exists()

    检查搜索索引是否存在。

    get_search_filters()

    必须由子类实现。返回当前用户的过滤器。

    返回值:

    
    {
    
        "field_name": "value",           # Single value
    
        "field_name": ["val1", "val2"],  # Multiple values
    
    }
    

    scoring_function()

    使用 @SQLiteSearch.scoring_function 装饰器将函数标记为评分函数。

    frappe.qb获取查询

    [["status", "in", ["Open", "Pending"]]]

    not in NOT IN {"role": ["not in", ["Guest"]]} [["role", "not in", ["Guest"]]] is IS NULL IS NOT NULL {"customer": ["is", "set"]} {"email": ["is", "not set"]} [["customer", "is", "set"]] [["email", "is", "not set"]] descendants of {"parent_account": ["descendants of", "Assets"]} [["parent_account", "descendants of", "Assets"]] ancestors of {"location": ["ancestors of", "Room 101"]} [["location", "ancestors of", "Room 101"]] not descendants of {"category": ["not descendants of", "Internal"]} [["category", "not descendants of", "Internal"]] not ancestors of {"territory": ["not ancestors of", "West Coast"]} [["territory", "not ancestors of", "West Coast"]] is set is not set IS NOT NULL IS NULL link_fieldname.target_fieldname child_table_fieldname.target_fieldname distinct=True lft rgt filters 'and' 'or' query .run() as_iterator=True as_iterator=True as_dict=True as_list=True frappe.db.unbuffered_cursor() order_by group_by limit offset distinct=True ignore_permissions frappe.qb.get_query ignore_permissions=True ignore_permissions=False ignore_permissions=False if_owner fields filters group_by order_by link_field.target_field child_field.target_field ignore_permissions=False fields filters frappe.PermissionError group_by frappe.PermissionError order_by frappe.PermissionError link_field.target_field child_field.target_field frappe.qb.get_query ignore_permissions=False __CODEBLOCK_156__ __CODEBLOCK_157__ __CODEBLOCK_158__ __CODEBLOCK_159__ __CODEBLOCK_160__ __CODEBLOCK_161__ __CODEBLOCK_162__ __CODEBLOCK_163__ __CODEBLOCK_164__ __CODEBLOCK_165__ __CODEBLOCK_166__ __CODEBLOCK_167__ __CODEBLOCK_168__ __CODEBLOCK_169__ __CODEBLOCK_170__ __CODEBLOCK_171__ __CODEBLOCK_172__ __CODEBLOCK_173__ __CODEBLOCK_174__ __CODEBLOCK_175__ __CODEBLOCK_176__ __CODEBLOCK_177__ __CODEBLOCK_178__ __CODEBLOCK_179__ __CODEBLOCK_180__ __CODEBLOCK_181__ __CODEBLOCK_182__ __CODEBLOCK_183__ __CODEBLOCK_184__ __CODEBLOCK_185__ __CODEBLOCK_186__ __CODEBLOCK_187__ __CODEBLOCK_188__ __CODEBLOCK_189__ __CODEBLOCK_190__ __CODEBLOCK_191__ __CODEBLOCK_192__ __CODEBLOCK_193__ __CODEBLOCK_194__ __CODEBLOCK_195__ __CODEBLOCK_196__ __CODEBLOCK_197__ __CODEBLOCK_198__ __CODEBLOCK_199__ __CODEBLOCK_200__ __CODEBLOCK_201__ __CODEBLOCK_202__ __CODEBLOCK_203__ __CODEBLOCK_204__ __CODEBLOCK_205__ __CODEBLOCK_206__ __CODEBLOCK_207__ __CODEBLOCK_208__ __CODEBLOCK_209__ __CODEBLOCK_210__ __CODEBLOCK_211__ __CODEBLOCK_212__ __CODEBLOCK_213__ __CODEBLOCK_214__ __CODEBLOCK_215__ __CODEBLOCK_216__ __CODEBLOCK_217__ __CODEBLOCK_218__ __CODEBLOCK_219__ __CODEBLOCK_220__ __CODEBLOCK_221__ __CODEBLOCK_222__ __CODEBLOCK_223__ __CODEBLOCK_224__ __CODEBLOCK_225__ __CODEBLOCK_226__ __CODEBLOCK_227__ __CODEBLOCK_228__ __CODEBLOCK_229__ __CODEBLOCK_230__ __CODEBLOCK_231__ __CODEBLOCK_232__ __CODEBLOCK_233__ __CODEBLOCK_234__ __CODEBLOCK_235__ __CODEBLOCK_236__ __CODEBLOCK_237__ __CODEBLOCK_238__ __CODEBLOCK_239__ __CODEBLOCK_240__ __CODEBLOCK_241__ __CODEBLOCK_242__ __CODEBLOCK_243__ __CODEBLOCK_244__ __CODEBLOCK_245__ __CODEBLOCK_246__ __CODEBLOCK_247__ __CODEBLOCK_248__ __CODEBLOCK_249__ __CODEBLOCK_250__ __CODEBLOCK_251__ __CODEBLOCK_252__ __CODEBLOCK_253__ __CODEBLOCK_254__ __CODEBLOCK_255__ __CODEBLOCK_256__ __CODEBLOCK_257__ __CODEBLOCK_258__ __CODEBLOCK_259__ __CODEBLOCK_260__ __CODEBLOCK_261__ __CODEBLOCK_262__ __CODEBLOCK_263__ __CODEBLOCK_264__ __CODEBLOCK_265__ __CODEBLOCK_266__ __CODEBLOCK_267__ __CODEBLOCK_268__ __CODEBLOCK_269__ __CODEBLOCK_270__ __CODEBLOCK_271__ __CODEBLOCK_272__ __CODEBLOCK_273__ __CODEBLOCK_274__ __CODEBLOCK_275__ __CODEBLOCK_276__ __CODEBLOCK_277__ __CODEBLOCK_278__ __CODEBLOCK_279__ __CODEBLOCK_280__ __CODEBLOCK_281__ __CODEBLOCK_282__ __CODEBLOCK_283__ __CODEBLOCK_284__ __CODEBLOCK_285__ __CODEBLOCK_286__ __CODEBLOCK_287__ __CODEBLOCK_288__ __CODEBLOCK_289__ __CODEBLOCK_290__ __CODEBLOCK_291__ __CODEBLOCK_292__ __CODEBLOCK_293__ __CODEBLOCK_294__ __CODEBLOCK_295__ __CODEBLOCK_296__ __CODEBLOCK_297__ __CODEBLOCK_298__ __CODEBLOCK_299__ __CODEBLOCK_300__ __CODEBLOCK_301__ __CODEBLOCK_302__ __CODEBLOCK_303__ __CODEBLOCK_304__ __CODEBLOCK_305__ __CODEBLOCK_306__ __CODEBLOCK_307__ __CODEBLOCK_308__ __CODEBLOCK_309__ __CODEBLOCK_310__ __CODEBLOCK_311__ __CODEBLOCK_312__ __CODEBLOCK_313__ __CODEBLOCK_314__ __CODEBLOCK_315__ __CODEBLOCK_316__ __CODEBLOCK_317__ __CODEBLOCK_318__ __CODEBLOCK_319__ __CODEBLOCK_320__ __CODEBLOCK_321__ __CODEBLOCK_322__ __CODEBLOCK_323__ __CODEBLOCK_324__ __CODEBLOCK_325__ __CODEBLOCK_326__ __CODEBLOCK_327__ __CODEBLOCK_328__ __CODEBLOCK_329__ __CODEBLOCK_330__ __CODEBLOCK_331__ __CODEBLOCK_332__ __CODEBLOCK_333__ __CODEBLOCK_334__ __CODEBLOCK_335__ __CODEBLOCK_336__ __CODEBLOCK_337__ __CODEBLOCK_338__ __CODEBLOCK_339__ __CODEBLOCK_340__ __CODEBLOCK_341__ __CODEBLOCK_342__ __CODEBLOCK_343__ __CODEBLOCK_344__ __CODEBLOCK_345__ __CODEBLOCK_346__ __CODEBLOCK_347__ __CODEBLOCK_348__ __CODEBLOCK_349__ __CODEBLOCK_350__ __CODEBLOCK_351__ __CODEBLOCK_352__ __CODEBLOCK_353__ __CODEBLOCK_354__ __CODEBLOCK_355__ __CODEBLOCK_356__ __CODEBLOCK_357__ __CODEBLOCK_358__ __CODEBLOCK_359__ __CODEBLOCK_360__ __CODEBLOCK_361__ __CODEBLOCK_362__ __CODEBLOCK_363__ __CODEBLOCK_364__ __CODEBLOCK_365__ __CODEBLOCK_366__ __CODEBLOCK_367__ __CODEBLOCK_368__ __CODEBLOCK_369__ __CODEBLOCK_370__ __CODEBLOCK_371__ __CODEBLOCK_372__ __CODEBLOCK_373__ __CODEBLOCK_374__ __CODEBLOCK_375__ __CODEBLOCK_376__ __CODEBLOCK_377__ __CODEBLOCK_378__ __CODEBLOCK_379__ __CODEBLOCK_380__ __CODEBLOCK_381__ __CODEBLOCK_382__ __CODEBLOCK_383__ __CODEBLOCK_384__ __CODEBLOCK_385__ __CODEBLOCK_386__ __CODEBLOCK_387__ __CODEBLOCK_388__ __CODEBLOCK_389__ __CODEBLOCK_390__ __CODEBLOCK_391__ __CODEBLOCK_392__ __CODEBLOCK_393__ __CODEBLOCK_394__ __CODEBLOCK_395__ __CODEBLOCK_396__ __CODEBLOCK_397__ __CODEBLOCK_398__ __CODEBLOCK_399__ __CODEBLOCK_400__ __CODEBLOCK_401__ __CODEBLOCK_402__ __CODEBLOCK_403__ __CODEBLOCK_404__ __CODEBLOCK_405__ __CODEBLOCK_406__ __CODEBLOCK_407__ __CODEBLOCK_408__ __CODEBLOCK_409__ __CODEBLOCK_410__ __CODEBLOCK_411__ __CODEBLOCK_412__ __CODEBLOCK_413__ __CODEBLOCK_414__ __CODEBLOCK_415__ __CODEBLOCK_416__ __CODEBLOCK_417__ __CODEBLOCK_418__ __CODEBLOCK_419__ __CODEBLOCK_420__ __CODEBLOCK_421__ __CODEBLOCK_422__ __CODEBLOCK_423__ __CODEBLOCK_424__ __CODEBLOCK_425__ __CODEBLOCK_426__ __CODEBLOCK_427__ __CODEBLOCK_428__ __CODEBLOCK_429__ __CODEBLOCK_430__ __CODEBLOCK_431__ __CODEBLOCK_432__ __CODEBLOCK_433__ __CODEBLOCK_434__ __CODEBLOCK_435__ __CODEBLOCK_436__ __CODEBLOCK_437__ __CODEBLOCK_438__ __CODEBLOCK_439__ __CODEBLOCK_440__ __CODEBLOCK_441__ __CODEBLOCK_442__ __CODEBLOCK_443__ __CODEBLOCK_444__ __CODEBLOCK_445__ __CODEBLOCK_446__ __CODEBLOCK_447__ __CODEBLOCK_448__ __CODEBLOCK_449__ __CODEBLOCK_450__ __CODEBLOCK_451__ __CODEBLOCK_452__ __CODEBLOCK_453__ __CODEBLOCK_454__ __CODEBLOCK_455__ __CODEBLOCK_456__ __CODEBLOCK_457__ __CODEBLOCK_458__ __CODEBLOCK_459__ __CODEBLOCK_460__ __CODEBLOCK_461__ __CODEBLOCK_462__ __CODEBLOCK_463__ __CODEBLOCK_464__ __CODEBLOCK_465__ __CODEBLOCK_466__ __CODEBLOCK_467__ __CODEBLOCK_468__ __CODEBLOCK_469__ __CODEBLOCK_470__ __CODEBLOCK_471__ __CODEBLOCK_472__ __CODEBLOCK_473__ __CODEBLOCK_474__ __CODEBLOCK_475__ __CODEBLOCK_476__ __CODEBLOCK_477__ __CODEBLOCK_478__ __CODEBLOCK_479__ __CODEBLOCK_480__ __CODEBLOCK_481__ __CODEBLOCK_482__ __CODEBLOCK_483__ __CODEBLOCK_484__ __CODEBLOCK_485__ __CODEBLOCK_486__ __CODEBLOCK_487__ __CODEBLOCK_488__ __CODEBLOCK_489__ __CODEBLOCK_490__ __CODEBLOCK_491__ __CODEBLOCK_492__ __CODEBLOCK_493__ __CODEBLOCK_494__ __CODEBLOCK_495__ __CODEBLOCK_496__ __CODEBLOCK_497__ __CODEBLOCK_498__ __CODEBLOCK_499__ __CODEBLOCK_500__ __CODEBLOCK_501__ __CODEBLOCK_502__ __CODEBLOCK_503__ __CODEBLOCK_504__ __CODEBLOCK_505__ __CODEBLOCK_506__ __CODEBLOCK_507__ __CODEBLOCK_508__ <td style="text-align:left [["status", "in", ["Open", "Pending"]]] not in NOT IN {"role": ["not in", ["Guest"]]} [["role", "not in", ["Guest"]]] is IS NULLIS NOT NULL {"customer": ["is", "set"]}{"email": ["is", "not set"]} [["customer", "is", "set"]][["email", "is", "not set"]] descendants of (嵌套集) {"parent_account": ["descendants of", "Assets"]} [["parent_account", "descendants of", "Assets"]] ancestors of (嵌套集) {"location": ["ancestors of", "Room 101"]} [["location", "ancestors of", "Room 101"]] not descendants of (嵌套集) {"category": ["not descendants of", "Internal"]} [["category", "not descendants of", "Internal"]] not ancestors of (嵌套集) {"territory": ["not ancestors of", "West Coast"]} [["territory", "not ancestors of", "West Coast"]]

    关于 is set / is not set 的说明: 这些用于检查字段是否有值(分别为 IS NOT NULLIS NULL)。

    按链接文档字段筛选

    您可以使用点号表示法根据链接文档中的字段进行筛选:link_fieldname.target_fieldname

    # Get Sales Orders where the linked Customer's territory is 'North America'
    query = frappe.qb.get_query(
        "Sales Order",
        fields=["name", "customer"],
        filters={"customer.territory": "North America"} # Filter on linked field
    )
    north_america_orders = query.run(as_dict=True)
    

    按子表字段筛选

    您可以使用点号表示法根据子表记录中的值筛选父记录:child_table_fieldname.target_fieldname

    # Get Sales Orders that contain 'Item A' in their items table
    # Use distinct=True to ensure each Sales Order appears only once
    query = frappe.qb.get_query(
        "Sales Order",
        fields=["name", "customer"],
        filters={"items.item_code": "Item A"}, # Filter based on child table field
        distinct=True
    )
    orders_with_item_a = query.run(as_dict=True)
    

    重要提示: 当基于子表字段进行筛选时,如果您只需要唯一的父记录,请使用 distinct=True

    嵌套集筛选

    对于树形结构的 DocType(使用 lftrgt 列,如科目、地区、仓库等),您可以使用特殊筛选器:

    # Get all accounts under 'Assets'
    query = frappe.qb.get_query(
        "Account",
        fields=["name"],
        filters={"parent_account": ["descendants of", "Assets"]}
    )
    
    # Get the parent territories of 'West Coast'
    query = frappe.qb.get_query(
        "Territory",
        fields=["name"],
        filters={"parent_territory": ["ancestors of", "West Coast"]}
    )
    

    逻辑运算符(AND/OR)

    对于复杂条件,请将您的 filters 构建为列表,并使用 'and''or' 组合条件。

    # Find users who are enabled AND have first name 'Admin'
    filters_and = [
        ["enabled", "=", 1],
        "and",
        ["first_name", "=", "Admin"],
    ]
    query = frappe.qb.get_query("User", filters=filters_and)
    
    # Find users who have first name 'Admin' OR 'Guest'
    filters_or = [
        ["first_name", "=", "Admin"],
        "or",
        ["first_name", "=", "Guest"],
    ]
    query = frappe.qb.get_query("User", filters=filters_or)
    
    # Combine AND and OR (use nested lists for grouping)
    # Find users who are enabled AND (have first name 'Admin' OR 'Guest')
    filters_nested = [
        ["enabled", "=", 1],
        "and",
        [
            ["first_name", "=", "Admin"],
            "or",
            ["first_name", "=", "Guest"],
        ]
    ]
    query = frappe.qb.get_query("User", filters=filters_nested)
    

    查询执行

    基本执行

    一旦您获得了 query 对象,请使用 .run() 执行它:

    # Returns a list of tuples by default
    results = query.run()
    
    # Returns a list of dictionaries
    results = query.run(as_dict=True)
    
    # Returns a list of lists
    results = query.run(as_list=True)
    
    # If selecting a single field, returns a flat list of values
    results = query.run(pluck=True)
    
    # Print the generated SQL query and execution time
    results = query.run(debug=True)
    

    获取 SQL 字符串

    您可以在不执行的情况下获取生成的 SQL 字符串:

    # Get the SQL string with values directly substituted (for debugging)
    sql_string = query.get_sql()
    print(sql_string)
    # Example Output: SELECT `name`, `email` FROM `tabUser` WHERE `first_name`='Admin'
    

    对大型数据集使用迭代器

    处理大型数据集时,请使用 as_iterator=True 逐行处理结果,而无需将所有内容加载到内存中:

    # Process a large number of tasks without loading all into memory
    query = frappe.qb.get_query(
        "Task",
        fields=["name", "subject", "status"],
        filters={"status": "Open"}
    )
    
    # Use unbuffered_cursor for optimal memory usage with the iterator
    with frappe.db.unbuffered_cursor():
        task_iterator = query.run(as_iterator=True, as_dict=True)
    
        processed_count = 0
        for task in task_iterator:
            # Process each task dictionary one by one
            print(f"Processing Task: {task['name']} - {task['subject']}")
            processed_count += 1
            if processed_count % 1000 == 0:
                print(f"Processed {processed_count} tasks...")
    

    要求:

    • 您必须将 as_iterator=Trueas_dict=Trueas_list=True 一起使用
    • 为获得最佳内存效率,请在 frappe.db.unbuffered_cursor() 上下文管理器中使用

    排序、分组和分页

    排序结果

    使用 order_by 参数对结果进行排序:

    # Order users by creation date, ascending
    query = frappe.qb.get_query("User", fields=["name", "creation"], order_by="creation asc")
    
    # Order by multiple fields
    query = frappe.qb.get_query(
        "Sales Invoice",
        fields=["name", "customer", "grand_total"],
        order_by="customer asc, grand_total desc"
    )
    

    分组结果

    使用 group_by 进行聚合:

    # Count invoices per customer
    query = frappe.qb.get_query(
        "Sales Invoice",
        fields=["customer", {"COUNT": "'*'", "as": "invoice_count"}],
        filters={"docstatus": 1},
        group_by="customer"
    )
    results = query.run(as_dict=True)
    # results: [{'customer': 'Cust A', 'invoice_count': 5}, {'customer': 'Cust B', 'invoice_count': 3}, ...]
    

    分页

    使用 limitoffset 进行分页:

    # Get the first 10 users
    query = frappe.qb.get_query("User", limit=10)
    
    # Get the next 10 users (page 2)
    query = frappe.qb.get_query("User", limit=10, offset=10)
    

    去重结果

    使用 distinct=True 获取唯一行:

    # Get distinct customers from submitted Sales Invoices
    query = frappe.qb.get_query(
        "Sales Invoice",
        fields=["customer"],
        filters={"docstatus": 1},
        distinct=True
    )
    

    权限

    ignore_permissions 标志

    默认情况下,frappe.qb.get_query 忽略权限(ignore_permissions=True)。要强制执行权限,请设置 ignore_permissions=False

    # This query bypasses all permission checks (default behavior)
    query_ignore = frappe.qb.get_query("DocType", fields=["name"], filters={"istable": 1})
    
    # This query enforces standard Frappe permissions for the current user
    query_enforce = frappe.qb.get_query(
        "DocType",
        fields=["name"],
        filters={"istable": 1},
        ignore_permissions=False # Explicitly enable permission checks
    )
    
    try:
        results = query_enforce.run()
    except frappe.PermissionError:
        print("User does not have permission to read DocType!")
    

    权限的应用方式

    ignore_permissions=False 时:

    1. 角色权限: 根据用户的角色检查其是否具有“读取”或“选择”权限。
    2. 用户权限: 应用为 DocType 和链接的 DocType 定义的用户权限(允许/限制)。
    3. 共享: 包含明确共享给用户的文档。
    4. 所有者约束: 如果角色权限仅授予 if_owner 访问权限,则查询会将结果限制为用户拥有的文档。
    5. 权限查询条件: 应用通过 Hooks 或服务器脚本定义的条件。
    6. 字段级安全: 筛选所选的 fields,如果用户没有权限级别访问权限,则不允许使用 filtersgroup_byorder_by 中使用的字段。同时检查 link_field.target_fieldchild_field.target_field 表示法中的字段。

    字段级安全

    ignore_permissions=False 时:

    • fields 仅包含用户最大允许权限级别下可访问的字段。请求不可访问的字段将静默移除该字段的选择。
    • filters 筛选仅允许在用户有权访问的字段上进行。尝试筛选不可访问的字段将引发 frappe.PermissionError
    • group_by 分组仅允许在用户有权访问的字段上进行。尝试按不可访问的字段分组将引发 frappe.PermissionError
    • order_by 仅允许对用户有权限访问的字段进行排序。尝试按无权限访问的字段排序将引发 frappe.PermissionError
    • 链接表和子表字段: 在上述任何子句中使用 link_field.target_fieldchild_field.target_field 表示法时,系统会同时检查链接/子字段本身的权限,以及链接/子 DocType 中目标字段的权限。
    # Assume 'published' field in Blog Post has permlevel 1
    # User '[email protected]' only has permlevel 0 access
    
    # This works, but 'published' field is silently removed from results
    query = frappe.qb.get_query(
        "Blog Post",
        fields=["name", "title", "published"], # 'published' requested but inaccessible
        ignore_permissions=False,
        user="[email protected]"
    )
    # result will contain 'name' and 'title', but NOT 'published'
    
    # This FAILS because filtering on 'published' is not allowed for this user
    try:
        query = frappe.qb.get_query(
            "Blog Post",
            fields=["name"],
            filters={"published": 1}, # Filtering on restricted field
            ignore_permissions=False,
            user="[email protected]"
        )
        query.run()
    except frappe.PermissionError as e:
        print(f"Permission error: {e}")
    

    指定用户和父级上下文

    # Check permissions for a specific user
    query = frappe.qb.get_query(
        "Task",
        ignore_permissions=False,
        user="[email protected]" # Check permissions for this user
    )
    
    # Provide parent context for child DocTypes
    query = frappe.qb.get_query(
        "Sales Order Item",
        fields=["item_code", "qty"],
        filters={"parent": "SO-00001"},
        ignore_permissions=False,
        parent_doctype="Sales Order" # Specify parent context for permission checks
    )
    

    高级功能

    使用 Pypika 对象

    对于涉及子查询、高级条件或字典语法中不可用函数的复杂场景,您可以直接使用 Pypika 对象。

    字段中的 Pypika 对象

    from frappe.query_builder import Field, functions, Query
    
    # Define Pypika objects
    user_table = frappe.qb.DocType("User")
    todo_table = frappe.qb.DocType("ToDo")
    
    # Build a subquery to count open ToDos for each user
    open_todo_subquery = (
        Query.from_(todo_table)
        .select(functions.Count("*"))
        .where(todo_table.owner == user_table.name) # Correlated subquery
        .where(todo_table.status == "Open")
    ).as_("open_todos_count")
    
    # Use Pypika objects in fields
    query = frappe.qb.get_query(
        "User",
        fields=[
            user_table.name,
            user_table.email,
            open_todo_subquery # Using the subquery object
        ],
        filters={"user_type": "System User"}
    )
    users_with_counts = query.run(as_dict=True)
    

    过滤器中的 Pypika 对象

    from frappe.query_builder import Field, functions
    
    # Define Pypika objects
    task_table = frappe.qb.DocType("Task")
    modified_field = task_table.modified
    creation_field = task_table.creation
    subject_field = task_table.subject
    status_field = task_table.status
    
    # Build complex criterion
    complex_filter = (
        (modified_field > creation_field) & (functions.Length(subject_field) > 10)
    ) | (status_field == "Cancelled")
    
    # Use the Criterion object in filters
    query = frappe.qb.get_query(
        "Task",
        fields=["name", "subject", "status", "creation", "modified"],
        filters=complex_filter
    )
    results = query.run(as_dict=True)
    

    记录锁定

    用于数据库事务中,防止其他事务修改特定行:

    基本锁定

    # Lock specific Stock Ledger Entries
    query = frappe.qb.get_query(
        "Stock Ledger Entry",
        fields=["name", "qty_after_transaction"],
        filters={"item_code": "ITEM001", "warehouse": "WH001"},
        for_update=True # Adds FOR UPDATE clause, will wait if rows are locked
    )
    entries = query.run(as_dict=True)
    

    跳过已锁定行

    # Skip rows that are already locked by another transaction
    query = frappe.qb.get_query(
        "ToDo",
        fields=["name", "description"],
        filters={"status": "Pending"},
        limit=5,
        order_by="creation asc",
        for_update=True,
        skip_locked=True # Skip locked rows
    )
    available_tasks = query.run(as_dict=True)
    

    非阻塞锁定尝试

    # Fail immediately if rows are already locked
    try:
        query = frappe.qb.get_query(
            "System Settings",
            fields=["name"],
            filters={"name": "System Settings"},
            for_update=True,
            wait=False # Don't wait for locks
        )
        settings = query.run(as_dict=True)
    except Exception as e:
        print(f"Could not acquire lock immediately: {e}")
    

    安全注意事项

    frappe.qb.get_query 在设计时充分考虑了安全性:

    • 字段验证: 字段名称会按照严格的模式进行验证,以防止 SQL 注入。
    • 参数化: 过滤器值由数据库驱动程序进行参数化处理。
    • 权限执行: 使用 ignore_permissions=False 可充分利用 Frappe 强大的权限系统。

    请始终确保,如果用于构建过滤器键或字段名称的任何动态值来自不受信任的来源,都经过适当的清理。始终依赖将用户输入作为过滤器值传递。

    由 Claude Sonnet 4 编写。经人工审核。

    查询构建器

    frappe.qb 是一个基于 PyPika 构建的查询构建器,用于为跨数据库查询提供统一接口。

    在开发应用程序时,您经常需要从数据库中检索特定数据。一种方法是使用 frappe.db.sql 并编写原始 SQL 查询。

    可能类似于这样

    result = frappe.db.sql(
     f"""
     SELECT `path`,
     COUNT(*) as count,
     COUNT(CASE WHEN CAST(`is_unique` as Integer) = 1 THEN 1 END) as unique_count
     FROM `tabWeb Page View`
     WHERE `creation` BETWEEN {some_date} AND {some_later_date}
     """
    )
    

    查询构建器 API 通过提供简单的 Pythonic API 来构建 SQL 查询,同时不限制手写 SQL 的灵活性,从而使这一过程更加容易。

    同样的查询在查询构建器中看起来会是这样

    import frappe
    from frappe.query_builder import DocType
    from frappe.query_builder.functions import Count
    from pypika.terms import Case
    
    WebPageView = DocType("Web Page View") # you can also use frappe.qb.DocType to bypass an import
    
    count_all = Count('*').as_("count")
    case = Case().when(WebPageView.is_unique == "1", "1")
    count_is_unique = Count(case).as_("unique_count")
    
    result = (
     frappe.qb.from_(WebPageView)
     .select(WebPageView.path, count_all, count_is_unique)
     .where(Web_Page_View.creation[some_date:some_later_date])
    ).run()
    

    frappe.qb

    返回一个 Pypika 查询对象,用于构建查询。使用此对象构建的查询将是 pypika.dialects 中的类型,并带有一些 Frappe 的增强功能。它的一些方法包括:

    frappe.qb.from_(doctype)

    允许您构建一个 from 查询来选择数据。

    选择查询

    query = frappe.qb.from_('Customer').select('id', 'fname', 'lname', 'phone')
    

    构建的 SQL 查询为

    SELECT `id`,`fname`,`lname`,`phone` FROM `tabCustomer`
    

    一个复杂的 Select 示例

    customers = frappe.qb.DocType('Customer')
    q = (
     frappe.qb.from_(customers)
     .select(customers.id, customers.fname,customers.lname, customers.phone)
     .where((customers.fname == 'Max') | (customers.id.like('RA%')) )
     .where(customers.lname == 'Mustermann')
    )
    

    构建的 SQL 查询为

    SELECT `id`,`fname`,`lname`,`phone` FROM `tabCustomer` WHERE (`fname`='Max' OR `id` LIKE 'RA%') AND `lname`='Mustermann'
    

    一些值得注意的事项

    • 我们创建了一个 customers 变量来引用查询中的表。
    • Select 可以接受任意数量的参数,选择各种字段。
    • 可以使用 ‘|’(管道符)或 ‘&’(与符号)运算符来表示 ‘OR’ 或 ‘AND’。
    • 链式调用 where() 方法默认会追加 ‘AND’。

    您可以在 Pypika 仓库中阅读有关其他函数的更多信息。

    frappe.qb.Doctype(name_of_table)

    返回一个 PyPika 表对象,可在其他地方使用。如有必要,它会自动添加 ‘tab’ 前缀。

    frappe.qb.Table(name_of_table)

    frappe.qb.DocType 功能相同,但不会追加 ‘tab’ 前缀。它旨在用于像 ‘__Auth’ 这样的表。

    注意:只有在您清楚自己在做什么的情况下才应使用此功能。

    frappe.qb.Field(name_of_coloum)

    返回一个 PyPika 字段对象,代表一个列。它们通常用于将列与值进行比较。

    一个例子是

    lname = frappe.qb.Field("lname")
    q = frapppe.qb.from_("customers").select("*").where(lname == 'Mustermann')
    

    执行查询

    使用 frappe.qb 命名空间构建的查询是 PyPika 对象。它们必须转换为字符串对象,以便您的数据库管理系统能够识别它们。

    要检查您的查询对象如何转换,您可以使用 str 进行类型转换,或使用它们自带的 .get_sql 方法。

    query = frappe.qb.from_('Customer').select('id', 'fname', 'lname', 'phone')
    
    str(query)
    # SELECT "id","fname","lname","phone" FROM "tabCustomer"
    
    query.get_sql()
    # SELECT "id","fname","lname","phone" FROM "tabCustomer"
    
    str(query) == query.get_sql()
    # True
    

    Walk 方法

    所有通过 frappe.qb 构建的查询默认都是参数化的。所有输入字段、原始值和函数都被分离为命名参数,并以字典形式发送到数据库。参数化是为了净化查询,防止 SQL 注入。

    您可以使用 walk 方法查看哪些部分被参数化了。它返回参数化的查询和相应的字典。

    doctype = frappe.qb.DocType("DocType")
    
    frappe.qb.from_(doctype).select('*').where(doctype.name == "somename").walk()
    # ('SELECT * FROM `tabDocType` WHERE `name`=%(param1)s', {'param1': 'somename'})
    

    Run 方法

    这是执行查询最推荐的方法。每个有效的查询都有 run 方法,您可以使用它来执行查询。

    frappe.qb.from_('Customer').select('id', 'fname', 'lname', 'phone').run()
    

    run 方法接受 kwargs,这些参数将在查询执行时传递。您可以通过 run 方法传递 frappe.db.sql 中可用的任何选项。

    要对查询进行调试,或以 List[Dict] 的形式获取结果,您可以分别使用以下方法:

    In [7]: frappe.qb.from_('ToDo').select('name').run(debug=True)
    SELECT "name" FROM "tabToDo"
    Execution time: 0.0 sec
    Out[7]: [('8d765f73a2',)]
    
    In [8]: frappe.qb.from_('ToDo').select('name').run(as_dict=True)
    Out[8]: [{'name': '8d765f73a2'}]
    

    run 方法在内部调用更底层的 frappe.db.sql API。

    frappe.db.sql

    您也可以选择直接将查询对象传递给 frappe.db.sql。但这会忽略查询的权限和参数化。

    query = frappe.qb.from_('Customer').select('id', 'fname', 'lname', 'phone')
    frappe.db.sql(query)
    

    frappe.query_builder.functions

    此模块提供了您在构建查询时可能需要的标准函数,例如 Count()Sum().

    连接和子查询

    您可以查看 pypika 文档来了解如何连接表和添加子查询。请使用 frappe.qb.DocType 代替 Table

    示例:

    HasRole = frappe.qb.DocType('Has Role')
    CustomRole = frappe.qb.DocType('Custom Role')
    
    query = (frappe.qb.from_(HasRole)
     .inner_join(CustomRole)
     .on(CustomRole.name == HasRole.parent)
     .select(CustomRole.page, HasRole.parent, HasRole.role))
    

    简单函数

    假设您想计算 Notes 表中的所有条目。您可以这样做

    from frappe.query_builder.functions import Count
    
    Notes = frappe.qb.DocType("Notes")
    count_pages = Count(Notes.content).as_("Pages")
    
    result = frappe.qb.from_(Notes).select(count_pages).run(as_dict=True)
    

    JSON 函数

    注意:此功能在 v16+ 版本中可用。

    这些辅助函数通过在内部映射到正确的 SQL 方言,使 JSON 查询能够在 MariaDB 和 Postgres 之间移植。

    使用场景:

    1. 按路径读取 JSON 对象值
    2. 读取标量/文本值以进行过滤
    3. 检查 JSON 对象/数组是否包含某个值

    可用的辅助函数:

    1. JSONExtract(field, path)
    2. JSONValue(field, path)
    3. JSONContains(target, candidate)

    示例:JSON 对象字段

    import frappe
    from frappe.query_builder.functions import JSONExtract, JSONValue
    
    CustomerProfile = frappe.qb.DocType("Customer Profile")
    
    # preferences_json:
    # {"notifications": {"email": true, "sms": false}, "language": "en"}
    
    query = (
        frappe.qb.from_(CustomerProfile)
        .select(
            CustomerProfile.customer_name,
            JSONValue(CustomerProfile.preferences_json, "$.language").as_("preferred_language"),
            JSONValue(CustomerProfile.preferences_json, "$.notifications.email").as_("email_notifications"),
        )
        .where(JSONValue(CustomerProfile.preferences_json, "$.notifications.email") == "true")
    )
    

    示例:JSON 列表字段

    import frappe
    from frappe.query_builder.functions import JSONContains, JSONExtract
    
    SalesOrder = frappe.qb.DocType("Sales Order")
    
    # applied_discounts_json:
    # {"codes": ["WELCOME10", "FREESHIP", "VIP"]}
    
    query = (
        frappe.qb.from_(SalesOrder)
        .select(SalesOrder.name, SalesOrder.customer)
        .where(JSONContains(JSONExtract(SalesOrder.applied_discounts_json, "$.codes"), "FREESHIP"))
    )
    

    自定义函数

    frappe.query_builder.functionspypika.functions 的超集,因此它拥有所有 PyPika 函数以及我们创建的一些自定义函数。您可以通过从 PyPika 导入 CustomFunction 类来创建自定义函数。

    DateDiff 函数的一个实现

    from pypika import CustomFunction
    
    customers = Tables('Customer')
    DateDiff = CustomFunction('DATE_DIFF', ['interval', 'start_date', 'end_date'])
    
    q = Query.from_(customers).select(
     DateDiff('day', customers.created_date, customers.updated_date)
    )
    

    如果我们打印 q,我们会得到

    SELECT DATE_DIFF('day',"created_date","updated_date") FROM "Customer"
    

    请注意我们如何指定参数和实际的 SQL 文本。确切的格式可能不适用于更复杂的函数。高级部分涵盖了更复杂的方法。

    常量列

    ConstantColumn 是一个用于定义具有常量值的伪列的类。

    from frappe.query_builder.custom import ConstantColumn
    
    frappe.qb.from_("DocType").select("name", ConstantColumn("john").as_("user"))
    # SELECT `name`,'john' `user` FROM `tabDocType`
    

    这里我们定义了一个值为“john”的列 user。

    高级

    特殊函数

    其中一个这样的函数是 Match Against。它之所以特殊,是因为它有一个链式的 against 参数。要实现类似的功能,你需要继承 PyPika 的 DistinctOptionFunction 类。

    当前的 MATCH 类看起来像这样

    
    from pypika.functions import DistinctOptionFunction
    from pypika.utils import builder
    
    class MATCH(DistinctOptionFunction):
     def __init__(self, column: str, *args:
     super(MATCH, self)._init_(" MATCH", column, *args)
     self._Against = False
    
     def get_function_sql(self, **kwargs):
     s = super(DistinctOptionFunction, self).get_function_sql(**kwargs)
    
     if self._Against:
     return f"{s} AGAINST (f'+{self._Against}*') IN BOOLEAN MODE)"
     return s
    
     @builder
     def Against(self, text: str):
     self._Against = text
    
    • __init__() 方法的工作方式类似于上面的 CustomFunction 类。你需要列出所有参数和 SQL 文本。
    • Against() 方法仅存储一个值,该值将在 get_function_sql() 中使用
    • 它还有 @builder 包装器。简而言之,它通过复制对象使这些函数可以链式调用。
    • 我们包装了 get_function_sql() 方法,这使我们能够追加 Against 所需的 SQL 文本。
    • 这可以进一步扩展以使用任意数量的其他链。

    在使用中,Match 类看起来像这样

    from frappe.query_builder.functions import Match
    
    match = Match("Coloum name").Against("Some_text_match")
    # MATCH('Coloum name') AGAINST ('+Some_text_match*' IN BOOLEAN MODE)
    

    工具

    ImportMapper(dict)

    在极少数情况下,对于不同的 SQL 方言,你有不同的函数,但它们执行相同的操作,你可以使用 ImportMapper 工具。它根据 SQL 方言映射函数,因此一个查询可以在不同的 SQL 方言中工作。

    它接受一个将函数映射到数据库的字典。

    例如,GroupConat 的映射看起来像这样

    
    from frappe.query_builder.utils import ImportMapper, db_type_is
    from frappe.query_builder.custom import GROUP_CONCAT, STRING_AGG
    
    GroupConcat = ImportMapper(
     {
     db_type_is.MARIADB: GROUP_CONCAT,
     db_type_is.POSTGRES: STRING_AGG
     }
    )
    

    对话框API

    Frappe 提供了一组标准、交互式且灵活的对话框,易于配置和使用。此外,还有一个更全面的 Javascript API。

    frappe.msgprint

    frappe.msgprint(msg, title, raise_exception, as_table, as_list, indicator, primary_action, is_minimizable, wide, realtime)

    此方法仅在请求/响应周期内有效。它会向发起请求并登录 Desk 的用户显示一条消息。

    参数列表包括:

    • msg:要显示的消息
    • title:模态框的标题
    • as_table:如果 msg 是列表的列表,则渲染为 HTML 表格
    • as_list:如果 msg 是列表,则渲染为 HTML 无序列表
    • primary_action:绑定一个主要的服务器端/客户端操作。
    • raise_exception:异常
    • is_wide:显示一个宽模态框
    • is_minimizable:允许用户最小化模态框
    • realtime:使用 websocket 立即发布,而不是添加到响应消息日志中
    frappe.msgprint(
        msg='This file does not exist',
        title='Error',
        raise_exception=FileNotFoundError
    )
    

    frappe.msgprint

    primary_action 可以包含一个 server_action client_side 操作,该操作必须包含指向相应方法的点分路径。JavaScript 函数必须是全局可用的函数。您还可以传递 hide_on_success 以在操作成功完成后关闭消息。

    # msgprint with server and client side action
    frappe.msgprint(msg='This file does not exist',
        title='Error',
        raise_exception=FileNotFoundError
        primary_action={
            'label': _('Perform Action'),
            'server_action': 'dotted.path.to.server.method',
            'client_action': 'dotted.path.to.client.method',
            'hide_on_success': True,
            'args': args
        }
    )
    

    带主要操作的 frappe.msgprint

    frappe.throw

    frappe.throw(msg, exc, title, is_minimizable, wide, as_list, primary_action)

    此方法将引发异常并在 Desk 中显示消息。它本质上是 frappe.msgprint 的封装。

    exc 可以传递一个可选的异常。默认情况下,它将引发一个 ValidationError 异常。

    frappe.throw(
        title='Error',
        msg='This file does not exist',
        exc=FileNotFoundError
    )
    

    frappe.throw

    全文搜索API

    用于 Whoosh 的 Frappe 封装器

    update_index_by_name(self, doc_name)

    封装 update_index 方法,根据名称获取文档并更新索引。此函数会更改当前用户,应仅以管理员身份运行或在后台任务中运行。

    参数:

    • self (对象):全文搜索实例
    • doc_name (字符串):要更新的文档名称

    remove_document_from_index(self, doc_name)

    从搜索索引中移除文档

    参数:

    • self (对象):全文搜索实例
    • doc_name (字符串):要移除的文档名称

    update_index(self, document)

    更新文档的搜索索引

    参数:

    • self (对象):全文搜索实例
    • document (字典):包含标题、路径和内容的字典

    build_index(self)

    为所有已解析的文档构建索引

    search(self, text, scope=None, limit=20)

    从当前索引中进行搜索

    参数:

    • text (字符串):要搜索的文本
    • scope (字符串,可选):限制搜索范围。默认为 None。
    • limit (整数,可选):限制搜索结果数量。默认为 20。

    返回:

    • [列表(字典)]:搜索结果

    REST API

    Frappe 框架会自动为您的所有 DocType 生成 REST API。您还可以使用它们的点分模块路径来运行任意的 Python 方法。

    身份验证

    有两种通过 Frappe REST API 进行身份验证的方式:基于令牌的身份验证和基于密码的身份验证。

    1. 基于令牌的身份验证

    令牌由 API 密钥(API Key)和 API 机密(API Secret)组成。要生成这些令牌,请按照以下步骤操作:

    1. 前往用户列表并打开一个用户。
    2. 点击“设置”选项卡。(如果您看不到选项卡,请跳过此步骤)
    3. 展开“API 访问”部分,然后点击“生成密钥”。
    4. 您将看到一个包含 API 机密的弹出窗口。复制此值并将其保存在安全的地方(例如密码管理器)。
    5. 您还会在此部分看到另一个字段“API 密钥”。

    令牌是通过使用冒号 : 连接 api_keyapi_secret 生成的。将字符串 token api_key:api_secret 传递给请求中的 Authorization 请求头。

    fetch('http://<base-url>/api/method/frappe.auth.get_logged_user', {
        headers: {
            'Authorization': 'token api_key:api_secret'
        }
    })
    .then(r => r.json())
    .then(r => {
        console.log(r);
    })
    
    ➜ curl http://<base-url>/api/method/frappe.auth.get_logged_user -H "Authorization: token api_key:api_secret"
    

    您使用这些令牌发出的每个请求都将记录在您在步骤 1 中选择的用户名下。这也意味着将针对该用户检查角色权限。您也可以创建一个仅用于 API 调用的新用户。

    2. 基于密码的身份验证

    基于密码的身份验证依赖于 Cookie 和会话数据来维持后续请求的身份验证状态。在大多数情况下,您用于发出 REST 调用的库会处理会话数据,但如果它不处理,您应该使用基于令牌的身份验证。

    fetch('http://<base-url>/api/method/login', {
        method: 'POST',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            usr: 'username or email',
            pwd: 'password'
        })
    })
    .then(r => r.json())
    .then(r => {
        console.log(r);
    })
    
    ➜ curl --cookie-jar snowcookie --request POST "http://<base-url>/api/method/login" -H 'Content-Type: application/json' -H 'Accept: application/json' --data-raw "{ "usr" : "<username>", "pwd": "<password>" }"
    {"message":"Logged In","home_page":"/app","full_name":"<user:full_name>","dashboard_route":"/sites"}
    
    ➜ curl --cookie snowcookie --request POST "http://<base-url>/api/method/frappe.auth.get_logged_user" -H 'Accept: application/json'
    {"message":"<username>"}
    

    3. 访问令牌

    请参阅有关如何设置 OAuth 的文档。

    在请求头中使用生成的 access_token

    fetch('http://<base-url>/api/method/frappe.auth.get_logged_user', {
        headers: {
            'Authorization': 'Bearer access_token'
        }
    })
    .then(r => r.json())
    .then(r => {
        console.log(r);
    })
    

    列出文档

    要获取某个 DocType 的记录列表,请向 /api/resource/:doctype 发送 GET 请求。默认情况下,它将返回 20 条记录,并且仅获取记录的 name 字段。查询结果可以在响应的 data 字段下找到。

    我们将使用 ToDo DocType 来展示以下查询的示例响应。

    GET /api/resource/:doctype
    

    响应

    {
      "data":[
        {"name":"f765eef382"},
        {"name":"2a26fa1c64"},
        {"name":"f32c68060f"},
        {"name":"9065fa9832"},
        {"name":"419082fc38"},
        {"name":"6234d15099"},
        {"name":"62f2181ee0"},
        {"name":"a50afbbfaa"},
        ...
      ]
    }
    

    您可以在 fields 参数中指定要获取的字段。它应该是一个 JSON 数组。

    GET /api/resource/:doctype?fields=["field1", "field2"]
    

    响应

    {
      "data":[
        {"description":"Business worker talk society. Each try theory prove notice middle. Crime couple trouble guy project hit.","name":"f765eef382"},
        {"description":"This reveal as look near sister. Car staff bar specific address.","name":"2a26fa1c64"},
        {"description":"Wear bag some walk. Movie partner new class tough run. Brother Democrat imagine.","name":"f32c68060f"},
        {"description":"Break laugh apply reveal new now focus heavy. Outside local staff research total. Else point try despite.","name":"9065fa9832"},
        {"description":"Truth reduce baby artist actually model. Cost phone us others himself wife almost. Language thing wonder share talk. Factor glass significant could window certain yet.","name":"419082fc38"},
        {"description":"Tv memory understand opportunity window beat physical.","name":"6234d15099"},
        {"description":"Should floor situation in response sell. Our assume company mean red majority shoulder.","name":"62f2181ee0"},
        {"description":"Performance seem sign recent. Court form me tonight simple trouble. Address job garden play teach. Happy speech amount offer change then.","name":"a50afbbfaa"},
        ...
      ]
    }
    

    您可以在 expand 参数中指定要展开的字段。它应该是一个 JSON 数组。

    GET /api/resource/:doctype?expand=["priority"]
    

    响应

    {
      "data":[
        {
            "name":"f765eef382"
            "priority": {
                "name":"a1b2c3", 
                "title": "Medium", 
                "creation": "2025-11-05 19:02:19.106966",
            },
        },
        {
            "name":"f765eef393"
            "priority": {
                "name":"a1b2c4", 
                "title": "High", 
                "creation": "2025-11-05 20:02:19.106966",
            },
        },
        ...
      ]
    }
    

    您可以通过传递 filters 参数来过滤记录。过滤器应该是一个数组,其中每个过滤器的格式为:[field, operator, value]

    GET /api/resource/:doctype?filters=[["field1", "=", "value1"], ["field2", ">", "value2"]]
    

    响应

    {
      "data":[
        {"name":"f765eef382"},
        {"name":"2a26fa1c64"},
        {"name":"f32c68060f"},
        {"name":"9065fa9832"},
        {"name":"419082fc38"},
        {"name":"6234d15099"},
        {"name":"62f2181ee0"},
        {"name":"a50afbbfaa"},
        ...
      ]
    }
    

    filters 参数使用 AND SQL 运算符连接所有指定的过滤器,如果您需要 OR 过滤器,则可以使用 or_filters 参数。or_filters 的语法与 `filters` 相同。

    您还可以提供排序字段和排序顺序。其格式应为 fieldname ascfieldname desc。空格应进行 URL 编码。在下面一行中,我们假设字段名为 title

    GET /api/resource/:doctype?order_by=title%20desc
    

    您还可以通过提供 limit_startlimit_page_length 参数来对结果进行分页。

    GET /api/resource/:doctype?limit_start=5&limit_page_length=10
    

    响应

    {
      "data": [
        {"name":"6234d15099"},
        {"name":"62f2181ee0"},
        {"name":"a50afbbfaa"},
        {"name":"aa12a5cf71"},
        {"name":"6ac9800d4e"},
        {"name":"4bcf8b701c"},
        {"name":"aee15f4c20"},
        {"name":"6ba753afef"},
        ...
      ]
    }
    

    limitlimit_page_length 的别名,用于在版本 13 中访问 /api/resource。这意味着以下请求也应返回与上述查询相同的响应。

    GET /api/resource/:doctype?limit_start=5&limit=10
    

    默认情况下,您将收到 List[dict] 格式的数据。您可以通过传递 as_dict=False 来以 List[List] 格式检索数据。

    GET /api/resource/:doctype?limit_start=5&limit=5&as_dict=False
    

    响应

    {
      "data": [
        ["6234d15099"],
        ["62f2181ee0"],
        ["a50afbbfaa"],
        ["aa12a5cf71"],
        ["6ac9800d4e"]
      ]
    }
    

    要调试为您的请求构建的查询,您可以在请求中传递 debug=True。这将在响应的 exc 字段下返回已执行的查询和执行时间。

    GET /api/resource/:doctype?limit_start=10&limit=5&debug=True
    

    响应

    {
      "data": [
        {"name":"4bcf8b701c"},
        {"name":"aee15f4c20"},
        {"name":"6ba753afef"},
        {"name":"f4b7e24abc"},
        {"name":"bd9156096c"}
      ],
      "exc": "[\"select `tabToDo`.`name`\\n\\t\\t\\tfrom `tabToDo`\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t order by `tabToDo`.`modified` DESC\\n\\t\\t\\tlimit 5 offset 10\", \"Execution time: 0.0 sec\"]"
    }
    

    CRUD 操作

    Frappe 会自动为所有 DocType 生成用于 CRUD 操作的 REST 端点。请确保在您的请求中设置以下请求头,以便获得正确的 JSON 响应。

    {
        "Accept": "application/json",
        "Content-Type": "application/json",
    }
    

    创建

    通过向 /api/resource/:doctype 发送 POST 请求来创建新文档。在请求体中发送 JSON 格式的文档。

    POST /api/resource/:doctype
    
    # Body
    {"description": "New ToDo"}
    

    响应

    {
      "data": {
        "name": "af2e2d0e33",
        "owner": "Administrator",
        "creation": "2019-06-03 14:19:00.281026",
        "modified": "2019-06-03 14:19:00.281026",
        "modified_by": "Administrator",
        "idx": 0,
        "docstatus": 0,
        "status": "Open",
        "priority": "Medium",
        "description": "New ToDo",
        "doctype": "ToDo"
      }
    }
    

    读取

    通过向 /api/resource/:doctype/:name 发送 GET 请求来获取文档。

    GET /api/resource/:doctype/:name
    

    响应

    {
      "data": {
        "name": "bf2e760e13",
        "owner": "Administrator",
        "creation": "2019-06-03 14:19:00.281026",
        "modified": "2019-06-03 14:19:00.281026",
        "modified_by": "Administrator",
        "idx": 0,
        "docstatus": 0,
        "status": "Open",
        "priority": "Medium",
        "description": "
    <p>Test description</p>",
        "doctype": "ToDo"
      }
    }
    

    通过向 /api/resource/:doctype/:name?expand_links=True 发送 GET 请求来展开所有链接字段。

    GET /api/resource/:doctype/:name?expand_links=True
    

    响应

    {
      "data": {
        "name": "bf2e760e13",
        "owner": "Administrator",
        "creation": "2019-06-03 14:19:00.281026",
        "modified": "2019-06-03 14:19:00.281026",
        "modified_by": "Administrator",
        "idx": 0,
        "docstatus": 0,
        "status": "Open",
        "priority": {
            "name":"a1b2c3", 
            "title": "Medium", 
            "creation": "2025-11-05 19:02:19.106966",
        },
        "description": "
    <p>Test description</p>",
        "doctype": "ToDo"
      }
    }
    

    更新

    通过向 /api/resource/:doctype/:name 发送 PUT 请求来更新文档。您无需发送整个文档,只需发送要更新的字段即可。

    PUT /api/resource/:doctype/:name
    
    # Body
    {"description": "New description"}
    

    响应

    {
      "data": {
        "name": "bf2e760e13",
        "owner": "Administrator",
        "creation": "2019-06-03 14:19:00.281026",
        "modified": "2019-06-03 14:21:00.785117",
        "modified_by": "Administrator",
        "idx": 0,
        "docstatus": 0,
        "status": "Open",
        "priority": "Medium",
        "description": "New description",
        "doctype": "ToDo"
      }
    }
    

    删除

    通过向 /api/resource/:doctype/:name 发送 DELETE 请求来删除文档。

    DELETE /api/resource/:doctype/:name
    

    响应

    {"message": "ok"}
    

    远程方法调用

    Frappe 允许您使用 REST API 触发任意的 Python 方法来处理自定义逻辑。这些方法必须被标记为 白名单 才能通过 REST 访问。

    要运行位于 frappe.auth.get_logged_user 的白名单 Python 方法,请向端点 /api/method/frappe.auth.get_logged_user 发送请求。

    GET /api/method/frappe.auth.get_logged_user
    

    响应

    {
      "message": "[email protected]"
    }
    
    • 如果您的方法返回一些值,您应该发送一个 GET 请求。
    • 如果您的方法更改了数据库的状态,请使用 POST。在成功的 POST 请求之后,框架将自动调用 frappe.db.commit() 将更改提交到数据库。
    • 成功的响应将返回一个包含 message 键的 JSON 对象。
    • 出错的响应将返回一个包含 exc 键的 JSON 对象,该键包含堆栈跟踪,以及包含所抛出异常的 exc_type 键。
    • 方法的返回值将被转换为 JSON 并作为响应发送。

    文件上传

    有一个专门的方法 /api/method/upload_file,它接受二进制文件数据并将其上传到系统中。

    以下是它的 curl 命令:

    ➜ curl -X POST \
      http://<base-url>/api/method/upload_file \
      -H 'Accept: application/json' \
      -H 'Authorization: token xxxx:yyyy' \
      -F file=@/path/to/file/file.png
    

    如果您使用客户端 Javascript 上传文件,您可以将上传的文件附加为 FormData 并发送 XHR 请求。以下是 Frappe Desk 中的实现代码。

    钩子

    钩子(Hooks)允许你“挂钩”到 Frappe 框架核心部分的功能和事件中。此页面记录了框架提供的所有钩子。

    跳转到 Frappe 中所有可用钩子的列表。

    钩子是如何工作的?

    钩子是核心代码中的一些位置,允许应用覆盖标准实现或对其进行扩展。钩子定义在你应用的 hooks.py 文件中。

    让我们通过示例来学习。在你应用的 hooks.py 文件中添加以下钩子。

    test_string = "value"
    test_list = ["value"]
    test_dict = {
        "key": "value"
    }
    

    现在,通过运行命令 bench --site sitename console 打开 Python 控制台,并运行以下代码行:

    ❯ bench --site sitename console
    Apps in this namespace:
    frappe, frappe_docs
    
    In [1]: frappe.get_hooks("test_string")
    Out[1]: ["value"]
    
    In [2]: frappe.get_hooks("test_dict")
    Out[2]: {"key": ["value"]}
    
    In [3]: frappe.get_hooks("test_list")
    Out[3]: ["value"]
    

    当你调用 frappe.get_hooks 时,它会将列表中的所有值进行转换。这意味着如果钩子在多个应用中定义,则会从这些应用中收集值。这就是实现钩子级联特性的方式。

    现在,钩子值可以通过不同方式使用。例如,使用 app_include_js 引入 JS 资源时,会包含所有值。但对于覆盖白名单方法,则使用列表中的最后一个值。

    因此,钩子的实现完全取决于功能作者打算如何使用它。

    如何解决钩子冲突?

    钩子使用“最后写入者获胜”策略进行解析。站点上最后安装的应用将拥有最高优先级。

    • 当钩子覆盖现有行为(如覆盖类)时,只有来自最后安装的应用的覆盖才会生效。
    • 当钩子扩展行为时,扩展将按照在站点上的安装顺序应用。

    如果你需要更改此顺序,可以前往“已安装应用”页面,并点击“更新钩子解析顺序”。

    应用元数据

    这些是在你创建新应用时自动生成的。大多数情况下,你无需更改此处任何内容。

    1. app_name – 应用的 slug 化名称
    2. app_title – 可展示的应用名称
    3. app_publisher
    4. app_description
    5. app_version
    6. app_icon
    7. app_color

    JavaScript / CSS 资源

    以下钩子允许你在站点的各个部分注入静态 JS 和 CSS 资源。

    后台工作台

    这些钩子允许你在渲染后台工作台的 desk.html 中注入 JS / CSS。

    # injected in desk.html
    app_include_js = "assets/js/app.min.js"
    app_include_css = "assets/js/app.min.css"
    
    # All of the above support a list of paths too
    app_include_js = ["assets/js/app1.min.js", "assets/js/app2.min.js"]
    

    门户网站

    这些钩子允许你在渲染门户网站的 web.html 中注入 JS / CSS。

    # injected in the web.html
    web_include_js = "assets/js/app-web.min.js"
    web_include_css = "assets/js/app-web.min.css"
    # All of the above support a list of paths too
    web_include_js = ["assets/js/web1.min.js", "assets/js/web2.min.js"]
    

    网页表单

    这些钩子允许你在用于渲染网页表单的 web_form.html 中添加静态 JS 和 CSS 资源。这些仅适用于标准网页表单。

    webform_include_js = {"ToDo": "public/js/custom_todo.js"}
    webform_include_css = {"ToDo": "public/css/custom_todo.css"}
    

    对于用户创建的网页表单,你可以直接在表单本身中编写脚本。

    页面

    这些钩子允许你在标准后台页面中注入 JS 资源。

    page_js = {"page_name" : "public/js/file.js"}
    

    例如,后台作业是 Frappe 框架核心模块中的一个标准页面。要在该页面中添加自定义行为,你可以在自定义应用中添加一个 JS 文件 custom_app/public/js/custom_background_jobs.js,并在你的钩子文件中添加以下代码行。

    custom_app/hooks.py

    page_js = {"background_jobs": "public/js/custom_background_jobs.js"}
    

    声音

    Frappe 附带了一组音频通知,用于成功操作、文档提交、错误等事件。你可以使用 sounds 钩子添加自己的声音。

    app/hooks.py

    sounds = [
        {"name": "ping", "src": "/assets/app/sounds/ping.mp3", "volume": 0.2}
    ]
    

    你可以使用客户端工具方法播放你添加的声音:

    frappe.utils.play_sound("ping")
    

    安装钩子

    这些钩子允许你在应用安装之前和之后运行代码。例如,ERPNext 定义了这些。

    # python module path
    before_install = "app.setup.install.before_install"
    after_install = "app.setup.install.after_install"
    after_sync = "app.setup.install.after_sync"
    

    app/setup/install.py

    # will run before app is installed on site
    def before_install():
        pass
    
    # will run after app is installed on site
    def after_install():
        pass
    
    # will run after app fixtures are synced
    def after_sync():
        pass
    

    卸载钩子

    这些钩子允许你在应用卸载之前和之后运行代码。

    app/hooks.py

    before_uninstall = "app.setup.uninstall.before_uninstall"
    after_uninstall = "app.setup.uninstall.after_uninstall"
    

    app/setup/uninstall.py

    # will run before app is uninstalled from site
    def before_uninstall():
        pass
    
    # will run after app is uninstalled from site
    def after_uninstall():
        pass
    

    迁移钩子

    这些钩子允许你在通过命令 bench --site sitename migrate 在站点上运行迁移之前和之后运行代码。

    app/hooks.py

    before_migrate = "app.migrate.before_migrate"
    after_migrate = "app.migrate.after_migrate"
    

    app/migrate.py

    def after_migrate():
        # run code after site migration
        pass
    

    构建钩子

    此钩子允许你通过 bench build 命令扩展构建系统。它在完成 bench 上所有应用的构建后运行。构建钩子允许你在 bench build 结束时,在资源和翻译编译完成后运行代码。

    after_build

    在你自己的应用构建时运行。用于对你的应用的资源进行后处理。

    app/hooks.py

    after_build = "app.build.after_build"
    

    app/build.py

    def after_build() -> None:
        pass
    

    after_app_build

    在每次构建时运行,无论正在构建哪些应用。当您的应用为其他应用生成资源时使用它 – 例如,Frappe Studio 编译其他应用内附带的 studio 前端。

    app/hooks.py

    after_app_build = "papp.build.after_app_build"
    
    **app/hooks.py**
    
    <pre><code>after_app_build = "app.build.after_app_build"
    

    app/build.py

    def after_app_build(built_apps: list[str]) -> None:
        for app in built_apps:
            pass
    
    
    **app/build.py**
    
    
    python def after_app_build(built_apps: list[str]) -> None: for app in built_apps: pass # `built_apps` is the value of `–app`/`–apps`, or all apps on the bench when # `bench build` runs without arguments.

    测试钩子

    此钩子允许您在站点上运行测试之前执行代码。您可以使用此钩子向数据库添加种子数据,这些数据将可用于您的测试。

    app/hooks.py

    before_tests = "app.tests.before_tests"
    

    app/migrate.py

    def before_tests():
        # add seed data to the database
        pass
    

    文件钩子

    这些钩子允许您更改处理用户上传文件的实现方式。

    app/hooks.py

    before_write_file = "app.overrides.file.before_write"
    write_file = "app.overrides.file.write_file"
    delete_file_data_content = "app.overrides.file.delete_file"
    

    app/overrides/file.py

    # will run before file is written to disk
    def before_write():
        pass
    
    # will override the implementation of writing file to disk
    # can be used to upload files to a CDN instead of writing
    # the file to disk
    def write_file():
        pass
    
    # will override the implementation of deleting file from disk
    # can be used to delete uploaded files from a CDN instead of
    # deleting file from disk
    def delete_file():
        pass
    

    电子邮件钩子

    这些钩子允许您更改默认电子邮件模块中发送电子邮件和设置默认发件人地址的实现方式。

    app/hooks.py

    override_email_send = "app.overrides.email.send"
    get_sender_details = "app.overrides.email.get_sender_details"
    

    默认情况下,frappe 在所有电子邮件中使用当前登录用户的姓名和 ID 作为发件人详细信息。这可以通过 get_sender_details 钩子覆盖。如果您想通过使用第三方服务器或应用发送电子邮件来扩展电子邮件模块的功能,则可以使用 override_email_send 钩子。此钩子会将所有电子邮件信息(发件人、收件人、内容(mime))发送到自定义应用中的函数。

    app/overrides/email.py

    # will be edited as "John Doe <[email protected]>"
    def get_sender_details():
        return "John Doe", "[email protected]"
    
    # self - EmailQueue object refrence for updating status
    def send(self, sender, recipient, msg):
        # smtp or http request
        self.update_status("Sending")
    

    注意:您需要根据邮件提供商/服务器返回的 webhook 响应,在自定义应用中处理电子邮件队列的状态更改

    扩展启动信息

    成功登录后,Desk 会被注入一个名为 bootinfo 的全局值字典。bootinfo 在 Javascript 中作为全局对象 frappe.boot 可用。

    bootinfo 字典包含许多值,包括:

    • 系统默认值
    • 通知状态
    • 权限
    • 用户设置
    • 语言和时区信息

    您可以通过 extend_bootinfo 钩子添加对您的应用有意义的全局值。

    # python module path
    extend_bootinfo = "app.boot.boot_session"
    

    该方法以一个参数 bootinfo 调用,您可以直接在其上添加/更新值。

    app/boot.py

    def boot_session(bootinfo):
     bootinfo.my_global_key = "my_global_value"
    

    现在,您可以在客户端代码的任何位置访问该值。

    console.log(frappe.boot.my_global_key)
    

    网站上下文

    当门户页面被渲染时,会构建一个包含页面可能需要的所有变量的字典。这个字典也称为 context。您可以使用这些钩子在此字典中添加或修改值。

    app/hooks.py

    website_context = {
        "favicon": "/assets/app/image/favicon.png"
    }
    update_website_context = "app.overrides.website_context"
    

    website_context 钩子是一个简单的键值对字典。使用此钩子进行简单的值覆盖。

    对于更复杂的场景,您可以使用 update_website_context 钩子,因为它允许您在 Python 方法中操作上下文字典。该方法以一个参数调用,即 context 字典。您可以直接通过修改它来更改上下文,或者返回一个将与 context 合并的字典。

    app/overrides.py

    def website_context(context):
     context.my_key = "my_value"
    

    网站控制器上下文

    Frappe 附带标准网页,如 /404/about。如果您想扩展这些页面的控制器上下文,可以使用 extend_website_page_controller_context 钩子。

    app/hooks.py

    extend_website_page_controller_context = {
        "frappe.www.404": "app.pages.context_404"
    }
    

    上述钩子配置将允许您扩展 404 页面的上下文,以便您可以添加自己的键或修改现有的键。

    app/pages.py

    def context_404(context):
        # context of the 404 page
        context.my_key = "my_value"
    

    具有动态路由的网页

    动态路由是其中包含动态值的路由。

    示例:

    /profile/<name>
    </name>

    这里“name”是动态部分,但渲染的是相同的个人资料页面。默认情况下,Frappe 支持来自“网页”文档类型的动态路由。要添加更多动态路由,可以使用 get_web_pages_with_dynamic_routes

    app/hooks.py

    get_web_pages_with_dynamic_routes = "script.get_web_pages_with_dynamic_routes"
    

    script.py

    def get_web_pages_with_dynamic_routes():
     return [{
           "doctype": "Custom Web Page", // Doctype extended from WebsiteGenerator https://frappeframework.com/docs/user/en/guides/portal-development/generators
           "route": "/profile/
    <name>",
           "name": "profile-page" // name of the web view document to render
        }, ...]
    </name>

    网站清除缓存

    Frappe 框架缓存大量静态网页以便后续快速渲染。如果您创建了使用缓存值的网页,并且想要使缓存失效,此钩子就是执行此操作的地方。

    app/hooks.py

    website_clear_cache = "app.overrides.clear_website_cache"
    

    该方法以一个参数 path 调用。当为单个路由清除缓存时,path 被设置,当为所有路由清除缓存时,None 被设置。如果您的缓存是特定于页面的,您需要处理这种情况。

    app/overrides.py

    def clear_website_cache(path=None):
        if path:
            # clear page related cache
        else:
            # clear all cache
    

    网站重定向

    网站重定向允许您定义从一个路由到另一个路由的重定向。当请求源 URL 时,Frappe 将生成 304 重定向响应并重定向到目标 URL。您可以重定向普通 URL,也可以使用正则表达式来匹配您的 URL。

    app/hooks.py

    website_redirects = [
        {"source": "/compare", "target": "/comparison"},
        {"source": "/docs(/.*)?", "target": "https://docs.tennismart.com/\1"},
        {"source": r'/items/item\?item_name=(.*)', "target": '/items/\1', match_with_query_string=True},
    ]
    

    上述配置将导致以下重定向:

    • /compare/comparison
    • /docs/getting-startedhttps://docs.tennismart.com/getting-started
    • /docs/helphttps://docs.tennismart.com/help
    • /items/item?item_name=rackethttps://docs.tennismart.com/items/racket

    网站路由规则

    网站路由规则允许您将 URL 映射到自定义控制器。这通常用于为页面生成简洁的 URL。

    假设您希望 /projects 路由显示项目列表。这可以通过在 www 文件夹中创建 projects.htmlprojects.py 来实现。

    您还希望 /project/<name></name> 路由显示项目页面,其中 name 是动态的。为此,您可以使用 website_route_rules 钩子。

    app/hooks.py

    website_route_rules = [
        {"from_route": "/projects/
    <name>", "to_route": "app/projects/project"},
    ]
    </name>

    现在,您可以在 app/projects 文件夹中创建控制器文件。

    app/projects/project.py

    def get_context(context):
        project_name = frappe.form_dict.name
        project = frappe.get_doc("Project", project_name)
        context.project = project
    

    app/projects/project.html

    <h1>{{ project.title }}</h1>
    <p>{{ project.description }}</p>
    

    网站路径解析器

    Frappe 会执行一些标准的路径解析,例如,任何对 “/profile” 的请求都会在内部转换为 “/me”。可以使用 website_path_resolver 来覆盖此行为。

    app/hooks.py

    website_path_resolver = "path.to.custom_resolver_method"
    

    注意: 您的自定义解析方法将接收请求的路由,并应返回处理后的路由。

    网站 404

    当页面未找到时,Frappe 会渲染默认的 /404 路由。您可以使用 website_catch_all 钩子来更改此设置。

    app/hooks.py

    website_catch_all = "not_found"
    

    上述配置将在发生 404 错误时渲染 /not_found。您需要自行实现模板 www/not_found.html 和控制器 www/not_found.py

    默认首页

    首页是当您访问站点根 URL(/)时渲染的页面。有多种方法可以配置默认渲染哪个页面作为首页。

    默认情况下,首页是 index。因此,frappe 将尝试从 www 文件夹渲染 index.html。这可以使用 homepage 钩子来覆盖。

    app/hooks.py

    homepage = "homepage"
    

    上述配置将加载 www/homepage.html 作为默认首页。

    您还可以通过使用 role_home_page 钩子来设置基于角色的首页。

    app/hooks.py

    role_home_page = {
        "Customer": "orders",
        "Supplier": "bills"
    }
    

    上述配置将使 /orders 成为具有 客户 角色的用户的默认首页,并使 /bills 成为具有 供应商 角色的用户的默认首页。

    您还可以通过使用 get_website_user_home_page 钩子对逻辑进行更精细的控制。

    app/hooks.py

    get_website_user_home_page = "app.website.get_home_page"
    

    app/website.py

    def get_home_page(user):
        if is_projects_user(user):
            return "projects"
        if is_partner(user):
            return "partner-dashboard"
        return "index"
    

    如果所有这些钩子都已定义,则 get_website_user_home_page 的优先级将高于其他钩子,而 role_home_page 的优先级将高于 homepage

    门户侧边栏

    某些门户视图会显示带有链接的侧边栏,以便快速跳转到页面。这些侧边栏项目可以通过钩子进行自定义。

    app/hooks.py

    portal_menu_items = [
        {"title": "Dashboard", "route": "/dashboard", "role": "Customer"},
        {"title": "Orders", "route": "/orders", "role": "Customer"},
    ]
    

    上述配置将为具有客户角色的用户添加两个侧边栏链接。

    这些侧边栏项目在您的应用程序中是硬编码的,因此无法从 Desk 进行自定义。例如,如果您想临时隐藏某个侧边栏链接,则必须修改代码。

    还有另一个名为 standard_portal_menu_items 的钩子允许您执行此操作。在 standard_portal_menu_items 钩子中设置的侧边栏链接将与数据库同步。

    app/hooks.py

    standard_portal_menu_items = [
        {"title": "Dashboard", "route": "/dashboard", "role": "Website Manager"},
        {"title": "Orders", "route": "/orders", "role": "Website Manager"},
    ]
    

    上述配置会将侧边栏项目同步到门户设置,之后任何系统用户都可以对其进行编辑。

    品牌 HTML

    此钩子允许您自定义网站导航栏中的品牌标志。

    app/hooks.py

    brand_html = '<div><img src="tennismart.png"> TennisMart</div>'
    

    如果定义了 brand_html,它将覆盖导航栏中的默认品牌 HTML。除非您想对其进行版本控制,否则不建议使用钩子来更改品牌标志,否则您可以使用网站设置来更改它。

    基础模板

    渲染网页时,默认会扩展 templates/base.html。您可以通过覆盖 base_template 钩子来覆盖基础模板。

    app/hooks.py

    base_template = "app/templates/my_custom_base.html"
    

    您还可以根据路由自定义基础模板。例如,如果您想为所有以 docs/* 开头的路由使用不同的基础模板,则可以使用 base_template_map 钩子。键必须是匹配路由的正则表达式。所有其他路由将回退到默认基础模板。

    app/hooks.py

    base_template_map = {
        r"docs.*": "app/templates/doc_template.html"
    }
    

    集成

    这些钩子允许您自定义 Frappe 中第三方集成的行为。

    Braintree 成功页面

    此钩子允许您在 Braintree 交易成功支付后覆盖默认的重定向 URL。

    app/hooks.py

    braintree_success_page = "app.integrations.braintree_success_page"
    

    该方法使用一个参数 data 调用,该参数包含付款的元数据。

    app/integrations.py

    def braintree_success_page(data):
        # data.reference_doctype
        # data.reference_docname
        return "/thank-you"
    

    日历

    日历钩子是一个文档类型名称列表,这些名称在 Desk 的日历页面中显示为菜单项,以便快速导航。

    app/hooks.py

    calendars = ["Appointment"]
    

    清除缓存

    此钩子允许您在 Frappe 清除全局缓存时,清除您应用特定的缓存值。

    app/hooks.py

    clear_cache = "app.cache.clear_cache"
    

    您可以使用此钩子来清除您应用特定的缓存。该方法在调用时不带任何参数。

    app/cache.py

    def clear_cache():
        frappe.cache().hdel("app_specific_cache")
    

    如果您想设置 Frappe 发送的所有邮件的默认页脚,可以使用 default_mail_footer 钩子。

    app/hooks.py

    default_mail_footer = """
    
    <div>
     Sent via <a href="https://tennismart.com" target="_blank">TennisMart</a>
    </div>
    """
    

    现在,所有邮件的页脚都将显示 通过 TennisMart 发送

    会话钩子

    这些钩子在用户登录生命周期中被触发。on_login 在成功登录后立即触发,on_session_creation 在会话设置完成后触发,on_logout 在用户注销后触发。

    app/hooks.py

    on_login = "app.overrides.successful_login"
    on_session_creation = "app.overrides.allocate_free_credits"
    on_logout = "app.overrides.clear_user_cache"
    

    该方法将使用一个参数 login_manager 被调用。

    app/overrides.py

    def allocate_free_credits(login_manager):
        # allocate free credits to frappe.session.user
        pass
    

    认证钩子

    这些钩子在请求认证期间被触发。可以在此处验证自定义标头、授权标头,用户通过 frappe.set_user() 被验证并映射到请求。使用 frappe.requestfrappe.* 来验证请求并映射用户。

    app/hooks.py

    auth_hooks = ["app.overrides.validate_custom_jwt"]
    

    该方法将在请求认证期间被调用。

    app/overrides.py

    def validate_custom_jwt():
        # validate jwt from header, verify signature, set user from jwt.
        pass
    

    使用此方法检查传入的请求标头,验证标头并将用户映射到请求。如果标头验证失败,请勿抛出错误以继续使用其他钩子。未经验证的请求默认被视为“访客”请求。您可以使用第三方服务器、共享数据库或任何其他选择来验证和映射请求与用户。

    固定数据

    固定数据是当您安装和更新站点时,通过 JSON 文件同步的数据库记录。

    假设您希望在安装应用时在数据库中创建一组类别。为此,请在您的本地站点中创建这组类别,并将文档类型名称添加到 fixtures 钩子中。

    fixtures = [
        # export all records from the Category table
        "Category"
    ]
    

    现在,运行以下命令:

    bench --site sitename export-fixtures
    

    此命令将为每个文档类型创建一个 JSON 文件,其中包含生成记录列表所需的数据。您可以通过创建一个新站点并在该站点上安装您的应用来测试此功能。

    您还可以为导出记录添加条件。

    fixtures = [
        # export all records from the Category table
        "Category",
        # export only those records that match the filters from the Role table
        {"dt": "Role", "filters": [["role_name", "like", "Admin%"]]},
    ]
    

    某些字段仅供内部使用。系统会自动设置并保持这些字段的最新状态。这些字段不会被导出:modified_bycreationowneridxlftrgt。对于子表记录,以下字段不会被导出:docstatusdoctypemodifiedname

    文档钩子

    修改列表查询

    您可以通过使用 permission_query_conditions 钩子添加自定义匹配条件,来自定义 DocType 记录列表的查询方式。此匹配条件必须是 SQL 查询的有效 WHERE 子句片段。

    app/hooks.py

    permission_query_conditions = {
        "ToDo": "app.permissions.todo_query",
    }
    

    该方法使用单个参数 user 被调用,该参数可以是 None。该方法应返回一个字符串,该字符串是有效的 SQL WHERE 子句。

    app/permissions.py

    def todo_query(user):
        if not user:
            user = frappe.session.user
        # todos that belong to user or assigned by user
        return "(`tabToDo`.owner = {user} or `tabToDo`.assigned_by = {user})".format(user=frappe.db.escape(user))
    

    现在,如果您使用 frappe.db.get_list 方法,您的 WHERE 子句将被附加到查询中。

    todos = frappe.db.get_list("ToDo", debug=1)
    
    # output
    '''
    select `tabToDo`.`name`
    from `tabToDo`
    where ((`tabToDo`.owner = '[email protected]' or `tabToDo`.assigned_by = '[email protected]'))
    order by `tabToDo`.`modified` DESC
    '''
    

    此钩子只会影响 frappe.db.get_list 方法的结果,而不会影响 > frappe.db.get_all

    文档权限

    您可以使用 has_permission 钩子修改任何 DocType 的 doc.has_permission 文档方法的行为,并添加自定义权限检查逻辑。

    app/hooks.py

    has_permission = {
        "Event": "app.permissions.event_has_permission",
    }
    

    该方法将接收 docuserpermission_type 作为参数。它应返回 True 或一个 False 值。如果返回 None,它将回退到默认行为。

    app/permissions.py

    def event_has_permission(doc, user=None, permission_type=None):
        # when reading a document allow if event is Public
        if permission_type == "read" and doc.event_type == "Public":
            return True
    
        # when writing a document allow if event owned by user
        if permission_type == "write" and doc.owner == user:
            return True
    
        return False
    

    扩展 DocType 类

    注意:此功能在 v16+ 中可用

    您可以使用 extend_doctype_class 钩子扩展标准文档类型的类。此钩子允许您向现有的 DocType 类添加属性、方法和功能,而无需完全覆盖它们,从而使多个应用能够扩展同一个 DocType 类。

    app/hooks.py

    extend_doctype_class = {
        "Address": ["app.extensions.address.AddressMixin"]
    }
    

    app/extensions/address.py

    from frappe.model.document import Document
    
    class AddressMixin(Document):
        @property
        def full_address(self):
            return f"{self.address_line1}, {self.city}, {self.country}"
    
        def custom_validation(self):
            # Custom validation logic
            pass
    
        def validate(self):
            super.validate()
            self.custom_validation()
    

    多个扩展

    您可以为同一个 DocType 定义多个扩展。例如,ValidationMixin 可以用于 联系人 以及 地址,而 GeocodingMixin 仅用于 地址

    app/hooks.py

    extend_doctype_class = {
        "Address": [
            "app.extensions.address.GeocodingMixin",
            "app.extensions.common.ValidationMixin"
        ],
        "Contact": [
            "app.extensions.common.ValidationMixin"
        ]
    }
    

    钩子解析顺序

    当多个应用为同一个 DocType 定义扩展时,扩展将按照钩子解析顺序应用。如果应用按 frappeapp1app2 的顺序解析,最终的类将是:

    class ExtendedAddress(App2Mixin, App1Mixin, Address):
        pass
    

    override_doctype_class 的交互

    extend_doctype_class 钩子在 [override_doctype_class](#override-doctype-class) 之上工作。如果同时定义了这两个钩子,扩展将应用于被覆盖的类,而不是基类。

    例如,如果 ERPNext 覆盖了地址类,而自定义应用对其进行了扩展:

    # ERPNext overrides Address
    override_doctype_class = {
        "Address": "erpnext.setup.doctype.address.address.Address"
    }
    
    # Custom apps extend it
    extend_doctype_class = {
        "Address": ["app1.extensions.Prop1Mixin", "app2.extensions.Prop2Mixin"]
    }
    
    # Final class becomes:
    class ExtendedAddress(Prop2Mixin, Prop1Mixin, <a href="https://erpnext.yuannext.com">ERPNext</a>Address):
        pass
    

    此钩子非常适合添加虚拟字段、计算属性和自定义方法,而不会干扰核心功能。当您只需要添加功能而不是替换功能时,建议使用此钩子而不是 [override_doctype_class](#override-doctype-class)

    覆盖 DocType 类

    您可以使用 override_doctype_class 钩子来覆盖/扩展标准 DocType 的类。与 [extend_doctype_class](#extend-doctype-class) 不同,此钩子会完全替换原始类。当多个应用覆盖同一个 DocType 类时,这可能会导致问题。在 v16+ 中,建议改用 [extend_doctype_class](#extend-doctype-class)

    app/hooks.py

    override_doctype_class = {
        "ToDo": "app.overrides.todo.CustomToDo"
    }
    

    app/overrides/todo.py

    from frappe.desk.doctype.todo.todo import ToDo
    
    class CustomToDo(ToDo):
        def on_update(self):
            self.my_custom_code()
            super().on_update()
    
        def my_custom_code(self):
            pass
    

    建议您扩展 DocType 的标准类,否则您将不得不自行实现所有核心功能。

    覆盖表单脚本

    您可以使用 doctype_js 钩子来覆盖/扩展标准表单脚本。

    app/hooks.py

    doctype_js = {
        "ToDo": "public/js/todo.js",
    }
    

    app/public/js/todo.js

    frappe.ui.form.on("Todo", {
        refresh: function(frm) {
            frm.trigger("my_custom_code");
        },
        my_custom_code: function(frm){
            console.log(frm.doc.name)
        }
    });
    

    app/public/todo.js 中定义的事件/函数将扩展 ToDo DocType 的标准表单脚本中的事件/函数。

    CRUD 事件

    您可以使用 doc_events 钩子来挂钩任何 DocType 的各种 CRUD 事件。

    app/hooks.py

    doc_events = {
        "*": {
            # will run after any DocType record is inserted into database
            "after_insert": "app.crud_events.after_insert_all"
        },
        "ToDo": {
            # will run before a ToDo record is inserted into database
            "before_insert": "app.crud_events.before_insert_todo",
        }
    }
    

    该方法将接收文档和方法名称作为参数。

    app/crud_events.py

    def after_insert_all(doc, method=None):
        pass
    
    def before_insert_todo(doc, method=None):
        pass
    

    有关所有可用钩子的列表,请参阅控制器钩子 >。

    覆盖白名单方法

    白名单方法是在 REST 端点上可访问并被客户端使用的 Python 方法。您可以使用 override_whitelisted_methods 钩子来覆盖核心框架中标准白名单方法。

    app/hooks.py

    override_whitelisted_methods = {
        "frappe.client.get_count": "app.whitelisted.custom_get_count"
    }
    

    该方法应具有与原始方法相同的签名。

    app/whitelisted.py

    def custom_get_count(doctype, filters=None, debug=False, cache=False):
        # your custom implementation of the standard get_count method provided by frappe
        pass
    

    要在删除文档时忽略对特定 DocType 的链接,您可以在 ignore_links_on_delete 钩子中指定它们,如下所示:

    app/hooks.py

    ignore_links_on_delete = ["Communication", "ToDo"]
    

    表单时间线

    文档表单视图的时间线部分显示了对该文档执行的操作的审计跟踪,例如查看、值更改、评论和相关通信等。

    除了这些标准操作之外,有时您可能需要添加自己的自定义操作。您可以通过 additional_timeline_content 钩子来实现。

    additional_timeline_content: {
        # show in each document's timeline
        "*": ["app.timeline.all_timeline"]
        # only show in ToDo's timeline
        "ToDo": ["app.timeline.todo_timeline"]
    }
    

    该方法将接收 doctype 和 docname 作为参数。您可以执行查询并返回与该文档相关的操作,作为字典列表,如示例所示。列表中的每个字典必须有一个 creation 值,该值将用于对时间线中的项目进行排序。

    def todo_timeline(doctype, docname):
        # this method should return a list of dicts
        return [
            {
                 # this will be used to sort the content in the timeline
                "creation": "22-05-2020 18:00:00",
                # this JS template will be rendered in the timeline
                "template": "custom_timeline_template",
                # this data will be passed to the template.
                "template_data": {"key": "value"},
            },
            ...
        ]
    

    调度器事件

    您可以使用 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

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

    • all

    all 事件每 60 秒触发一次。这可以通过 common_site_config.json 中的 scheduler_tick_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"
            ]
        }
    }
    

    Jinja 自定义

    Frappe 在 Jinja 模板中提供了一系列全局实用方法。要添加您自己的方法和过滤器,您可以使用 jinja 钩子。

    app/hooks.py

    jinja = {
        "methods": [
            "app.jinja.methods",
            "app.utils.get_fullname"
        ],
        "filters": [
            "app.jinja.filters",
            "app.utils.format_currency"
        ]
    }
    

    app/jinja/methods.py

    def sum(a, b):
        return a + b
    
    def multiply(a, b):
        return a * b
    

    如果路径是模块路径,则该模块中的所有方法都将被添加。

    app/utils.py

    def get_fullname(user):
        first_name, last_name = frappe.db.get_value("User", user, ["first_name", "last_name"])
        return first_name + " " + last_name
    
    def format_currency(value, currency):
        return currency + " " + str(value)
    

    现在,您可以在 Jinja 模板中使用这些实用程序,如下所示:

    <h1>Hi, {{ get_fullname(frappe.session.user) }}</h1>
    <p>Your account balance is {{ account_balance | format_currency("INR") }}</p>
    <p>1 + 2 = {{ sum(1, 2) }}</p>
    

    防止链接文档自动取消

    要防止特定 DocType 的文档在取消任何链接文档时被自动取消,您可以使用 auto_cancel_exempted_doctypes 钩子。

    app/hooks.py

    auto_cancel_exempted_doctypes = ["Payment Entry"]
    

    在上面的示例中,如果任何与付款条目链接的文档(例如销售发票)被取消,它将跳过链接的付款条目文档的自动取消。

    通知配置

    通知配置钩子用于自定义工作台中通知下拉列表中显示的项目。它可以通过 notification_config 钩子进行配置。

    app/hooks.py

    notification_config = "app.notification.get_config"
    

    该方法在没有任何参数的情况下被调用。

    app/notification.py

    def get_config():
     return {
            "for_doctype": {
                "Issue": {"status":"Open"},
                "Issue": {"status":"Open"},
            },
            "for_module_doctypes": {
                "ToDo": "To Do",
                "Event": "Calendar",
                "Comment": "Messages"
            },
            "for_module": {
                "To Do": "frappe.core.notifications.get_things_todo",
                "Calendar": "frappe.core.notifications.get_todays_events",
                "Messages": "frappe.core.notifications.get_unread_messages"
            }
        }
    

    上述配置包含三个部分:

    1. 上述配置中的 for_doctype 部分会将状态为“开启”的“问题”或“客户问题”标记为未读
    2. for_module_doctypes 将文档类型映射到模块的未读计数。
    3. for_module 将模块映射到获取其未读计数的函数。这些函数在调用时不带任何参数。

    所需应用

    在构建应用时,您可能会创建基于其他应用构建的应用。为确保在有人安装您的应用时同时安装依赖应用,您可以使用 required_apps 钩子。

    app/hooks.py

    required_apps = ["erpnext"]
    

    上述配置将确保在有人安装您的应用时,erpnext 也会被安装。

    用户数据保护与隐私

    Frappe 内置了个人数据下载和个人数据删除等用户数据隐私功能。哪些数据构成个人数据可由应用发布者在应用的 hooks.py 文件中以 user_data_fields 的形式定义。

    app/hooks.py

    user_data_fields = [
        {"doctype": "Access Log"},
        {"doctype": "Comment", "strict": True},
        {
            "doctype": "Contact",
            "filter_by": "email_id",
            "rename": True,
        },
        {"doctype": "Contact Email", "filter_by": "email_id"},
        {
            "doctype": "File",
            "filter_by": "attached_to_name",
            "redact_fields": ["file_name", "file_url"],
        },
        {"doctype": "Email Unsubscribe", "filter_by": "email", "partial": True},
    ]
    

    包含用户数据的文档类型应按照上述格式映射到此钩子下。当用户提出数据删除或下载请求时,将利用此钩子映射到指定的文档类型。可用于修改文档的选项如下:

    字段 说明
    doctype 包含用户数据的文档类型。
    filter_by 用于筛选文档的文档字段。如果未设置,默认为 owner
    partial 如果设置,将解析所有文本字段并删除用户的姓名和用户名引用。
    redact_fields 需要删除的字段。如果未指定,则视为对所有文本字段进行部分数据删除。
    rename 如果文档名称包含用户数据,设置此字段以重命名文档,使其匿名化。
    strict 如果设置为 True,将从当前文档类型的所有文档中删除任何用户数据。如果未设置,默认为 False,这意味着仅筛选用户为所有者的文档。

    注意:个人数据下载仅使用 user_data_fields 中定义的文档类型和筛选字段。

    相关主题:

    1. 个人数据删除
    2. 个人数据下载

    注册表单模板

    如果您想向注册表单添加额外字段,可以使用此钩子。创建一个包含自定义注册表单的模板文件。将此模板传递给自定义注册钩子。

    signup_form_template = "school/templates/signup-form.html"
    

    注意:如果您希望在注册表单中添加自定义字段,则需要在用户文档类型中添加额外字段。您必须使用固定数据(fixtures)来添加这些字段。此外,您还需要为这个注册表单编写自己的提交处理程序,并在服务器端编写一个用于注册用户的函数。这样,您还可以为您添加的自定义字段编写验证逻辑。

    短信钩子

    这些钩子允许您通过集成第三方短信服务提供商或实现自定义短信逻辑,来自定义 Frappe 中的短信发送功能。

    发送短信

    此钩子允许您覆盖通知、群发短信和其他常规短信功能的默认短信发送逻辑。

    app/hooks.py

    send_sms = "app.overrides.sms.send_sms"
    

    调用该方法时会传入短信详情,包括收件人、消息内容和其他元数据。

    app/overrides/sms.py

    def send_sms(receiver_list, msg, sender=None, success_msg=True):
        """
        Override default SMS sending logic
    
        Args:
            receiver_list: List of mobile numbers or single mobile number
            msg: SMS message content
            sender: Sender ID (optional)
            success_msg: Whether to show success message (optional)
        """
    
        # Send SMS via custom provider
        response = custom_sms_provider.send_sms(
            to=receiver_list,
            message=msg,
            from_number=sender
        )
    
        if response.status_code == 200:
            frappe.msgprint(_("SMS sent successfully"))
            return True
        else:
            frappe.throw(_("Failed to send SMS"))
            return False
    

    通过短信发送令牌

    此钩子允许您覆盖双因素认证和移动登录流程中的短信 OTP 发送逻辑。

    app/hooks.py

    send_token_via_sms = "app.overrides.sms.send_token_via_sms"
    

    调用该方法时会传入用于认证目的的 OTP 详情。

    app/overrides/sms.py

    def send_token_via_sms(otpsecret, token=None, phone_no=None):
       """
        Generate OTP and send using local send_otp function.
        :param otpsecret: OTP secret for generating HOTP
        :param token: Token to use for HOTP generation
        :param phone_no: Phone number to send OTP to
        """
        if not phone_no:
            return False
        try:
            hotp = pyotp.HOTP(otpsecret)
            otp_code = hotp.at(token_int)
            result = send_otp(
                number=phone_no,
                otp_length=len(otp_code),
                otp_expiry=5,  # 5 minutes expiry
                otp=otp_code
            )
            return result.get("success", False)
        except Exception as e:
            frappe.log_error(
                message=f"Failed to send OTP: {str(e)}",
                title="OTP Error"
            )
            return False
    

    可用钩子列表

    钩子名称 说明
    additional_timeline_content 表单时间线
    after_install 安装钩子
    after_migrate 迁移钩子
    after_sync 安装钩子
    app_include_css 工作台资源
    app_include_js 工作台资源
    app_logo_url 应用元数据
    app_title 应用元数据
    auto_cancel_exempted_doctypes 防止自动取消
    base_template_map 基础模板
    base_template 基础模板
    before_install 安装钩子
    before_migrate 迁移钩子
    before_tests 测试钩子
    before_write_file 文件钩子
    bot_parsers 已弃用
    braintree_success_page Braintree 成功页面
    brand_html 品牌 HTML
    calendars 日历
    clear_cache 清除缓存
    communication_doctypes
    default_mail_footer 默认邮件页脚
    delete_file_data_content 文件钩子
    doc_events 文档增删改查事件
    doctype_js 覆盖表单脚本
    domains
    dump_report_map 已弃用
    extend_bootinfo 扩展启动信息
    extend_website_page_controller_context 网站控制器上下文
    filters_config
    fixtures 测试数据
    get_site_info
    get_translated_dict
    get_website_user_home_page 默认首页
    get_web_pages_with_dynamic_routes 带动态路由的网页
    has_permission 文档权限
    has_website_permission
    home_page 默认首页
    jenv Jinja 自定义
    leaderboards
    look_for_sidebar_json
    make_email_body_message
    notification_config 通知配置
    on_login 会话钩子
    on_logout 会话钩子
    on_print_pdf 打印时
    on_session_creation 会话钩子
    extend_doctype_class 扩展 DocType 类
    override_doctype_class 覆盖 DocType 类
    override_doctype_dashboards
    override_whitelisted_methods 覆盖白名单方法
    ignore_links_on_delete 删除时忽略链接
    permission_query_conditions 修改列表查询
    portal_menu_items 门户侧边栏
    required_apps 所需应用
    role_home_page 默认首页
    scheduler_events 计划任务事件
    setup_wizard_complete
    setup_wizard_exception
    setup_wizard_requires
    setup_wizard_stages
    setup_wizard_success
    signup_form_template 注册表单模板
    sounds 提示音
    standard_portal_menu_items 门户侧边栏
    standard_queries 标准查询
    send_sms 短信钩子
    send_token_via_sms 短信钩子
    template_apps
    translated_languages_for_website
    translator_url
    treeviews 默认使用树形视图(而非列表视图)作为默认视图的 DocType
    update_website_context 网站上下文
    user_privacy_documents 已弃用(改用 user_data_fields 钩子)
    user_data_fields 用户数据保护与隐私
    web_include_css 门户资源
    web_include_js 门户资源
    website_catch_all 网站 404 页面
    website_clear_cache 网站缓存清理
    website_context 网站上下文
    website_generators 已弃用(请改用 DocType 中的“具有网页视图”选项)
    website_redirects 网站重定向
    website_route_rules 网站路由规则
    website_user_home_page 已弃用(改用 homepage 钩子)
    welcome_email
    write_file_keys 已弃用
    write_file 文件钩子

    搜索

    在 Frappe 中进行搜索由 Search 模块管理。它是 Whoosh 的封装,Whoosh 是一个用 Python 编写的全文搜索库。

    您可以扩展 FullTextSearch 类,为特定需求创建搜索类。例如,WebsiteSearch 是一个用于索引公开网页并暴露搜索功能的封装。

    FullTextSearch

    每个 FullTextSearch(FTS)实例都持有一个由类本身定义的 Schema。这意味着,特定的 FTS 实现会有其特定的 schema。如果您希望使用不同的 schema 进行索引,可以创建新的实现。除此之外,FTS 类还提供了其他控制器,用于创建、更新和查询索引。

    扩展 FTS 类

    初始化基于 FTS 的类时,您需要提供一个索引名称。实例化时,会初始化以下参数:

    • index_name:提供的索引名称。
    • index_path:索引在站点文件夹中的路径。
    • schema:由 get_schema 函数返回。
    • id:用于在索引中识别文档的 ID。

    实例化后,您可以运行 build 函数。它会从 get_items_to_index 获取所有文档,这些文档是符合定义 schema 的 frappe._dict(frappe 字典)列表。然后,这些文档会被添加到索引中并写入文件。

    您可以使用 FTS 类的 search 方法搜索索引。这些函数在 API 参考文档中有详细说明。

    博客的示例实现如下所示:

    class BlogWrapper(FullTextSearch):
     # Default Schema
     # def get_schema(self):
     # return Schema(name=ID(stored=True), content=TEXT(stored=True))
    
     # def get_id(self):
     # return "name"
    
     def get_items_to_index(self):
     docs = []
     for blog_name in get_all_blogs():
     docs.append(get_document_to_index(blog_name))
     return docs
    
     def get_document_to_index(self, name):
     blog = frappe.get_doc("Blog Post", name)
     return frappe._dict(name=name, content=blog.content)
    
     def parse_result(self, result):
     return result["name"]
    
    • get_items_to_index:获取所有需要索引的路由,包括 www/ 目录下的静态页面以及已发布文档的路由。
    • get_document_to_index:渲染页面并使用 BeautifulSoup 进行解析。
    • parse_result:所有搜索结果都通过此函数进行解析。

    好的,请发送需要翻译的英文标题。

    在这里,让我们来看看 Frappe 中响应是如何构建的,以及你如何在你的
    Frappe 应用或脚本中使用它们。

    如果你已经阅读过路由器
    文档,你可能已经注意到 Frappe 内部用来根据内容类型
    构建响应的 build_response 函数。定义此行为的逻辑属于
    frappe.utils.response
    模块的一部分,其中 build_response 是该模块的核心内容。

    def build_response(response_type=None):
     if "docs" in frappe.local.response and not frappe.local.response.docs:
     del frappe.local.response["docs"]
    
     response_type_map = {
     "csv": as_csv,
     "txt": as_txt,
     "download": as_raw,
     "json": as_json,
     "pdf": as_pdf,
     "page": as_page,
     "redirect": redirect,
     "binary": as_binary,
     }
    
     return response_type_map[frappe.response.get("type") or response_type]()
    

    上面的代码片段展示了 build_response 的当前实现,
    它映射了不同的函数,这些函数充当不同内容类型的处理器。
    让我们更深入地了解一下 Frappe v13 中针对 “download”
    response_type 的响应处理器。

    def as_raw():
     response = Response()
     response.mimetype = (
     frappe.response.get("content_type")
     or mimetypes.guess_type(frappe.response["filename"])[0]
     or "application/unknown"
     )
     response.headers["Content-Disposition"] = (
     f'{frappe.response.get("display_content_as", "attachment")};'
     f' filename="{frappe.response["filename"].replace(" ", "_")}"'
     ).encode("utf-8")
     response.data = frappe.response["filecontent"]
     return response
    

    根据 Content-Disposition 头的值,
    接收响应的浏览器可能会有不同的行为。如果未设置,该值默认为
    “attachment”

    如果 frappe.response.display_content_as 设置为 “inline”,则表示
    内容预期在浏览器中内联显示,即作为
    网页或网页的一部分显示,而 “attachment” 则表示内容
    将被下载并保存在本地。

    要创建一个可以直接下载所需文件的 API 端点,
    你可以编写类似以下代码来实现直接下载文件。

    @frappe.whitelist()
    def download(name):
     file = frappe.get_doc("File", name)
     frappe.response.filename = file.file_name
     frappe.response.filecontent = file.get_content()
     frappe.response.type = "download"
     frappe.response.display_content_as = "attachment"
    

    工具函数

    Frappe 框架附带各种实用函数,用于处理常见的操作,例如站点特定的日期时间管理、日期和货币格式化、PDF 生成等等。

    这些实用方法可以从 frappe.utils 模块(及其嵌套模块,如 frappe.utils.loggerfrappe.utils.data)中导入到您的 Frappe 应用的任何 Python 文件中。此列表并非详尽无遗,您可以查看框架代码库以了解可用的内容。

    now(当前时间)

    now()

    返回当前日期时间,格式为 yyyy-mm-dd hh:mm:ss

    from frappe.utils import now
    
    now() # '2021-05-25 06:38:52.242515'
    

    getdate(获取日期)

    getdate(string_date=None)

    string_date(yyyy-mm-dd)转换为 datetime.date 对象。如果未提供输入,则返回当前日期。如果 string_date 是无效的日期字符串,则会抛出异常。

    from frappe.utils import getdate
    
    getdate() # datetime.date(2021, 5, 25)
    getdate('2000-03-18') # datetime.date(2000, 3, 18)
    

    today(今天)

    today()

    返回当前日期,格式为 yyyy-mm-dd

    from frappe.utils import today
    
    today() # '2021-05-25'
    

    add_to_date(日期加减)

    add_to_date(date, years=0, months=0, weeks=0, days=0, hours=0, minutes=0, seconds=0, as_string=False, as_datetime=False)

    `date`: A string representation or `datetime` object, uses the current `datetime` if `None` is passed
    `as_string`: Return as string
    `as_datetime`: If `as_string` is True and `as_datetime` is also True, returns a `datetime` string otherwise just the `date` string.
    

    此函数在进行日期/日期时间增减时非常方便,例如,从特定日期/日期时间加上或减去一定数量的天数。

    from datetime import datetime # from python std library
    from frappe.utils import add_to_date
    
    today = datetime.now().strftime('%Y-%m-%d')
    print(today) # '2021-05-21'
    
    after_10_days = add_to_date(datetime.now(), days=10, as_string=True)
    print(after_10_days) # '2021-05-31'
    
    add_to_date(datetime.now(), months=2) # datetime.datetime(2021, 7, 21, 15, 31, 18, 119999)
    add_to_date(datetime.now(), days=10, as_string=True, as_datetime=True) # '2021-05-31 15:30:23.757661'
    add_to_date(None, years=6) # datetime.datetime(2027, 5, 21, 15, 32, 31, 652089)
    

    date_diff(日期差)

    date_diff(date_2, date_1)

    返回给定两个日期之间的天数差。

    from frappe.utils import add_to_date, today, date_diff
    date_1 = today()
    date_2 = add_to_date(date_1, days=10)
    
    print(date_diff(date_2, date_1)) #10
    

    days_diff(天数差)

    days_diff(date_2, date_1)

    返回给定两个日期之间的天数差。

    from frappe.utils import add_to_date, today, days_diff
    date_1 = today()
    date_2 = add_to_date(date_1, days=10)
    
    print(days_diff(date_2, date_1)) #10
    

    month_diff(月份差)

    month_diff(date_2, date_1)

    返回给定两个日期之间的月份差。

    from frappe.utils import add_to_date, month_diff
    date_1 = "2024-07-01"
    date_2 = add_to_date(date_1, days=60)
    
    print(month_diff(date_2, date_1)) #2
    

    pretty_date(友好日期)

    pretty_date(iso_datetime)

    接受一个 ISO 时间,并返回一个字符串,表示该日期距今有多长时间。这在即时通讯等通信应用中非常常见。

    from frappe.utils import pretty_date, now, add_to_date
    
    pretty_date(now()) # 'just now'
    
    # Some example outputs:
    
    # 1 hour ago
    # 20 minutes ago
    # 1 week ago
    # 5 years ago
    

    format_duration(格式化时长)

    format_duration(seconds, hide_days=False)

    将给定的以秒(浮点数)为单位的时长值转换为时长格式。

    from frappe.utils import format_duration
    
    format_duration(50) # '50s'
    format_duration(10000) # '2h 46m 40s'
    format_duration(1000000) # '11d 13h 46m 40s'
    
    # Convert days to hours
    format_duration(1000000, hide_days=True) # '277h 46m 40s'
    

    comma_and(逗号和)

    comma_and(some_list, add_quotes=True)

    给定一个列表或元组 some_list,返回格式为 1st item, 2nd item, .... and last item 的字符串。此函数使用 frappe._,因此您不必担心单词 and 的翻译问题。如果 add_quotesFalse,则返回不带引号的项目,否则返回带引号的项目。如果作为参数传递的 some_list 的类型不是列表或元组,则原样返回它(some_list)。

    from frappe.utils import comma_and
    
    comma_and([1, 2, 3]) # "'1', '2' and '3'"
    comma_and(['Apple', 'Ball', 'Cat'], add_quotes=False) # 'Apple, Ball and Cat'
    comma_and('abcd') # 'abcd'
    

    还有一个 comma_or 函数,它与 comma_and 类似,只是分隔符不同,在 comma_or 的情况下,分隔符是 or

    money_in_words(金额转大写)

    money_in_words(number, main_currency=None, fraction_currency=None)

    `number`: A floating point money amount
    `main_currency`: Uses this as the main currency. If not given, tries to fetch from default settings or uses `INR` if not found there.
    

    此函数返回带有货币和辅币单位的字符串(金额大写形式)。

    from frappe.utils import money_in_words
    
    money_in_words(900) # 'INR Nine Hundred and Fifty Paisa only.'
    money_in_words(900.50) # 'INR Nine Hundred and Fifty Paisa only.'
    money_in_words(900.50, 'USD') # 'USD Nine Hundred and Fifty Centavo only.'
    money_in_words(900.50, 'USD', 'Cents') # 'USD Nine Hundred and Fifty Cents only.'
    

    validate_json_string(验证 JSON 字符串)

    validate_json_string(string)

    如果给定的 string 是有效的 JSON(JavaScript 对象表示法)字符串,则抛出 frappe.ValidationError。您可以使用 try-except 块来处理对此函数的调用,如下面的代码片段所示。

    import frappe
    from frappe.utils import validate_json_string
    
    # No Exception thrown
    validate_json_string('[]')
    validate_json_string('[{}]')
    validate_json_string('[{"player": "one", "score": 199}]')
    
    try:
        # Throws frappe.ValidationError
        validate_json_string('invalid json')
    except frappe.ValidationError:
        print('Not a valid JSON string')
    

    random_string(随机字符串)

    random_string(length)

    此函数生成一个包含 length 个字符的随机字符串。这在某些情况下对于生成加密或秘密信息非常有用。

    from frappe.utils import random_string
    
    random_string(40) # 'mcrLCrlvkUdkaOe8m5xMI8IwDB8lszwJsWtZFveQ'
    random_string(6) # 'htrB4L'
    random_string(6) #'HNRirG'
    

    mask_string(掩码字符串)

    mask_string(input_string, mask_char="*", show_first=4, show_last=3)

    出于隐私保护目的,通过隐藏中间字符来掩码字符串,同时显示可配置的首尾字符以用于识别。

    from frappe.utils import mask_string
    
    mask_string("1234567890")  # "1234***890"
    mask_string("12345")       # "******"
    mask_string("1234567890", mask_char="#", show_first=2, show_last=2)  # "12####90"
    

    unique(去重)

    unique(seq)

    seq:一个可迭代对象 / 序列

    此函数在移除重复项后返回给定序列的元素列表。此外,与 list(set(seq)) 不同,它会保留顺序。

    from frappe.utils import unique
    
    unique([1, 2, 3, 1, 1, 1]) # [1, 2, 3]
    unique('abcda') # ['a', 'b', 'c', 'd']
    unique(('Apple', 'Apple', 'Banana', 'Apple')) # ['Apple', 'Banana']
    

    get_pdf(生成 PDF)

    get_pdf(html, options=None, output=None)

    `html`: HTML string to render
    `options`: An optional `dict` for configuration
    `output`: A optional `PdfFileWriter` object.
    

    此函数使用 pdfkitpyPDF2 模块从 HTML 生成 PDF 文件。如果提供了 output,则将生成的页面追加到此对象并返回它,否则返回 PDF 的 byte 流。

    例如,生成 PDF 并将其作为对 Web 请求的响应返回:

    import frappe
    from frappe.utils.pdf import get_pdf
    
    @frappe.whitelist(allow_guest=True)
    def generate_invoice():
        cart = [{
            'Samsung Galaxy S20': 10,
            'iPhone 13': 80
        }]
    
        html = '
    <h1>Invoice from Star Electronics e-Store!</h1>'
    
        # Add items to PDF HTML
        html += '
    <ol>'
        for item, qty in cart.items():
            html += f'
    <li>{item} - {qty}</li>'
        html += '</ol>'
    
        # Attaching PDF to response
        frappe.local.response.filename = 'invoice.pdf'
        frappe.local.response.filecontent = get_pdf(html)
        frappe.local.response.type = 'pdf'
    

    get_abbr(获取缩写)

    get_abbr(string, max_len=2)

    返回给定 string 的缩写(仅首字母)版本,最多包含 max_len 个字母。它广泛用于 Frappe 框架和 ERPNext 中,以生成缩略图或占位图像。

    from frappe.utils import get_abbr
    
    get_abbr('Gavin') # 'G'
    get_abbr('Coca Cola Company') # 'CC'
    get_abbr('Mohammad Hussain Nagaria', max_len=3) # 'MHN'
    

    validate_url(验证 URL)

    validate_url(txt, throw=False, valid_schemes=None)

    `txt`: A string to check validity
    `throw`: Weather to throw an exception if `txt` does not represent a valid URL, `False` by default
    `valid_schemes`: A string or an iterable (list, tuple or set). If provided, checks the given URL's scheme against this.
    

    此实用函数可用于检查字符串是否表示有效的 URL 地址。

    from frappe.utils import validate_url
    
    validate_url('google') # False
    validate_url('https://google.com') # True
    validate_url('https://google.com', throw=True) # throws ValidationError
    

    validate_email_address(验证电子邮件地址)

    validate_email_address(email_str, throw=False)

    返回一个字符串,其中包含给定 email_str 中存在的电子邮件地址或以逗号分隔的有效电子邮件地址列表。如果 throwTrue,则在给定字符串中不存在有效电子邮件地址时抛出 frappe.InvalidEmailAddressError,否则返回空字符串。

    from frappe.utils import validate_email_address
    
    # Single valid email address
    validate_email_address('[email protected]') # '[email protected]'
    validate_email_address('other text, [email protected], some other text') # '[email protected]'
    
    # Multiple valid email address
    validate_email_address(
        'some text, [email protected], some other text, [email protected], yet another no-emailic phrase.'
    ) # '[email protected], [email protected]'
    
    # Invalid email address
    validate_email_address('some other text') # ''
    

    validate_phone_number(验证电话号码)

    validate_phone_number(phone_number, throw=False)

    如果 phone_number(字符串)是有效的电话号码,则返回 True。如果 phone_number 无效且 throwTrue,则会抛出 frappe.InvalidPhoneNumberError

    from frappe.utils import validate_phone_number
    
    # Valid phone numbers
    validate_phone_number('753858375') # True
    validate_phone_number('+91-75385837') # True
    
    # Invalid phone numbers
    validate_phone_number('invalid') # False
    validate_phone_number('87345%%', throw=True) # InvalidPhoneNumberError
    

    frappe.cache()

    cache()

    返回 Redis 连接,该连接是 RedisWrapper 类的实例,继承自 redis.Redis 类。您可以使用此连接来利用 Redis 缓存存储/检索键值对。

    import frappe
    
    cache = frappe.cache()
    
    cache.set('name', 'frappe') # True
    cache.get('name') # b'frappe'
    

    frappe.sendmail()

    sendmail(recipients=[], sender="", subject="No Subject", message="No Message", as_markdown=False, template=None, args=None, **kwargs)

    `recipients`: List of recipients
    `sender`: Email sender. Default is current user or default outgoing account
    `subject`: Email Subject
    `message`: (or `content`) Email Content
    `as_markdown`: Convert content markdown to HTML
    `template`: Name of html template (jinja) from templates/emails folder
    `args`: Arguments for rendering the template
    

    在大多数情况下,上述参数已经足够,但此函数还可以接受许多其他关键字参数。要查看所有关键字参数,请查看此函数的实现(frappe/__init__.py)。

    此函数可以使用用户的默认电子邮件账户或全局默认电子邮件账户发送邮件。

    import frappe
    
    recipients = [
        '[email protected]',
        '[email protected]'
    ]
    
    frappe.sendmail(
        recipients=recipients,
        subject=frappe._('Birthday Reminder'),
        template='birthday_reminder',
        args=dict(
            reminder_text=reminder_text,
            birthday_persons=birthday_persons,
            message=message,
        ),
        header=_('Birthday Reminder 🎂')
    )
    

    示例 Jinja 模板文件:

    <!-- templates/emails/birthday_reminder.html -->
    <div>
    <div class="gray-container text-center">
    <div>
     {% for person in birthday_persons %}
     {% if person.image %}
     <img src="%7B%7B%20person.image%20%7D%7D" title="{{ person.name }}">
     {% endif %}
     {% endfor %}
     </div>
    <div style="margin-top: 15px;"><span>{{ reminder_text }}</span>
    <p class="text-muted">{{ message }}</p>
    </div>
    </div>
    
    <h3 id="attaching-files">附加文件</h3>
    <p>您可以通过向 <code>sendmail 函数传递附件列表来轻松地将文件附加到邮件中:

    frappe.sendmail(
        ["[email protected]", "[email protected]"],
        message="## hello, *bro*"
        attachments=[{"file_url": "/files/hello.png"}],
        as_markdown=True
    )
    

    请注意,附件是一个字典列表,其中包含键 file_url。您可以在 File 文档的 file_url 字段中找到此 file_url

    文件锁

    文件锁可用于同步进程,以避免竞争条件。

    示例:如果多个写入者尝试写入同一个文件,可能会导致竞争条件。因此,我们创建一个命名锁,以便进程可以看到该锁并等待其可用于写入。

    from frappe.utils.synchronization import filelock
    
    def update_important_config(config, file):
        with filelock("config_name"):
            json.dumps(config, file)
    

    get_filtered_list_url

    get_filtered_list_url(doctype, docnames=None)

    当创建了多个文档,并且您希望在列表视图中向用户展示所有这些文档时,此功能非常有用。

    from frappe.utils import get_filtered_list_url
    
    get_filtered_list_url("Work Order", [
        "MFG-WO-2025-00027",
        "MFG-WO-2025-00028",
        "MFG-WO-2025-00029"
    ])
    # → 'http://
    <site>/app/work-order?name=["in",["MFG-WO-2025-00027","MFG-WO-2025-00028","MFG-WO-2025-00029"]]'
    </site>

    get_filtered_list_link(doctype, docnames=None, label=None)

    当创建了多个文档,并且您希望显示一个简洁的链接而不是冗长的消息对话框链接时,此功能非常有用。

    from frappe.utils import get_filtered_list_link
    
    get_filtered_list_link("Work Order", [
        "MFG-WO-2025-00027",
        "MFG-WO-2025-00028",
    ])
    # → '<a href="http://<site>/app/work-order?name=%5B...%5D">Work Order</a>'