浏览代码

style: format all python files using black (#16453)

Co-authored-by: Frappe Bot <developers@frappe.io>
version-14
Suraj Shetty 3 年前
committed by GitHub
父节点
当前提交
c0c5b2ebdd
找不到此签名对应的密钥 GPG 密钥 ID: 4AEE18F83AFDEB23
共有 100 个文件被更改,包括 5864 次插入3859 次删除
  1. +3
    -0
      .git-blame-ignore-revs
  2. +11
    -0
      .pre-commit-config.yaml
  3. +640
    -305
      frappe/__init__.py
  4. +43
    -44
      frappe/api.py
  5. +110
    -82
      frappe/app.py
  6. +117
    -74
      frappe/auth.py
  7. +104
    -83
      frappe/automation/doctype/assignment_rule/assignment_rule.py
  8. +170
    -156
      frappe/automation/doctype/assignment_rule/test_assignment_rule.py
  9. +1
    -0
      frappe/automation/doctype/assignment_rule_day/assignment_rule_day.py
  10. +1
    -0
      frappe/automation/doctype/assignment_rule_user/assignment_rule_user.py
  11. +176
    -114
      frappe/automation/doctype/auto_repeat/auto_repeat.py
  12. +156
    -93
      frappe/automation/doctype/auto_repeat/test_auto_repeat.py
  13. +1
    -0
      frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py
  14. +2
    -0
      frappe/automation/doctype/milestone/milestone.py
  15. +2
    -1
      frappe/automation/doctype/milestone/test_milestone.py
  16. +25
    -18
      frappe/automation/doctype/milestone_tracker/milestone_tracker.py
  17. +25
    -25
      frappe/automation/doctype/milestone_tracker/test_milestone_tracker.py
  18. +111
    -63
      frappe/boot.py
  19. +28
    -20
      frappe/build.py
  20. +103
    -44
      frappe/cache_manager.py
  21. +135
    -91
      frappe/client.py
  22. +28
    -26
      frappe/commands/__init__.py
  23. +50
    -30
      frappe/commands/redis_utils.py
  24. +69
    -44
      frappe/commands/scheduler.py
  25. +424
    -238
      frappe/commands/site.py
  26. +38
    -21
      frappe/commands/translate.py
  27. +405
    -221
      frappe/commands/utils.py
  28. +19
    -11
      frappe/config/__init__.py
  29. +67
    -45
      frappe/contacts/address_and_contact.py
  30. +84
    -55
      frappe/contacts/doctype/address/address.py
  31. +21
    -20
      frappe/contacts/doctype/address/test_address.py
  32. +20
    -8
      frappe/contacts/doctype/address_template/address_template.py
  33. +13
    -15
      frappe/contacts/doctype/address_template/test_address_template.py
  34. +107
    -69
      frappe/contacts/doctype/contact/contact.py
  35. +8
    -9
      frappe/contacts/doctype/contact/test_contact.py
  36. +1
    -0
      frappe/contacts/doctype/contact_email/contact_email.py
  37. +1
    -0
      frappe/contacts/doctype/contact_phone/contact_phone.py
  38. +1
    -0
      frappe/contacts/doctype/gender/gender.py
  39. +1
    -0
      frappe/contacts/doctype/gender/test_gender.py
  40. +1
    -0
      frappe/contacts/doctype/salutation/salutation.py
  41. +1
    -0
      frappe/contacts/doctype/salutation/test_salutation.py
  42. +40
    -10
      frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py
  43. +66
    -58
      frappe/contacts/report/addresses_and_contacts/test_addresses_and_contacts.py
  44. +0
    -1
      frappe/core/doctype/__init__.py
  45. +26
    -18
      frappe/core/doctype/access_log/access_log.py
  46. +32
    -30
      frappe/core/doctype/access_log/test_access_log.py
  47. +13
    -10
      frappe/core/doctype/activity_log/activity_log.py
  48. +46
    -35
      frappe/core/doctype/activity_log/feed.py
  49. +34
    -36
      frappe/core/doctype/activity_log/test_activity_log.py
  50. +1
    -0
      frappe/core/doctype/block_module/block_module.py
  51. +79
    -55
      frappe/core/doctype/comment/comment.py
  52. +51
    -35
      frappe/core/doctype/comment/test_comment.py
  53. +0
    -1
      frappe/core/doctype/communication/__init__.py
  54. +173
    -125
      frappe/core/doctype/communication/communication.py
  55. +83
    -67
      frappe/core/doctype/communication/email.py
  56. +85
    -65
      frappe/core/doctype/communication/mixins.py
  57. +212
    -159
      frappe/core/doctype/communication/test_communication.py
  58. +3
    -1
      frappe/core/doctype/communication_link/communication_link.py
  59. +2
    -1
      frappe/core/doctype/custom_docperm/custom_docperm.py
  60. +3
    -1
      frappe/core/doctype/custom_docperm/test_custom_docperm.py
  61. +6
    -4
      frappe/core/doctype/custom_role/custom_role.py
  62. +3
    -1
      frappe/core/doctype/custom_role/test_custom_role.py
  63. +1
    -0
      frappe/core/doctype/data_export/data_export.py
  64. +168
    -100
      frappe/core/doctype/data_export/exporter.py
  65. +59
    -49
      frappe/core/doctype/data_export/test_data_exporter.py
  66. +25
    -30
      frappe/core/doctype/data_import/data_import.py
  67. +10
    -17
      frappe/core/doctype/data_import/exporter.py
  68. +136
    -115
      frappe/core/doctype/data_import/importer.py
  69. +1
    -0
      frappe/core/doctype/data_import/test_data_import.py
  70. +8
    -8
      frappe/core/doctype/data_import/test_exporter.py
  71. +144
    -105
      frappe/core/doctype/data_import/test_importer.py
  72. +1
    -0
      frappe/core/doctype/data_import_log/data_import_log.py
  73. +1
    -0
      frappe/core/doctype/data_import_log/test_data_import_log.py
  74. +0
    -1
      frappe/core/doctype/defaultvalue/__init__.py
  75. +12
    -7
      frappe/core/doctype/defaultvalue/defaultvalue.py
  76. +8
    -11
      frappe/core/doctype/deleted_document/deleted_document.py
  77. +3
    -1
      frappe/core/doctype/deleted_document/test_deleted_document.py
  78. +0
    -1
      frappe/core/doctype/docfield/__init__.py
  79. +13
    -13
      frappe/core/doctype/docfield/docfield.py
  80. +0
    -1
      frappe/core/doctype/docperm/__init__.py
  81. +1
    -1
      frappe/core/doctype/docperm/docperm.py
  82. +24
    -10
      frappe/core/doctype/docshare/docshare.py
  83. +21
    -9
      frappe/core/doctype/docshare/test_docshare.py
  84. +0
    -1
      frappe/core/doctype/doctype/__init__.py
  85. +574
    -322
      frappe/core/doctype/doctype/doctype.py
  86. +4
    -3
      frappe/core/doctype/doctype/patches/set_route.py
  87. +220
    -197
      frappe/core/doctype/doctype/test_doctype.py
  88. +1
    -0
      frappe/core/doctype/doctype_action/doctype_action.py
  89. +1
    -0
      frappe/core/doctype/doctype_link/doctype_link.py
  90. +1
    -0
      frappe/core/doctype/doctype_state/doctype_state.py
  91. +19
    -11
      frappe/core/doctype/document_naming_rule/document_naming_rule.py
  92. +35
    -46
      frappe/core/doctype/document_naming_rule/test_document_naming_rule.py
  93. +1
    -0
      frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py
  94. +1
    -0
      frappe/core/doctype/document_naming_rule_condition/test_document_naming_rule_condition.py
  95. +41
    -31
      frappe/core/doctype/domain/domain.py
  96. +3
    -1
      frappe/core/doctype/domain/test_domain.py
  97. +29
    -23
      frappe/core/doctype/domain_settings/domain_settings.py
  98. +4
    -1
      frappe/core/doctype/dynamic_link/dynamic_link.py
  99. +10
    -5
      frappe/core/doctype/error_log/error_log.py
  100. +3
    -1
      frappe/core/doctype/error_log/test_error_log.py

+ 3
- 0
.git-blame-ignore-revs 查看文件

@@ -19,3 +19,6 @@ fe20515c23a3ac41f1092bf0eaf0a0a452ec2e85


# Clean up whitespace # Clean up whitespace
b2fc959307c7c79f5584625569d5aed04133ba13 b2fc959307c7c79f5584625569d5aed04133ba13

# Format codebase and sort imports
cb6f68e8c106ee2d037dd4b39dbb6d7c68caf1c8

+ 11
- 0
.pre-commit-config.yaml 查看文件

@@ -16,6 +16,17 @@ repos:
- id: check-merge-conflict - id: check-merge-conflict
- id: check-ast - id: check-ast


- repo: https://github.com/adityahase/black
rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119
hooks:
- id: black
additional_dependencies: ['click==8.0.4']

- repo: https://github.com/timothycrosley/isort
rev: 5.9.1
hooks:
- id: isort
exclude: ".*setup.py$"


ci: ci:
autoupdate_schedule: weekly autoupdate_schedule: weekly


+ 640
- 305
frappe/__init__.py
文件差异内容过多而无法显示
查看文件


+ 43
- 44
frappe/api.py 查看文件

@@ -9,8 +9,8 @@ import frappe
import frappe.client import frappe.client
import frappe.handler import frappe.handler
from frappe import _ from frappe import _
from frappe.utils.response import build_response
from frappe.utils.data import sbool from frappe.utils.data import sbool
from frappe.utils.response import build_response




def handle(): def handle():
@@ -22,22 +22,22 @@ def handle():
`/api/method/{methodname}` will call a whitelisted method `/api/method/{methodname}` will call a whitelisted method


`/api/resource/{doctype}` will query a table `/api/resource/{doctype}` will query a table
examples:
- `?fields=["name", "owner"]`
- `?filters=[["Task", "name", "like", "%005"]]`
- `?limit_start=0`
- `?limit_page_length=20`
examples:
- `?fields=["name", "owner"]`
- `?filters=[["Task", "name", "like", "%005"]]`
- `?limit_start=0`
- `?limit_page_length=20`


`/api/resource/{doctype}/{name}` will point to a resource `/api/resource/{doctype}/{name}` will point to a resource
`GET` will return doclist
`POST` will insert
`PUT` will update
`DELETE` will delete
`GET` will return doclist
`POST` will insert
`PUT` will update
`DELETE` will delete


`/api/resource/{doctype}/{name}?run_method={method}` will run a whitelisted controller method `/api/resource/{doctype}/{name}?run_method={method}` will run a whitelisted controller method
""" """


parts = frappe.request.path[1:].split("/",3)
parts = frappe.request.path[1:].split("/", 3)
call = doctype = name = None call = doctype = name = None


if len(parts) > 1: if len(parts) > 1:
@@ -49,22 +49,22 @@ def handle():
if len(parts) > 3: if len(parts) > 3:
name = parts[3] name = parts[3]


if call=="method":
if call == "method":
frappe.local.form_dict.cmd = doctype frappe.local.form_dict.cmd = doctype
return frappe.handler.handle() return frappe.handler.handle()


elif call=="resource":
elif call == "resource":
if "run_method" in frappe.local.form_dict: if "run_method" in frappe.local.form_dict:
method = frappe.local.form_dict.pop("run_method") method = frappe.local.form_dict.pop("run_method")
doc = frappe.get_doc(doctype, name) doc = frappe.get_doc(doctype, name)
doc.is_whitelisted(method) doc.is_whitelisted(method)


if frappe.local.request.method=="GET":
if frappe.local.request.method == "GET":
if not doc.has_permission("read"): if not doc.has_permission("read"):
frappe.throw(_("Not permitted"), frappe.PermissionError) frappe.throw(_("Not permitted"), frappe.PermissionError)
frappe.local.response.update({"data": doc.run_method(method, **frappe.local.form_dict)}) frappe.local.response.update({"data": doc.run_method(method, **frappe.local.form_dict)})


if frappe.local.request.method=="POST":
if frappe.local.request.method == "POST":
if not doc.has_permission("write"): if not doc.has_permission("write"):
frappe.throw(_("Not permitted"), frappe.PermissionError) frappe.throw(_("Not permitted"), frappe.PermissionError)


@@ -73,13 +73,13 @@ def handle():


else: else:
if name: if name:
if frappe.local.request.method=="GET":
if frappe.local.request.method == "GET":
doc = frappe.get_doc(doctype, name) doc = frappe.get_doc(doctype, name)
if not doc.has_permission("read"): if not doc.has_permission("read"):
raise frappe.PermissionError raise frappe.PermissionError
frappe.local.response.update({"data": doc}) frappe.local.response.update({"data": doc})


if frappe.local.request.method=="PUT":
if frappe.local.request.method == "PUT":
data = get_request_form_data() data = get_request_form_data()


doc = frappe.get_doc(doctype, name, for_update=True) doc = frappe.get_doc(doctype, name, for_update=True)
@@ -90,9 +90,7 @@ def handle():
# Not checking permissions here because it's checked in doc.save # Not checking permissions here because it's checked in doc.save
doc.update(data) doc.update(data)


frappe.local.response.update({
"data": doc.save().as_dict()
})
frappe.local.response.update({"data": doc.save().as_dict()})


# check for child table doctype # check for child table doctype
if doc.get("parenttype"): if doc.get("parenttype"):
@@ -183,7 +181,7 @@ def validate_oauth(authorization_header):
Authenticate request using OAuth and set session user Authenticate request using OAuth and set session user


Args: Args:
authorization_header (list of str): The 'Authorization' header containing the prefix and token
authorization_header (list of str): The 'Authorization' header containing the prefix and token
""" """


from frappe.integrations.oauth2 import get_oauth_server from frappe.integrations.oauth2 import get_oauth_server
@@ -194,7 +192,9 @@ def validate_oauth(authorization_header):
req = frappe.request req = frappe.request
parsed_url = urlparse(req.url) parsed_url = urlparse(req.url)
access_token = {"access_token": token} access_token = {"access_token": token}
uri = parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + "?" + urlencode(access_token)
uri = (
parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + "?" + urlencode(access_token)
)
http_method = req.method http_method = req.method
headers = req.headers headers = req.headers
body = req.get_data() body = req.get_data()
@@ -202,8 +202,12 @@ def validate_oauth(authorization_header):
body = None body = None


try: try:
required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(get_url_delimiter())
valid, oauthlib_request = get_oauth_server().verify_request(uri, http_method, body, headers, required_scopes)
required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(
get_url_delimiter()
)
valid, oauthlib_request = get_oauth_server().verify_request(
uri, http_method, body, headers, required_scopes
)
if valid: if valid:
frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user")) frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user"))
frappe.local.form_dict = form_dict frappe.local.form_dict = form_dict
@@ -216,48 +220,43 @@ def validate_auth_via_api_keys(authorization_header):
Authenticate request using API keys and set session user Authenticate request using API keys and set session user


Args: Args:
authorization_header (list of str): The 'Authorization' header containing the prefix and token
authorization_header (list of str): The 'Authorization' header containing the prefix and token
""" """


try: try:
auth_type, auth_token = authorization_header auth_type, auth_token = authorization_header
authorization_source = frappe.get_request_header("Frappe-Authorization-Source") authorization_source = frappe.get_request_header("Frappe-Authorization-Source")
if auth_type.lower() == 'basic':
if auth_type.lower() == "basic":
api_key, api_secret = frappe.safe_decode(base64.b64decode(auth_token)).split(":") api_key, api_secret = frappe.safe_decode(base64.b64decode(auth_token)).split(":")
validate_api_key_secret(api_key, api_secret, authorization_source) validate_api_key_secret(api_key, api_secret, authorization_source)
elif auth_type.lower() == 'token':
elif auth_type.lower() == "token":
api_key, api_secret = auth_token.split(":") api_key, api_secret = auth_token.split(":")
validate_api_key_secret(api_key, api_secret, authorization_source) validate_api_key_secret(api_key, api_secret, authorization_source)
except binascii.Error: except binascii.Error:
frappe.throw(_("Failed to decode token, please provide a valid base64-encoded token."), frappe.InvalidAuthorizationToken)
frappe.throw(
_("Failed to decode token, please provide a valid base64-encoded token."),
frappe.InvalidAuthorizationToken,
)
except (AttributeError, TypeError, ValueError): except (AttributeError, TypeError, ValueError):
pass pass




def validate_api_key_secret(api_key, api_secret, frappe_authorization_source=None): def validate_api_key_secret(api_key, api_secret, frappe_authorization_source=None):
"""frappe_authorization_source to provide api key and secret for a doctype apart from User""" """frappe_authorization_source to provide api key and secret for a doctype apart from User"""
doctype = frappe_authorization_source or 'User'
doc = frappe.db.get_value(
doctype=doctype,
filters={"api_key": api_key},
fieldname=["name"]
)
doctype = frappe_authorization_source or "User"
doc = frappe.db.get_value(doctype=doctype, filters={"api_key": api_key}, fieldname=["name"])
form_dict = frappe.local.form_dict form_dict = frappe.local.form_dict
doc_secret = frappe.utils.password.get_decrypted_password(doctype, doc, fieldname='api_secret')
doc_secret = frappe.utils.password.get_decrypted_password(doctype, doc, fieldname="api_secret")
if api_secret == doc_secret: if api_secret == doc_secret:
if doctype == 'User':
user = frappe.db.get_value(
doctype="User",
filters={"api_key": api_key},
fieldname=["name"]
)
if doctype == "User":
user = frappe.db.get_value(doctype="User", filters={"api_key": api_key}, fieldname=["name"])
else: else:
user = frappe.db.get_value(doctype, doc, 'user')
if frappe.local.login_manager.user in ('', 'Guest'):
user = frappe.db.get_value(doctype, doc, "user")
if frappe.local.login_manager.user in ("", "Guest"):
frappe.set_user(user) frappe.set_user(user)
frappe.local.form_dict = form_dict frappe.local.form_dict = form_dict




def validate_auth_via_hooks(): def validate_auth_via_hooks():
for auth_hook in frappe.get_hooks('auth_hooks', []):
for auth_hook in frappe.get_hooks("auth_hooks", []):
frappe.get_attr(auth_hook)() frappe.get_attr(auth_hook)()

+ 110
- 82
frappe/app.py 查看文件

@@ -2,37 +2,37 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE


import os
import logging import logging
import os


from werkzeug.local import LocalManager
from werkzeug.wrappers import Request, Response
from werkzeug.exceptions import HTTPException, NotFound from werkzeug.exceptions import HTTPException, NotFound
from werkzeug.local import LocalManager
from werkzeug.middleware.profiler import ProfilerMiddleware from werkzeug.middleware.profiler import ProfilerMiddleware
from werkzeug.middleware.shared_data import SharedDataMiddleware from werkzeug.middleware.shared_data import SharedDataMiddleware
from werkzeug.wrappers import Request, Response


import frappe import frappe
import frappe.handler
import frappe.auth
import frappe.api import frappe.api
import frappe.auth
import frappe.handler
import frappe.monitor
import frappe.rate_limiter
import frappe.recorder
import frappe.utils.response import frappe.utils.response
from frappe.utils import get_site_name, sanitize_html
from frappe import _
from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request
from frappe.middlewares import StaticDataMiddleware from frappe.middlewares import StaticDataMiddleware
from frappe.website.serve import get_response
from frappe.utils import get_site_name, sanitize_html
from frappe.utils.error import make_error_snapshot from frappe.utils.error import make_error_snapshot
from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request
from frappe import _
import frappe.recorder
import frappe.monitor
import frappe.rate_limiter
from frappe.website.serve import get_response


local_manager = LocalManager([frappe.local]) local_manager = LocalManager([frappe.local])


_site = None _site = None
_sites_path = os.environ.get("SITES_PATH", ".") _sites_path = os.environ.get("SITES_PATH", ".")


class RequestContext(object):


class RequestContext(object):
def __init__(self, environ): def __init__(self, environ):
self.request = Request(environ) self.request = Request(environ)


@@ -42,6 +42,7 @@ class RequestContext(object):
def __exit__(self, type, value, traceback): def __exit__(self, type, value, traceback):
frappe.destroy() frappe.destroy()



@Request.application @Request.application
def application(request): def application(request):
response = None response = None
@@ -65,13 +66,13 @@ def application(request):
elif request.path.startswith("/api/"): elif request.path.startswith("/api/"):
response = frappe.api.handle() response = frappe.api.handle()


elif request.path.startswith('/backups'):
elif request.path.startswith("/backups"):
response = frappe.utils.response.download_backup(request.path) response = frappe.utils.response.download_backup(request.path)


elif request.path.startswith('/private/files/'):
elif request.path.startswith("/private/files/"):
response = frappe.utils.response.download_private_file(request.path) response = frappe.utils.response.download_private_file(request.path)


elif request.method in ('GET', 'HEAD', 'POST'):
elif request.method in ("GET", "HEAD", "POST"):
response = get_response() response = get_response()


else: else:
@@ -103,41 +104,45 @@ def application(request):


return response return response



def init_request(request): def init_request(request):
frappe.local.request = request frappe.local.request = request
frappe.local.is_ajax = frappe.get_request_header("X-Requested-With")=="XMLHttpRequest"
frappe.local.is_ajax = frappe.get_request_header("X-Requested-With") == "XMLHttpRequest"


site = _site or request.headers.get('X-Frappe-Site-Name') or get_site_name(request.host)
site = _site or request.headers.get("X-Frappe-Site-Name") or get_site_name(request.host)
frappe.init(site=site, sites_path=_sites_path) frappe.init(site=site, sites_path=_sites_path)


if not (frappe.local.conf and frappe.local.conf.db_name): if not (frappe.local.conf and frappe.local.conf.db_name):
# site does not exist # site does not exist
raise NotFound raise NotFound


if frappe.local.conf.get('maintenance_mode'):
if frappe.local.conf.get("maintenance_mode"):
frappe.connect() frappe.connect()
raise frappe.SessionStopped('Session Stopped')
raise frappe.SessionStopped("Session Stopped")
else: else:
frappe.connect(set_admin_as_user=False) frappe.connect(set_admin_as_user=False)


request.max_content_length = frappe.local.conf.get('max_file_size') or 10 * 1024 * 1024
request.max_content_length = frappe.local.conf.get("max_file_size") or 10 * 1024 * 1024


make_form_dict(request) make_form_dict(request)


if request.method != "OPTIONS": if request.method != "OPTIONS":
frappe.local.http_request = frappe.auth.HTTPRequest() frappe.local.http_request = frappe.auth.HTTPRequest()



def log_request(request, response): def log_request(request, response):
if hasattr(frappe.local, 'conf') and frappe.local.conf.enable_frappe_logger:
frappe.logger("frappe.web", allow_site=frappe.local.site).info({
"site": get_site_name(request.host),
"remote_addr": getattr(request, "remote_addr", "NOTFOUND"),
"base_url": getattr(request, "base_url", "NOTFOUND"),
"full_path": getattr(request, "full_path", "NOTFOUND"),
"method": getattr(request, "method", "NOTFOUND"),
"scheme": getattr(request, "scheme", "NOTFOUND"),
"http_status_code": getattr(response, "status_code", "NOTFOUND")
})
if hasattr(frappe.local, "conf") and frappe.local.conf.enable_frappe_logger:
frappe.logger("frappe.web", allow_site=frappe.local.site).info(
{
"site": get_site_name(request.host),
"remote_addr": getattr(request, "remote_addr", "NOTFOUND"),
"base_url": getattr(request, "base_url", "NOTFOUND"),
"full_path": getattr(request, "full_path", "NOTFOUND"),
"method": getattr(request, "method", "NOTFOUND"),
"scheme": getattr(request, "scheme", "NOTFOUND"),
"http_status_code": getattr(response, "status_code", "NOTFOUND"),
}
)




def process_response(response): def process_response(response):
@@ -145,19 +150,20 @@ def process_response(response):
return return


# set cookies # set cookies
if hasattr(frappe.local, 'cookie_manager'):
if hasattr(frappe.local, "cookie_manager"):
frappe.local.cookie_manager.flush_cookies(response=response) frappe.local.cookie_manager.flush_cookies(response=response)


# rate limiter headers # rate limiter headers
if hasattr(frappe.local, 'rate_limiter'):
if hasattr(frappe.local, "rate_limiter"):
response.headers.extend(frappe.local.rate_limiter.headers()) response.headers.extend(frappe.local.rate_limiter.headers())


# CORS headers # CORS headers
if hasattr(frappe.local, 'conf') and frappe.conf.allow_cors:
if hasattr(frappe.local, "conf") and frappe.conf.allow_cors:
set_cors_headers(response) set_cors_headers(response)



def set_cors_headers(response): def set_cors_headers(response):
origin = frappe.request.headers.get('Origin')
origin = frappe.request.headers.get("Origin")
allow_cors = frappe.conf.allow_cors allow_cors = frappe.conf.allow_cors
if not (origin and allow_cors): if not (origin and allow_cors):
return return
@@ -169,20 +175,25 @@ def set_cors_headers(response):
if origin not in allow_cors: if origin not in allow_cors:
return return


response.headers.extend({
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': ('Authorization,DNT,X-Mx-ReqToken,'
'Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,'
'Cache-Control,Content-Type')
})
response.headers.extend(
{
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": (
"Authorization,DNT,X-Mx-ReqToken,"
"Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,"
"Cache-Control,Content-Type"
),
}
)



def make_form_dict(request): def make_form_dict(request):
import json import json


request_data = request.get_data(as_text=True) request_data = request.get_data(as_text=True)
if 'application/json' in (request.content_type or '') and request_data:
if "application/json" in (request.content_type or "") and request_data:
args = json.loads(request_data) args = json.loads(request_data)
else: else:
args = {} args = {}
@@ -198,20 +209,19 @@ def make_form_dict(request):
# _ is passed by $.ajax so that the request is not cached by the browser. So, remove _ from form_dict # _ is passed by $.ajax so that the request is not cached by the browser. So, remove _ from form_dict
frappe.local.form_dict.pop("_") frappe.local.form_dict.pop("_")



def handle_exception(e): def handle_exception(e):
response = None response = None
http_status_code = getattr(e, "http_status_code", 500) http_status_code = getattr(e, "http_status_code", 500)
return_as_message = False return_as_message = False
accept_header = frappe.get_request_header("Accept") or "" accept_header = frappe.get_request_header("Accept") or ""
respond_as_json = ( respond_as_json = (
frappe.get_request_header('Accept')
and (frappe.local.is_ajax or 'application/json' in accept_header)
or (
frappe.local.request.path.startswith("/api/") and not accept_header.startswith("text")
)
frappe.get_request_header("Accept")
and (frappe.local.is_ajax or "application/json" in accept_header)
or (frappe.local.request.path.startswith("/api/") and not accept_header.startswith("text"))
) )


if frappe.conf.get('developer_mode'):
if frappe.conf.get("developer_mode"):
# don't fail silently # don't fail silently
print(frappe.get_traceback()) print(frappe.get_traceback())


@@ -220,27 +230,38 @@ def handle_exception(e):
# if the request is ajax, send back the trace or error message # if the request is ajax, send back the trace or error message
response = frappe.utils.response.report_error(http_status_code) response = frappe.utils.response.report_error(http_status_code)


elif (http_status_code==500
elif (
http_status_code == 500
and (frappe.db and isinstance(e, frappe.db.InternalError)) and (frappe.db and isinstance(e, frappe.db.InternalError))
and (frappe.db and (frappe.db.is_deadlocked(e) or frappe.db.is_timedout(e)))):
http_status_code = 508
and (frappe.db and (frappe.db.is_deadlocked(e) or frappe.db.is_timedout(e)))
):
http_status_code = 508


elif http_status_code==401:
frappe.respond_as_web_page(_("Session Expired"),
elif http_status_code == 401:
frappe.respond_as_web_page(
_("Session Expired"),
_("Your session has expired, please login again to continue."), _("Your session has expired, please login again to continue."),
http_status_code=http_status_code, indicator_color='red')
http_status_code=http_status_code,
indicator_color="red",
)
return_as_message = True return_as_message = True


elif http_status_code==403:
frappe.respond_as_web_page(_("Not Permitted"),
elif http_status_code == 403:
frappe.respond_as_web_page(
_("Not Permitted"),
_("You do not have enough permissions to complete the action"), _("You do not have enough permissions to complete the action"),
http_status_code=http_status_code, indicator_color='red')
http_status_code=http_status_code,
indicator_color="red",
)
return_as_message = True return_as_message = True


elif http_status_code==404:
frappe.respond_as_web_page(_("Not Found"),
elif http_status_code == 404:
frappe.respond_as_web_page(
_("Not Found"),
_("The resource you are looking for is not available"), _("The resource you are looking for is not available"),
http_status_code=http_status_code, indicator_color='red')
http_status_code=http_status_code,
indicator_color="red",
)
return_as_message = True return_as_message = True


elif http_status_code == 429: elif http_status_code == 429:
@@ -252,9 +273,9 @@ def handle_exception(e):
if frappe.local.flags.disable_traceback and not frappe.local.dev_server: if frappe.local.flags.disable_traceback and not frappe.local.dev_server:
traceback = "" traceback = ""


frappe.respond_as_web_page("Server Error",
traceback, http_status_code=http_status_code,
indicator_color='red', width=640)
frappe.respond_as_web_page(
"Server Error", traceback, http_status_code=http_status_code, indicator_color="red", width=640
)
return_as_message = True return_as_message = True


if e.__class__ == frappe.AuthenticationError: if e.__class__ == frappe.AuthenticationError:
@@ -269,6 +290,7 @@ def handle_exception(e):


return response return response



def after_request(rollback): def after_request(rollback):
if (frappe.local.request.method in ("POST", "PUT") or frappe.local.flags.commit) and frappe.db: if (frappe.local.request.method in ("POST", "PUT") or frappe.local.flags.commit) and frappe.db:
if frappe.db.transaction_writes: if frappe.db.transaction_writes:
@@ -286,41 +308,47 @@ def after_request(rollback):


return rollback return rollback



application = local_manager.make_middleware(application) application = local_manager.make_middleware(application)


def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=None, sites_path='.'):

def serve(
port=8000, profile=False, no_reload=False, no_threading=False, site=None, sites_path="."
):
global application, _site, _sites_path global application, _site, _sites_path
_site = site _site = site
_sites_path = sites_path _sites_path = sites_path


from werkzeug.serving import run_simple from werkzeug.serving import run_simple


if profile or os.environ.get('USE_PROFILER'):
application = ProfilerMiddleware(application, sort_by=('cumtime', 'calls'))
if profile or os.environ.get("USE_PROFILER"):
application = ProfilerMiddleware(application, sort_by=("cumtime", "calls"))


if not os.environ.get('NO_STATICS'):
application = SharedDataMiddleware(application, {
str('/assets'): str(os.path.join(sites_path, 'assets'))
})
if not os.environ.get("NO_STATICS"):
application = SharedDataMiddleware(
application, {str("/assets"): str(os.path.join(sites_path, "assets"))}
)


application = StaticDataMiddleware(application, {
str('/files'): str(os.path.abspath(sites_path))
})
application = StaticDataMiddleware(
application, {str("/files"): str(os.path.abspath(sites_path))}
)


application.debug = True application.debug = True
application.config = {
'SERVER_NAME': 'localhost:8000'
}
application.config = {"SERVER_NAME": "localhost:8000"}


log = logging.getLogger('werkzeug')
log = logging.getLogger("werkzeug")
log.propagate = False log.propagate = False


in_test_env = os.environ.get('CI')
in_test_env = os.environ.get("CI")
if in_test_env: if in_test_env:
log.setLevel(logging.ERROR) log.setLevel(logging.ERROR)


run_simple('0.0.0.0', int(port), application,
run_simple(
"0.0.0.0",
int(port),
application,
use_reloader=False if in_test_env else not no_reload, use_reloader=False if in_test_env else not no_reload,
use_debugger=not in_test_env, use_debugger=not in_test_env,
use_evalex=not in_test_env, use_evalex=not in_test_env,
threaded=not no_threading)
threaded=not no_threading,
)

+ 117
- 74
frappe/auth.py 查看文件

@@ -11,7 +11,12 @@ from frappe.core.doctype.activity_log.activity_log import add_authentication_log
from frappe.modules.patch_handler import check_session_stopped from frappe.modules.patch_handler import check_session_stopped
from frappe.sessions import Session, clear_sessions, delete_session from frappe.sessions import Session, clear_sessions, delete_session
from frappe.translate import get_language from frappe.translate import get_language
from frappe.twofactor import authenticate_for_2factor, confirm_otp_token, get_cached_user_pass, should_run_2fa
from frappe.twofactor import (
authenticate_for_2factor,
confirm_otp_token,
get_cached_user_pass,
should_run_2fa,
)
from frappe.utils import cint, date_diff, datetime, get_datetime, today from frappe.utils import cint, date_diff, datetime, get_datetime, today
from frappe.utils.password import check_password from frappe.utils.password import check_password
from frappe.website.utils import get_home_page from frappe.website.utils import get_home_page
@@ -47,20 +52,20 @@ class HTTPRequest:
def domain(self): def domain(self):
if not getattr(self, "_domain", None): if not getattr(self, "_domain", None):
self._domain = frappe.request.host self._domain = frappe.request.host
if self._domain and self._domain.startswith('www.'):
if self._domain and self._domain.startswith("www."):
self._domain = self._domain[4:] self._domain = self._domain[4:]


return self._domain return self._domain


def set_request_ip(self): def set_request_ip(self):
if frappe.get_request_header('X-Forwarded-For'):
frappe.local.request_ip = (frappe.get_request_header('X-Forwarded-For').split(",")[0]).strip()
if frappe.get_request_header("X-Forwarded-For"):
frappe.local.request_ip = (frappe.get_request_header("X-Forwarded-For").split(",")[0]).strip()


elif frappe.get_request_header('REMOTE_ADDR'):
frappe.local.request_ip = frappe.get_request_header('REMOTE_ADDR')
elif frappe.get_request_header("REMOTE_ADDR"):
frappe.local.request_ip = frappe.get_request_header("REMOTE_ADDR")


else: else:
frappe.local.request_ip = '127.0.0.1'
frappe.local.request_ip = "127.0.0.1"


def set_cookies(self): def set_cookies(self):
frappe.local.cookie_manager = CookieManager() frappe.local.cookie_manager = CookieManager()
@@ -75,7 +80,7 @@ class HTTPRequest:
if ( if (
not frappe.local.session.data.csrf_token not frappe.local.session.data.csrf_token
or frappe.local.session.data.device == "mobile" or frappe.local.session.data.device == "mobile"
or frappe.conf.get('ignore_csrf', None)
or frappe.conf.get("ignore_csrf", None)
): ):
# not via boot # not via boot
return return
@@ -99,10 +104,10 @@ class HTTPRequest:
def connect(self): def connect(self):
"""connect to db, from ac_name or db_name""" """connect to db, from ac_name or db_name"""
frappe.local.db = frappe.database.get_db( frappe.local.db = frappe.database.get_db(
user=self.get_db_name(),
password=getattr(conf, 'db_password', '')
user=self.get_db_name(), password=getattr(conf, "db_password", "")
) )



class LoginManager: class LoginManager:
def __init__(self): def __init__(self):
self.user = None self.user = None
@@ -110,13 +115,15 @@ class LoginManager:
self.full_name = None self.full_name = None
self.user_type = None self.user_type = None


if frappe.local.form_dict.get('cmd')=='login' or frappe.local.request.path=="/api/method/login":
if (
frappe.local.form_dict.get("cmd") == "login" or frappe.local.request.path == "/api/method/login"
):
if self.login() is False: if self.login() is False:
return return
self.resume = False self.resume = False


# run login triggers # run login triggers
self.run_trigger('on_session_creation')
self.run_trigger("on_session_creation")
else: else:
try: try:
self.resume = True self.resume = True
@@ -131,12 +138,14 @@ class LoginManager:


def login(self): def login(self):
# clear cache # clear cache
frappe.clear_cache(user = frappe.form_dict.get('usr'))
frappe.clear_cache(user=frappe.form_dict.get("usr"))
user, pwd = get_cached_user_pass() user, pwd = get_cached_user_pass()
self.authenticate(user=user, pwd=pwd) self.authenticate(user=user, pwd=pwd)
if self.force_user_to_reset_password(): if self.force_user_to_reset_password():
doc = frappe.get_doc("User", self.user) doc = frappe.get_doc("User", self.user)
frappe.local.response["redirect_to"] = doc.reset_password(send_email=False, password_expired=True)
frappe.local.response["redirect_to"] = doc.reset_password(
send_email=False, password_expired=True
)
frappe.local.response["message"] = "Password Reset" frappe.local.response["message"] = "Password Reset"
return False return False


@@ -147,7 +156,7 @@ class LoginManager:
self.post_login() self.post_login()


def post_login(self): def post_login(self):
self.run_trigger('on_login')
self.run_trigger("on_login")
validate_ip_address(self.user) validate_ip_address(self.user)
self.validate_hour() self.validate_hour()
self.get_user_info() self.get_user_info()
@@ -156,8 +165,9 @@ class LoginManager:
self.set_user_info() self.set_user_info()


def get_user_info(self): def get_user_info(self):
self.info = frappe.db.get_value("User", self.user,
["user_type", "first_name", "last_name", "user_image"], as_dict=1)
self.info = frappe.db.get_value(
"User", self.user, ["user_type", "first_name", "last_name", "user_image"], as_dict=1
)


self.user_type = self.info.user_type self.user_type = self.info.user_type


@@ -170,28 +180,27 @@ class LoginManager:
# set sid again # set sid again
frappe.local.cookie_manager.init_cookies() frappe.local.cookie_manager.init_cookies()


self.full_name = " ".join(filter(None, [self.info.first_name,
self.info.last_name]))
self.full_name = " ".join(filter(None, [self.info.first_name, self.info.last_name]))


if self.info.user_type=="Website User":
if self.info.user_type == "Website User":
frappe.local.cookie_manager.set_cookie("system_user", "no") frappe.local.cookie_manager.set_cookie("system_user", "no")
if not resume: if not resume:
frappe.local.response["message"] = "No App" frappe.local.response["message"] = "No App"
frappe.local.response["home_page"] = '/' + get_home_page()
frappe.local.response["home_page"] = "/" + get_home_page()
else: else:
frappe.local.cookie_manager.set_cookie("system_user", "yes") frappe.local.cookie_manager.set_cookie("system_user", "yes")
if not resume: if not resume:
frappe.local.response['message'] = 'Logged In'
frappe.local.response["message"] = "Logged In"
frappe.local.response["home_page"] = "/app" frappe.local.response["home_page"] = "/app"


if not resume: if not resume:
frappe.response["full_name"] = self.full_name frappe.response["full_name"] = self.full_name


# redirect information # redirect information
redirect_to = frappe.cache().hget('redirect_after_login', self.user)
redirect_to = frappe.cache().hget("redirect_after_login", self.user)
if redirect_to: if redirect_to:
frappe.local.response["redirect_to"] = redirect_to frappe.local.response["redirect_to"] = redirect_to
frappe.cache().hdel('redirect_after_login', self.user)
frappe.cache().hdel("redirect_after_login", self.user)


frappe.local.cookie_manager.set_cookie("full_name", self.full_name) frappe.local.cookie_manager.set_cookie("full_name", self.full_name)
frappe.local.cookie_manager.set_cookie("user_id", self.user) frappe.local.cookie_manager.set_cookie("user_id", self.user)
@@ -202,8 +211,9 @@ class LoginManager:


def make_session(self, resume=False): def make_session(self, resume=False):
# start session # start session
frappe.local.session_obj = Session(user=self.user, resume=resume,
full_name=self.full_name, user_type=self.user_type)
frappe.local.session_obj = Session(
user=self.user, resume=resume, full_name=self.full_name, user_type=self.user_type
)


# reset user if changed to Guest # reset user if changed to Guest
self.user = frappe.local.session_obj.user self.user = frappe.local.session_obj.user
@@ -212,7 +222,10 @@ class LoginManager:


def clear_active_sessions(self): def clear_active_sessions(self):
"""Clear other sessions of the current user if `deny_multiple_sessions` is not set""" """Clear other sessions of the current user if `deny_multiple_sessions` is not set"""
if not (cint(frappe.conf.get("deny_multiple_sessions")) or cint(frappe.db.get_system_setting('deny_multiple_sessions'))):
if not (
cint(frappe.conf.get("deny_multiple_sessions"))
or cint(frappe.db.get_system_setting("deny_multiple_sessions"))
):
return return


if frappe.session.user != "Guest": if frappe.session.user != "Guest":
@@ -222,27 +235,27 @@ class LoginManager:
from frappe.core.doctype.user.user import User from frappe.core.doctype.user.user import User


if not (user and pwd): if not (user and pwd):
user, pwd = frappe.form_dict.get('usr'), frappe.form_dict.get('pwd')
user, pwd = frappe.form_dict.get("usr"), frappe.form_dict.get("pwd")
if not (user and pwd): if not (user and pwd):
self.fail(_('Incomplete login details'), user=user)
self.fail(_("Incomplete login details"), user=user)


user = User.find_by_credentials(user, pwd) user = User.find_by_credentials(user, pwd)


if not user: if not user:
self.fail('Invalid login credentials')
self.fail("Invalid login credentials")


# Current login flow uses cached credentials for authentication while checking OTP. # Current login flow uses cached credentials for authentication while checking OTP.
# Incase of OTP check, tracker for auth needs to be disabled(If not, it can remove tracker history as it is going to succeed anyway) # Incase of OTP check, tracker for auth needs to be disabled(If not, it can remove tracker history as it is going to succeed anyway)
# Tracker is activated for 2FA incase of OTP. # Tracker is activated for 2FA incase of OTP.
ignore_tracker = should_run_2fa(user.name) and ('otp' in frappe.form_dict)
ignore_tracker = should_run_2fa(user.name) and ("otp" in frappe.form_dict)
tracker = None if ignore_tracker else get_login_attempt_tracker(user.name) tracker = None if ignore_tracker else get_login_attempt_tracker(user.name)


if not user.is_authenticated: if not user.is_authenticated:
tracker and tracker.add_failure_attempt() tracker and tracker.add_failure_attempt()
self.fail('Invalid login credentials', user=user.name)
elif not (user.name == 'Administrator' or user.enabled):
self.fail("Invalid login credentials", user=user.name)
elif not (user.name == "Administrator" or user.enabled):
tracker and tracker.add_failure_attempt() tracker and tracker.add_failure_attempt()
self.fail('User disabled or missing', user=user.name)
self.fail("User disabled or missing", user=user.name)
else: else:
tracker and tracker.add_success_attempt() tracker and tracker.add_success_attempt()
self.user = user.name self.user = user.name
@@ -254,12 +267,14 @@ class LoginManager:
if self.user in frappe.STANDARD_USERS: if self.user in frappe.STANDARD_USERS:
return False return False


reset_pwd_after_days = cint(frappe.db.get_single_value("System Settings",
"force_user_to_reset_password"))
reset_pwd_after_days = cint(
frappe.db.get_single_value("System Settings", "force_user_to_reset_password")
)


if reset_pwd_after_days: if reset_pwd_after_days:
last_password_reset_date = frappe.db.get_value("User",
self.user, "last_password_reset_date") or today()
last_password_reset_date = (
frappe.db.get_value("User", self.user, "last_password_reset_date") or today()
)


last_pwd_reset_days = date_diff(today(), last_password_reset_date) last_pwd_reset_days = date_diff(today(), last_password_reset_date)


@@ -272,30 +287,31 @@ class LoginManager:
# returns user in correct case # returns user in correct case
return check_password(user, pwd) return check_password(user, pwd)
except frappe.AuthenticationError: except frappe.AuthenticationError:
self.fail('Incorrect password', user=user)
self.fail("Incorrect password", user=user)


def fail(self, message, user=None): def fail(self, message, user=None):
if not user: if not user:
user = _('Unknown User')
frappe.local.response['message'] = message
user = _("Unknown User")
frappe.local.response["message"] = message
add_authentication_log(message, user, status="Failed") add_authentication_log(message, user, status="Failed")
frappe.db.commit() frappe.db.commit()
raise frappe.AuthenticationError raise frappe.AuthenticationError


def run_trigger(self, event='on_login'):
def run_trigger(self, event="on_login"):
for method in frappe.get_hooks().get(event, []): for method in frappe.get_hooks().get(event, []):
frappe.call(frappe.get_attr(method), login_manager=self) frappe.call(frappe.get_attr(method), login_manager=self)


def validate_hour(self): def validate_hour(self):
"""check if user is logging in during restricted hours""" """check if user is logging in during restricted hours"""
login_before = int(frappe.db.get_value('User', self.user, 'login_before', ignore=True) or 0)
login_after = int(frappe.db.get_value('User', self.user, 'login_after', ignore=True) or 0)
login_before = int(frappe.db.get_value("User", self.user, "login_before", ignore=True) or 0)
login_after = int(frappe.db.get_value("User", self.user, "login_after", ignore=True) or 0)


if not (login_before or login_after): if not (login_before or login_after):
return return


from frappe.utils import now_datetime from frappe.utils import now_datetime
current_hour = int(now_datetime().strftime('%H'))

current_hour = int(now_datetime().strftime("%H"))


if login_before and current_hour > login_before: if login_before and current_hour > login_before:
frappe.throw(_("Login not allowed at this time"), frappe.AuthenticationError) frappe.throw(_("Login not allowed at this time"), frappe.AuthenticationError)
@@ -311,9 +327,10 @@ class LoginManager:
self.user = user self.user = user
self.post_login() self.post_login()


def logout(self, arg='', user=None):
if not user: user = frappe.session.user
self.run_trigger('on_logout')
def logout(self, arg="", user=None):
if not user:
user = frappe.session.user
self.run_trigger("on_logout")


if user == frappe.session.user: if user == frappe.session.user:
delete_session(frappe.session.sid, user=user, reason="User Manually Logged Out") delete_session(frappe.session.sid, user=user, reason="User Manually Logged Out")
@@ -324,13 +341,15 @@ class LoginManager:
def clear_cookies(self): def clear_cookies(self):
clear_cookies() clear_cookies()



class CookieManager: class CookieManager:
def __init__(self): def __init__(self):
self.cookies = {} self.cookies = {}
self.to_delete = [] self.to_delete = []


def init_cookies(self): def init_cookies(self):
if not frappe.local.session.get('sid'): return
if not frappe.local.session.get("sid"):
return


# sid expires in 3 days # sid expires in 3 days
expires = datetime.datetime.now() + datetime.timedelta(days=3) expires = datetime.datetime.now() + datetime.timedelta(days=3)
@@ -340,7 +359,7 @@ class CookieManager:
self.set_cookie("country", frappe.session.session_country) self.set_cookie("country", frappe.session.session_country)


def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Lax"): def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Lax"):
if not secure and hasattr(frappe.local, 'request'):
if not secure and hasattr(frappe.local, "request"):
secure = frappe.local.request.scheme == "https" secure = frappe.local.request.scheme == "https"


# Cordova does not work with Lax # Cordova does not work with Lax
@@ -352,7 +371,7 @@ class CookieManager:
"expires": expires, "expires": expires,
"secure": secure, "secure": secure,
"httponly": httponly, "httponly": httponly,
"samesite": samesite
"samesite": samesite,
} }


def delete_cookie(self, to_delete): def delete_cookie(self, to_delete):
@@ -363,11 +382,14 @@ class CookieManager:


def flush_cookies(self, response): def flush_cookies(self, response):
for key, opts in self.cookies.items(): for key, opts in self.cookies.items():
response.set_cookie(key, quote((opts.get("value") or "").encode('utf-8')),
response.set_cookie(
key,
quote((opts.get("value") or "").encode("utf-8")),
expires=opts.get("expires"), expires=opts.get("expires"),
secure=opts.get("secure"), secure=opts.get("secure"),
httponly=opts.get("httponly"), httponly=opts.get("httponly"),
samesite=opts.get("samesite"))
samesite=opts.get("samesite"),
)


# expires yesterday! # expires yesterday!
expires = datetime.datetime.now() + datetime.timedelta(days=-1) expires = datetime.datetime.now() + datetime.timedelta(days=-1)
@@ -379,19 +401,29 @@ class CookieManager:
def get_logged_user(): def get_logged_user():
return frappe.session.user return frappe.session.user



def clear_cookies(): def clear_cookies():
if hasattr(frappe.local, "session"): if hasattr(frappe.local, "session"):
frappe.session.sid = "" frappe.session.sid = ""
frappe.local.cookie_manager.delete_cookie(["full_name", "user_id", "sid", "user_image", "system_user"])
frappe.local.cookie_manager.delete_cookie(
["full_name", "user_id", "sid", "user_image", "system_user"]
)



def validate_ip_address(user): def validate_ip_address(user):
"""check if IP Address is valid""" """check if IP Address is valid"""
user = frappe.get_cached_doc("User", user) if not frappe.flags.in_test else frappe.get_doc("User", user)
user = (
frappe.get_cached_doc("User", user) if not frappe.flags.in_test else frappe.get_doc("User", user)
)
ip_list = user.get_restricted_ip_list() ip_list = user.get_restricted_ip_list()
if not ip_list: if not ip_list:
return return


system_settings = frappe.get_cached_doc("System Settings") if not frappe.flags.in_test else frappe.get_single("System Settings")
system_settings = (
frappe.get_cached_doc("System Settings")
if not frappe.flags.in_test
else frappe.get_single("System Settings")
)
# check if bypass restrict ip is enabled for all users # check if bypass restrict ip is enabled for all users
bypass_restrict_ip_check = system_settings.bypass_restrict_ip_check_if_2fa_enabled bypass_restrict_ip_check = system_settings.bypass_restrict_ip_check_if_2fa_enabled


@@ -406,6 +438,7 @@ def validate_ip_address(user):


frappe.throw(_("Access not allowed from this IP Address"), frappe.AuthenticationError) frappe.throw(_("Access not allowed from this IP Address"), frappe.AuthenticationError)



def get_login_attempt_tracker(user_name: str, raise_locked_exception: bool = True): def get_login_attempt_tracker(user_name: str, raise_locked_exception: bool = True):
"""Get login attempt tracker instance. """Get login attempt tracker instance.


@@ -413,18 +446,22 @@ def get_login_attempt_tracker(user_name: str, raise_locked_exception: bool = Tru
:param raise_locked_exception: If set, raises an exception incase of user not allowed to login :param raise_locked_exception: If set, raises an exception incase of user not allowed to login
""" """
sys_settings = frappe.get_doc("System Settings") sys_settings = frappe.get_doc("System Settings")
track_login_attempts = (sys_settings.allow_consecutive_login_attempts >0)
track_login_attempts = sys_settings.allow_consecutive_login_attempts > 0
tracker_kwargs = {} tracker_kwargs = {}


if track_login_attempts: if track_login_attempts:
tracker_kwargs['lock_interval'] = sys_settings.allow_login_after_fail
tracker_kwargs['max_consecutive_login_attempts'] = sys_settings.allow_consecutive_login_attempts
tracker_kwargs["lock_interval"] = sys_settings.allow_login_after_fail
tracker_kwargs["max_consecutive_login_attempts"] = sys_settings.allow_consecutive_login_attempts


tracker = LoginAttemptTracker(user_name, **tracker_kwargs) tracker = LoginAttemptTracker(user_name, **tracker_kwargs)


if raise_locked_exception and track_login_attempts and not tracker.is_user_allowed(): if raise_locked_exception and track_login_attempts and not tracker.is_user_allowed():
frappe.throw(_("Your account has been locked and will resume after {0} seconds")
.format(sys_settings.allow_login_after_fail), frappe.SecurityException)
frappe.throw(
_("Your account has been locked and will resume after {0} seconds").format(
sys_settings.allow_login_after_fail
),
frappe.SecurityException,
)
return tracker return tracker




@@ -433,8 +470,11 @@ class LoginAttemptTracker(object):


Lock the account for s number of seconds if there have been n consecutive unsuccessful attempts to log in. Lock the account for s number of seconds if there have been n consecutive unsuccessful attempts to log in.
""" """
def __init__(self, user_name: str, max_consecutive_login_attempts: int=3, lock_interval:int = 5*60):
""" Initialize the tracker.

def __init__(
self, user_name: str, max_consecutive_login_attempts: int = 3, lock_interval: int = 5 * 60
):
"""Initialize the tracker.


:param user_name: Name of the loggedin user :param user_name: Name of the loggedin user
:param max_consecutive_login_attempts: Maximum allowed consecutive failed login attempts :param max_consecutive_login_attempts: Maximum allowed consecutive failed login attempts
@@ -446,15 +486,15 @@ class LoginAttemptTracker(object):


@property @property
def login_failed_count(self): def login_failed_count(self):
return frappe.cache().hget('login_failed_count', self.user_name)
return frappe.cache().hget("login_failed_count", self.user_name)


@login_failed_count.setter @login_failed_count.setter
def login_failed_count(self, count): def login_failed_count(self, count):
frappe.cache().hset('login_failed_count', self.user_name, count)
frappe.cache().hset("login_failed_count", self.user_name, count)


@login_failed_count.deleter @login_failed_count.deleter
def login_failed_count(self): def login_failed_count(self):
frappe.cache().hdel('login_failed_count', self.user_name)
frappe.cache().hdel("login_failed_count", self.user_name)


@property @property
def login_failed_time(self): def login_failed_time(self):
@@ -462,23 +502,23 @@ class LoginAttemptTracker(object):


For every user we track only First failed login attempt time within lock interval of time. For every user we track only First failed login attempt time within lock interval of time.
""" """
return frappe.cache().hget('login_failed_time', self.user_name)
return frappe.cache().hget("login_failed_time", self.user_name)


@login_failed_time.setter @login_failed_time.setter
def login_failed_time(self, timestamp): def login_failed_time(self, timestamp):
frappe.cache().hset('login_failed_time', self.user_name, timestamp)
frappe.cache().hset("login_failed_time", self.user_name, timestamp)


@login_failed_time.deleter @login_failed_time.deleter
def login_failed_time(self): def login_failed_time(self):
frappe.cache().hdel('login_failed_time', self.user_name)
frappe.cache().hdel("login_failed_time", self.user_name)


def add_failure_attempt(self): def add_failure_attempt(self):
""" Log user failure attempts into the system.
"""Log user failure attempts into the system.


Increase the failure count if new failure is with in current lock interval time period, if not reset the login failure count. Increase the failure count if new failure is with in current lock interval time period, if not reset the login failure count.
""" """
login_failed_time = self.login_failed_time login_failed_time = self.login_failed_time
login_failed_count = self.login_failed_count # Consecutive login failure count
login_failed_count = self.login_failed_count # Consecutive login failure count
current_time = get_datetime() current_time = get_datetime()


if not (login_failed_time and login_failed_count): if not (login_failed_time and login_failed_count):
@@ -493,8 +533,7 @@ class LoginAttemptTracker(object):
self.login_failed_count = login_failed_count self.login_failed_count = login_failed_count


def add_success_attempt(self): def add_success_attempt(self):
"""Reset login failures.
"""
"""Reset login failures."""
del self.login_failed_count del self.login_failed_count
del self.login_failed_time del self.login_failed_time


@@ -507,6 +546,10 @@ class LoginAttemptTracker(object):
login_failed_count = self.login_failed_count or 0 login_failed_count = self.login_failed_count or 0
current_time = get_datetime() current_time = get_datetime()


if login_failed_time and login_failed_time + self.lock_interval > current_time and login_failed_count > self.max_failed_logins:
if (
login_failed_time
and login_failed_time + self.lock_interval > current_time
and login_failed_count > self.max_failed_logins
):
return False return False
return True return True

+ 104
- 83
frappe/automation/doctype/assignment_rule/assignment_rule.py 查看文件

@@ -24,9 +24,7 @@ class AssignmentRule(Document):
def validate_document_types(self): def validate_document_types(self):
if self.document_type == "ToDo": if self.document_type == "ToDo":
frappe.throw( frappe.throw(
_('Assignment Rule is not allowed on {0} document type').format(
frappe.bold("ToDo")
)
_("Assignment Rule is not allowed on {0} document type").format(frappe.bold("ToDo"))
) )


def validate_assignment_days(self): def validate_assignment_days(self):
@@ -38,70 +36,70 @@ class AssignmentRule(Document):


frappe.throw( frappe.throw(
_("Assignment Day{0} {1} has been repeated.").format( _("Assignment Day{0} {1} has been repeated.").format(
plural,
frappe.bold(", ".join(repeated_days))
plural, frappe.bold(", ".join(repeated_days))
) )
) )


def apply_unassign(self, doc, assignments): def apply_unassign(self, doc, assignments):
if (self.unassign_condition and
self.name in [d.assignment_rule for d in assignments]):
if self.unassign_condition and self.name in [d.assignment_rule for d in assignments]:
return self.clear_assignment(doc) return self.clear_assignment(doc)


return False return False


def apply_assign(self, doc): def apply_assign(self, doc):
if self.safe_eval('assign_condition', doc):
if self.safe_eval("assign_condition", doc):
return self.do_assignment(doc) return self.do_assignment(doc)


def do_assignment(self, doc): def do_assignment(self, doc):
# clear existing assignment, to reassign # clear existing assignment, to reassign
assign_to.clear(doc.get('doctype'), doc.get('name'))
assign_to.clear(doc.get("doctype"), doc.get("name"))


user = self.get_user(doc) user = self.get_user(doc)


if user: if user:
assign_to.add(dict(
assign_to = [user],
doctype = doc.get('doctype'),
name = doc.get('name'),
description = frappe.render_template(self.description, doc),
assignment_rule = self.name,
notify = True,
date = doc.get(self.due_date_based_on) if self.due_date_based_on else None
))
assign_to.add(
dict(
assign_to=[user],
doctype=doc.get("doctype"),
name=doc.get("name"),
description=frappe.render_template(self.description, doc),
assignment_rule=self.name,
notify=True,
date=doc.get(self.due_date_based_on) if self.due_date_based_on else None,
)
)


# set for reference in round robin # set for reference in round robin
self.db_set('last_user', user)
self.db_set("last_user", user)
return True return True


return False return False


def clear_assignment(self, doc): def clear_assignment(self, doc):
'''Clear assignments'''
if self.safe_eval('unassign_condition', doc):
return assign_to.clear(doc.get('doctype'), doc.get('name'))
"""Clear assignments"""
if self.safe_eval("unassign_condition", doc):
return assign_to.clear(doc.get("doctype"), doc.get("name"))


def close_assignments(self, doc): def close_assignments(self, doc):
'''Close assignments'''
if self.safe_eval('close_condition', doc):
return assign_to.close_all_assignments(doc.get('doctype'), doc.get('name'))
"""Close assignments"""
if self.safe_eval("close_condition", doc):
return assign_to.close_all_assignments(doc.get("doctype"), doc.get("name"))


def get_user(self, doc): def get_user(self, doc):
'''
"""
Get the next user for assignment Get the next user for assignment
'''
if self.rule == 'Round Robin':
"""
if self.rule == "Round Robin":
return self.get_user_round_robin() return self.get_user_round_robin()
elif self.rule == 'Load Balancing':
elif self.rule == "Load Balancing":
return self.get_user_load_balancing() return self.get_user_load_balancing()
elif self.rule == 'Based on Field':
elif self.rule == "Based on Field":
return self.get_user_based_on_field(doc) return self.get_user_based_on_field(doc)


def get_user_round_robin(self): def get_user_round_robin(self):
'''
"""
Get next user based on round robin Get next user based on round robin
'''
"""


# first time, or last in list, pick the first # first time, or last in list, pick the first
if not self.last_user or self.last_user == self.users[-1].user: if not self.last_user or self.last_user == self.users[-1].user:
@@ -110,32 +108,33 @@ class AssignmentRule(Document):
# find out the next user in the list # find out the next user in the list
for i, d in enumerate(self.users): for i, d in enumerate(self.users):
if self.last_user == d.user: if self.last_user == d.user:
return self.users[i+1].user
return self.users[i + 1].user


# bad last user, assign to the first one # bad last user, assign to the first one
return self.users[0].user return self.users[0].user


def get_user_load_balancing(self): def get_user_load_balancing(self):
'''Assign to the user with least number of open assignments'''
"""Assign to the user with least number of open assignments"""
counts = [] counts = []
for d in self.users: for d in self.users:
counts.append(dict(
user = d.user,
count = frappe.db.count('ToDo', dict(
reference_type = self.document_type,
allocated_to = d.user,
status = "Open"))
))
counts.append(
dict(
user=d.user,
count=frappe.db.count(
"ToDo", dict(reference_type=self.document_type, allocated_to=d.user, status="Open")
),
)
)


# sort by dict value # sort by dict value
sorted_counts = sorted(counts, key = lambda k: k['count'])
sorted_counts = sorted(counts, key=lambda k: k["count"])


# pick the first user # pick the first user
return sorted_counts[0].get('user')
return sorted_counts[0].get("user")


def get_user_based_on_field(self, doc): def get_user_based_on_field(self, doc):
val = doc.get(self.field) val = doc.get(self.field)
if frappe.db.exists('User', val):
if frappe.db.exists("User", val):
return val return val


def safe_eval(self, fieldname, doc): def safe_eval(self, fieldname, doc):
@@ -145,12 +144,12 @@ class AssignmentRule(Document):
except Exception as e: except Exception as e:
# when assignment fails, don't block the document as it may be # when assignment fails, don't block the document as it may be
# a part of the email pulling # a part of the email pulling
frappe.msgprint(frappe._('Auto assignment failed: {0}').format(str(e)), indicator = 'orange')
frappe.msgprint(frappe._("Auto assignment failed: {0}").format(str(e)), indicator="orange")


return False return False


def get_assignment_days(self): def get_assignment_days(self):
return [d.day for d in self.get('assignment_days', [])]
return [d.day for d in self.get("assignment_days", [])]


def is_rule_not_applicable_today(self): def is_rule_not_applicable_today(self):
today = frappe.flags.assignment_day or frappe.utils.get_weekday() today = frappe.flags.assignment_day or frappe.utils.get_weekday()
@@ -159,11 +158,14 @@ class AssignmentRule(Document):




def get_assignments(doc) -> List[Dict]: def get_assignments(doc) -> List[Dict]:
return frappe.get_all('ToDo', fields = ['name', 'assignment_rule'], filters = dict(
reference_type = doc.get('doctype'),
reference_name = doc.get('name'),
status = ('!=', 'Cancelled')
), limit=5)
return frappe.get_all(
"ToDo",
fields=["name", "assignment_rule"],
filters=dict(
reference_type=doc.get("doctype"), reference_name=doc.get("name"), status=("!=", "Cancelled")
),
limit=5,
)




@frappe.whitelist() @frappe.whitelist()
@@ -173,21 +175,30 @@ def bulk_apply(doctype, docnames):


for name in docnames: for name in docnames:
if background: if background:
frappe.enqueue('frappe.automation.doctype.assignment_rule.assignment_rule.apply', doc=None, doctype=doctype, name=name)
frappe.enqueue(
"frappe.automation.doctype.assignment_rule.assignment_rule.apply",
doc=None,
doctype=doctype,
name=name,
)
else: else:
apply(doctype=doctype, name=name) apply(doctype=doctype, name=name)




def reopen_closed_assignment(doc): def reopen_closed_assignment(doc):
todo_list = frappe.get_all("ToDo", filters={
"reference_type": doc.doctype,
"reference_name": doc.name,
"status": "Closed",
}, pluck="name")
todo_list = frappe.get_all(
"ToDo",
filters={
"reference_type": doc.doctype,
"reference_name": doc.name,
"status": "Closed",
},
pluck="name",
)


for todo in todo_list: for todo in todo_list:
todo_doc = frappe.get_doc('ToDo', todo)
todo_doc.status = 'Open'
todo_doc = frappe.get_doc("ToDo", todo)
todo_doc.status = "Open"
todo_doc.save(ignore_permissions=True) todo_doc.save(ignore_permissions=True)


return bool(todo_list) return bool(todo_list)
@@ -209,13 +220,16 @@ def apply(doc=None, method=None, doctype=None, name=None):
if not doc and doctype and name: if not doc and doctype and name:
doc = frappe.get_doc(doctype, name) doc = frappe.get_doc(doctype, name)


assignment_rules = get_doctype_map("Assignment Rule", doc.doctype, filters={
"document_type": doc.doctype, "disabled": 0
}, order_by="priority desc")
assignment_rules = get_doctype_map(
"Assignment Rule",
doc.doctype,
filters={"document_type": doc.doctype, "disabled": 0},
order_by="priority desc",
)


# multiple auto assigns # multiple auto assigns
assignment_rule_docs: List[AssignmentRule] = [ assignment_rule_docs: List[AssignmentRule] = [
frappe.get_cached_doc("Assignment Rule", d.get('name')) for d in assignment_rules
frappe.get_cached_doc("Assignment Rule", d.get("name")) for d in assignment_rules
] ]


if not assignment_rule_docs: if not assignment_rule_docs:
@@ -224,8 +238,8 @@ def apply(doc=None, method=None, doctype=None, name=None):
doc = doc.as_dict() doc = doc.as_dict()
assignments = get_assignments(doc) assignments = get_assignments(doc)


clear = True # are all assignments cleared
new_apply = False # are new assignments applied
clear = True # are all assignments cleared
new_apply = False # are new assignments applied


if assignments: if assignments:
# first unassign # first unassign
@@ -260,14 +274,18 @@ def apply(doc=None, method=None, doctype=None, name=None):


if not new_apply: if not new_apply:
# only reopen if close condition is not satisfied # only reopen if close condition is not satisfied
to_close_todos = assignment_rule.safe_eval('close_condition', doc)
to_close_todos = assignment_rule.safe_eval("close_condition", doc)


if to_close_todos: if to_close_todos:
# close todo status # close todo status
todos_to_close = frappe.get_all("ToDo", filters={
"reference_type": doc.doctype,
"reference_name": doc.name,
}, pluck="name")
todos_to_close = frappe.get_all(
"ToDo",
filters={
"reference_type": doc.doctype,
"reference_name": doc.name,
},
pluck="name",
)


for todo in todos_to_close: for todo in todos_to_close:
_todo = frappe.get_doc("ToDo", todo) _todo = frappe.get_doc("ToDo", todo)
@@ -286,8 +304,7 @@ def apply(doc=None, method=None, doctype=None, name=None):




def update_due_date(doc, state=None): def update_due_date(doc, state=None):
"""Run on_update on every Document (via hooks.py)
"""
"""Run on_update on every Document (via hooks.py)"""
skip_document_update = ( skip_document_update = (
frappe.flags.in_migrate frappe.flags.in_migrate
or frappe.flags.in_patch or frappe.flags.in_patch
@@ -306,7 +323,7 @@ def update_due_date(doc, state=None):
"due_date_based_on": ["is", "set"], "due_date_based_on": ["is", "set"],
"document_type": doc.doctype, "document_type": doc.doctype,
"disabled": 0, "disabled": 0,
}
},
) )


for rule in assignment_rules: for rule in assignment_rules:
@@ -319,20 +336,24 @@ def update_due_date(doc, state=None):
) )


if field_updated: if field_updated:
assignment_todos = frappe.get_all("ToDo", filters={
"assignment_rule": rule.get("name"),
"reference_type": doc.doctype,
"reference_name": doc.name,
"status": "Open",
}, pluck="name")
assignment_todos = frappe.get_all(
"ToDo",
filters={
"assignment_rule": rule.get("name"),
"reference_type": doc.doctype,
"reference_name": doc.name,
"status": "Open",
},
pluck="name",
)


for todo in assignment_todos: for todo in assignment_todos:
todo_doc = frappe.get_doc('ToDo', todo)
todo_doc = frappe.get_doc("ToDo", todo)
todo_doc.date = doc.get(due_date_field) todo_doc.date = doc.get(due_date_field)
todo_doc.flags.updater_reference = { todo_doc.flags.updater_reference = {
'doctype': 'Assignment Rule',
'docname': rule.get('name'),
'label': _('via Assignment Rule')
"doctype": "Assignment Rule",
"docname": rule.get("name"),
"label": _("via Assignment Rule"),
} }
todo_doc.save(ignore_permissions=True) todo_doc.save(ignore_permissions=True)




+ 170
- 156
frappe/automation/doctype/assignment_rule/test_assignment_rule.py 查看文件

@@ -20,13 +20,13 @@ class TestAutoAssign(unittest.TestCase):
def setUp(self): def setUp(self):
make_test_records("User") make_test_records("User")
days = [ days = [
dict(day = 'Sunday'),
dict(day = 'Monday'),
dict(day = 'Tuesday'),
dict(day = 'Wednesday'),
dict(day = 'Thursday'),
dict(day = 'Friday'),
dict(day = 'Saturday'),
dict(day="Sunday"),
dict(day="Monday"),
dict(day="Tuesday"),
dict(day="Wednesday"),
dict(day="Thursday"),
dict(day="Friday"),
dict(day="Saturday"),
] ]
self.days = days self.days = days
self.assignment_rule = get_assignment_rule([days, days]) self.assignment_rule = get_assignment_rule([days, days])
@@ -36,20 +36,22 @@ class TestAutoAssign(unittest.TestCase):
note = make_note(dict(public=1)) note = make_note(dict(public=1))


# check if auto assigned to first user # check if auto assigned to first user
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'allocated_to'), 'test@example.com')
self.assertEqual(
frappe.db.get_value(
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
),
"test@example.com",
)


note = make_note(dict(public=1)) note = make_note(dict(public=1))


# check if auto assigned to second user # check if auto assigned to second user
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'allocated_to'), 'test1@example.com')
self.assertEqual(
frappe.db.get_value(
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
),
"test1@example.com",
)


clear_assignments() clear_assignments()


@@ -57,35 +59,41 @@ class TestAutoAssign(unittest.TestCase):


# check if auto assigned to third user, even if # check if auto assigned to third user, even if
# previous assignments where closed # previous assignments where closed
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'allocated_to'), 'test2@example.com')
self.assertEqual(
frappe.db.get_value(
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
),
"test2@example.com",
)


# check loop back to first user # check loop back to first user
note = make_note(dict(public=1)) note = make_note(dict(public=1))


self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'allocated_to'), 'test@example.com')
self.assertEqual(
frappe.db.get_value(
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
),
"test@example.com",
)


def test_load_balancing(self): def test_load_balancing(self):
self.assignment_rule.rule = 'Load Balancing'
self.assignment_rule.rule = "Load Balancing"
self.assignment_rule.save() self.assignment_rule.save()


for _ in range(30): for _ in range(30):
note = make_note(dict(public=1)) note = make_note(dict(public=1))


# check if each user has 10 assignments (?) # check if each user has 10 assignments (?)
for user in ('test@example.com', 'test1@example.com', 'test2@example.com'):
self.assertEqual(len(frappe.get_all('ToDo', dict(allocated_to = user, reference_type = 'Note'))), 10)
for user in ("test@example.com", "test1@example.com", "test2@example.com"):
self.assertEqual(
len(frappe.get_all("ToDo", dict(allocated_to=user, reference_type="Note"))), 10
)


# clear 5 assignments for first user # clear 5 assignments for first user
# can't do a limit in "delete" since postgres does not support it # can't do a limit in "delete" since postgres does not support it
for d in frappe.get_all('ToDo', dict(reference_type = 'Note', allocated_to = 'test@example.com'), limit=5):
for d in frappe.get_all(
"ToDo", dict(reference_type="Note", allocated_to="test@example.com"), limit=5
):
frappe.db.delete("ToDo", {"name": d.name}) frappe.db.delete("ToDo", {"name": d.name})


# add 5 more assignments # add 5 more assignments
@@ -93,56 +101,59 @@ class TestAutoAssign(unittest.TestCase):
make_note(dict(public=1)) make_note(dict(public=1))


# check if each user still has 10 assignments # check if each user still has 10 assignments
for user in ('test@example.com', 'test1@example.com', 'test2@example.com'):
self.assertEqual(len(frappe.get_all('ToDo', dict(allocated_to = user, reference_type = 'Note'))), 10)
for user in ("test@example.com", "test1@example.com", "test2@example.com"):
self.assertEqual(
len(frappe.get_all("ToDo", dict(allocated_to=user, reference_type="Note"))), 10
)


def test_based_on_field(self): def test_based_on_field(self):
self.assignment_rule.rule = 'Based on Field'
self.assignment_rule.field = 'owner'
self.assignment_rule.rule = "Based on Field"
self.assignment_rule.field = "owner"
self.assignment_rule.save() self.assignment_rule.save()


frappe.set_user('test1@example.com')
frappe.set_user("test1@example.com")
note = make_note(dict(public=1)) note = make_note(dict(public=1))
# check if auto assigned to doc owner, test1@example.com # check if auto assigned to doc owner, test1@example.com
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'owner'), 'test1@example.com')

frappe.set_user('test2@example.com')
self.assertEqual(
frappe.db.get_value(
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "owner"
),
"test1@example.com",
)

frappe.set_user("test2@example.com")
note = make_note(dict(public=1)) note = make_note(dict(public=1))
# check if auto assigned to doc owner, test2@example.com # check if auto assigned to doc owner, test2@example.com
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'owner'), 'test2@example.com')
self.assertEqual(
frappe.db.get_value(
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "owner"
),
"test2@example.com",
)


frappe.set_user('Administrator')
frappe.set_user("Administrator")


def test_assign_condition(self): def test_assign_condition(self):
# check condition # check condition
note = make_note(dict(public=0)) note = make_note(dict(public=0))


self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'allocated_to'), None)
self.assertEqual(
frappe.db.get_value(
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
),
None,
)


def test_clear_assignment(self): def test_clear_assignment(self):
note = make_note(dict(public=1)) note = make_note(dict(public=1))


# check if auto assigned to first user # check if auto assigned to first user
todo = frappe.get_list('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), limit=1)[0]
todo = frappe.get_list(
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), limit=1
)[0]


todo = frappe.get_doc('ToDo', todo['name'])
self.assertEqual(todo.allocated_to, 'test@example.com')
todo = frappe.get_doc("ToDo", todo["name"])
self.assertEqual(todo.allocated_to, "test@example.com")


# test auto unassign # test auto unassign
note.public = 0 note.public = 0
@@ -151,99 +162,101 @@ class TestAutoAssign(unittest.TestCase):
todo.load_from_db() todo.load_from_db()


# check if todo is cancelled # check if todo is cancelled
self.assertEqual(todo.status, 'Cancelled')
self.assertEqual(todo.status, "Cancelled")


def test_close_assignment(self): def test_close_assignment(self):
note = make_note(dict(public=1, content="valid")) note = make_note(dict(public=1, content="valid"))


# check if auto assigned # check if auto assigned
todo = frappe.get_list('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), limit=1)[0]
todo = frappe.get_list(
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), limit=1
)[0]


todo = frappe.get_doc('ToDo', todo['name'])
self.assertEqual(todo.allocated_to, 'test@example.com')
todo = frappe.get_doc("ToDo", todo["name"])
self.assertEqual(todo.allocated_to, "test@example.com")


note.content="Closed"
note.content = "Closed"
note.save() note.save()


todo.load_from_db() todo.load_from_db()


# check if todo is closed # check if todo is closed
self.assertEqual(todo.status, 'Closed')
self.assertEqual(todo.status, "Closed")
# check if closed todo retained assignment # check if closed todo retained assignment
self.assertEqual(todo.allocated_to, 'test@example.com')
self.assertEqual(todo.allocated_to, "test@example.com")


def check_multiple_rules(self): def check_multiple_rules(self):
note = make_note(dict(public=1, notify_on_login=1)) note = make_note(dict(public=1, notify_on_login=1))


# check if auto assigned to test3 (2nd rule is applied, as it has higher priority) # check if auto assigned to test3 (2nd rule is applied, as it has higher priority)
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'allocated_to'), 'test@example.com')
self.assertEqual(
frappe.db.get_value(
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
),
"test@example.com",
)


def check_assignment_rule_scheduling(self): def check_assignment_rule_scheduling(self):
frappe.db.delete("Assignment Rule") frappe.db.delete("Assignment Rule")


days_1 = [dict(day = 'Sunday'), dict(day = 'Monday'), dict(day = 'Tuesday')]
days_1 = [dict(day="Sunday"), dict(day="Monday"), dict(day="Tuesday")]


days_2 = [dict(day = 'Wednesday'), dict(day = 'Thursday'), dict(day = 'Friday'), dict(day = 'Saturday')]
days_2 = [dict(day="Wednesday"), dict(day="Thursday"), dict(day="Friday"), dict(day="Saturday")]


get_assignment_rule([days_1, days_2], ['public == 1', 'public == 1'])
get_assignment_rule([days_1, days_2], ["public == 1", "public == 1"])


frappe.flags.assignment_day = "Monday" frappe.flags.assignment_day = "Monday"
note = make_note(dict(public=1)) note = make_note(dict(public=1))


self.assertIn(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'allocated_to'), ['test@example.com', 'test1@example.com', 'test2@example.com'])
self.assertIn(
frappe.db.get_value(
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
),
["test@example.com", "test1@example.com", "test2@example.com"],
)


frappe.flags.assignment_day = "Friday" frappe.flags.assignment_day = "Friday"
note = make_note(dict(public=1)) note = make_note(dict(public=1))


self.assertIn(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'allocated_to'), ['test3@example.com'])
self.assertIn(
frappe.db.get_value(
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
),
["test3@example.com"],
)


def test_assignment_rule_condition(self): def test_assignment_rule_condition(self):
frappe.db.delete("Assignment Rule") frappe.db.delete("Assignment Rule")


# Add expiry_date custom field # Add expiry_date custom field
from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.custom.doctype.custom_field.custom_field import create_custom_field
df = dict(fieldname='expiry_date', label='Expiry Date', fieldtype='Date')
create_custom_field('Note', df)

assignment_rule = frappe.get_doc(dict(
name = 'Assignment with Due Date',
doctype = 'Assignment Rule',
document_type = 'Note',
assign_condition = 'public == 0',
due_date_based_on = 'expiry_date',
assignment_days = self.days,
users = [
dict(user = 'test@example.com'),
]
)).insert()

df = dict(fieldname="expiry_date", label="Expiry Date", fieldtype="Date")
create_custom_field("Note", df)

assignment_rule = frappe.get_doc(
dict(
name="Assignment with Due Date",
doctype="Assignment Rule",
document_type="Note",
assign_condition="public == 0",
due_date_based_on="expiry_date",
assignment_days=self.days,
users=[
dict(user="test@example.com"),
],
)
).insert()


expiry_date = frappe.utils.add_days(frappe.utils.nowdate(), 2) expiry_date = frappe.utils.add_days(frappe.utils.nowdate(), 2)
note1 = make_note({'expiry_date': expiry_date})
note2 = make_note({'expiry_date': expiry_date})
note1 = make_note({"expiry_date": expiry_date})
note2 = make_note({"expiry_date": expiry_date})


note1_todo = frappe.get_all('ToDo', filters=dict(
reference_type = 'Note',
reference_name = note1.name,
status = 'Open'
))[0]
note1_todo = frappe.get_all(
"ToDo", filters=dict(reference_type="Note", reference_name=note1.name, status="Open")
)[0]


note1_todo_doc = frappe.get_doc('ToDo', note1_todo.name)
note1_todo_doc = frappe.get_doc("ToDo", note1_todo.name)
self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), expiry_date) self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), expiry_date)


# due date should be updated if the reference doc's date is updated. # due date should be updated if the reference doc's date is updated.
@@ -253,66 +266,67 @@ class TestAutoAssign(unittest.TestCase):
self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), note1.expiry_date) self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), note1.expiry_date)


# saving one note's expiry should not update other note todo's due date # saving one note's expiry should not update other note todo's due date
note2_todo = frappe.get_all('ToDo', filters=dict(
reference_type = 'Note',
reference_name = note2.name,
status = 'Open'
), fields=['name', 'date'])[0]
note2_todo = frappe.get_all(
"ToDo",
filters=dict(reference_type="Note", reference_name=note2.name, status="Open"),
fields=["name", "date"],
)[0]
self.assertNotEqual(frappe.utils.get_date_str(note2_todo.date), note1.expiry_date) self.assertNotEqual(frappe.utils.get_date_str(note2_todo.date), note1.expiry_date)
self.assertEqual(frappe.utils.get_date_str(note2_todo.date), expiry_date) self.assertEqual(frappe.utils.get_date_str(note2_todo.date), expiry_date)
assignment_rule.delete() assignment_rule.delete()



def clear_assignments(): def clear_assignments():
frappe.db.delete("ToDo", {"reference_type": "Note"}) frappe.db.delete("ToDo", {"reference_type": "Note"})



def get_assignment_rule(days, assign=None): def get_assignment_rule(days, assign=None):
frappe.delete_doc_if_exists('Assignment Rule', 'For Note 1')
frappe.delete_doc_if_exists("Assignment Rule", "For Note 1")


if not assign: if not assign:
assign = ['public == 1', 'notify_on_login == 1']

assignment_rule = frappe.get_doc(dict(
name = 'For Note 1',
doctype = 'Assignment Rule',
priority = 0,
document_type = 'Note',
assign_condition = assign[0],
unassign_condition = 'public == 0 or notify_on_login == 1',
close_condition = '"Closed" in content',
rule = 'Round Robin',
assignment_days = days[0],
users = [
dict(user = 'test@example.com'),
dict(user = 'test1@example.com'),
dict(user = 'test2@example.com'),
]
)).insert()

frappe.delete_doc_if_exists('Assignment Rule', 'For Note 2')
assign = ["public == 1", "notify_on_login == 1"]

assignment_rule = frappe.get_doc(
dict(
name="For Note 1",
doctype="Assignment Rule",
priority=0,
document_type="Note",
assign_condition=assign[0],
unassign_condition="public == 0 or notify_on_login == 1",
close_condition='"Closed" in content',
rule="Round Robin",
assignment_days=days[0],
users=[
dict(user="test@example.com"),
dict(user="test1@example.com"),
dict(user="test2@example.com"),
],
)
).insert()

frappe.delete_doc_if_exists("Assignment Rule", "For Note 2")


# 2nd rule # 2nd rule
frappe.get_doc(dict(
name = 'For Note 2',
doctype = 'Assignment Rule',
priority = 1,
document_type = 'Note',
assign_condition = assign[1],
unassign_condition = 'notify_on_login == 0',
rule = 'Round Robin',
assignment_days = days[1],
users = [
dict(user = 'test3@example.com')
]
)).insert()
frappe.get_doc(
dict(
name="For Note 2",
doctype="Assignment Rule",
priority=1,
document_type="Note",
assign_condition=assign[1],
unassign_condition="notify_on_login == 0",
rule="Round Robin",
assignment_days=days[1],
users=[dict(user="test3@example.com")],
)
).insert()


return assignment_rule return assignment_rule



def make_note(values=None): def make_note(values=None):
note = frappe.get_doc(dict(
doctype = 'Note',
title = random_string(10),
content = random_string(20)
))
note = frappe.get_doc(dict(doctype="Note", title=random_string(10), content=random_string(20)))


if values: if values:
note.update(values) note.update(values)


+ 1
- 0
frappe/automation/doctype/assignment_rule_day/assignment_rule_day.py 查看文件

@@ -5,5 +5,6 @@
# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document



class AssignmentRuleDay(Document): class AssignmentRuleDay(Document):
pass pass

+ 1
- 0
frappe/automation/doctype/assignment_rule_user/assignment_rule_user.py 查看文件

@@ -5,5 +5,6 @@
# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document



class AssignmentRuleUser(Document): class AssignmentRuleUser(Document):
pass pass

+ 176
- 114
frappe/automation/doctype/auto_repeat/auto_repeat.py 查看文件

@@ -2,23 +2,45 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE


from datetime import timedelta

from dateutil.relativedelta import relativedelta

import frappe import frappe
from frappe import _ from frappe import _
from datetime import timedelta
from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeated
from frappe.contacts.doctype.contact.contact import (
get_contacts_linked_from,
get_contacts_linking_to,
)
from frappe.core.doctype.communication.email import make
from frappe.desk.form import assign_to from frappe.desk.form import assign_to
from frappe.utils.jinja import validate_template
from dateutil.relativedelta import relativedelta
from frappe.utils.user import get_system_managers
from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_day, get_first_day, month_diff
from frappe.model.document import Document from frappe.model.document import Document
from frappe.core.doctype.communication.email import make
from frappe.utils import (
add_days,
cstr,
get_first_day,
get_last_day,
getdate,
month_diff,
split_emails,
today,
)
from frappe.utils.background_jobs import get_jobs from frappe.utils.background_jobs import get_jobs
from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeated
from frappe.contacts.doctype.contact.contact import get_contacts_linked_from
from frappe.contacts.doctype.contact.contact import get_contacts_linking_to
from frappe.utils.jinja import validate_template
from frappe.utils.user import get_system_managers

month_map = {"Monthly": 1, "Quarterly": 3, "Half-yearly": 6, "Yearly": 12}
week_map = {
"Monday": 0,
"Tuesday": 1,
"Wednesday": 2,
"Thursday": 3,
"Friday": 4,
"Saturday": 5,
"Sunday": 6,
}


month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}
week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6}


class AutoRepeat(Document): class AutoRepeat(Document):
def validate(self): def validate(self):
@@ -46,7 +68,7 @@ class AutoRepeat(Document):
frappe.get_doc(self.reference_doctype, self.reference_document).notify_update() frappe.get_doc(self.reference_doctype, self.reference_document).notify_update()


def on_trash(self): def on_trash(self):
frappe.db.set_value(self.reference_doctype, self.reference_document, 'auto_repeat', '')
frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", "")
frappe.get_doc(self.reference_doctype, self.reference_document).notify_update() frappe.get_doc(self.reference_doctype, self.reference_document).notify_update()


def set_dates(self): def set_dates(self):
@@ -56,29 +78,36 @@ class AutoRepeat(Document):
self.next_schedule_date = self.get_next_schedule_date(schedule_date=self.start_date) self.next_schedule_date = self.get_next_schedule_date(schedule_date=self.start_date)


def unlink_if_applicable(self): def unlink_if_applicable(self):
if self.status == 'Completed' or self.disabled:
frappe.db.set_value(self.reference_doctype, self.reference_document, 'auto_repeat', '')
if self.status == "Completed" or self.disabled:
frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", "")


def validate_reference_doctype(self): def validate_reference_doctype(self):
if frappe.flags.in_test or frappe.flags.in_patch: if frappe.flags.in_test or frappe.flags.in_patch:
return return
if not frappe.get_meta(self.reference_doctype).allow_auto_repeat: if not frappe.get_meta(self.reference_doctype).allow_auto_repeat:
frappe.throw(_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(self.reference_doctype))
frappe.throw(
_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(
self.reference_doctype
)
)


def validate_submit_on_creation(self): def validate_submit_on_creation(self):
if self.submit_on_creation and not frappe.get_meta(self.reference_doctype).is_submittable: if self.submit_on_creation and not frappe.get_meta(self.reference_doctype).is_submittable:
frappe.throw(_('Cannot enable {0} for a non-submittable doctype').format(
frappe.bold('Submit on Creation')))
frappe.throw(
_("Cannot enable {0} for a non-submittable doctype").format(frappe.bold("Submit on Creation"))
)


def validate_dates(self): def validate_dates(self):
if frappe.flags.in_patch: if frappe.flags.in_patch:
return return


if self.end_date: if self.end_date:
self.validate_from_to_dates('start_date', 'end_date')
self.validate_from_to_dates("start_date", "end_date")


if self.end_date == self.start_date: if self.end_date == self.start_date:
frappe.throw(_('{0} should not be same as {1}').format(frappe.bold('End Date'), frappe.bold('Start Date')))
frappe.throw(
_("{0} should not be same as {1}").format(frappe.bold("End Date"), frappe.bold("Start Date"))
)


def validate_email_id(self): def validate_email_id(self):
if self.notify_by_email: if self.notify_by_email:
@@ -100,17 +129,17 @@ class AutoRepeat(Document):


frappe.throw( frappe.throw(
_("Auto Repeat Day{0} {1} has been repeated.").format( _("Auto Repeat Day{0} {1} has been repeated.").format(
plural,
frappe.bold(", ".join(repeated_days))
plural, frappe.bold(", ".join(repeated_days))
) )
) )



def update_auto_repeat_id(self): def update_auto_repeat_id(self):
#check if document is already on auto repeat
# check if document is already on auto repeat
auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat") auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat")
if auto_repeat and auto_repeat != self.name and not frappe.flags.in_patch: if auto_repeat and auto_repeat != self.name and not frappe.flags.in_patch:
frappe.throw(_("The {0} is already on auto repeat {1}").format(self.reference_document, auto_repeat))
frappe.throw(
_("The {0} is already on auto repeat {1}").format(self.reference_document, auto_repeat)
)
else: else:
frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", self.name) frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", self.name)


@@ -136,18 +165,18 @@ class AutoRepeat(Document):
row = { row = {
"reference_document": self.reference_document, "reference_document": self.reference_document,
"frequency": self.frequency, "frequency": self.frequency,
"next_scheduled_date": next_date
"next_scheduled_date": next_date,
} }
schedule_details.append(row) schedule_details.append(row)


if self.end_date: if self.end_date:
next_date = self.get_next_schedule_date(schedule_date=start_date, for_full_schedule=True) next_date = self.get_next_schedule_date(schedule_date=start_date, for_full_schedule=True)


while (getdate(next_date) < getdate(end_date)):
while getdate(next_date) < getdate(end_date):
row = { row = {
"reference_document" : self.reference_document,
"frequency" : self.frequency,
"next_scheduled_date" : next_date
"reference_document": self.reference_document,
"frequency": self.frequency,
"next_scheduled_date": next_date,
} }
schedule_details.append(row) schedule_details.append(row)
next_date = self.get_next_schedule_date(schedule_date=next_date, for_full_schedule=True) next_date = self.get_next_schedule_date(schedule_date=next_date, for_full_schedule=True)
@@ -169,9 +198,9 @@ class AutoRepeat(Document):


def make_new_document(self): def make_new_document(self):
reference_doc = frappe.get_doc(self.reference_doctype, self.reference_document) reference_doc = frappe.get_doc(self.reference_doctype, self.reference_document)
new_doc = frappe.copy_doc(reference_doc, ignore_no_copy = False)
new_doc = frappe.copy_doc(reference_doc, ignore_no_copy=False)
self.update_doc(new_doc, reference_doc) self.update_doc(new_doc, reference_doc)
new_doc.insert(ignore_permissions = True)
new_doc.insert(ignore_permissions=True)


if self.submit_on_creation: if self.submit_on_creation:
new_doc.submit() new_doc.submit()
@@ -180,61 +209,72 @@ class AutoRepeat(Document):


def update_doc(self, new_doc, reference_doc): def update_doc(self, new_doc, reference_doc):
new_doc.docstatus = 0 new_doc.docstatus = 0
if new_doc.meta.get_field('set_posting_time'):
new_doc.set('set_posting_time', 1)

if new_doc.meta.get_field('auto_repeat'):
new_doc.set('auto_repeat', self.name)

for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'user_remark', 'remarks', 'owner']:
if new_doc.meta.get_field("set_posting_time"):
new_doc.set("set_posting_time", 1)

if new_doc.meta.get_field("auto_repeat"):
new_doc.set("auto_repeat", self.name)

for fieldname in [
"naming_series",
"ignore_pricing_rule",
"posting_time",
"select_print_heading",
"user_remark",
"remarks",
"owner",
]:
if new_doc.meta.get_field(fieldname): if new_doc.meta.get_field(fieldname):
new_doc.set(fieldname, reference_doc.get(fieldname)) new_doc.set(fieldname, reference_doc.get(fieldname))


for data in new_doc.meta.fields: for data in new_doc.meta.fields:
if data.fieldtype == 'Date' and data.reqd:
if data.fieldtype == "Date" and data.reqd:
new_doc.set(data.fieldname, self.next_schedule_date) new_doc.set(data.fieldname, self.next_schedule_date)


self.set_auto_repeat_period(new_doc) self.set_auto_repeat_period(new_doc)


auto_repeat_doc = frappe.get_doc('Auto Repeat', self.name)
auto_repeat_doc = frappe.get_doc("Auto Repeat", self.name)


#for any action that needs to take place after the recurring document creation
#on recurring method of that doctype is triggered
new_doc.run_method('on_recurring', reference_doc = reference_doc, auto_repeat_doc = auto_repeat_doc)
# for any action that needs to take place after the recurring document creation
# on recurring method of that doctype is triggered
new_doc.run_method("on_recurring", reference_doc=reference_doc, auto_repeat_doc=auto_repeat_doc)


def set_auto_repeat_period(self, new_doc): def set_auto_repeat_period(self, new_doc):
mcount = month_map.get(self.frequency) mcount = month_map.get(self.frequency)
if mcount and new_doc.meta.get_field('from_date') and new_doc.meta.get_field('to_date'):
last_ref_doc = frappe.db.get_all(doctype = self.reference_doctype,
fields = ['name', 'from_date', 'to_date'],
filters = [
['auto_repeat', '=', self.name],
['docstatus', '<', 2],
if mcount and new_doc.meta.get_field("from_date") and new_doc.meta.get_field("to_date"):
last_ref_doc = frappe.db.get_all(
doctype=self.reference_doctype,
fields=["name", "from_date", "to_date"],
filters=[
["auto_repeat", "=", self.name],
["docstatus", "<", 2],
], ],
order_by = 'creation desc',
limit = 1)
order_by="creation desc",
limit=1,
)


if not last_ref_doc: if not last_ref_doc:
return return


from_date = get_next_date(last_ref_doc[0].from_date, mcount) from_date = get_next_date(last_ref_doc[0].from_date, mcount)


if (cstr(get_first_day(last_ref_doc[0].from_date)) == cstr(last_ref_doc[0].from_date)) and \
(cstr(get_last_day(last_ref_doc[0].to_date)) == cstr(last_ref_doc[0].to_date)):
if (cstr(get_first_day(last_ref_doc[0].from_date)) == cstr(last_ref_doc[0].from_date)) and (
cstr(get_last_day(last_ref_doc[0].to_date)) == cstr(last_ref_doc[0].to_date)
):
to_date = get_last_day(get_next_date(last_ref_doc[0].to_date, mcount)) to_date = get_last_day(get_next_date(last_ref_doc[0].to_date, mcount))
else: else:
to_date = get_next_date(last_ref_doc[0].to_date, mcount) to_date = get_next_date(last_ref_doc[0].to_date, mcount)


new_doc.set('from_date', from_date)
new_doc.set('to_date', to_date)
new_doc.set("from_date", from_date)
new_doc.set("to_date", to_date)


def get_next_schedule_date(self, schedule_date, for_full_schedule=False): def get_next_schedule_date(self, schedule_date, for_full_schedule=False):
""" """
Returns the next schedule date for auto repeat after a recurring document has been created.
Adds required offset to the schedule_date param and returns the next schedule date.
Returns the next schedule date for auto repeat after a recurring document has been created.
Adds required offset to the schedule_date param and returns the next schedule date.


:param schedule_date: The date when the last recurring document was created.
:param for_full_schedule: If True, returns the immediate next schedule date, else the full schedule.
:param schedule_date: The date when the last recurring document was created.
:param for_full_schedule: If True, returns the immediate next schedule date, else the full schedule.
""" """
if month_map.get(self.frequency): if month_map.get(self.frequency):
month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1 month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1
@@ -295,60 +335,75 @@ class AutoRepeat(Document):
return 7 return 7


def get_auto_repeat_days(self): def get_auto_repeat_days(self):
return [d.day for d in self.get('repeat_on_days', [])]
return [d.day for d in self.get("repeat_on_days", [])]


def send_notification(self, new_doc): def send_notification(self, new_doc):
"""Notify concerned people about recurring document generation""" """Notify concerned people about recurring document generation"""
subject = self.subject or ''
message = self.message or ''
subject = self.subject or ""
message = self.message or ""


if not self.subject: if not self.subject:
subject = _("New {0}: {1}").format(new_doc.doctype, new_doc.name) subject = _("New {0}: {1}").format(new_doc.doctype, new_doc.name)
elif "{" in self.subject: elif "{" in self.subject:
subject = frappe.render_template(self.subject, {'doc': new_doc})
subject = frappe.render_template(self.subject, {"doc": new_doc})


print_format = self.print_format or 'Standard'
print_format = self.print_format or "Standard"
error_string = None error_string = None


try: try:
attachments = [frappe.attach_print(new_doc.doctype, new_doc.name,
file_name=new_doc.name, print_format=print_format)]
attachments = [
frappe.attach_print(
new_doc.doctype, new_doc.name, file_name=new_doc.name, print_format=print_format
)
]


except frappe.PermissionError: except frappe.PermissionError:
error_string = _("A recurring {0} {1} has been created for you via Auto Repeat {2}.").format(new_doc.doctype, new_doc.name, self.name)
error_string = _("A recurring {0} {1} has been created for you via Auto Repeat {2}.").format(
new_doc.doctype, new_doc.name, self.name
)
error_string += "<br><br>" error_string += "<br><br>"


error_string += _("{0}: Failed to attach new recurring document. To enable attaching document in the auto repeat notification email, enable {1} in Print Settings").format(
frappe.bold(_('Note')),
frappe.bold(_('Allow Print for Draft'))
)
attachments = '[]'
error_string += _(
"{0}: Failed to attach new recurring document. To enable attaching document in the auto repeat notification email, enable {1} in Print Settings"
).format(frappe.bold(_("Note")), frappe.bold(_("Allow Print for Draft")))
attachments = "[]"


if error_string: if error_string:
message = error_string message = error_string
elif not self.message: elif not self.message:
message = _("Please find attached {0}: {1}").format(new_doc.doctype, new_doc.name) message = _("Please find attached {0}: {1}").format(new_doc.doctype, new_doc.name)
elif "{" in self.message: elif "{" in self.message:
message = frappe.render_template(self.message, {'doc': new_doc})
message = frappe.render_template(self.message, {"doc": new_doc})


recipients = self.recipients.split('\n')
recipients = self.recipients.split("\n")


make(doctype=new_doc.doctype, name=new_doc.name, recipients=recipients,
subject=subject, content=message, attachments=attachments, send_email=1)
make(
doctype=new_doc.doctype,
name=new_doc.name,
recipients=recipients,
subject=subject,
content=message,
attachments=attachments,
send_email=1,
)


@frappe.whitelist() @frappe.whitelist()
def fetch_linked_contacts(self): def fetch_linked_contacts(self):
if self.reference_doctype and self.reference_document: if self.reference_doctype and self.reference_document:
res = get_contacts_linking_to(self.reference_doctype, self.reference_document, fields=['email_id'])
res += get_contacts_linked_from(self.reference_doctype, self.reference_document, fields=['email_id'])
res = get_contacts_linking_to(
self.reference_doctype, self.reference_document, fields=["email_id"]
)
res += get_contacts_linked_from(
self.reference_doctype, self.reference_document, fields=["email_id"]
)
email_ids = {d.email_id for d in res} email_ids = {d.email_id for d in res}
if not email_ids: if not email_ids:
frappe.msgprint(_('No contacts linked to document'), alert=True)
frappe.msgprint(_("No contacts linked to document"), alert=True)
else: else:
self.recipients = ', '.join(email_ids)
self.recipients = ", ".join(email_ids)


def disable_auto_repeat(self): def disable_auto_repeat(self):
frappe.db.set_value('Auto Repeat', self.name, 'disabled', 1)
frappe.db.set_value("Auto Repeat", self.name, "disabled", 1)


def notify_error_to_user(self, error_log): def notify_error_to_user(self, error_log):
recipients = list(get_system_managers(only_name=True)) recipients = list(get_system_managers(only_name=True))
@@ -356,20 +411,17 @@ class AutoRepeat(Document):
subject = _("Auto Repeat Document Creation Failed") subject = _("Auto Repeat Document Creation Failed")


form_link = frappe.utils.get_link_to_form(self.reference_doctype, self.reference_document) form_link = frappe.utils.get_link_to_form(self.reference_doctype, self.reference_document)
auto_repeat_failed_for = _('Auto Repeat failed for {0}').format(form_link)
auto_repeat_failed_for = _("Auto Repeat failed for {0}").format(form_link)


error_log_link = frappe.utils.get_link_to_form('Error Log', error_log.name)
error_log_message = _('Check the Error Log for more information: {0}').format(error_log_link)
error_log_link = frappe.utils.get_link_to_form("Error Log", error_log.name)
error_log_message = _("Check the Error Log for more information: {0}").format(error_log_link)


frappe.sendmail( frappe.sendmail(
recipients=recipients, recipients=recipients,
subject=subject, subject=subject,
template="auto_repeat_fail", template="auto_repeat_fail",
args={
'auto_repeat_failed_for': auto_repeat_failed_for,
'error_log_message': error_log_message
},
header=[subject, 'red']
args={"auto_repeat_failed_for": auto_repeat_failed_for, "error_log_message": error_log_message},
header=[subject, "red"],
) )




@@ -382,18 +434,18 @@ def get_next_date(dt, mcount, day=None):
def get_next_weekday(current_schedule_day, weekdays): def get_next_weekday(current_schedule_day, weekdays):
days = list(week_map.keys()) days = list(week_map.keys())
if current_schedule_day > 0: if current_schedule_day > 0:
days = days[(current_schedule_day + 1):] + days[:current_schedule_day]
days = days[(current_schedule_day + 1) :] + days[:current_schedule_day]
else: else:
days = days[(current_schedule_day + 1):]
days = days[(current_schedule_day + 1) :]


for entry in days: for entry in days:
if entry in weekdays: if entry in weekdays:
return entry return entry




#called through hooks
# called through hooks
def make_auto_repeat_entry(): def make_auto_repeat_entry():
enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries'
enqueued_method = "frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries"
jobs = get_jobs() jobs = get_jobs()


if not jobs or enqueued_method not in jobs[frappe.local.site]: if not jobs or enqueued_method not in jobs[frappe.local.site]:
@@ -404,7 +456,7 @@ def make_auto_repeat_entry():


def create_repeated_entries(data): def create_repeated_entries(data):
for d in data: for d in data:
doc = frappe.get_doc('Auto Repeat', d.name)
doc = frappe.get_doc("Auto Repeat", d.name)


current_date = getdate(today()) current_date = getdate(today())
schedule_date = getdate(doc.next_schedule_date) schedule_date = getdate(doc.next_schedule_date)
@@ -413,33 +465,32 @@ def create_repeated_entries(data):
doc.create_documents() doc.create_documents()
schedule_date = doc.get_next_schedule_date(schedule_date=schedule_date) schedule_date = doc.get_next_schedule_date(schedule_date=schedule_date)
if schedule_date and not doc.disabled: if schedule_date and not doc.disabled:
frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date)
frappe.db.set_value("Auto Repeat", doc.name, "next_schedule_date", schedule_date)




def get_auto_repeat_entries(date=None): def get_auto_repeat_entries(date=None):
if not date: if not date:
date = getdate(today()) date = getdate(today())
return frappe.db.get_all('Auto Repeat', filters=[
['next_schedule_date', '<=', date],
['status', '=', 'Active']
])
return frappe.db.get_all(
"Auto Repeat", filters=[["next_schedule_date", "<=", date], ["status", "=", "Active"]]
)




#called through hooks
# called through hooks
def set_auto_repeat_as_completed(): def set_auto_repeat_as_completed():
auto_repeat = frappe.get_all("Auto Repeat", filters = {'status': ['!=', 'Disabled']})
auto_repeat = frappe.get_all("Auto Repeat", filters={"status": ["!=", "Disabled"]})
for entry in auto_repeat: for entry in auto_repeat:
doc = frappe.get_doc("Auto Repeat", entry.name) doc = frappe.get_doc("Auto Repeat", entry.name)
if doc.is_completed(): if doc.is_completed():
doc.status = 'Completed'
doc.status = "Completed"
doc.save() doc.save()




@frappe.whitelist() @frappe.whitelist()
def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, end_date = None):
def make_auto_repeat(doctype, docname, frequency="Daily", start_date=None, end_date=None):
if not start_date: if not start_date:
start_date = getdate(today()) start_date = getdate(today())
doc = frappe.new_doc('Auto Repeat')
doc = frappe.new_doc("Auto Repeat")
doc.reference_doctype = doctype doc.reference_doctype = doctype
doc.reference_document = docname doc.reference_document = docname
doc.frequency = frequency doc.frequency = frequency
@@ -449,24 +500,34 @@ def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, e
doc.save() doc.save()
return doc return doc



# method for reference_doctype filter # method for reference_doctype filter
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_auto_repeat_doctypes(doctype, txt, searchfield, start, page_len, filters): def get_auto_repeat_doctypes(doctype, txt, searchfield, start, page_len, filters):
res = frappe.db.get_all('Property Setter', {
'property': 'allow_auto_repeat',
'value': '1',
}, ['doc_type'])
res = frappe.db.get_all(
"Property Setter",
{
"property": "allow_auto_repeat",
"value": "1",
},
["doc_type"],
)
docs = [r.doc_type for r in res] docs = [r.doc_type for r in res]


res = frappe.db.get_all('DocType', {
'allow_auto_repeat': 1,
}, ['name'])
res = frappe.db.get_all(
"DocType",
{
"allow_auto_repeat": 1,
},
["name"],
)
docs += [r.name for r in res] docs += [r.name for r in res]
docs = set(list(docs)) docs = set(list(docs))


return [[d] for d in docs] return [[d] for d in docs]



@frappe.whitelist() @frappe.whitelist()
def update_reference(docname, reference): def update_reference(docname, reference):
result = "" result = ""
@@ -478,13 +539,14 @@ def update_reference(docname, reference):
raise e raise e
return result return result



@frappe.whitelist() @frappe.whitelist()
def generate_message_preview(reference_dt, reference_doc, message=None, subject=None): def generate_message_preview(reference_dt, reference_doc, message=None, subject=None):
frappe.has_permission("Auto Repeat", "write", throw=True) frappe.has_permission("Auto Repeat", "write", throw=True)
doc = frappe.get_doc(reference_dt, reference_doc) doc = frappe.get_doc(reference_dt, reference_doc)
subject_preview = _("Please add a subject to your email") subject_preview = _("Please add a subject to your email")
msg_preview = frappe.render_template(message, {'doc': doc})
msg_preview = frappe.render_template(message, {"doc": doc})
if subject: if subject:
subject_preview = frappe.render_template(subject, {'doc': doc})
subject_preview = frappe.render_template(subject, {"doc": doc})


return {'message': msg_preview, 'subject': subject_preview}
return {"message": msg_preview, "subject": subject_preview}

+ 156
- 93
frappe/automation/doctype/auto_repeat/test_auto_repeat.py 查看文件

@@ -4,24 +4,40 @@
import unittest import unittest


import frappe import frappe
from frappe.automation.doctype.auto_repeat.auto_repeat import (
create_repeated_entries,
get_auto_repeat_entries,
week_map,
)
from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries, week_map
from frappe.utils import today, add_days, getdate, add_months
from frappe.utils import add_days, add_months, getdate, today


def add_custom_fields(): def add_custom_fields():
df = dict( df = dict(
fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', insert_after='sender',
options='Auto Repeat', hidden=1, print_hide=1, read_only=1)
create_custom_field('ToDo', df)
fieldname="auto_repeat",
label="Auto Repeat",
fieldtype="Link",
insert_after="sender",
options="Auto Repeat",
hidden=1,
print_hide=1,
read_only=1,
)
create_custom_field("ToDo", df)



class TestAutoRepeat(unittest.TestCase): class TestAutoRepeat(unittest.TestCase):
def setUp(self): def setUp(self):
if not frappe.db.sql("SELECT `fieldname` FROM `tabCustom Field` WHERE `fieldname`='auto_repeat' and `dt`=%s", "Todo"):
if not frappe.db.sql(
"SELECT `fieldname` FROM `tabCustom Field` WHERE `fieldname`='auto_repeat' and `dt`=%s", "Todo"
):
add_custom_fields() add_custom_fields()


def test_daily_auto_repeat(self): def test_daily_auto_repeat(self):
todo = frappe.get_doc( todo = frappe.get_doc(
dict(doctype='ToDo', description='test recurring todo', assigned_by='Administrator')).insert()
dict(doctype="ToDo", description="test recurring todo", assigned_by="Administrator")
).insert()


doc = make_auto_repeat(reference_document=todo.name) doc = make_auto_repeat(reference_document=todo.name)
self.assertEqual(doc.next_schedule_date, today()) self.assertEqual(doc.next_schedule_date, today())
@@ -32,19 +48,25 @@ class TestAutoRepeat(unittest.TestCase):
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document) todo = frappe.get_doc(doc.reference_doctype, doc.reference_document)
self.assertEqual(todo.auto_repeat, doc.name) self.assertEqual(todo.auto_repeat, doc.name)


new_todo = frappe.db.get_value('ToDo',
{'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name')
new_todo = frappe.db.get_value(
"ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name"
)


new_todo = frappe.get_doc('ToDo', new_todo)
new_todo = frappe.get_doc("ToDo", new_todo)


self.assertEqual(todo.get('description'), new_todo.get('description'))
self.assertEqual(todo.get("description"), new_todo.get("description"))


def test_weekly_auto_repeat(self): def test_weekly_auto_repeat(self):
todo = frappe.get_doc( todo = frappe.get_doc(
dict(doctype='ToDo', description='test weekly todo', assigned_by='Administrator')).insert()
dict(doctype="ToDo", description="test weekly todo", assigned_by="Administrator")
).insert()


doc = make_auto_repeat(reference_doctype='ToDo',
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7))
doc = make_auto_repeat(
reference_doctype="ToDo",
frequency="Weekly",
reference_document=todo.name,
start_date=add_days(today(), -7),
)


self.assertEqual(doc.next_schedule_date, today()) self.assertEqual(doc.next_schedule_date, today())
data = get_auto_repeat_entries(getdate(today())) data = get_auto_repeat_entries(getdate(today()))
@@ -54,25 +76,29 @@ class TestAutoRepeat(unittest.TestCase):
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document) todo = frappe.get_doc(doc.reference_doctype, doc.reference_document)
self.assertEqual(todo.auto_repeat, doc.name) self.assertEqual(todo.auto_repeat, doc.name)


new_todo = frappe.db.get_value('ToDo',
{'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name')
new_todo = frappe.db.get_value(
"ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name"
)


new_todo = frappe.get_doc('ToDo', new_todo)
new_todo = frappe.get_doc("ToDo", new_todo)


self.assertEqual(todo.get('description'), new_todo.get('description'))
self.assertEqual(todo.get("description"), new_todo.get("description"))


def test_weekly_auto_repeat_with_weekdays(self): def test_weekly_auto_repeat_with_weekdays(self):
todo = frappe.get_doc( todo = frappe.get_doc(
dict(doctype='ToDo', description='test auto repeat with weekdays', assigned_by='Administrator')).insert()
dict(doctype="ToDo", description="test auto repeat with weekdays", assigned_by="Administrator")
).insert()


weekdays = list(week_map.keys()) weekdays = list(week_map.keys())
current_weekday = getdate().weekday() current_weekday = getdate().weekday()
days = [
{'day': weekdays[current_weekday]},
{'day': weekdays[(current_weekday + 2) % 7]}
]
doc = make_auto_repeat(reference_doctype='ToDo',
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7), days=days)
days = [{"day": weekdays[current_weekday]}, {"day": weekdays[(current_weekday + 2) % 7]}]
doc = make_auto_repeat(
reference_doctype="ToDo",
frequency="Weekly",
reference_document=todo.name,
start_date=add_days(today(), -7),
days=days,
)


self.assertEqual(doc.next_schedule_date, today()) self.assertEqual(doc.next_schedule_date, today())
data = get_auto_repeat_entries(getdate(today())) data = get_auto_repeat_entries(getdate(today()))
@@ -90,136 +116,173 @@ class TestAutoRepeat(unittest.TestCase):
end_date = add_months(start_date, 12) end_date = add_months(start_date, 12)


todo = frappe.get_doc( todo = frappe.get_doc(
dict(doctype='ToDo', description='test recurring todo', assigned_by='Administrator')).insert()
dict(doctype="ToDo", description="test recurring todo", assigned_by="Administrator")
).insert()


self.monthly_auto_repeat('ToDo', todo.name, start_date, end_date)
#test without end_date
todo = frappe.get_doc(dict(doctype='ToDo', description='test recurring todo without end_date', assigned_by='Administrator')).insert()
self.monthly_auto_repeat('ToDo', todo.name, start_date)
self.monthly_auto_repeat("ToDo", todo.name, start_date, end_date)
# test without end_date
todo = frappe.get_doc(
dict(
doctype="ToDo", description="test recurring todo without end_date", assigned_by="Administrator"
)
).insert()
self.monthly_auto_repeat("ToDo", todo.name, start_date)


def monthly_auto_repeat(self, doctype, docname, start_date, end_date = None):
def monthly_auto_repeat(self, doctype, docname, start_date, end_date=None):
def get_months(start, end): def get_months(start, end):
diff = (12 * end.year + end.month) - (12 * start.year + start.month) diff = (12 * end.year + end.month) - (12 * start.year + start.month)
return diff + 1 return diff + 1


doc = make_auto_repeat( doc = make_auto_repeat(
reference_doctype=doctype, frequency='Monthly', reference_document=docname, start_date=start_date,
end_date=end_date)
reference_doctype=doctype,
frequency="Monthly",
reference_document=docname,
start_date=start_date,
end_date=end_date,
)


doc.disable_auto_repeat() doc.disable_auto_repeat()


data = get_auto_repeat_entries(getdate(today())) data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data) create_repeated_entries(data)
docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name})
docnames = frappe.get_all(doc.reference_doctype, {"auto_repeat": doc.name})
self.assertEqual(len(docnames), 1) self.assertEqual(len(docnames), 1)


doc = frappe.get_doc('Auto Repeat', doc.name)
doc.db_set('disabled', 0)
doc = frappe.get_doc("Auto Repeat", doc.name)
doc.db_set("disabled", 0)


months = get_months(getdate(start_date), getdate(today())) months = get_months(getdate(start_date), getdate(today()))
data = get_auto_repeat_entries(getdate(today())) data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data) create_repeated_entries(data)


docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name})
docnames = frappe.get_all(doc.reference_doctype, {"auto_repeat": doc.name})
self.assertEqual(len(docnames), months) self.assertEqual(len(docnames), months)


def test_notification_is_attached(self): def test_notification_is_attached(self):
todo = frappe.get_doc( todo = frappe.get_doc(
dict(doctype='ToDo', description='Test recurring notification attachment', assigned_by='Administrator')).insert()
dict(
doctype="ToDo",
description="Test recurring notification attachment",
assigned_by="Administrator",
)
).insert()


doc = make_auto_repeat(reference_document=todo.name, notify=1, recipients="test@domain.com", subject="New ToDo",
message="A new ToDo has just been created for you")
doc = make_auto_repeat(
reference_document=todo.name,
notify=1,
recipients="test@domain.com",
subject="New ToDo",
message="A new ToDo has just been created for you",
)
data = get_auto_repeat_entries(getdate(today())) data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data) create_repeated_entries(data)
frappe.db.commit() frappe.db.commit()


new_todo = frappe.db.get_value('ToDo',
{'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name')
new_todo = frappe.db.get_value(
"ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name"
)


linked_comm = frappe.db.exists("Communication", dict(reference_doctype="ToDo", reference_name=new_todo))
linked_comm = frappe.db.exists(
"Communication", dict(reference_doctype="ToDo", reference_name=new_todo)
)
self.assertTrue(linked_comm) self.assertTrue(linked_comm)


def test_next_schedule_date(self): def test_next_schedule_date(self):
current_date = getdate(today()) current_date = getdate(today())
todo = frappe.get_doc( todo = frappe.get_doc(
dict(doctype='ToDo', description='test next schedule date for monthly', assigned_by='Administrator')).insert()
doc = make_auto_repeat(frequency='Monthly', reference_document=todo.name, start_date=add_months(today(), -2))
dict(
doctype="ToDo", description="test next schedule date for monthly", assigned_by="Administrator"
)
).insert()
doc = make_auto_repeat(
frequency="Monthly", reference_document=todo.name, start_date=add_months(today(), -2)
)


# next_schedule_date is set as on or after current date # next_schedule_date is set as on or after current date
# it should not be a previous month's date # it should not be a previous month's date
self.assertTrue((doc.next_schedule_date >= current_date)) self.assertTrue((doc.next_schedule_date >= current_date))


todo = frappe.get_doc( todo = frappe.get_doc(
dict(doctype='ToDo', description='test next schedule date for daily', assigned_by='Administrator')).insert()
doc = make_auto_repeat(frequency='Daily', reference_document=todo.name, start_date=add_days(today(), -2))
dict(
doctype="ToDo", description="test next schedule date for daily", assigned_by="Administrator"
)
).insert()
doc = make_auto_repeat(
frequency="Daily", reference_document=todo.name, start_date=add_days(today(), -2)
)
self.assertEqual(getdate(doc.next_schedule_date), current_date) self.assertEqual(getdate(doc.next_schedule_date), current_date)


def test_submit_on_creation(self): def test_submit_on_creation(self):
doctype = 'Test Submittable DocType'
doctype = "Test Submittable DocType"
create_submittable_doctype(doctype) create_submittable_doctype(doctype)


current_date = getdate() current_date = getdate()
submittable_doc = frappe.get_doc(dict(doctype=doctype, test='test submit on creation')).insert()
submittable_doc = frappe.get_doc(dict(doctype=doctype, test="test submit on creation")).insert()
submittable_doc.submit() submittable_doc.submit()
doc = make_auto_repeat(frequency='Daily', reference_doctype=doctype, reference_document=submittable_doc.name,
start_date=add_days(current_date, -1), submit_on_creation=1)
doc = make_auto_repeat(
frequency="Daily",
reference_doctype=doctype,
reference_document=submittable_doc.name,
start_date=add_days(current_date, -1),
submit_on_creation=1,
)


data = get_auto_repeat_entries(current_date) data = get_auto_repeat_entries(current_date)
create_repeated_entries(data) create_repeated_entries(data)
docnames = frappe.db.get_all(doc.reference_doctype,
filters={'auto_repeat': doc.name},
fields=['docstatus'],
limit=1
docnames = frappe.db.get_all(
doc.reference_doctype, filters={"auto_repeat": doc.name}, fields=["docstatus"], limit=1
) )
self.assertEqual(docnames[0].docstatus, 1) self.assertEqual(docnames[0].docstatus, 1)




def make_auto_repeat(**args): def make_auto_repeat(**args):
args = frappe._dict(args) args = frappe._dict(args)
doc = frappe.get_doc({
'doctype': 'Auto Repeat',
'reference_doctype': args.reference_doctype or 'ToDo',
'reference_document': args.reference_document or frappe.db.get_value('ToDo', 'name'),
'submit_on_creation': args.submit_on_creation or 0,
'frequency': args.frequency or 'Daily',
'start_date': args.start_date or add_days(today(), -1),
'end_date': args.end_date or "",
'notify_by_email': args.notify or 0,
'recipients': args.recipients or "",
'subject': args.subject or "",
'message': args.message or "",
'repeat_on_days': args.days or []
}).insert(ignore_permissions=True)
doc = frappe.get_doc(
{
"doctype": "Auto Repeat",
"reference_doctype": args.reference_doctype or "ToDo",
"reference_document": args.reference_document or frappe.db.get_value("ToDo", "name"),
"submit_on_creation": args.submit_on_creation or 0,
"frequency": args.frequency or "Daily",
"start_date": args.start_date or add_days(today(), -1),
"end_date": args.end_date or "",
"notify_by_email": args.notify or 0,
"recipients": args.recipients or "",
"subject": args.subject or "",
"message": args.message or "",
"repeat_on_days": args.days or [],
}
).insert(ignore_permissions=True)


return doc return doc




def create_submittable_doctype(doctype, submit_perms=1): def create_submittable_doctype(doctype, submit_perms=1):
if frappe.db.exists('DocType', doctype):
if frappe.db.exists("DocType", doctype):
return return
else: else:
doc = frappe.get_doc({
'doctype': 'DocType',
'__newname': doctype,
'module': 'Custom',
'custom': 1,
'is_submittable': 1,
'fields': [{
'fieldname': 'test',
'label': 'Test',
'fieldtype': 'Data'
}],
'permissions': [{
'role': 'System Manager',
'read': 1,
'write': 1,
'create': 1,
'delete': 1,
'submit': submit_perms,
'cancel': submit_perms,
'amend': submit_perms
}]
}).insert()
doc = frappe.get_doc(
{
"doctype": "DocType",
"__newname": doctype,
"module": "Custom",
"custom": 1,
"is_submittable": 1,
"fields": [{"fieldname": "test", "label": "Test", "fieldtype": "Data"}],
"permissions": [
{
"role": "System Manager",
"read": 1,
"write": 1,
"create": 1,
"delete": 1,
"submit": submit_perms,
"cancel": submit_perms,
"amend": submit_perms,
}
],
}
).insert()


doc.allow_auto_repeat = 1 doc.allow_auto_repeat = 1
doc.save()
doc.save()

+ 1
- 0
frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py 查看文件

@@ -5,5 +5,6 @@
# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document



class AutoRepeatDay(Document): class AutoRepeatDay(Document):
pass pass

+ 2
- 0
frappe/automation/doctype/milestone/milestone.py 查看文件

@@ -5,8 +5,10 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document



class Milestone(Document): class Milestone(Document):
pass pass



def on_doctype_update(): def on_doctype_update():
frappe.db.add_index("Milestone", ["reference_type", "reference_name"]) frappe.db.add_index("Milestone", ["reference_type", "reference_name"])

+ 2
- 1
frappe/automation/doctype/milestone/test_milestone.py 查看文件

@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors # Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
#import frappe
# import frappe
import unittest import unittest



class TestMilestone(unittest.TestCase): class TestMilestone(unittest.TestCase):
pass pass

+ 25
- 18
frappe/automation/doctype/milestone_tracker/milestone_tracker.py 查看文件

@@ -3,43 +3,50 @@
# License: MIT. See LICENSE # License: MIT. See LICENSE


import frappe import frappe
from frappe.model.document import Document
import frappe.cache_manager import frappe.cache_manager
from frappe.model import log_types from frappe.model import log_types
from frappe.model.document import Document



class MilestoneTracker(Document): class MilestoneTracker(Document):
def on_update(self): def on_update(self):
frappe.cache_manager.clear_doctype_map('Milestone Tracker', self.document_type)
frappe.cache_manager.clear_doctype_map("Milestone Tracker", self.document_type)


def on_trash(self): def on_trash(self):
frappe.cache_manager.clear_doctype_map('Milestone Tracker', self.document_type)
frappe.cache_manager.clear_doctype_map("Milestone Tracker", self.document_type)


def apply(self, doc): def apply(self, doc):
before_save = doc.get_doc_before_save() before_save = doc.get_doc_before_save()
from_value = before_save and before_save.get(self.track_field) or None from_value = before_save and before_save.get(self.track_field) or None
if from_value != doc.get(self.track_field): if from_value != doc.get(self.track_field):
frappe.get_doc(dict(
doctype = 'Milestone',
reference_type = doc.doctype,
reference_name = doc.name,
track_field = self.track_field,
from_value = from_value,
value = doc.get(self.track_field),
milestone_tracker = self.name,
)).insert(ignore_permissions=True)
frappe.get_doc(
dict(
doctype="Milestone",
reference_type=doc.doctype,
reference_name=doc.name,
track_field=self.track_field,
from_value=from_value,
value=doc.get(self.track_field),
milestone_tracker=self.name,
)
).insert(ignore_permissions=True)



def evaluate_milestone(doc, event): def evaluate_milestone(doc, event):
if (frappe.flags.in_install
if (
frappe.flags.in_install
or frappe.flags.in_migrate or frappe.flags.in_migrate
or frappe.flags.in_setup_wizard or frappe.flags.in_setup_wizard
or doc.doctype in log_types):
or doc.doctype in log_types
):
return return


# track milestones related to this doctype # track milestones related to this doctype
for d in get_milestone_trackers(doc.doctype): for d in get_milestone_trackers(doc.doctype):
frappe.get_doc('Milestone Tracker', d.get('name')).apply(doc)
frappe.get_doc("Milestone Tracker", d.get("name")).apply(doc)


def get_milestone_trackers(doctype):
return frappe.cache_manager.get_doctype_map('Milestone Tracker', doctype,
dict(document_type = doctype, disabled=0))


def get_milestone_trackers(doctype):
return frappe.cache_manager.get_doctype_map(
"Milestone Tracker", doctype, dict(document_type=doctype, disabled=0)
)

+ 25
- 25
frappe/automation/doctype/milestone_tracker/test_milestone_tracker.py 查看文件

@@ -1,48 +1,48 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors # Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import unittest

import frappe import frappe
import frappe.cache_manager import frappe.cache_manager
import unittest


class TestMilestoneTracker(unittest.TestCase): class TestMilestoneTracker(unittest.TestCase):
def test_milestone(self): def test_milestone(self):
frappe.db.delete("Milestone Tracker") frappe.db.delete("Milestone Tracker")


frappe.cache().delete_key('milestone_tracker_map')
frappe.cache().delete_key("milestone_tracker_map")


milestone_tracker = frappe.get_doc(dict(
doctype = 'Milestone Tracker',
document_type = 'ToDo',
track_field = 'status'
)).insert()
milestone_tracker = frappe.get_doc(
dict(doctype="Milestone Tracker", document_type="ToDo", track_field="status")
).insert()


todo = frappe.get_doc(dict(
doctype = 'ToDo',
description = 'test milestone',
status = 'Open'
)).insert()
todo = frappe.get_doc(dict(doctype="ToDo", description="test milestone", status="Open")).insert()


milestones = frappe.get_all('Milestone',
fields = ['track_field', 'value', 'milestone_tracker'],
filters = dict(reference_type = todo.doctype, reference_name=todo.name))
milestones = frappe.get_all(
"Milestone",
fields=["track_field", "value", "milestone_tracker"],
filters=dict(reference_type=todo.doctype, reference_name=todo.name),
)


self.assertEqual(len(milestones), 1) self.assertEqual(len(milestones), 1)
self.assertEqual(milestones[0].track_field, 'status')
self.assertEqual(milestones[0].value, 'Open')
self.assertEqual(milestones[0].track_field, "status")
self.assertEqual(milestones[0].value, "Open")


todo.status = 'Closed'
todo.status = "Closed"
todo.save() todo.save()


milestones = frappe.get_all('Milestone',
fields = ['track_field', 'value', 'milestone_tracker'],
filters = dict(reference_type = todo.doctype, reference_name=todo.name),
order_by = 'modified desc')
milestones = frappe.get_all(
"Milestone",
fields=["track_field", "value", "milestone_tracker"],
filters=dict(reference_type=todo.doctype, reference_name=todo.name),
order_by="modified desc",
)


self.assertEqual(len(milestones), 2) self.assertEqual(len(milestones), 2)
self.assertEqual(milestones[0].track_field, 'status')
self.assertEqual(milestones[0].value, 'Closed')
self.assertEqual(milestones[0].track_field, "status")
self.assertEqual(milestones[0].value, "Closed")


# cleanup # cleanup
frappe.db.delete("Milestone") frappe.db.delete("Milestone")
milestone_tracker.delete()
milestone_tracker.delete()

+ 111
- 63
frappe/boot.py 查看文件

@@ -7,20 +7,22 @@ bootstrap client session
import frappe import frappe
import frappe.defaults import frappe.defaults
import frappe.desk.desk_page import frappe.desk.desk_page
from frappe.core.doctype.navbar_settings.navbar_settings import get_app_logo, get_navbar_settings
from frappe.desk.doctype.route_history.route_history import frequently_visited_links from frappe.desk.doctype.route_history.route_history import frequently_visited_links
from frappe.desk.form.load import get_meta_bundle from frappe.desk.form.load import get_meta_bundle
from frappe.utils.change_log import get_versions
from frappe.translate import get_lang_dict
from frappe.email.inbox import get_email_accounts from frappe.email.inbox import get_email_accounts
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
from frappe.model.base_document import get_controller from frappe.model.base_document import get_controller
from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings, get_app_logo
from frappe.utils import get_time_zone, add_user_info
from frappe.query_builder import DocType from frappe.query_builder import DocType
from frappe.query_builder.functions import Count from frappe.query_builder.functions import Count
from frappe.query_builder.terms import subqry from frappe.query_builder.terms import subqry
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
from frappe.social.doctype.energy_point_settings.energy_point_settings import (
is_energy_point_enabled,
)
from frappe.translate import get_lang_dict
from frappe.utils import add_user_info, get_time_zone
from frappe.utils.change_log import get_versions
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled




def get_bootinfo(): def get_bootinfo():
@@ -38,9 +40,9 @@ def get_bootinfo():
bootinfo.sysdefaults = frappe.defaults.get_defaults() bootinfo.sysdefaults = frappe.defaults.get_defaults()
bootinfo.server_date = frappe.utils.nowdate() bootinfo.server_date = frappe.utils.nowdate()


if frappe.session['user'] != 'Guest':
if frappe.session["user"] != "Guest":
bootinfo.user_info = get_user_info() bootinfo.user_info = get_user_info()
bootinfo.sid = frappe.session['sid']
bootinfo.sid = frappe.session["sid"]


bootinfo.modules = {} bootinfo.modules = {}
bootinfo.module_list = [] bootinfo.module_list = []
@@ -51,8 +53,10 @@ def get_bootinfo():
add_layouts(bootinfo) add_layouts(bootinfo)


bootinfo.module_app = frappe.local.module_app bootinfo.module_app = frappe.local.module_app
bootinfo.single_types = [d.name for d in frappe.get_all('DocType', {'issingle': 1})]
bootinfo.nested_set_doctypes = [d.parent for d in frappe.get_all('DocField', {'fieldname': 'lft'}, ['parent'])]
bootinfo.single_types = [d.name for d in frappe.get_all("DocType", {"issingle": 1})]
bootinfo.nested_set_doctypes = [
d.parent for d in frappe.get_all("DocField", {"fieldname": "lft"}, ["parent"])
]
add_home_page(bootinfo, doclist) add_home_page(bootinfo, doclist)
bootinfo.page_info = get_allowed_pages() bootinfo.page_info = get_allowed_pages()
load_translations(bootinfo) load_translations(bootinfo)
@@ -66,8 +70,8 @@ def get_bootinfo():
set_time_zone(bootinfo) set_time_zone(bootinfo)


# ipinfo # ipinfo
if frappe.session.data.get('ipinfo'):
bootinfo.ipinfo = frappe.session['data']['ipinfo']
if frappe.session.data.get("ipinfo"):
bootinfo.ipinfo = frappe.session["data"]["ipinfo"]


# add docs # add docs
bootinfo.docs = doclist bootinfo.docs = doclist
@@ -77,7 +81,7 @@ def get_bootinfo():


if bootinfo.lang: if bootinfo.lang:
bootinfo.lang = str(bootinfo.lang) bootinfo.lang = str(bootinfo.lang)
bootinfo.versions = {k: v['version'] for k, v in get_versions().items()}
bootinfo.versions = {k: v["version"] for k, v in get_versions().items()}


bootinfo.error_report_email = frappe.conf.error_report_email bootinfo.error_report_email = frappe.conf.error_report_email
bootinfo.calendars = sorted(frappe.get_hooks("calendars")) bootinfo.calendars = sorted(frappe.get_hooks("calendars"))
@@ -97,37 +101,47 @@ def get_bootinfo():


return bootinfo return bootinfo



def get_letter_heads(): def get_letter_heads():
letter_heads = {} letter_heads = {}
for letter_head in frappe.get_all("Letter Head", fields = ["name", "content", "footer"]):
letter_heads.setdefault(letter_head.name,
{'header': letter_head.content, 'footer': letter_head.footer})
for letter_head in frappe.get_all("Letter Head", fields=["name", "content", "footer"]):
letter_heads.setdefault(
letter_head.name, {"header": letter_head.content, "footer": letter_head.footer}
)


return letter_heads return letter_heads



def load_conf_settings(bootinfo): def load_conf_settings(bootinfo):
from frappe import conf from frappe import conf
bootinfo.max_file_size = conf.get('max_file_size') or 10485760
for key in ('developer_mode', 'socketio_port', 'file_watcher_port'):
if key in conf: bootinfo[key] = conf.get(key)

bootinfo.max_file_size = conf.get("max_file_size") or 10485760
for key in ("developer_mode", "socketio_port", "file_watcher_port"):
if key in conf:
bootinfo[key] = conf.get(key)



def load_desktop_data(bootinfo): def load_desktop_data(bootinfo):
from frappe.desk.desktop import get_workspace_sidebar_items from frappe.desk.desktop import get_workspace_sidebar_items
bootinfo.allowed_workspaces = get_workspace_sidebar_items().get('pages')

bootinfo.allowed_workspaces = get_workspace_sidebar_items().get("pages")
bootinfo.module_page_map = get_controller("Workspace").get_module_page_map() bootinfo.module_page_map = get_controller("Workspace").get_module_page_map()
bootinfo.dashboards = frappe.get_all("Dashboard") bootinfo.dashboards = frappe.get_all("Dashboard")



def get_allowed_pages(cache=False): def get_allowed_pages(cache=False):
return get_user_pages_or_reports('Page', cache=cache)
return get_user_pages_or_reports("Page", cache=cache)



def get_allowed_reports(cache=False): def get_allowed_reports(cache=False):
return get_user_pages_or_reports('Report', cache=cache)
return get_user_pages_or_reports("Report", cache=cache)



def get_user_pages_or_reports(parent, cache=False): def get_user_pages_or_reports(parent, cache=False):
_cache = frappe.cache() _cache = frappe.cache()


if cache: if cache:
has_role = _cache.get_value('has_role:' + parent, user=frappe.session.user)
has_role = _cache.get_value("has_role:" + parent, user=frappe.session.user)
if has_role: if has_role:
return has_role return has_role


@@ -140,8 +154,7 @@ def get_user_pages_or_reports(parent, cache=False):
if parent == "Report": if parent == "Report":
columns = (report.name.as_("title"), report.ref_doctype, report.report_type) columns = (report.name.as_("title"), report.ref_doctype, report.report_type)
else: else:
columns = (page.title.as_("title"), )

columns = (page.title.as_("title"),)


customRole = DocType("Custom Role") customRole = DocType("Custom Role")
hasRole = DocType("Has Role") hasRole = DocType("Has Role")
@@ -149,31 +162,39 @@ def get_user_pages_or_reports(parent, cache=False):


# get pages or reports set on custom role # get pages or reports set on custom role
pages_with_custom_roles = ( pages_with_custom_roles = (
frappe.qb.from_(customRole).from_(hasRole).from_(parentTable)
.select(customRole[parent.lower()].as_("name"), customRole.modified, customRole.ref_doctype, *columns)
frappe.qb.from_(customRole)
.from_(hasRole)
.from_(parentTable)
.select(
customRole[parent.lower()].as_("name"), customRole.modified, customRole.ref_doctype, *columns
)
.where( .where(
(hasRole.parent == customRole.name) (hasRole.parent == customRole.name)
& (parentTable.name == customRole[parent.lower()]) & (parentTable.name == customRole[parent.lower()])
& (customRole[parent.lower()].isnotnull()) & (customRole[parent.lower()].isnotnull())
& (hasRole.role.isin(roles)))
& (hasRole.role.isin(roles))
)
).run(as_dict=True) ).run(as_dict=True)


for p in pages_with_custom_roles: for p in pages_with_custom_roles:
has_role[p.name] = {"modified":p.modified, "title": p.title, "ref_doctype": p.ref_doctype}
has_role[p.name] = {"modified": p.modified, "title": p.title, "ref_doctype": p.ref_doctype}


subq = ( subq = (
frappe.qb.from_(customRole).select(customRole[parent.lower()])
frappe.qb.from_(customRole)
.select(customRole[parent.lower()])
.where(customRole[parent.lower()].isnotnull()) .where(customRole[parent.lower()].isnotnull())
) )


pages_with_standard_roles = ( pages_with_standard_roles = (
frappe.qb.from_(hasRole).from_(parentTable)
frappe.qb.from_(hasRole)
.from_(parentTable)
.select(parentTable.name.as_("name"), parentTable.modified, *columns) .select(parentTable.name.as_("name"), parentTable.modified, *columns)
.where( .where(
(hasRole.role.isin(roles)) (hasRole.role.isin(roles))
& (hasRole.parent == parentTable.name) & (hasRole.parent == parentTable.name)
& (parentTable.name.notin(subq)) & (parentTable.name.notin(subq))
).distinct()
)
.distinct()
) )


if parent == "Report": if parent == "Report":
@@ -183,18 +204,20 @@ def get_user_pages_or_reports(parent, cache=False):


for p in pages_with_standard_roles: for p in pages_with_standard_roles:
if p.name not in has_role: if p.name not in has_role:
has_role[p.name] = {"modified":p.modified, "title": p.title}
has_role[p.name] = {"modified": p.modified, "title": p.title}
if parent == "Report": if parent == "Report":
has_role[p.name].update({'ref_doctype': p.ref_doctype})
has_role[p.name].update({"ref_doctype": p.ref_doctype})


no_of_roles = (frappe.qb.from_(hasRole).select(Count("*"))
.where(hasRole.parent == parentTable.name)
no_of_roles = (
frappe.qb.from_(hasRole).select(Count("*")).where(hasRole.parent == parentTable.name)
) )


# pages with no role are allowed # pages with no role are allowed
if parent =="Page":
if parent == "Page":


pages_with_no_roles = (frappe.qb.from_(parentTable).select(parentTable.name, parentTable.modified, *columns)
pages_with_no_roles = (
frappe.qb.from_(parentTable)
.select(parentTable.name, parentTable.modified, *columns)
.where(subqry(no_of_roles) == 0) .where(subqry(no_of_roles) == 0)
).run(as_dict=True) ).run(as_dict=True)


@@ -203,18 +226,20 @@ def get_user_pages_or_reports(parent, cache=False):
has_role[p.name] = {"modified": p.modified, "title": p.title} has_role[p.name] = {"modified": p.modified, "title": p.title}


elif parent == "Report": elif parent == "Report":
reports = frappe.get_all("Report",
reports = frappe.get_all(
"Report",
fields=["name", "report_type"], fields=["name", "report_type"],
filters={"name": ("in", has_role.keys())}, filters={"name": ("in", has_role.keys())},
ignore_ifnull=True
ignore_ifnull=True,
) )
for report in reports: for report in reports:
has_role[report.name]["report_type"] = report.report_type has_role[report.name]["report_type"] = report.report_type


# Expire every six hours # Expire every six hours
_cache.set_value('has_role:' + parent, has_role, frappe.session.user, 21600)
_cache.set_value("has_role:" + parent, has_role, frappe.session.user, 21600)
return has_role return has_role



def load_translations(bootinfo): def load_translations(bootinfo):
messages = frappe.get_lang_dict("boot") messages = frappe.get_lang_dict("boot")


@@ -225,27 +250,30 @@ def load_translations(bootinfo):
messages[name] = frappe._(name) messages[name] = frappe._(name)


# only untranslated # only untranslated
messages = {k: v for k, v in messages.items() if k!=v}
messages = {k: v for k, v in messages.items() if k != v}


bootinfo["__messages"] = messages bootinfo["__messages"] = messages



def get_user_info(): def get_user_info():
# get info for current user # get info for current user
user_info = frappe._dict() user_info = frappe._dict()
add_user_info(frappe.session.user, user_info) add_user_info(frappe.session.user, user_info)


if frappe.session.user == 'Administrator' and user_info.Administrator.email:
if frappe.session.user == "Administrator" and user_info.Administrator.email:
user_info[user_info.Administrator.email] = user_info.Administrator user_info[user_info.Administrator.email] = user_info.Administrator


return user_info return user_info



def get_user(bootinfo): def get_user(bootinfo):
"""get user info""" """get user info"""
bootinfo.user = frappe.get_user().load_user() bootinfo.user = frappe.get_user().load_user()



def add_home_page(bootinfo, docs): def add_home_page(bootinfo, docs):
"""load home page""" """load home page"""
if frappe.session.user=="Guest":
if frappe.session.user == "Guest":
return return
home_page = frappe.db.get_default("desktop:home_page") home_page = frappe.db.get_default("desktop:home_page")


@@ -255,50 +283,65 @@ def add_home_page(bootinfo, docs):
try: try:
page = frappe.desk.desk_page.get(home_page) page = frappe.desk.desk_page.get(home_page)
docs.append(page) docs.append(page)
bootinfo['home_page'] = page.name
bootinfo["home_page"] = page.name
except (frappe.DoesNotExistError, frappe.PermissionError): except (frappe.DoesNotExistError, frappe.PermissionError):
if frappe.message_log: if frappe.message_log:
frappe.message_log.pop() frappe.message_log.pop()
bootinfo['home_page'] = 'Workspaces'
bootinfo["home_page"] = "Workspaces"



def add_timezone_info(bootinfo): def add_timezone_info(bootinfo):
system = bootinfo.sysdefaults.get("time_zone") system = bootinfo.sysdefaults.get("time_zone")
import frappe.utils.momentjs import frappe.utils.momentjs
bootinfo.timezone_info = {"zones":{}, "rules":{}, "links":{}}

bootinfo.timezone_info = {"zones": {}, "rules": {}, "links": {}}
frappe.utils.momentjs.update(system, bootinfo.timezone_info) frappe.utils.momentjs.update(system, bootinfo.timezone_info)



def load_print(bootinfo, doclist): def load_print(bootinfo, doclist):
print_settings = frappe.db.get_singles_dict("Print Settings") print_settings = frappe.db.get_singles_dict("Print Settings")
print_settings.doctype = ":Print Settings" print_settings.doctype = ":Print Settings"
doclist.append(print_settings) doclist.append(print_settings)
load_print_css(bootinfo, print_settings) load_print_css(bootinfo, print_settings)



def load_print_css(bootinfo, print_settings): def load_print_css(bootinfo, print_settings):
import frappe.www.printview import frappe.www.printview
bootinfo.print_css = frappe.www.printview.get_print_style(print_settings.print_style or "Redesign", for_legacy=True)

bootinfo.print_css = frappe.www.printview.get_print_style(
print_settings.print_style or "Redesign", for_legacy=True
)



def get_unseen_notes(): def get_unseen_notes():
note = DocType("Note") note = DocType("Note")
nsb = DocType("Note Seen By").as_("nsb") nsb = DocType("Note Seen By").as_("nsb")


return ( return (
frappe.qb.from_(note).select(note.name, note.title, note.content, note.notify_on_every_login)
frappe.qb.from_(note)
.select(note.name, note.title, note.content, note.notify_on_every_login)
.where( .where(
(note.notify_on_every_login == 1) (note.notify_on_every_login == 1)
& (note.expire_notification_on > frappe.utils.now()) & (note.expire_notification_on > frappe.utils.now())
& (subqry(frappe.qb.from_(nsb).select(nsb.user).where(nsb.parent == note.name)).notin([frappe.session.user])))
).run(as_dict=1)
& (
subqry(frappe.qb.from_(nsb).select(nsb.user).where(nsb.parent == note.name)).notin(
[frappe.session.user]
)
)
)
).run(as_dict=1)



def get_success_action(): def get_success_action():
return frappe.get_all("Success Action", fields=["*"]) return frappe.get_all("Success Action", fields=["*"])



def get_link_preview_doctypes(): def get_link_preview_doctypes():
from frappe.utils import cint from frappe.utils import cint


link_preview_doctypes = [d.name for d in frappe.db.get_all('DocType', {'show_preview_popup': 1})]
customizations = frappe.get_all("Property Setter",
fields=['doc_type', 'value'],
filters={'property': 'show_preview_popup'}
link_preview_doctypes = [d.name for d in frappe.db.get_all("DocType", {"show_preview_popup": 1})]
customizations = frappe.get_all(
"Property Setter", fields=["doc_type", "value"], filters={"property": "show_preview_popup"}
) )


for custom in customizations: for custom in customizations:
@@ -309,22 +352,23 @@ def get_link_preview_doctypes():


return link_preview_doctypes return link_preview_doctypes



def get_additional_filters_from_hooks(): def get_additional_filters_from_hooks():
filter_config = frappe._dict() filter_config = frappe._dict()
filter_hooks = frappe.get_hooks('filters_config')
filter_hooks = frappe.get_hooks("filters_config")
for hook in filter_hooks: for hook in filter_hooks:
filter_config.update(frappe.get_attr(hook)()) filter_config.update(frappe.get_attr(hook)())


return filter_config return filter_config



def add_layouts(bootinfo): def add_layouts(bootinfo):
# add routes for readable doctypes # add routes for readable doctypes
bootinfo.doctype_layouts = frappe.get_all('DocType Layout', ['name', 'route', 'document_type'])
bootinfo.doctype_layouts = frappe.get_all("DocType Layout", ["name", "route", "document_type"])



def get_desk_settings(): def get_desk_settings():
role_list = frappe.get_all('Role', fields=['*'], filters=dict(
name=['in', frappe.get_roles()]
))
role_list = frappe.get_all("Role", fields=["*"], filters=dict(name=["in", frappe.get_roles()]))
desk_settings = {} desk_settings = {}


from frappe.core.doctype.role.role import desk_properties from frappe.core.doctype.role.role import desk_properties
@@ -335,8 +379,10 @@ def get_desk_settings():


return desk_settings return desk_settings



def get_notification_settings(): def get_notification_settings():
return frappe.get_cached_doc('Notification Settings', frappe.session.user)
return frappe.get_cached_doc("Notification Settings", frappe.session.user)



@frappe.whitelist() @frappe.whitelist()
def get_link_title_doctypes(): def get_link_title_doctypes():
@@ -348,8 +394,10 @@ def get_link_title_doctypes():
) )
return [d.name for d in dts + custom_dts if d] return [d.name for d in dts + custom_dts if d]



def set_time_zone(bootinfo): def set_time_zone(bootinfo):
bootinfo.time_zone = { bootinfo.time_zone = {
"system": get_time_zone(), "system": get_time_zone(),
"user": bootinfo.get("user_info", {}).get(frappe.session.user, {}).get("time_zone", None) or get_time_zone()
"user": bootinfo.get("user_info", {}).get(frappe.session.user, {}).get("time_zone", None)
or get_time_zone(),
} }

+ 28
- 20
frappe/build.py 查看文件

@@ -1,8 +1,8 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import os import os
import shutil
import re import re
import shutil
import subprocess import subprocess
from distutils.spawn import find_executable from distutils.spawn import find_executable
from subprocess import getoutput from subprocess import getoutput
@@ -25,6 +25,7 @@ sites_path = os.path.abspath(os.getcwd())
class AssetsNotDownloadedError(Exception): class AssetsNotDownloadedError(Exception):
pass pass



class AssetsDontExistError(HTTPError): class AssetsDontExistError(HTTPError):
pass pass


@@ -43,7 +44,7 @@ def download_file(url, prefix):




def build_missing_files(): def build_missing_files():
'''Check which files dont exist yet from the assets.json and run build for those files'''
"""Check which files dont exist yet from the assets.json and run build for those files"""


missing_assets = [] missing_assets = []
current_asset_files = [] current_asset_files = []
@@ -60,7 +61,7 @@ def build_missing_files():
assets_json = frappe.parse_json(assets_json) assets_json = frappe.parse_json(assets_json)


for bundle_file, output_file in assets_json.items(): for bundle_file, output_file in assets_json.items():
if not output_file.startswith('/assets/frappe'):
if not output_file.startswith("/assets/frappe"):
continue continue


if os.path.basename(output_file) not in current_asset_files: if os.path.basename(output_file) not in current_asset_files:
@@ -78,8 +79,7 @@ def build_missing_files():
def get_assets_link(frappe_head) -> str: def get_assets_link(frappe_head) -> str:
tag = getoutput( tag = getoutput(
r"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*" r"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*"
r" refs/tags/,,' -e 's/\^{}//'"
% frappe_head
r" refs/tags/,,' -e 's/\^{}//'" % frappe_head
) )


if tag: if tag:
@@ -111,6 +111,7 @@ def fetch_assets(url, frappe_head):


def setup_assets(assets_archive): def setup_assets(assets_archive):
import tarfile import tarfile

directories_created = set() directories_created = set()


click.secho("\nExtracting assets...\n", fg="yellow") click.secho("\nExtracting assets...\n", fg="yellow")
@@ -221,7 +222,16 @@ def setup():
assets_path = os.path.join(frappe.local.sites_path, "assets") assets_path = os.path.join(frappe.local.sites_path, "assets")




def bundle(mode, apps=None, hard_link=False, make_copy=False, restore=False, verbose=False, skip_frappe=False, files=None):
def bundle(
mode,
apps=None,
hard_link=False,
make_copy=False,
restore=False,
verbose=False,
skip_frappe=False,
files=None,
):
"""concat / minify js files""" """concat / minify js files"""
setup() setup()
make_asset_dirs(hard_link=hard_link) make_asset_dirs(hard_link=hard_link)
@@ -236,7 +246,7 @@ def bundle(mode, apps=None, hard_link=False, make_copy=False, restore=False, ver
command += " --skip_frappe" command += " --skip_frappe"


if files: if files:
command += " --files {files}".format(files=','.join(files))
command += " --files {files}".format(files=",".join(files))


command += " --run-build-command" command += " --run-build-command"


@@ -253,9 +263,7 @@ def watch(apps=None):
if apps: if apps:
command += " --apps {apps}".format(apps=apps) command += " --apps {apps}".format(apps=apps)


live_reload = frappe.utils.cint(
os.environ.get("LIVE_RELOAD", frappe.conf.live_reload)
)
live_reload = frappe.utils.cint(os.environ.get("LIVE_RELOAD", frappe.conf.live_reload))


if live_reload: if live_reload:
command += " --live-reload" command += " --live-reload"
@@ -266,8 +274,8 @@ def watch(apps=None):




def check_node_executable(): def check_node_executable():
node_version = Version(subprocess.getoutput('node -v')[1:])
warn = '⚠️ '
node_version = Version(subprocess.getoutput("node -v")[1:])
warn = "⚠️ "
if node_version.major < 14: if node_version.major < 14:
click.echo(f"{warn} Please update your node version to 14") click.echo(f"{warn} Please update your node version to 14")
if not find_executable("yarn"): if not find_executable("yarn"):
@@ -276,9 +284,7 @@ def check_node_executable():




def get_node_env(): def get_node_env():
node_env = {
"NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}"
}
node_env = {"NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}"}
return node_env return node_env




@@ -345,8 +351,7 @@ def clear_broken_symlinks():




def unstrip(message: str) -> str: def unstrip(message: str) -> str:
"""Pads input string on the right side until the last available column in the terminal
"""
"""Pads input string on the right side until the last available column in the terminal"""
_len = len(message) _len = len(message)
try: try:
max_str = os.get_terminal_size().columns max_str = os.get_terminal_size().columns
@@ -367,7 +372,9 @@ def make_asset_dirs(hard_link=False):
symlinks = generate_assets_map() symlinks = generate_assets_map()


for source, target in symlinks.items(): for source, target in symlinks.items():
start_message = unstrip(f"{'Copying assets from' if hard_link else 'Linking'} {source} to {target}")
start_message = unstrip(
f"{'Copying assets from' if hard_link else 'Linking'} {source} to {target}"
)
fail_message = unstrip(f"Cannot {'copy' if hard_link else 'link'} {source} to {target}") fail_message = unstrip(f"Cannot {'copy' if hard_link else 'link'} {source} to {target}")


# Used '\r' instead of '\x1b[1K\r' to print entire lines in smaller terminal sizes # Used '\r' instead of '\x1b[1K\r' to print entire lines in smaller terminal sizes
@@ -404,10 +411,11 @@ def scrub_html_template(content):
# strip comments # strip comments
content = re.sub(r"(<!--.*?-->)", "", content) content = re.sub(r"(<!--.*?-->)", "", content)


return content.replace("'", "\'")
return content.replace("'", "'")




def html_to_js_template(path, content): def html_to_js_template(path, content):
"""returns HTML template content as Javascript code, adding it to `frappe.templates`""" """returns HTML template content as Javascript code, adding it to `frappe.templates`"""
return """frappe.templates["{key}"] = '{content}';\n""".format( return """frappe.templates["{key}"] = '{content}';\n""".format(
key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content))
key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content)
)

+ 103
- 44
frappe/cache_manager.py 查看文件

@@ -1,33 +1,75 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE


import frappe, json
import json

import frappe
from frappe.desk.notifications import clear_notifications, delete_notification_count_for
from frappe.model.document import Document from frappe.model.document import Document
from frappe.desk.notifications import (delete_notification_count_for,
clear_notifications)


common_default_keys = ["__default", "__global"] common_default_keys = ["__default", "__global"]


doctype_map_keys = ('energy_point_rule_map', 'assignment_rule_map',
'milestone_tracker_map', 'event_consumer_document_type_map')

bench_cache_keys = ('assets_json',)

global_cache_keys = ("app_hooks", "installed_apps", 'all_apps',
"app_modules", "module_app", "system_settings",
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version',
'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts',
'sitemap_routes', 'db_tables', 'server_script_autocompletion_items') + doctype_map_keys

user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang",
"defaults", "user_permissions", "home_page", "linked_with",
"desktop_icons", 'portal_menu_items', 'user_perm_can_read',
"has_role:Page", "has_role:Report", "desk_sidebar_items")
doctype_map_keys = (
"energy_point_rule_map",
"assignment_rule_map",
"milestone_tracker_map",
"event_consumer_document_type_map",
)

bench_cache_keys = ("assets_json",)

global_cache_keys = (
"app_hooks",
"installed_apps",
"all_apps",
"app_modules",
"module_app",
"system_settings",
"scheduler_events",
"time_zone",
"webhooks",
"active_domains",
"active_modules",
"assignment_rule",
"server_script_map",
"wkhtmltopdf_version",
"domain_restricted_doctypes",
"domain_restricted_pages",
"information_schema:counts",
"sitemap_routes",
"db_tables",
"server_script_autocompletion_items",
) + doctype_map_keys

user_cache_keys = (
"bootinfo",
"user_recent",
"roles",
"user_doc",
"lang",
"defaults",
"user_permissions",
"home_page",
"linked_with",
"desktop_icons",
"portal_menu_items",
"user_perm_can_read",
"has_role:Page",
"has_role:Report",
"desk_sidebar_items",
)

doctype_cache_keys = (
"meta",
"form_meta",
"table_columns",
"last_modified",
"linked_doctypes",
"notifications",
"workflow",
"data_import_column_header_map",
) + doctype_map_keys


doctype_cache_keys = ("meta", "form_meta", "table_columns", "last_modified",
"linked_doctypes", 'notifications', 'workflow' ,
'data_import_column_header_map') + doctype_map_keys


def clear_user_cache(user=None): def clear_user_cache(user=None):
cache = frappe.cache() cache = frappe.cache()
@@ -47,11 +89,13 @@ def clear_user_cache(user=None):
clear_defaults_cache() clear_defaults_cache()
clear_global_cache() clear_global_cache()



def clear_domain_cache(user=None): def clear_domain_cache(user=None):
cache = frappe.cache() cache = frappe.cache()
domain_cache_keys = ('domain_restricted_doctypes', 'domain_restricted_pages')
domain_cache_keys = ("domain_restricted_doctypes", "domain_restricted_pages")
cache.delete_value(domain_cache_keys) cache.delete_value(domain_cache_keys)



def clear_global_cache(): def clear_global_cache():
from frappe.website.utils import clear_website_cache from frappe.website.utils import clear_website_cache


@@ -61,21 +105,23 @@ def clear_global_cache():
frappe.cache().delete_value(bench_cache_keys) frappe.cache().delete_value(bench_cache_keys)
frappe.setup_module_map() frappe.setup_module_map()



def clear_defaults_cache(user=None): def clear_defaults_cache(user=None):
if user: if user:
for p in ([user] + common_default_keys):
for p in [user] + common_default_keys:
frappe.cache().hdel("defaults", p) frappe.cache().hdel("defaults", p)
elif frappe.flags.in_install!="frappe":
elif frappe.flags.in_install != "frappe":
frappe.cache().delete_key("defaults") frappe.cache().delete_key("defaults")



def clear_doctype_cache(doctype=None): def clear_doctype_cache(doctype=None):
clear_controller_cache(doctype) clear_controller_cache(doctype)
cache = frappe.cache() cache = frappe.cache()


if getattr(frappe.local, 'meta_cache') and (doctype in frappe.local.meta_cache):
if getattr(frappe.local, "meta_cache") and (doctype in frappe.local.meta_cache):
del frappe.local.meta_cache[doctype] del frappe.local.meta_cache[doctype]


for key in ('is_table', 'doctype_modules', 'document_cache'):
for key in ("is_table", "doctype_modules", "document_cache"):
cache.delete_value(key) cache.delete_value(key)


frappe.local.document_cache = {} frappe.local.document_cache = {}
@@ -89,8 +135,9 @@ def clear_doctype_cache(doctype=None):


# clear all parent doctypes # clear all parent doctypes


for dt in frappe.db.get_all('DocField', 'parent',
dict(fieldtype=['in', frappe.model.table_fields], options=doctype)):
for dt in frappe.db.get_all(
"DocField", "parent", dict(fieldtype=["in", frappe.model.table_fields], options=doctype)
):
clear_single(dt.parent) clear_single(dt.parent)


# clear all notifications # clear all notifications
@@ -101,6 +148,7 @@ def clear_doctype_cache(doctype=None):
for name in doctype_cache_keys: for name in doctype_cache_keys:
cache.delete_value(name) cache.delete_value(name)



def clear_controller_cache(doctype=None): def clear_controller_cache(doctype=None):
if not doctype: if not doctype:
del frappe.controllers del frappe.controllers
@@ -110,9 +158,10 @@ def clear_controller_cache(doctype=None):
for site_controllers in frappe.controllers.values(): for site_controllers in frappe.controllers.values():
site_controllers.pop(doctype, None) site_controllers.pop(doctype, None)



def get_doctype_map(doctype, name, filters=None, order_by=None): def get_doctype_map(doctype, name, filters=None, order_by=None):
cache = frappe.cache() cache = frappe.cache()
cache_key = frappe.scrub(doctype) + '_map'
cache_key = frappe.scrub(doctype) + "_map"
doctype_map = cache.hget(cache_key, name) doctype_map = cache.hget(cache_key, name)


if doctype_map is not None: if doctype_map is not None:
@@ -121,7 +170,7 @@ def get_doctype_map(doctype, name, filters=None, order_by=None):
else: else:
# non cached, build cache # non cached, build cache
try: try:
items = frappe.get_all(doctype, filters=filters, order_by = order_by)
items = frappe.get_all(doctype, filters=filters, order_by=order_by)
cache.hset(cache_key, name, json.dumps(items)) cache.hset(cache_key, name, json.dumps(items))
except frappe.db.TableMissingError: except frappe.db.TableMissingError:
# executed from inside patch, ignore # executed from inside patch, ignore
@@ -129,15 +178,19 @@ def get_doctype_map(doctype, name, filters=None, order_by=None):


return items return items



def clear_doctype_map(doctype, name): def clear_doctype_map(doctype, name):
frappe.cache().hdel(frappe.scrub(doctype) + '_map', name)
frappe.cache().hdel(frappe.scrub(doctype) + "_map", name)



def build_table_count_cache(): def build_table_count_cache():
if (frappe.flags.in_patch
if (
frappe.flags.in_patch
or frappe.flags.in_install or frappe.flags.in_install
or frappe.flags.in_migrate or frappe.flags.in_migrate
or frappe.flags.in_import or frappe.flags.in_import
or frappe.flags.in_setup_wizard):
or frappe.flags.in_setup_wizard
):
return return


_cache = frappe.cache() _cache = frappe.cache()
@@ -145,39 +198,45 @@ def build_table_count_cache():
table_rows = frappe.qb.Field("table_rows").as_("count") table_rows = frappe.qb.Field("table_rows").as_("count")
information_schema = frappe.qb.Schema("information_schema") information_schema = frappe.qb.Schema("information_schema")


data = (
frappe.qb.from_(information_schema.tables).select(table_name, table_rows)
).run(as_dict=True)
counts = {d.get('name').replace('tab', '', 1): d.get('count', None) for d in data}
data = (frappe.qb.from_(information_schema.tables).select(table_name, table_rows)).run(
as_dict=True
)
counts = {d.get("name").replace("tab", "", 1): d.get("count", None) for d in data}
_cache.set_value("information_schema:counts", counts) _cache.set_value("information_schema:counts", counts)


return counts return counts



def build_domain_restriced_doctype_cache(*args, **kwargs): def build_domain_restriced_doctype_cache(*args, **kwargs):
if (frappe.flags.in_patch
if (
frappe.flags.in_patch
or frappe.flags.in_install or frappe.flags.in_install
or frappe.flags.in_migrate or frappe.flags.in_migrate
or frappe.flags.in_import or frappe.flags.in_import
or frappe.flags.in_setup_wizard):
or frappe.flags.in_setup_wizard
):
return return
_cache = frappe.cache() _cache = frappe.cache()
active_domains = frappe.get_active_domains() active_domains = frappe.get_active_domains()
doctypes = frappe.get_all("DocType", filters={'restrict_to_domain': ('IN', active_domains)})
doctypes = frappe.get_all("DocType", filters={"restrict_to_domain": ("IN", active_domains)})
doctypes = [doc.name for doc in doctypes] doctypes = [doc.name for doc in doctypes]
_cache.set_value("domain_restricted_doctypes", doctypes) _cache.set_value("domain_restricted_doctypes", doctypes)


return doctypes return doctypes



def build_domain_restriced_page_cache(*args, **kwargs): def build_domain_restriced_page_cache(*args, **kwargs):
if (frappe.flags.in_patch
if (
frappe.flags.in_patch
or frappe.flags.in_install or frappe.flags.in_install
or frappe.flags.in_migrate or frappe.flags.in_migrate
or frappe.flags.in_import or frappe.flags.in_import
or frappe.flags.in_setup_wizard):
or frappe.flags.in_setup_wizard
):
return return
_cache = frappe.cache() _cache = frappe.cache()
active_domains = frappe.get_active_domains() active_domains = frappe.get_active_domains()
pages = frappe.get_all("Page", filters={'restrict_to_domain': ('IN', active_domains)})
pages = frappe.get_all("Page", filters={"restrict_to_domain": ("IN", active_domains)})
pages = [page.name for page in pages] pages = [page.name for page in pages]
_cache.set_value("domain_restricted_pages", pages) _cache.set_value("domain_restricted_pages", pages)




+ 135
- 91
frappe/client.py 查看文件

@@ -1,32 +1,44 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import json
import os

import frappe import frappe
from frappe import _
import frappe.model import frappe.model
import frappe.utils import frappe.utils
import json, os
from frappe.utils import get_safe_filters
from frappe import _
from frappe.desk.reportview import validate_args from frappe.desk.reportview import validate_args
from frappe.model.db_query import check_parent_permission from frappe.model.db_query import check_parent_permission
from frappe.utils import get_safe_filters



'''
"""
Handle RESTful requests that are mapped to the `/api/resource` route. Handle RESTful requests that are mapped to the `/api/resource` route.


Requests via FrappeClient are also handled here. Requests via FrappeClient are also handled here.
'''
"""



@frappe.whitelist() @frappe.whitelist()
def get_list(doctype, fields=None, filters=None, order_by=None,
limit_start=None, limit_page_length=20, parent=None, debug=False, as_dict=True, or_filters=None):
'''Returns a list of records by filters, fields, ordering and limit
def get_list(
doctype,
fields=None,
filters=None,
order_by=None,
limit_start=None,
limit_page_length=20,
parent=None,
debug=False,
as_dict=True,
or_filters=None,
):
"""Returns a list of records by filters, fields, ordering and limit


:param doctype: DocType of the data to be queried :param doctype: DocType of the data to be queried
:param fields: fields to be returned. Default is `name` :param fields: fields to be returned. Default is `name`
:param filters: filter list by this dict :param filters: filter list by this dict
:param order_by: Order by this fieldname :param order_by: Order by this fieldname
:param limit_start: Start at this index :param limit_start: Start at this index
:param limit_page_length: Number of records to be returned (default 20)'''
:param limit_page_length: Number of records to be returned (default 20)"""
if frappe.is_table(doctype): if frappe.is_table(doctype):
check_parent_permission(parent, doctype) check_parent_permission(parent, doctype)


@@ -40,23 +52,25 @@ def get_list(doctype, fields=None, filters=None, order_by=None,
limit_start=limit_start, limit_start=limit_start,
limit_page_length=limit_page_length, limit_page_length=limit_page_length,
debug=debug, debug=debug,
as_list=not as_dict
as_list=not as_dict,
) )


validate_args(args) validate_args(args)
return frappe.get_list(**args) return frappe.get_list(**args)



@frappe.whitelist() @frappe.whitelist()
def get_count(doctype, filters=None, debug=False, cache=False): def get_count(doctype, filters=None, debug=False, cache=False):
return frappe.db.count(doctype, get_safe_filters(filters), debug, cache) return frappe.db.count(doctype, get_safe_filters(filters), debug, cache)



@frappe.whitelist() @frappe.whitelist()
def get(doctype, name=None, filters=None, parent=None): def get(doctype, name=None, filters=None, parent=None):
'''Returns a document by name or filters
"""Returns a document by name or filters


:param doctype: DocType of the document to be returned :param doctype: DocType of the document to be returned
:param name: return document of this `name` :param name: return document of this `name`
:param filters: If name is not set, filter by these values and return the first match'''
:param filters: If name is not set, filter by these values and return the first match"""
if frappe.is_table(doctype): if frappe.is_table(doctype):
check_parent_permission(parent, doctype) check_parent_permission(parent, doctype)


@@ -71,13 +85,14 @@ def get(doctype, name=None, filters=None, parent=None):


return frappe.get_doc(doctype, name).as_dict() return frappe.get_doc(doctype, name).as_dict()



@frappe.whitelist() @frappe.whitelist()
def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, parent=None): def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, parent=None):
'''Returns a value form a document
"""Returns a value form a document


:param doctype: DocType to be queried :param doctype: DocType to be queried
:param fieldname: Field to be returned (default `name`) :param fieldname: Field to be returned (default `name`)
:param filters: dict or string for identifying the record'''
:param filters: dict or string for identifying the record"""
if frappe.is_table(doctype): if frappe.is_table(doctype):
check_parent_permission(parent, doctype) check_parent_permission(parent, doctype)


@@ -102,7 +117,15 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, paren
if frappe.get_meta(doctype).issingle: if frappe.get_meta(doctype).issingle:
value = frappe.db.get_values_from_single(fields, filters, doctype, as_dict=as_dict, debug=debug) value = frappe.db.get_values_from_single(fields, filters, doctype, as_dict=as_dict, debug=debug)
else: else:
value = get_list(doctype, filters=filters, fields=fields, debug=debug, limit_page_length=1, parent=parent, as_dict=as_dict)
value = get_list(
doctype,
filters=filters,
fields=fields,
debug=debug,
limit_page_length=1,
parent=parent,
as_dict=as_dict,
)


if as_dict: if as_dict:
return value[0] if value else {} return value[0] if value else {}
@@ -112,6 +135,7 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, paren


return value[0] if len(fields) > 1 else value[0][0] return value[0] if len(fields) > 1 else value[0][0]



@frappe.whitelist() @frappe.whitelist()
def get_single_value(doctype, field): def get_single_value(doctype, field):
if not frappe.has_permission(doctype): if not frappe.has_permission(doctype):
@@ -119,14 +143,15 @@ def get_single_value(doctype, field):
value = frappe.db.get_single_value(doctype, field) value = frappe.db.get_single_value(doctype, field)
return value return value


@frappe.whitelist(methods=['POST', 'PUT'])

@frappe.whitelist(methods=["POST", "PUT"])
def set_value(doctype, name, fieldname, value=None): def set_value(doctype, name, fieldname, value=None):
'''Set a value using get_doc, group of values
"""Set a value using get_doc, group of values


:param doctype: DocType of the document :param doctype: DocType of the document
:param name: name of the document :param name: name of the document
:param fieldname: fieldname string or JSON / dict with key value pair :param fieldname: fieldname string or JSON / dict with key value pair
:param value: value if fieldname is JSON / dict'''
:param value: value if fieldname is JSON / dict"""


if fieldname in (frappe.model.default_fields + frappe.model.child_table_fields): if fieldname in (frappe.model.default_fields + frappe.model.child_table_fields):
frappe.throw(_("Cannot edit standard fields")) frappe.throw(_("Cannot edit standard fields"))
@@ -137,7 +162,7 @@ def set_value(doctype, name, fieldname, value=None):
try: try:
values = json.loads(fieldname) values = json.loads(fieldname)
except ValueError: except ValueError:
values = {fieldname: ''}
values = {fieldname: ""}
else: else:
values = {fieldname: value} values = {fieldname: value}


@@ -155,11 +180,12 @@ def set_value(doctype, name, fieldname, value=None):


return doc.as_dict() return doc.as_dict()


@frappe.whitelist(methods=['POST', 'PUT'])

@frappe.whitelist(methods=["POST", "PUT"])
def insert(doc=None): def insert(doc=None):
'''Insert a document
"""Insert a document


:param doc: JSON or dict object to be inserted'''
:param doc: JSON or dict object to be inserted"""
if isinstance(doc, str): if isinstance(doc, str):
doc = json.loads(doc) doc = json.loads(doc)


@@ -173,18 +199,19 @@ def insert(doc=None):
doc = frappe.get_doc(doc).insert() doc = frappe.get_doc(doc).insert()
return doc.as_dict() return doc.as_dict()


@frappe.whitelist(methods=['POST', 'PUT'])

@frappe.whitelist(methods=["POST", "PUT"])
def insert_many(docs=None): def insert_many(docs=None):
'''Insert multiple documents
"""Insert multiple documents


:param docs: JSON or list of dict objects to be inserted in one request'''
:param docs: JSON or list of dict objects to be inserted in one request"""
if isinstance(docs, str): if isinstance(docs, str):
docs = json.loads(docs) docs = json.loads(docs)


out = [] out = []


if len(docs) > 200: if len(docs) > 200:
frappe.throw(_('Only 200 inserts allowed in one request'))
frappe.throw(_("Only 200 inserts allowed in one request"))


for doc in docs: for doc in docs:
if doc.get("parenttype"): if doc.get("parenttype"):
@@ -199,11 +226,12 @@ def insert_many(docs=None):


return out return out


@frappe.whitelist(methods=['POST', 'PUT'])

@frappe.whitelist(methods=["POST", "PUT"])
def save(doc): def save(doc):
'''Update (save) an existing document
"""Update (save) an existing document


:param doc: JSON or dict object with the properties of the document to be updated'''
:param doc: JSON or dict object with the properties of the document to be updated"""
if isinstance(doc, str): if isinstance(doc, str):
doc = json.loads(doc) doc = json.loads(doc)


@@ -212,21 +240,23 @@ def save(doc):


return doc.as_dict() return doc.as_dict()


@frappe.whitelist(methods=['POST', 'PUT'])

@frappe.whitelist(methods=["POST", "PUT"])
def rename_doc(doctype, old_name, new_name, merge=False): def rename_doc(doctype, old_name, new_name, merge=False):
'''Rename document
"""Rename document


:param doctype: DocType of the document to be renamed :param doctype: DocType of the document to be renamed
:param old_name: Current `name` of the document to be renamed :param old_name: Current `name` of the document to be renamed
:param new_name: New `name` to be set'''
:param new_name: New `name` to be set"""
new_name = frappe.rename_doc(doctype, old_name, new_name, merge=merge) new_name = frappe.rename_doc(doctype, old_name, new_name, merge=merge)
return new_name return new_name


@frappe.whitelist(methods=['POST', 'PUT'])

@frappe.whitelist(methods=["POST", "PUT"])
def submit(doc): def submit(doc):
'''Submit a document
"""Submit a document


:param doc: JSON or dict object to be submitted remotely'''
:param doc: JSON or dict object to be submitted remotely"""
if isinstance(doc, str): if isinstance(doc, str):
doc = json.loads(doc) doc = json.loads(doc)


@@ -235,52 +265,57 @@ def submit(doc):


return doc.as_dict() return doc.as_dict()


@frappe.whitelist(methods=['POST', 'PUT'])

@frappe.whitelist(methods=["POST", "PUT"])
def cancel(doctype, name): def cancel(doctype, name):
'''Cancel a document
"""Cancel a document


:param doctype: DocType of the document to be cancelled :param doctype: DocType of the document to be cancelled
:param name: name of the document to be cancelled'''
:param name: name of the document to be cancelled"""
wrapper = frappe.get_doc(doctype, name) wrapper = frappe.get_doc(doctype, name)
wrapper.cancel() wrapper.cancel()


return wrapper.as_dict() return wrapper.as_dict()


@frappe.whitelist(methods=['DELETE', 'POST'])

@frappe.whitelist(methods=["DELETE", "POST"])
def delete(doctype, name): def delete(doctype, name):
'''Delete a remote document
"""Delete a remote document


:param doctype: DocType of the document to be deleted :param doctype: DocType of the document to be deleted
:param name: name of the document to be deleted'''
:param name: name of the document to be deleted"""
frappe.delete_doc(doctype, name, ignore_missing=False) frappe.delete_doc(doctype, name, ignore_missing=False)


@frappe.whitelist(methods=['POST', 'PUT'])

@frappe.whitelist(methods=["POST", "PUT"])
def set_default(key, value, parent=None): def set_default(key, value, parent=None):
"""set a user default value""" """set a user default value"""
frappe.db.set_default(key, value, parent or frappe.session.user) frappe.db.set_default(key, value, parent or frappe.session.user)
frappe.clear_cache(user=frappe.session.user) frappe.clear_cache(user=frappe.session.user)



@frappe.whitelist() @frappe.whitelist()
def get_default(key, parent=None): def get_default(key, parent=None):
"""set a user default value""" """set a user default value"""
return frappe.db.get_default(key, parent) return frappe.db.get_default(key, parent)




@frappe.whitelist(methods=['POST', 'PUT'])
@frappe.whitelist(methods=["POST", "PUT"])
def make_width_property_setter(doc): def make_width_property_setter(doc):
'''Set width Property Setter
"""Set width Property Setter


:param doc: Property Setter document with `width` property'''
:param doc: Property Setter document with `width` property"""
if isinstance(doc, str): if isinstance(doc, str):
doc = json.loads(doc) doc = json.loads(doc)
if doc["doctype"]=="Property Setter" and doc["property"]=="width":
frappe.get_doc(doc).insert(ignore_permissions = True)
if doc["doctype"] == "Property Setter" and doc["property"] == "width":
frappe.get_doc(doc).insert(ignore_permissions=True)


@frappe.whitelist(methods=['POST', 'PUT'])

@frappe.whitelist(methods=["POST", "PUT"])
def bulk_update(docs): def bulk_update(docs):
'''Bulk update documents
"""Bulk update documents


:param docs: JSON list of documents to be updated remotely. Each document must have `docname` property'''
:param docs: JSON list of documents to be updated remotely. Each document must have `docname` property"""
docs = json.loads(docs) docs = json.loads(docs)
failed_docs = [] failed_docs = []
for doc in docs: for doc in docs:
@@ -290,41 +325,40 @@ def bulk_update(docs):
existing_doc.update(doc) existing_doc.update(doc)
existing_doc.save() existing_doc.save()
except Exception: except Exception:
failed_docs.append({
'doc': doc,
'exc': frappe.utils.get_traceback()
})
failed_docs.append({"doc": doc, "exc": frappe.utils.get_traceback()})

return {"failed_docs": failed_docs}


return {'failed_docs': failed_docs}


@frappe.whitelist() @frappe.whitelist()
def has_permission(doctype, docname, perm_type="read"): def has_permission(doctype, docname, perm_type="read"):
'''Returns a JSON with data whether the document has the requested permission
"""Returns a JSON with data whether the document has the requested permission


:param doctype: DocType of the document to be checked :param doctype: DocType of the document to be checked
:param docname: `name` of the document to be checked :param docname: `name` of the document to be checked
:param perm_type: one of `read`, `write`, `create`, `submit`, `cancel`, `report`. Default is `read`'''
:param perm_type: one of `read`, `write`, `create`, `submit`, `cancel`, `report`. Default is `read`"""
# perm_type can be one of read, write, create, submit, cancel, report # perm_type can be one of read, write, create, submit, cancel, report
return {"has_permission": frappe.has_permission(doctype, perm_type.lower(), docname)} return {"has_permission": frappe.has_permission(doctype, perm_type.lower(), docname)}



@frappe.whitelist() @frappe.whitelist()
def get_password(doctype, name, fieldname): def get_password(doctype, name, fieldname):
'''Return a password type property. Only applicable for System Managers
"""Return a password type property. Only applicable for System Managers


:param doctype: DocType of the document that holds the password :param doctype: DocType of the document that holds the password
:param name: `name` of the document that holds the password :param name: `name` of the document that holds the password
:param fieldname: `fieldname` of the password property :param fieldname: `fieldname` of the password property
'''
"""
frappe.only_for("System Manager") frappe.only_for("System Manager")
return frappe.get_doc(doctype, name).get_password(fieldname) return frappe.get_doc(doctype, name).get_password(fieldname)




@frappe.whitelist() @frappe.whitelist()
def get_js(items): def get_js(items):
'''Load JS code files. Will also append translations
"""Load JS code files. Will also append translations
and extend `frappe._messages` and extend `frappe._messages`


:param items: JSON list of paths of the js files to be loaded.'''
:param items: JSON list of paths of the js files to be loaded."""
items = json.loads(items) items = json.loads(items)
out = [] out = []
for src in items: for src in items:
@@ -346,14 +380,25 @@ def get_js(items):


return out return out



@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def get_time_zone(): def get_time_zone():
'''Returns default time zone'''
"""Returns default time zone"""
return {"time_zone": frappe.defaults.get_defaults().get("time_zone")} return {"time_zone": frappe.defaults.get_defaults().get("time_zone")}


@frappe.whitelist(methods=['POST', 'PUT'])
def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder=None, decode_base64=False, is_private=None, docfield=None):
'''Attach a file to Document (POST)

@frappe.whitelist(methods=["POST", "PUT"])
def attach_file(
filename=None,
filedata=None,
doctype=None,
docname=None,
folder=None,
decode_base64=False,
is_private=None,
docfield=None,
):
"""Attach a file to Document (POST)


:param filename: filename e.g. test-file.txt :param filename: filename e.g. test-file.txt
:param filedata: base64 encode filedata which must be urlencoded :param filedata: base64 encode filedata which must be urlencoded
@@ -362,7 +407,7 @@ def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder
:param folder: Folder to add File into :param folder: Folder to add File into
:param decode_base64: decode filedata from base64 encode, default is False :param decode_base64: decode filedata from base64 encode, default is False
:param is_private: Attach file as private file (1 or 0) :param is_private: Attach file as private file (1 or 0)
:param docfield: file to attach to (optional)'''
:param docfield: file to attach to (optional)"""


request_method = frappe.local.request.environ.get("REQUEST_METHOD") request_method = frappe.local.request.environ.get("REQUEST_METHOD")


@@ -374,16 +419,19 @@ def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder
if not doc.has_permission(): if not doc.has_permission():
frappe.throw(_("Not permitted"), frappe.PermissionError) frappe.throw(_("Not permitted"), frappe.PermissionError)


_file = frappe.get_doc({
"doctype": "File",
"file_name": filename,
"attached_to_doctype": doctype,
"attached_to_name": docname,
"attached_to_field": docfield,
"folder": folder,
"is_private": is_private,
"content": filedata,
"decode": decode_base64})
_file = frappe.get_doc(
{
"doctype": "File",
"file_name": filename,
"attached_to_doctype": doctype,
"attached_to_name": docname,
"attached_to_field": docfield,
"folder": folder,
"is_private": is_private,
"content": filedata,
"decode": decode_base64,
}
)
_file.save() _file.save()


if docfield and doctype: if docfield and doctype:
@@ -392,22 +440,23 @@ def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder


return _file.as_dict() return _file.as_dict()



@frappe.whitelist() @frappe.whitelist()
def get_hooks(hook, app_name=None): def get_hooks(hook, app_name=None):
return frappe.get_hooks(hook, app_name) return frappe.get_hooks(hook, app_name)



@frappe.whitelist() @frappe.whitelist()
def is_document_amended(doctype, docname): def is_document_amended(doctype, docname):
if frappe.permissions.has_permission(doctype): if frappe.permissions.has_permission(doctype):
try: try:
return frappe.db.exists(doctype, {
'amended_from': docname
})
return frappe.db.exists(doctype, {"amended_from": docname})
except frappe.db.InternalError: except frappe.db.InternalError:
pass pass


return False return False



@frappe.whitelist() @frappe.whitelist()
def validate_link(doctype: str, docname: str, fields=None): def validate_link(doctype: str, docname: str, fields=None):
if not isinstance(doctype, str): if not isinstance(doctype, str):
@@ -417,13 +466,11 @@ def validate_link(doctype: str, docname: str, fields=None):
frappe.throw(_("Document Name must be a string")) frappe.throw(_("Document Name must be a string"))


if doctype != "DocType" and not ( if doctype != "DocType" and not (
frappe.has_permission(doctype, "select")
or frappe.has_permission(doctype, "read")
frappe.has_permission(doctype, "select") or frappe.has_permission(doctype, "read")
): ):
frappe.throw( frappe.throw(
_("You do not have Read or Select Permissions for {}")
.format(frappe.bold(doctype)),
frappe.PermissionError
_("You do not have Read or Select Permissions for {}").format(frappe.bold(doctype)),
frappe.PermissionError,
) )


values = frappe._dict() values = frappe._dict()
@@ -438,14 +485,11 @@ def validate_link(doctype: str, docname: str, fields=None):
except frappe.PermissionError: except frappe.PermissionError:
frappe.clear_last_message() frappe.clear_last_message()
frappe.msgprint( frappe.msgprint(
_("You need {0} permission to fetch values from {1} {2}")
.format(
frappe.bold(_("Read")),
frappe.bold(doctype),
frappe.bold(docname)
_("You need {0} permission to fetch values from {1} {2}").format(
frappe.bold(_("Read")), frappe.bold(doctype), frappe.bold(docname)
), ),
title=_("Cannot Fetch Values"), title=_("Cannot Fetch Values"),
indicator="orange"
indicator="orange",
) )


return values return values

+ 28
- 26
frappe/commands/__init__.py 查看文件

@@ -1,23 +1,26 @@
# Copyright (c) 2015, Web Notes Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Web Notes Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE


import sys
import click
import cProfile import cProfile
import pstats import pstats
import frappe
import frappe.utils
import subprocess # nosec
import subprocess # nosec
import sys
from functools import wraps from functools import wraps
from io import StringIO from io import StringIO
from os import environ from os import environ


import click

import frappe
import frappe.utils

click.disable_unicode_literals_warning = True click.disable_unicode_literals_warning = True



def pass_context(f): def pass_context(f):
@wraps(f) @wraps(f)
def _func(ctx, *args, **kwargs): def _func(ctx, *args, **kwargs):
profile = ctx.obj['profile']
profile = ctx.obj["profile"]
if profile: if profile:
pr = cProfile.Profile() pr = cProfile.Profile()
pr.enable() pr.enable()
@@ -25,18 +28,17 @@ def pass_context(f):
try: try:
ret = f(frappe._dict(ctx.obj), *args, **kwargs) ret = f(frappe._dict(ctx.obj), *args, **kwargs)
except frappe.exceptions.SiteNotSpecifiedError as e: except frappe.exceptions.SiteNotSpecifiedError as e:
click.secho(str(e), fg='yellow')
click.secho(str(e), fg="yellow")
sys.exit(1) sys.exit(1)
except frappe.exceptions.IncorrectSitePath: except frappe.exceptions.IncorrectSitePath:
site = ctx.obj.get("sites", "")[0] site = ctx.obj.get("sites", "")[0]
click.secho(f'Site {site} does not exist!', fg='yellow')
click.secho(f"Site {site} does not exist!", fg="yellow")
sys.exit(1) sys.exit(1)


if profile: if profile:
pr.disable() pr.disable()
s = StringIO() s = StringIO()
ps = pstats.Stats(pr, stream=s)\
.sort_stats('cumtime', 'tottime', 'ncalls')
ps = pstats.Stats(pr, stream=s).sort_stats("cumtime", "tottime", "ncalls")
ps.print_stats() ps.print_stats()


# print the top-100 # print the top-100
@@ -47,6 +49,7 @@ def pass_context(f):


return click.pass_context(_func) return click.pass_context(_func)



def get_site(context, raise_err=True): def get_site(context, raise_err=True):
try: try:
site = context.sites[0] site = context.sites[0]
@@ -56,17 +59,19 @@ def get_site(context, raise_err=True):
raise frappe.SiteNotSpecifiedError raise frappe.SiteNotSpecifiedError
return None return None



def popen(command, *args, **kwargs): def popen(command, *args, **kwargs):
output = kwargs.get('output', True)
cwd = kwargs.get('cwd')
shell = kwargs.get('shell', True)
raise_err = kwargs.get('raise_err')
env = kwargs.get('env')
output = kwargs.get("output", True)
cwd = kwargs.get("cwd")
shell = kwargs.get("shell", True)
raise_err = kwargs.get("raise_err")
env = kwargs.get("env")
if env: if env:
env = dict(environ, **env) env = dict(environ, **env)


def set_low_prio(): def set_low_prio():
import psutil import psutil

if psutil.LINUX: if psutil.LINUX:
psutil.Process().nice(19) psutil.Process().nice(19)
psutil.Process().ionice(psutil.IOPRIO_CLASS_IDLE) psutil.Process().ionice(psutil.IOPRIO_CLASS_IDLE)
@@ -77,13 +82,14 @@ def popen(command, *args, **kwargs):
psutil.Process().nice(19) psutil.Process().nice(19)
# ionice not supported # ionice not supported


proc = subprocess.Popen(command,
proc = subprocess.Popen(
command,
stdout=None if output else subprocess.PIPE, stdout=None if output else subprocess.PIPE,
stderr=None if output else subprocess.PIPE, stderr=None if output else subprocess.PIPE,
shell=shell, shell=shell,
cwd=cwd, cwd=cwd,
preexec_fn=set_low_prio, preexec_fn=set_low_prio,
env=env
env=env,
) )


return_ = proc.wait() return_ = proc.wait()
@@ -93,26 +99,22 @@ def popen(command, *args, **kwargs):


return return_ return return_



def call_command(cmd, context): def call_command(cmd, context):
return click.Context(cmd, obj=context).forward(cmd) return click.Context(cmd, obj=context).forward(cmd)



def get_commands(): def get_commands():
# prevent circular imports # prevent circular imports
from .redis_utils import commands as redis_commands
from .scheduler import commands as scheduler_commands from .scheduler import commands as scheduler_commands
from .site import commands as site_commands from .site import commands as site_commands
from .translate import commands as translate_commands from .translate import commands as translate_commands
from .utils import commands as utils_commands from .utils import commands as utils_commands
from .redis_utils import commands as redis_commands


clickable_link = (
"\x1b]8;;https://frappeframework.com/docs\afrappeframework.com\x1b]8;;\a"
)
clickable_link = "\x1b]8;;https://frappeframework.com/docs\afrappeframework.com\x1b]8;;\a"
all_commands = ( all_commands = (
scheduler_commands
+ site_commands
+ translate_commands
+ utils_commands
+ redis_commands
scheduler_commands + site_commands + translate_commands + utils_commands + redis_commands
) )


for command in all_commands: for command in all_commands:


+ 50
- 30
frappe/commands/redis_utils.py 查看文件

@@ -3,51 +3,71 @@ import os
import click import click


import frappe import frappe
from frappe.utils.redis_queue import RedisQueue
from frappe.installer import update_site_config from frappe.installer import update_site_config
from frappe.utils.redis_queue import RedisQueue


@click.command('create-rq-users')
@click.option('--set-admin-password', is_flag=True, default=False, help='Set new Redis admin(default user) password')
@click.option('--use-rq-auth', is_flag=True, default=False, help='Enable Redis authentication for sites')

@click.command("create-rq-users")
@click.option(
"--set-admin-password",
is_flag=True,
default=False,
help="Set new Redis admin(default user) password",
)
@click.option(
"--use-rq-auth", is_flag=True, default=False, help="Enable Redis authentication for sites"
)
def create_rq_users(set_admin_password=False, use_rq_auth=False): def create_rq_users(set_admin_password=False, use_rq_auth=False):
"""Create Redis Queue users and add to acl and app configs. """Create Redis Queue users and add to acl and app configs.


acl config file will be used by redis server while starting the server acl config file will be used by redis server while starting the server
and app config is used by app while connecting to redis server. and app config is used by app while connecting to redis server.
""" """
acl_file_path = os.path.abspath('../config/redis_queue.acl')
acl_file_path = os.path.abspath("../config/redis_queue.acl")


with frappe.init_site(): with frappe.init_site():
acl_list, user_credentials = RedisQueue.gen_acl_list(
set_admin_password=set_admin_password)
acl_list, user_credentials = RedisQueue.gen_acl_list(set_admin_password=set_admin_password)


with open(acl_file_path, 'w') as f:
f.writelines([acl+'\n' for acl in acl_list])
with open(acl_file_path, "w") as f:
f.writelines([acl + "\n" for acl in acl_list])


sites_path = os.getcwd() sites_path = os.getcwd()
common_site_config_path = os.path.join(sites_path, 'common_site_config.json')
update_site_config("rq_username", user_credentials['bench'][0], validate=False,
site_config_path=common_site_config_path)
update_site_config("rq_password", user_credentials['bench'][1], validate=False,
site_config_path=common_site_config_path)
update_site_config("use_rq_auth", use_rq_auth, validate=False,
site_config_path=common_site_config_path)

click.secho('* ACL and site configs are updated with new user credentials. '
'Please restart Redis Queue server to enable namespaces.',
fg='green')
common_site_config_path = os.path.join(sites_path, "common_site_config.json")
update_site_config(
"rq_username",
user_credentials["bench"][0],
validate=False,
site_config_path=common_site_config_path,
)
update_site_config(
"rq_password",
user_credentials["bench"][1],
validate=False,
site_config_path=common_site_config_path,
)
update_site_config(
"use_rq_auth", use_rq_auth, validate=False, site_config_path=common_site_config_path
)

click.secho(
"* ACL and site configs are updated with new user credentials. "
"Please restart Redis Queue server to enable namespaces.",
fg="green",
)


if set_admin_password: if set_admin_password:
env_key = 'RQ_ADMIN_PASWORD'
click.secho('* Redis admin password is successfully set up. '
'Include below line in .bashrc file for system to use',
fg='green')
env_key = "RQ_ADMIN_PASWORD"
click.secho(
"* Redis admin password is successfully set up. "
"Include below line in .bashrc file for system to use",
fg="green",
)
click.secho(f"`export {env_key}={user_credentials['default'][1]}`") click.secho(f"`export {env_key}={user_credentials['default'][1]}`")
click.secho('NOTE: Please save the admin password as you '
'can not access redis server without the password',
fg='yellow')
click.secho(
"NOTE: Please save the admin password as you "
"can not access redis server without the password",
fg="yellow",
)




commands = [
create_rq_users
]
commands = [create_rq_users]

+ 69
- 44
frappe/commands/scheduler.py 查看文件

@@ -1,15 +1,20 @@
import click
import sys import sys

import click

import frappe import frappe
from frappe.utils import cint
from frappe.commands import pass_context, get_site
from frappe.commands import get_site, pass_context
from frappe.exceptions import SiteNotSpecifiedError from frappe.exceptions import SiteNotSpecifiedError
from frappe.utils import cint



def _is_scheduler_enabled(): def _is_scheduler_enabled():
enable_scheduler = False enable_scheduler = False
try: try:
frappe.connect() frappe.connect()
enable_scheduler = cint(frappe.db.get_single_value("System Settings", "enable_scheduler")) and True or False
enable_scheduler = (
cint(frappe.db.get_single_value("System Settings", "enable_scheduler")) and True or False
)
except: except:
pass pass
finally: finally:
@@ -44,11 +49,12 @@ def trigger_scheduler_event(context, event):
sys.exit(exit_code) sys.exit(exit_code)




@click.command('enable-scheduler')
@click.command("enable-scheduler")
@pass_context @pass_context
def enable_scheduler(context): def enable_scheduler(context):
"Enable scheduler" "Enable scheduler"
import frappe.utils.scheduler import frappe.utils.scheduler

for site in context.sites: for site in context.sites:
try: try:
frappe.init(site=site) frappe.init(site=site)
@@ -61,11 +67,13 @@ def enable_scheduler(context):
if not context.sites: if not context.sites:
raise SiteNotSpecifiedError raise SiteNotSpecifiedError


@click.command('disable-scheduler')

@click.command("disable-scheduler")
@pass_context @pass_context
def disable_scheduler(context): def disable_scheduler(context):
"Disable scheduler" "Disable scheduler"
import frappe.utils.scheduler import frappe.utils.scheduler

for site in context.sites: for site in context.sites:
try: try:
frappe.init(site=site) frappe.init(site=site)
@@ -79,13 +87,13 @@ def disable_scheduler(context):
raise SiteNotSpecifiedError raise SiteNotSpecifiedError




@click.command('scheduler')
@click.option('--site', help='site name')
@click.argument('state', type=click.Choice(['pause', 'resume', 'disable', 'enable']))
@click.command("scheduler")
@click.option("--site", help="site name")
@click.argument("state", type=click.Choice(["pause", "resume", "disable", "enable"]))
@pass_context @pass_context
def scheduler(context, state, site=None): def scheduler(context, state, site=None):
from frappe.installer import update_site_config
import frappe.utils.scheduler import frappe.utils.scheduler
from frappe.installer import update_site_config


if not site: if not site:
site = get_site(context) site = get_site(context)
@@ -93,58 +101,64 @@ def scheduler(context, state, site=None):
try: try:
frappe.init(site=site) frappe.init(site=site)


if state == 'pause':
update_site_config('pause_scheduler', 1)
elif state == 'resume':
update_site_config('pause_scheduler', 0)
elif state == 'disable':
if state == "pause":
update_site_config("pause_scheduler", 1)
elif state == "resume":
update_site_config("pause_scheduler", 0)
elif state == "disable":
frappe.connect() frappe.connect()
frappe.utils.scheduler.disable_scheduler() frappe.utils.scheduler.disable_scheduler()
frappe.db.commit() frappe.db.commit()
elif state == 'enable':
elif state == "enable":
frappe.connect() frappe.connect()
frappe.utils.scheduler.enable_scheduler() frappe.utils.scheduler.enable_scheduler()
frappe.db.commit() frappe.db.commit()


print('Scheduler {0}d for site {1}'.format(state, site))
print("Scheduler {0}d for site {1}".format(state, site))


finally: finally:
frappe.destroy() frappe.destroy()




@click.command('set-maintenance-mode')
@click.option('--site', help='site name')
@click.argument('state', type=click.Choice(['on', 'off']))
@click.command("set-maintenance-mode")
@click.option("--site", help="site name")
@click.argument("state", type=click.Choice(["on", "off"]))
@pass_context @pass_context
def set_maintenance_mode(context, state, site=None): def set_maintenance_mode(context, state, site=None):
from frappe.installer import update_site_config from frappe.installer import update_site_config

if not site: if not site:
site = get_site(context) site = get_site(context)


try: try:
frappe.init(site=site) frappe.init(site=site)
update_site_config('maintenance_mode', 1 if (state == 'on') else 0)
update_site_config("maintenance_mode", 1 if (state == "on") else 0)


finally: finally:
frappe.destroy() frappe.destroy()




@click.command('doctor') #Passing context always gets a site and if there is no use site it breaks
@click.option('--site', help='site name')
@click.command(
"doctor"
) # Passing context always gets a site and if there is no use site it breaks
@click.option("--site", help="site name")
@pass_context @pass_context
def doctor(context, site=None): def doctor(context, site=None):
"Get diagnostic info about background workers" "Get diagnostic info about background workers"
from frappe.utils.doctor import doctor as _doctor from frappe.utils.doctor import doctor as _doctor

if not site: if not site:
site = get_site(context, raise_err=False) site = get_site(context, raise_err=False)
return _doctor(site=site) return _doctor(site=site)


@click.command('show-pending-jobs')
@click.option('--site', help='site name')

@click.command("show-pending-jobs")
@click.option("--site", help="site name")
@pass_context @pass_context
def show_pending_jobs(context, site=None): def show_pending_jobs(context, site=None):
"Get diagnostic info about background jobs" "Get diagnostic info about background jobs"
from frappe.utils.doctor import pending_jobs as _pending_jobs from frappe.utils.doctor import pending_jobs as _pending_jobs

if not site: if not site:
site = get_site(context) site = get_site(context)


@@ -153,35 +167,45 @@ def show_pending_jobs(context, site=None):


return pending_jobs return pending_jobs


@click.command('purge-jobs')
@click.option('--site', help='site name')
@click.option('--queue', default=None, help='one of "low", "default", "high')
@click.option('--event', default=None, help='one of "all", "weekly", "monthly", "hourly", "daily", "weekly_long", "daily_long"')

@click.command("purge-jobs")
@click.option("--site", help="site name")
@click.option("--queue", default=None, help='one of "low", "default", "high')
@click.option(
"--event",
default=None,
help='one of "all", "weekly", "monthly", "hourly", "daily", "weekly_long", "daily_long"',
)
def purge_jobs(site=None, queue=None, event=None): def purge_jobs(site=None, queue=None, event=None):
"Purge any pending periodic tasks, if event option is not given, it will purge everything for the site" "Purge any pending periodic tasks, if event option is not given, it will purge everything for the site"
from frappe.utils.doctor import purge_pending_jobs from frappe.utils.doctor import purge_pending_jobs
frappe.init(site or '')

frappe.init(site or "")
count = purge_pending_jobs(event=event, site=site, queue=queue) count = purge_pending_jobs(event=event, site=site, queue=queue)
print("Purged {} jobs".format(count)) print("Purged {} jobs".format(count))


@click.command('schedule')

@click.command("schedule")
def start_scheduler(): def start_scheduler():
from frappe.utils.scheduler import start_scheduler from frappe.utils.scheduler import start_scheduler

start_scheduler() start_scheduler()


@click.command('worker')
@click.option('--queue', type=str)
@click.option('--quiet', is_flag = True, default = False, help = 'Hide Log Outputs')
@click.option('-u', '--rq-username', default=None, help='Redis ACL user')
@click.option('-p', '--rq-password', default=None, help='Redis ACL user password')
def start_worker(queue, quiet = False, rq_username=None, rq_password=None):
"""Site is used to find redis credentals.
"""
@click.command("worker")
@click.option("--queue", type=str)
@click.option("--quiet", is_flag=True, default=False, help="Hide Log Outputs")
@click.option("-u", "--rq-username", default=None, help="Redis ACL user")
@click.option("-p", "--rq-password", default=None, help="Redis ACL user password")
def start_worker(queue, quiet=False, rq_username=None, rq_password=None):
"""Site is used to find redis credentals."""
from frappe.utils.background_jobs import start_worker from frappe.utils.background_jobs import start_worker
start_worker(queue, quiet = quiet, rq_username=rq_username, rq_password=rq_password)


@click.command('ready-for-migration')
@click.option('--site', help='site name')
start_worker(queue, quiet=quiet, rq_username=rq_username, rq_password=rq_password)


@click.command("ready-for-migration")
@click.option("--site", help="site name")
@pass_context @pass_context
def ready_for_migration(context, site=None): def ready_for_migration(context, site=None):
from frappe.utils.doctor import get_pending_jobs from frappe.utils.doctor import get_pending_jobs
@@ -194,16 +218,17 @@ def ready_for_migration(context, site=None):
pending_jobs = get_pending_jobs(site=site) pending_jobs = get_pending_jobs(site=site)


if pending_jobs: if pending_jobs:
print('NOT READY for migration: site {0} has pending background jobs'.format(site))
print("NOT READY for migration: site {0} has pending background jobs".format(site))
sys.exit(1) sys.exit(1)


else: else:
print('READY for migration: site {0} does not have any background jobs'.format(site))
print("READY for migration: site {0} does not have any background jobs".format(site))
return 0 return 0


finally: finally:
frappe.destroy() frappe.destroy()



commands = [ commands = [
disable_scheduler, disable_scheduler,
doctor, doctor,


+ 424
- 238
frappe/commands/site.py
文件差异内容过多而无法显示
查看文件


+ 38
- 21
frappe/commands/translate.py 查看文件

@@ -1,13 +1,16 @@
import click import click
from frappe.commands import pass_context, get_site

from frappe.commands import get_site, pass_context
from frappe.exceptions import SiteNotSpecifiedError from frappe.exceptions import SiteNotSpecifiedError



# translation # translation
@click.command('build-message-files')
@click.command("build-message-files")
@pass_context @pass_context
def build_message_files(context): def build_message_files(context):
"Build message files for translation" "Build message files for translation"
import frappe.translate import frappe.translate

for site in context.sites: for site in context.sites:
try: try:
frappe.init(site=site) frappe.init(site=site)
@@ -18,32 +21,41 @@ def build_message_files(context):
if not context.sites: if not context.sites:
raise SiteNotSpecifiedError raise SiteNotSpecifiedError


@click.command('new-language') #, help="Create lang-code.csv for given app")

@click.command("new-language") # , help="Create lang-code.csv for given app")
@pass_context @pass_context
@click.argument('lang_code') #, help="Language code eg. en")
@click.argument('app') #, help="App name eg. frappe")
@click.argument("lang_code") # , help="Language code eg. en")
@click.argument("app") # , help="App name eg. frappe")
def new_language(context, lang_code, app): def new_language(context, lang_code, app):
"""Create lang-code.csv for given app""" """Create lang-code.csv for given app"""
import frappe.translate import frappe.translate


if not context['sites']:
raise Exception('--site is required')
if not context["sites"]:
raise Exception("--site is required")


# init site # init site
frappe.connect(site=context['sites'][0])
frappe.connect(site=context["sites"][0])
frappe.translate.write_translations_file(app, lang_code) frappe.translate.write_translations_file(app, lang_code)


print("File created at ./apps/{app}/{app}/translations/{lang_code}.csv".format(app=app, lang_code=lang_code))
print("You will need to add the language in frappe/geo/languages.json, if you haven't done it already.")
print(
"File created at ./apps/{app}/{app}/translations/{lang_code}.csv".format(
app=app, lang_code=lang_code
)
)
print(
"You will need to add the language in frappe/geo/languages.json, if you haven't done it already."
)



@click.command('get-untranslated')
@click.argument('lang')
@click.argument('untranslated_file')
@click.option('--all', default=False, is_flag=True, help='Get all message strings')
@click.command("get-untranslated")
@click.argument("lang")
@click.argument("untranslated_file")
@click.option("--all", default=False, is_flag=True, help="Get all message strings")
@pass_context @pass_context
def get_untranslated(context, lang, untranslated_file, all=None): def get_untranslated(context, lang, untranslated_file, all=None):
"Get untranslated strings for language" "Get untranslated strings for language"
import frappe.translate import frappe.translate

site = get_site(context) site = get_site(context)
try: try:
frappe.init(site=site) frappe.init(site=site)
@@ -52,14 +64,16 @@ def get_untranslated(context, lang, untranslated_file, all=None):
finally: finally:
frappe.destroy() frappe.destroy()


@click.command('update-translations')
@click.argument('lang')
@click.argument('untranslated_file')
@click.argument('translated-file')

@click.command("update-translations")
@click.argument("lang")
@click.argument("untranslated_file")
@click.argument("translated-file")
@pass_context @pass_context
def update_translations(context, lang, untranslated_file, translated_file): def update_translations(context, lang, untranslated_file, translated_file):
"Update translated strings" "Update translated strings"
import frappe.translate import frappe.translate

site = get_site(context) site = get_site(context)
try: try:
frappe.init(site=site) frappe.init(site=site)
@@ -68,13 +82,15 @@ def update_translations(context, lang, untranslated_file, translated_file):
finally: finally:
frappe.destroy() frappe.destroy()


@click.command('import-translations')
@click.argument('lang')
@click.argument('path')

@click.command("import-translations")
@click.argument("lang")
@click.argument("path")
@pass_context @pass_context
def import_translations(context, lang, path): def import_translations(context, lang, path):
"Update translated strings" "Update translated strings"
import frappe.translate import frappe.translate

site = get_site(context) site = get_site(context)
try: try:
frappe.init(site=site) frappe.init(site=site)
@@ -83,6 +99,7 @@ def import_translations(context, lang, path):
finally: finally:
frappe.destroy() frappe.destroy()



commands = [ commands = [
build_message_files, build_message_files,
get_untranslated, get_untranslated,


+ 405
- 221
frappe/commands/utils.py
文件差异内容过多而无法显示
查看文件


+ 19
- 11
frappe/config/__init__.py 查看文件

@@ -1,14 +1,20 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.desk.moduleview import (get_data, get_onboard_items, config_exists, get_module_link_items_from_list)
from frappe.desk.moduleview import (
config_exists,
get_data,
get_module_link_items_from_list,
get_onboard_items,
)



def get_modules_from_all_apps_for_user(user=None): def get_modules_from_all_apps_for_user(user=None):
if not user: if not user:
user = frappe.session.user user = frappe.session.user


all_modules = get_modules_from_all_apps() all_modules = get_modules_from_all_apps()
global_blocked_modules = frappe.get_doc('User', 'Administrator').get_blocked_modules()
user_blocked_modules = frappe.get_doc('User', user).get_blocked_modules()
global_blocked_modules = frappe.get_doc("User", "Administrator").get_blocked_modules()
user_blocked_modules = frappe.get_doc("User", user).get_blocked_modules()
blocked_modules = global_blocked_modules + user_blocked_modules blocked_modules = global_blocked_modules + user_blocked_modules
allowed_modules_list = [m for m in all_modules if m.get("module_name") not in blocked_modules] allowed_modules_list = [m for m in all_modules if m.get("module_name") not in blocked_modules]


@@ -22,31 +28,31 @@ def get_modules_from_all_apps_for_user(user=None):
module["onboard_present"] = 1 module["onboard_present"] = 1


# Set defaults links # Set defaults links
module["links"] = get_onboard_items(module["app"], frappe.scrub(module_name))[:5]
module["links"] = get_onboard_items(module["app"], frappe.scrub(module_name))[:5]


return allowed_modules_list return allowed_modules_list



def get_modules_from_all_apps(): def get_modules_from_all_apps():
modules_list = [] modules_list = []
for app in frappe.get_installed_apps(): for app in frappe.get_installed_apps():
modules_list += get_modules_from_app(app) modules_list += get_modules_from_app(app)
return modules_list return modules_list



def get_modules_from_app(app): def get_modules_from_app(app):
return frappe.get_all('Module Def',
filters={'app_name': app},
fields=['module_name', 'app_name as app']
return frappe.get_all(
"Module Def", filters={"app_name": app}, fields=["module_name", "app_name as app"]
) )



def get_all_empty_tables_by_module(): def get_all_empty_tables_by_module():
table_rows = frappe.qb.Field("table_rows") table_rows = frappe.qb.Field("table_rows")
table_name = frappe.qb.Field("table_name") table_name = frappe.qb.Field("table_name")
information_schema = frappe.qb.Schema("information_schema") information_schema = frappe.qb.Schema("information_schema")


empty_tables = ( empty_tables = (
frappe.qb.from_(information_schema.tables)
.select(table_name)
.where(table_rows == 0)
frappe.qb.from_(information_schema.tables).select(table_name).where(table_rows == 0)
).run() ).run()


empty_tables = {r[0] for r in empty_tables} empty_tables = {r[0] for r in empty_tables}
@@ -62,8 +68,10 @@ def get_all_empty_tables_by_module():
empty_tables_by_module[module] = [doctype] empty_tables_by_module[module] = [doctype]
return empty_tables_by_module return empty_tables_by_module



def is_domain(module): def is_domain(module):
return module.get("category") == "Domains" return module.get("category") == "Domains"



def is_module(module): def is_module(module):
return module.get("type") == "module"
return module.get("type") == "module"

+ 67
- 45
frappe/contacts/address_and_contact.py 查看文件

@@ -1,12 +1,13 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE


import frappe

from frappe import _
import functools import functools
import re import re


import frappe
from frappe import _


def load_address_and_contact(doc, key=None): def load_address_and_contact(doc, key=None):
"""Loads address list and contact list in `__onload`""" """Loads address list and contact list in `__onload`"""
from frappe.contacts.doctype.address.address import get_address_display, get_condensed_address from frappe.contacts.doctype.address.address import get_address_display, get_condensed_address
@@ -18,15 +19,18 @@ def load_address_and_contact(doc, key=None):
] ]
address_list = frappe.get_list("Address", filters=filters, fields=["*"]) address_list = frappe.get_list("Address", filters=filters, fields=["*"])


address_list = [a.update({"display": get_address_display(a)})
for a in address_list]
address_list = [a.update({"display": get_address_display(a)}) for a in address_list]


address_list = sorted(address_list,
key = functools.cmp_to_key(lambda a, b:
(int(a.is_primary_address - b.is_primary_address)) or
(1 if a.modified - b.modified else 0)), reverse=True)
address_list = sorted(
address_list,
key=functools.cmp_to_key(
lambda a, b: (int(a.is_primary_address - b.is_primary_address))
or (1 if a.modified - b.modified else 0)
),
reverse=True,
)


doc.set_onload('addr_list', address_list)
doc.set_onload("addr_list", address_list)


contact_list = [] contact_list = []
filters = [ filters = [
@@ -37,29 +41,38 @@ def load_address_and_contact(doc, key=None):
contact_list = frappe.get_list("Contact", filters=filters, fields=["*"]) contact_list = frappe.get_list("Contact", filters=filters, fields=["*"])


for contact in contact_list: for contact in contact_list:
contact["email_ids"] = frappe.get_all("Contact Email", filters={
"parenttype": "Contact",
"parent": contact.name,
"is_primary": 0
}, fields=["email_id"])

contact["phone_nos"] = frappe.get_all("Contact Phone", filters={
contact["email_ids"] = frappe.get_all(
"Contact Email",
filters={"parenttype": "Contact", "parent": contact.name, "is_primary": 0},
fields=["email_id"],
)

contact["phone_nos"] = frappe.get_all(
"Contact Phone",
filters={
"parenttype": "Contact", "parenttype": "Contact",
"parent": contact.name, "parent": contact.name,
"is_primary_phone": 0, "is_primary_phone": 0,
"is_primary_mobile_no": 0
}, fields=["phone"])
"is_primary_mobile_no": 0,
},
fields=["phone"],
)


if contact.address: if contact.address:
address = frappe.get_doc("Address", contact.address) address = frappe.get_doc("Address", contact.address)
contact["address"] = get_condensed_address(address) contact["address"] = get_condensed_address(address)


contact_list = sorted(contact_list,
key = functools.cmp_to_key(lambda a, b:
(int(a.is_primary_contact - b.is_primary_contact)) or
(1 if a.modified - b.modified else 0)), reverse=True)
contact_list = sorted(
contact_list,
key=functools.cmp_to_key(
lambda a, b: (int(a.is_primary_contact - b.is_primary_contact))
or (1 if a.modified - b.modified else 0)
),
reverse=True,
)

doc.set_onload("contact_list", contact_list)


doc.set_onload('contact_list', contact_list)


def has_permission(doc, ptype, user): def has_permission(doc, ptype, user):
links = get_permitted_and_not_permitted_links(doc.doctype) links = get_permitted_and_not_permitted_links(doc.doctype)
@@ -69,7 +82,7 @@ def has_permission(doc, ptype, user):


# True if any one is True or all are empty # True if any one is True or all are empty
names = [] names = []
for df in (links.get("permitted_links") + links.get("not_permitted_links")):
for df in links.get("permitted_links") + links.get("not_permitted_links"):
doctype = df.options doctype = df.options
name = doc.get(df.fieldname) name = doc.get(df.fieldname)
names.append(name) names.append(name)
@@ -81,12 +94,15 @@ def has_permission(doc, ptype, user):
return True return True
return False return False



def get_permission_query_conditions_for_contact(user): def get_permission_query_conditions_for_contact(user):
return get_permission_query_conditions("Contact") return get_permission_query_conditions("Contact")



def get_permission_query_conditions_for_address(user): def get_permission_query_conditions_for_address(user):
return get_permission_query_conditions("Address") return get_permission_query_conditions("Address")



def get_permission_query_conditions(doctype): def get_permission_query_conditions(doctype):
links = get_permitted_and_not_permitted_links(doctype) links = get_permitted_and_not_permitted_links(doctype)


@@ -100,7 +116,9 @@ def get_permission_query_conditions(doctype):
# when everything is not permitted # when everything is not permitted
for df in links.get("not_permitted_links"): for df in links.get("not_permitted_links"):
# like ifnull(customer, '')='' and ifnull(supplier, '')='' # like ifnull(customer, '')='' and ifnull(supplier, '')=''
conditions.append("ifnull(`tab{doctype}`.`{fieldname}`, '')=''".format(doctype=doctype, fieldname=df.fieldname))
conditions.append(
"ifnull(`tab{doctype}`.`{fieldname}`, '')=''".format(doctype=doctype, fieldname=df.fieldname)
)


return "( " + " and ".join(conditions) + " )" return "( " + " and ".join(conditions) + " )"


@@ -109,10 +127,13 @@ def get_permission_query_conditions(doctype):


for df in links.get("permitted_links"): for df in links.get("permitted_links"):
# like ifnull(customer, '')!='' or ifnull(supplier, '')!='' # like ifnull(customer, '')!='' or ifnull(supplier, '')!=''
conditions.append("ifnull(`tab{doctype}`.`{fieldname}`, '')!=''".format(doctype=doctype, fieldname=df.fieldname))
conditions.append(
"ifnull(`tab{doctype}`.`{fieldname}`, '')!=''".format(doctype=doctype, fieldname=df.fieldname)
)


return "( " + " or ".join(conditions) + " )" return "( " + " or ".join(conditions) + " )"



def get_permitted_and_not_permitted_links(doctype): def get_permitted_and_not_permitted_links(doctype):
permitted_links = [] permitted_links = []
not_permitted_links = [] not_permitted_links = []
@@ -129,40 +150,40 @@ def get_permitted_and_not_permitted_links(doctype):
else: else:
not_permitted_links.append(df) not_permitted_links.append(df)


return {
"permitted_links": permitted_links,
"not_permitted_links": not_permitted_links
}
return {"permitted_links": permitted_links, "not_permitted_links": not_permitted_links}



def delete_contact_and_address(doctype, docname): def delete_contact_and_address(doctype, docname):
for parenttype in ('Contact', 'Address'):
items = frappe.db.sql_list("""select parent from `tabDynamic Link`
for parenttype in ("Contact", "Address"):
items = frappe.db.sql_list(
"""select parent from `tabDynamic Link`
where parenttype=%s and link_doctype=%s and link_name=%s""", where parenttype=%s and link_doctype=%s and link_name=%s""",
(parenttype, doctype, docname))
(parenttype, doctype, docname),
)


for name in items: for name in items:
doc = frappe.get_doc(parenttype, name) doc = frappe.get_doc(parenttype, name)
if len(doc.links)==1:
if len(doc.links) == 1:
doc.delete() doc.delete()



@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, filters): def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, filters):
if not txt: txt = ""
if not txt:
txt = ""


doctypes = frappe.db.get_all("DocField", filters=filters, fields=["parent"],
distinct=True, as_list=True)
doctypes = frappe.db.get_all(
"DocField", filters=filters, fields=["parent"], distinct=True, as_list=True
)


doctypes = tuple(d for d in doctypes if re.search(txt+".*", _(d[0]), re.IGNORECASE))
doctypes = tuple(d for d in doctypes if re.search(txt + ".*", _(d[0]), re.IGNORECASE))


filters.update({
"dt": ("not in", [d[0] for d in doctypes])
})
filters.update({"dt": ("not in", [d[0] for d in doctypes])})


_doctypes = frappe.db.get_all("Custom Field", filters=filters, fields=["dt"],
as_list=True)
_doctypes = frappe.db.get_all("Custom Field", filters=filters, fields=["dt"], as_list=True)


_doctypes = tuple([d for d in _doctypes if re.search(txt+".*", _(d[0]), re.IGNORECASE)])
_doctypes = tuple([d for d in _doctypes if re.search(txt + ".*", _(d[0]), re.IGNORECASE)])


all_doctypes = [d[0] for d in doctypes + _doctypes] all_doctypes = [d[0] for d in doctypes + _doctypes]
allowed_doctypes = frappe.permissions.get_doctypes_with_read() allowed_doctypes = frappe.permissions.get_doctypes_with_read()
@@ -172,6 +193,7 @@ def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, fil


return valid_doctypes return valid_doctypes



def set_link_title(doc): def set_link_title(doc):
if not doc.links: if not doc.links:
return return


+ 84
- 55
frappe/contacts/doctype/address/address.py 查看文件

@@ -2,16 +2,15 @@
# Copyright (c) 2015, Frappe Technologies and contributors # Copyright (c) 2015, Frappe Technologies and contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE


import frappe

from frappe import throw, _
from frappe.utils import cstr
from jinja2 import TemplateSyntaxError


import frappe
from frappe import _, throw
from frappe.contacts.address_and_contact import set_link_title
from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links
from frappe.model.document import Document from frappe.model.document import Document
from jinja2 import TemplateSyntaxError
from frappe.model.naming import make_autoname from frappe.model.naming import make_autoname
from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links
from frappe.contacts.address_and_contact import set_link_title
from frappe.utils import cstr




class Address(Document): class Address(Document):
@@ -24,10 +23,11 @@ class Address(Document):
self.address_title = self.links[0].link_name self.address_title = self.links[0].link_name


if self.address_title: if self.address_title:
self.name = (cstr(self.address_title).strip() + "-" + cstr(_(self.address_type)).strip())
self.name = cstr(self.address_title).strip() + "-" + cstr(_(self.address_type)).strip()
if frappe.db.exists("Address", self.name): if frappe.db.exists("Address", self.name):
self.name = make_autoname(cstr(self.address_title).strip() + "-" +
cstr(self.address_type).strip() + "-.#")
self.name = make_autoname(
cstr(self.address_title).strip() + "-" + cstr(self.address_type).strip() + "-.#"
)
else: else:
throw(_("Address Title is mandatory.")) throw(_("Address Title is mandatory."))


@@ -42,15 +42,15 @@ class Address(Document):
if not self.links: if not self.links:
contact_name = frappe.db.get_value("Contact", {"email_id": self.owner}) contact_name = frappe.db.get_value("Contact", {"email_id": self.owner})
if contact_name: if contact_name:
contact = frappe.get_cached_doc('Contact', contact_name)
contact = frappe.get_cached_doc("Contact", contact_name)
for link in contact.links: for link in contact.links:
self.append('links', dict(link_doctype=link.link_doctype, link_name=link.link_name))
self.append("links", dict(link_doctype=link.link_doctype, link_name=link.link_name))
return True return True


return False return False


def validate_preferred_address(self): def validate_preferred_address(self):
preferred_fields = ['is_primary_address', 'is_shipping_address']
preferred_fields = ["is_primary_address", "is_shipping_address"]


for field in preferred_fields: for field in preferred_fields:
if self.get(field): if self.get(field):
@@ -76,9 +76,11 @@ class Address(Document):


return False return False


def get_preferred_address(doctype, name, preferred_key='is_primary_address'):
if preferred_key in ['is_shipping_address', 'is_primary_address']:
address = frappe.db.sql(""" SELECT

def get_preferred_address(doctype, name, preferred_key="is_primary_address"):
if preferred_key in ["is_shipping_address", "is_primary_address"]:
address = frappe.db.sql(
""" SELECT
addr.name addr.name
FROM FROM
`tabAddress` addr, `tabDynamic Link` dl `tabAddress` addr, `tabDynamic Link` dl
@@ -86,27 +88,37 @@ def get_preferred_address(doctype, name, preferred_key='is_primary_address'):
dl.parent = addr.name and dl.link_doctype = %s and dl.parent = addr.name and dl.link_doctype = %s and
dl.link_name = %s and ifnull(addr.disabled, 0) = 0 and dl.link_name = %s and ifnull(addr.disabled, 0) = 0 and
%s = %s %s = %s
""" % ('%s', '%s', preferred_key, '%s'), (doctype, name, 1), as_dict=1)
"""
% ("%s", "%s", preferred_key, "%s"),
(doctype, name, 1),
as_dict=1,
)


if address: if address:
return address[0].name return address[0].name


return return



@frappe.whitelist() @frappe.whitelist()
def get_default_address(doctype, name, sort_key='is_primary_address'):
'''Returns default Address name for the given doctype, name'''
if sort_key not in ['is_shipping_address', 'is_primary_address']:
def get_default_address(doctype, name, sort_key="is_primary_address"):
"""Returns default Address name for the given doctype, name"""
if sort_key not in ["is_shipping_address", "is_primary_address"]:
return None return None


out = frappe.db.sql(""" SELECT
out = frappe.db.sql(
""" SELECT
addr.name, addr.%s addr.name, addr.%s
FROM FROM
`tabAddress` addr, `tabDynamic Link` dl `tabAddress` addr, `tabDynamic Link` dl
WHERE WHERE
dl.parent = addr.name and dl.link_doctype = %s and dl.parent = addr.name and dl.link_doctype = %s and
dl.link_name = %s and ifnull(addr.disabled, 0) = 0 dl.link_name = %s and ifnull(addr.disabled, 0) = 0
""" %(sort_key, '%s', '%s'), (doctype, name), as_dict=True)
"""
% (sort_key, "%s", "%s"),
(doctype, name),
as_dict=True,
)


if out: if out:
for contact in out: for contact in out:
@@ -150,84 +162,96 @@ def get_territory_from_address(address):


return territory return territory



def get_list_context(context=None): def get_list_context(context=None):
return { return {
"title": _("Addresses"), "title": _("Addresses"),
"get_list": get_address_list, "get_list": get_address_list,
"row_template": "templates/includes/address_row.html", "row_template": "templates/includes/address_row.html",
'no_breadcrumbs': True,
"no_breadcrumbs": True,
} }


def get_address_list(doctype, txt, filters, limit_start, limit_page_length = 20, order_by = None):

def get_address_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by=None):
from frappe.www.list import get_list from frappe.www.list import get_list

user = frappe.session.user user = frappe.session.user
ignore_permissions = True ignore_permissions = True


if not filters: filters = []
if not filters:
filters = []
filters.append(("Address", "owner", "=", user)) filters.append(("Address", "owner", "=", user))


return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions)
return get_list(
doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions
)



def has_website_permission(doc, ptype, user, verbose=False): def has_website_permission(doc, ptype, user, verbose=False):
"""Returns true if there is a related lead or contact related to this document""" """Returns true if there is a related lead or contact related to this document"""
contact_name = frappe.db.get_value("Contact", {"email_id": frappe.session.user}) contact_name = frappe.db.get_value("Contact", {"email_id": frappe.session.user})


if contact_name: if contact_name:
contact = frappe.get_doc('Contact', contact_name)
contact = frappe.get_doc("Contact", contact_name)
return contact.has_common_link(doc) return contact.has_common_link(doc)


return False return False



def get_address_templates(address): def get_address_templates(address):
result = frappe.db.get_value("Address Template", \
{"country": address.get("country")}, ["name", "template"])
result = frappe.db.get_value(
"Address Template", {"country": address.get("country")}, ["name", "template"]
)


if not result: if not result:
result = frappe.db.get_value("Address Template", \
{"is_default": 1}, ["name", "template"])
result = frappe.db.get_value("Address Template", {"is_default": 1}, ["name", "template"])


if not result: if not result:
frappe.throw(_("No default Address Template found. Please create a new one from Setup > Printing and Branding > Address Template."))
frappe.throw(
_(
"No default Address Template found. Please create a new one from Setup > Printing and Branding > Address Template."
)
)
else: else:
return result return result



def get_company_address(company): def get_company_address(company):
ret = frappe._dict() ret = frappe._dict()
ret.company_address = get_default_address('Company', company)
ret.company_address = get_default_address("Company", company)
ret.company_address_display = get_address_display(ret.company_address) ret.company_address_display = get_address_display(ret.company_address)


return ret return ret



@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def address_query(doctype, txt, searchfield, start, page_len, filters): def address_query(doctype, txt, searchfield, start, page_len, filters):
from frappe.desk.reportview import get_match_cond from frappe.desk.reportview import get_match_cond


link_doctype = filters.pop('link_doctype')
link_name = filters.pop('link_name')
link_doctype = filters.pop("link_doctype")
link_name = filters.pop("link_name")


condition = "" condition = ""
meta = frappe.get_meta("Address") meta = frappe.get_meta("Address")
for fieldname, value in filters.items(): for fieldname, value in filters.items():
if meta.get_field(fieldname) or fieldname in frappe.db.DEFAULT_COLUMNS: if meta.get_field(fieldname) or fieldname in frappe.db.DEFAULT_COLUMNS:
condition += " and {field}={value}".format(
field=fieldname,
value=frappe.db.escape(value))
condition += " and {field}={value}".format(field=fieldname, value=frappe.db.escape(value))


searchfields = meta.get_search_fields() searchfields = meta.get_search_fields()


if searchfield and (meta.get_field(searchfield)\
or searchfield in frappe.db.DEFAULT_COLUMNS):
if searchfield and (meta.get_field(searchfield) or searchfield in frappe.db.DEFAULT_COLUMNS):
searchfields.append(searchfield) searchfields.append(searchfield)


search_condition = ''
search_condition = ""
for field in searchfields: for field in searchfields:
if search_condition == '':
search_condition += '`tabAddress`.`{field}` like %(txt)s'.format(field=field)
if search_condition == "":
search_condition += "`tabAddress`.`{field}` like %(txt)s".format(field=field)
else: else:
search_condition += ' or `tabAddress`.`{field}` like %(txt)s'.format(field=field)
search_condition += " or `tabAddress`.`{field}` like %(txt)s".format(field=field)


return frappe.db.sql("""select
return frappe.db.sql(
"""select
`tabAddress`.name, `tabAddress`.city, `tabAddress`.country `tabAddress`.name, `tabAddress`.city, `tabAddress`.country
from from
`tabAddress`, `tabDynamic Link` `tabAddress`, `tabDynamic Link`
@@ -245,19 +269,24 @@ def address_query(doctype, txt, searchfield, start, page_len, filters):
limit %(start)s, %(page_len)s """.format( limit %(start)s, %(page_len)s """.format(
mcond=get_match_cond(doctype), mcond=get_match_cond(doctype),
key=searchfield, key=searchfield,
search_condition = search_condition,
condition=condition or ""), {
'txt': '%' + txt + '%',
'_txt': txt.replace("%", ""),
'start': start,
'page_len': page_len,
'link_name': link_name,
'link_doctype': link_doctype
})
search_condition=search_condition,
condition=condition or "",
),
{
"txt": "%" + txt + "%",
"_txt": txt.replace("%", ""),
"start": start,
"page_len": page_len,
"link_name": link_name,
"link_doctype": link_doctype,
},
)



def get_condensed_address(doc): def get_condensed_address(doc):
fields = ["address_title", "address_line1", "address_line2", "city", "county", "state", "country"] fields = ["address_title", "address_line1", "address_line2", "city", "county", "state", "country"]
return ", ".join(doc.get(d) for d in fields if doc.get(d)) return ", ".join(doc.get(d) for d in fields if doc.get(d))



def update_preferred_address(address, field): def update_preferred_address(address, field):
frappe.db.set_value('Address', address, field, 0)
frappe.db.set_value("Address", address, field, 0)

+ 21
- 20
frappe/contacts/doctype/address/test_address.py 查看文件

@@ -1,31 +1,32 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors # Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe, unittest
import unittest

import frappe
from frappe.contacts.doctype.address.address import get_address_display from frappe.contacts.doctype.address.address import get_address_display



class TestAddress(unittest.TestCase): class TestAddress(unittest.TestCase):
def test_template_works(self): def test_template_works(self):
if not frappe.db.exists('Address Template', 'India'):
frappe.get_doc({
"doctype": "Address Template",
"country": 'India',
"is_default": 1
}).insert()
if not frappe.db.exists("Address Template", "India"):
frappe.get_doc({"doctype": "Address Template", "country": "India", "is_default": 1}).insert()


if not frappe.db.exists('Address', '_Test Address-Office'):
frappe.get_doc({
"address_line1": "_Test Address Line 1",
"address_title": "_Test Address",
"address_type": "Office",
"city": "_Test City",
"state": "Test State",
"country": "India",
"doctype": "Address",
"is_primary_address": 1,
"phone": "+91 0000000000"
}).insert()
if not frappe.db.exists("Address", "_Test Address-Office"):
frappe.get_doc(
{
"address_line1": "_Test Address Line 1",
"address_title": "_Test Address",
"address_type": "Office",
"city": "_Test City",
"state": "Test State",
"country": "India",
"doctype": "Address",
"is_primary_address": 1,
"phone": "+91 0000000000",
}
).insert()


address = frappe.get_list("Address")[0].name address = frappe.get_list("Address")[0].name
display = get_address_display(frappe.get_doc("Address", address).as_dict()) display = get_address_display(frappe.get_doc("Address", address).as_dict())
self.assertTrue(display)
self.assertTrue(display)

+ 20
- 8
frappe/contacts/doctype/address_template/address_template.py 查看文件

@@ -3,21 +3,24 @@
# License: MIT. See LICENSE # License: MIT. See LICENSE


import frappe import frappe
from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint from frappe.utils import cint
from frappe.utils.jinja import validate_template from frappe.utils.jinja import validate_template
from frappe import _


class AddressTemplate(Document): class AddressTemplate(Document):
def validate(self): def validate(self):
if not self.template: if not self.template:
self.template = get_default_address_template() self.template = get_default_address_template()


self.defaults = frappe.db.get_values("Address Template", {"is_default":1, "name":("!=", self.name)})
self.defaults = frappe.db.get_values(
"Address Template", {"is_default": 1, "name": ("!=", self.name)}
)
if not self.is_default: if not self.is_default:
if not self.defaults: if not self.defaults:
self.is_default = 1 self.is_default = 1
if cint(frappe.db.get_single_value('System Settings', 'setup_complete')):
if cint(frappe.db.get_single_value("System Settings", "setup_complete")):
frappe.msgprint(_("Setting this Address Template as default as there is no other default")) frappe.msgprint(_("Setting this Address Template as default as there is no other default"))


validate_template(self.template) validate_template(self.template)
@@ -31,14 +34,23 @@ class AddressTemplate(Document):
if self.is_default: if self.is_default:
frappe.throw(_("Default Address Template cannot be deleted")) frappe.throw(_("Default Address Template cannot be deleted"))



@frappe.whitelist() @frappe.whitelist()
def get_default_address_template(): def get_default_address_template():
'''Get default address template (translated)'''
return '''{{ address_line1 }}<br>{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}\
"""Get default address template (translated)"""
return (
"""{{ address_line1 }}<br>{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}\
{{ city }}<br> {{ city }}<br>
{% if state %}{{ state }}<br>{% endif -%} {% if state %}{{ state }}<br>{% endif -%}
{% if pincode %}{{ pincode }}<br>{% endif -%} {% if pincode %}{{ pincode }}<br>{% endif -%}
{{ country }}<br> {{ country }}<br>
{% if phone %}'''+_('Phone')+''': {{ phone }}<br>{% endif -%}
{% if fax %}'''+_('Fax')+''': {{ fax }}<br>{% endif -%}
{% if email_id %}'''+_('Email')+''': {{ email_id }}<br>{% endif -%}'''
{% if phone %}"""
+ _("Phone")
+ """: {{ phone }}<br>{% endif -%}
{% if fax %}"""
+ _("Fax")
+ """: {{ fax }}<br>{% endif -%}
{% if email_id %}"""
+ _("Email")
+ """: {{ email_id }}<br>{% endif -%}"""
)

+ 13
- 15
frappe/contacts/doctype/address_template/test_address_template.py 查看文件

@@ -1,7 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors # Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe, unittest
import unittest

import frappe



class TestAddressTemplate(unittest.TestCase): class TestAddressTemplate(unittest.TestCase):
def setUp(self): def setUp(self):
@@ -27,17 +30,12 @@ class TestAddressTemplate(unittest.TestCase):
def make_default_address_template(self): def make_default_address_template(self):
template = """{{ address_line1 }}<br>{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}{{ city }}<br>{% if state %}{{ state }}<br>{% endif -%}{% if pincode %}{{ pincode }}<br>{% endif -%}{{ country }}<br>{% if phone %}Phone: {{ phone }}<br>{% endif -%}{% if fax %}Fax: {{ fax }}<br>{% endif -%}{% if email_id %}Email: {{ email_id }}<br>{% endif -%}""" template = """{{ address_line1 }}<br>{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}{{ city }}<br>{% if state %}{{ state }}<br>{% endif -%}{% if pincode %}{{ pincode }}<br>{% endif -%}{{ country }}<br>{% if phone %}Phone: {{ phone }}<br>{% endif -%}{% if fax %}Fax: {{ fax }}<br>{% endif -%}{% if email_id %}Email: {{ email_id }}<br>{% endif -%}"""


if not frappe.db.exists('Address Template', 'India'):
frappe.get_doc({
"doctype": "Address Template",
"country": 'India',
"is_default": 1,
"template": template
}).insert()

if not frappe.db.exists('Address Template', 'Brazil'):
frappe.get_doc({
"doctype": "Address Template",
"country": 'Brazil',
"template": template
}).insert()
if not frappe.db.exists("Address Template", "India"):
frappe.get_doc(
{"doctype": "Address Template", "country": "India", "is_default": 1, "template": template}
).insert()

if not frappe.db.exists("Address Template", "Brazil"):
frappe.get_doc(
{"doctype": "Address Template", "country": "Brazil", "template": template}
).insert()

+ 107
- 69
frappe/contacts/doctype/contact/contact.py 查看文件

@@ -1,26 +1,27 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe import frappe
from frappe.utils import cstr, has_gravatar
from frappe import _ from frappe import _
from frappe.model.document import Document
from frappe.contacts.address_and_contact import set_link_title
from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links
from frappe.model.document import Document
from frappe.model.naming import append_number_if_name_exists from frappe.model.naming import append_number_if_name_exists
from frappe.contacts.address_and_contact import set_link_title
from frappe.utils import cstr, has_gravatar




class Contact(Document): class Contact(Document):
def autoname(self): def autoname(self):
# concat first and last name # concat first and last name
self.name = " ".join(filter(None,
[cstr(self.get(f)).strip() for f in ["first_name", "last_name"]]))
self.name = " ".join(
filter(None, [cstr(self.get(f)).strip() for f in ["first_name", "last_name"]])
)


if frappe.db.exists("Contact", self.name): if frappe.db.exists("Contact", self.name):
self.name = append_number_if_name_exists('Contact', self.name)
self.name = append_number_if_name_exists("Contact", self.name)


# concat party name if reqd # concat party name if reqd
for link in self.links: for link in self.links:
self.name = self.name + '-' + link.link_name.strip()
self.name = self.name + "-" + link.link_name.strip()
break break


def validate(self): def validate(self):
@@ -45,7 +46,7 @@ class Contact(Document):
self.user = frappe.db.get_value("User", {"email": self.email_id}) self.user = frappe.db.get_value("User", {"email": self.email_id})


def get_link_for(self, link_doctype): def get_link_for(self, link_doctype):
'''Return the link name, if exists for the given link DocType'''
"""Return the link name, if exists for the given link DocType"""
for link in self.links: for link in self.links:
if link.link_doctype == link_doctype: if link.link_doctype == link_doctype:
return link.link_name return link.link_name
@@ -65,21 +66,21 @@ class Contact(Document):


def add_email(self, email_id, is_primary=0, autosave=False): def add_email(self, email_id, is_primary=0, autosave=False):
if not frappe.db.exists("Contact Email", {"email_id": email_id, "parent": self.name}): if not frappe.db.exists("Contact Email", {"email_id": email_id, "parent": self.name}):
self.append("email_ids", {
"email_id": email_id,
"is_primary": is_primary
})
self.append("email_ids", {"email_id": email_id, "is_primary": is_primary})


if autosave: if autosave:
self.save(ignore_permissions=True) self.save(ignore_permissions=True)


def add_phone(self, phone, is_primary_phone=0, is_primary_mobile_no=0, autosave=False): def add_phone(self, phone, is_primary_phone=0, is_primary_mobile_no=0, autosave=False):
if not frappe.db.exists("Contact Phone", {"phone": phone, "parent": self.name}): if not frappe.db.exists("Contact Phone", {"phone": phone, "parent": self.name}):
self.append("phone_nos", {
"phone": phone,
"is_primary_phone": is_primary_phone,
"is_primary_mobile_no": is_primary_mobile_no
})
self.append(
"phone_nos",
{
"phone": phone,
"is_primary_phone": is_primary_phone,
"is_primary_mobile_no": is_primary_mobile_no,
},
)


if autosave: if autosave:
self.save(ignore_permissions=True) self.save(ignore_permissions=True)
@@ -113,7 +114,9 @@ class Contact(Document):
is_primary = [phone.phone for phone in self.phone_nos if phone.get(field_name)] is_primary = [phone.phone for phone in self.phone_nos if phone.get(field_name)]


if len(is_primary) > 1: if len(is_primary) > 1:
frappe.throw(_("Only one {0} can be set as primary.").format(frappe.bold(frappe.unscrub(fieldname))))
frappe.throw(
_("Only one {0} can be set as primary.").format(frappe.bold(frappe.unscrub(fieldname)))
)


primary_number_exists = False primary_number_exists = False
for d in self.phone_nos: for d in self.phone_nos:
@@ -125,9 +128,11 @@ class Contact(Document):
if not primary_number_exists: if not primary_number_exists:
setattr(self, fieldname, "") setattr(self, fieldname, "")



def get_default_contact(doctype, name): def get_default_contact(doctype, name):
'''Returns default contact for the given doctype, name'''
out = frappe.db.sql('''select parent,
"""Returns default contact for the given doctype, name"""
out = frappe.db.sql(
'''select parent,
IFNULL((select is_primary_contact from tabContact c where c.name = dl.parent), 0) IFNULL((select is_primary_contact from tabContact c where c.name = dl.parent), 0)
as is_primary_contact as is_primary_contact
from from
@@ -135,7 +140,10 @@ def get_default_contact(doctype, name):
where where
dl.link_doctype=%s and dl.link_doctype=%s and
dl.link_name=%s and dl.link_name=%s and
dl.parenttype = "Contact"''', (doctype, name), as_dict=True)
dl.parenttype = "Contact"''',
(doctype, name),
as_dict=True,
)


if out: if out:
for contact in out: for contact in out:
@@ -145,6 +153,7 @@ def get_default_contact(doctype, name):
else: else:
return None return None



@frappe.whitelist() @frappe.whitelist()
def invite_user(contact): def invite_user(contact):
contact = frappe.get_doc("Contact", contact) contact = frappe.get_doc("Contact", contact)
@@ -153,34 +162,39 @@ def invite_user(contact):
frappe.throw(_("Please set Email Address")) frappe.throw(_("Please set Email Address"))


if contact.has_permission("write"): if contact.has_permission("write"):
user = frappe.get_doc({
"doctype": "User",
"first_name": contact.first_name,
"last_name": contact.last_name,
"email": contact.email_id,
"user_type": "Website User",
"send_welcome_email": 1
}).insert(ignore_permissions = True)
user = frappe.get_doc(
{
"doctype": "User",
"first_name": contact.first_name,
"last_name": contact.last_name,
"email": contact.email_id,
"user_type": "Website User",
"send_welcome_email": 1,
}
).insert(ignore_permissions=True)


return user.name return user.name



@frappe.whitelist() @frappe.whitelist()
def get_contact_details(contact): def get_contact_details(contact):
contact = frappe.get_doc("Contact", contact) contact = frappe.get_doc("Contact", contact)
out = { out = {
"contact_person": contact.get("name"), "contact_person": contact.get("name"),
"contact_display": " ".join(filter(None,
[contact.get("salutation"), contact.get("first_name"), contact.get("last_name")])),
"contact_display": " ".join(
filter(None, [contact.get("salutation"), contact.get("first_name"), contact.get("last_name")])
),
"contact_email": contact.get("email_id"), "contact_email": contact.get("email_id"),
"contact_mobile": contact.get("mobile_no"), "contact_mobile": contact.get("mobile_no"),
"contact_phone": contact.get("phone"), "contact_phone": contact.get("phone"),
"contact_designation": contact.get("designation"), "contact_designation": contact.get("designation"),
"contact_department": contact.get("department")
"contact_department": contact.get("department"),
} }
return out return out



def update_contact(doc, method): def update_contact(doc, method):
'''Update contact when user is updated, if contact is found. Called via hooks'''
"""Update contact when user is updated, if contact is found. Called via hooks"""
contact_name = frappe.db.get_value("Contact", {"email_id": doc.name}) contact_name = frappe.db.get_value("Contact", {"email_id": doc.name})
if contact_name: if contact_name:
contact = frappe.get_doc("Contact", contact_name) contact = frappe.get_doc("Contact", contact_name)
@@ -190,19 +204,23 @@ def update_contact(doc, method):
contact.flags.ignore_mandatory = True contact.flags.ignore_mandatory = True
contact.save(ignore_permissions=True) contact.save(ignore_permissions=True)



@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def contact_query(doctype, txt, searchfield, start, page_len, filters): def contact_query(doctype, txt, searchfield, start, page_len, filters):
from frappe.desk.reportview import get_match_cond from frappe.desk.reportview import get_match_cond


if not frappe.get_meta("Contact").get_field(searchfield)\
and searchfield not in frappe.db.DEFAULT_COLUMNS:
if (
not frappe.get_meta("Contact").get_field(searchfield)
and searchfield not in frappe.db.DEFAULT_COLUMNS
):
return [] return []


link_doctype = filters.pop('link_doctype')
link_name = filters.pop('link_name')
link_doctype = filters.pop("link_doctype")
link_name = filters.pop("link_name")


return frappe.db.sql("""select
return frappe.db.sql(
"""select
`tabContact`.name, `tabContact`.first_name, `tabContact`.last_name `tabContact`.name, `tabContact`.first_name, `tabContact`.last_name
from from
`tabContact`, `tabDynamic Link` `tabContact`, `tabDynamic Link`
@@ -216,68 +234,90 @@ def contact_query(doctype, txt, searchfield, start, page_len, filters):
order by order by
if(locate(%(_txt)s, `tabContact`.name), locate(%(_txt)s, `tabContact`.name), 99999), if(locate(%(_txt)s, `tabContact`.name), locate(%(_txt)s, `tabContact`.name), 99999),
`tabContact`.idx desc, `tabContact`.name `tabContact`.idx desc, `tabContact`.name
limit %(start)s, %(page_len)s """.format(mcond=get_match_cond(doctype), key=searchfield), {
'txt': '%' + txt + '%',
'_txt': txt.replace("%", ""),
'start': start,
'page_len': page_len,
'link_name': link_name,
'link_doctype': link_doctype
})
limit %(start)s, %(page_len)s """.format(
mcond=get_match_cond(doctype), key=searchfield
),
{
"txt": "%" + txt + "%",
"_txt": txt.replace("%", ""),
"start": start,
"page_len": page_len,
"link_name": link_name,
"link_doctype": link_doctype,
},
)



@frappe.whitelist() @frappe.whitelist()
def address_query(links): def address_query(links):
import json import json


links = [{"link_doctype": d.get("link_doctype"), "link_name": d.get("link_name")} for d in json.loads(links)]
links = [
{"link_doctype": d.get("link_doctype"), "link_name": d.get("link_name")}
for d in json.loads(links)
]
result = [] result = []


for link in links: for link in links:
if not frappe.has_permission(doctype=link.get("link_doctype"), ptype="read", doc=link.get("link_name")):
if not frappe.has_permission(
doctype=link.get("link_doctype"), ptype="read", doc=link.get("link_name")
):
continue continue


res = frappe.db.sql("""
res = frappe.db.sql(
"""
SELECT `tabAddress`.name SELECT `tabAddress`.name
FROM `tabAddress`, `tabDynamic Link` FROM `tabAddress`, `tabDynamic Link`
WHERE `tabDynamic Link`.parenttype='Address' WHERE `tabDynamic Link`.parenttype='Address'
AND `tabDynamic Link`.parent=`tabAddress`.name AND `tabDynamic Link`.parent=`tabAddress`.name
AND `tabDynamic Link`.link_doctype = %(link_doctype)s AND `tabDynamic Link`.link_doctype = %(link_doctype)s
AND `tabDynamic Link`.link_name = %(link_name)s AND `tabDynamic Link`.link_name = %(link_name)s
""", {
"link_doctype": link.get("link_doctype"),
"link_name": link.get("link_name"),
}, as_dict=True)
""",
{
"link_doctype": link.get("link_doctype"),
"link_name": link.get("link_name"),
},
as_dict=True,
)


result.extend([l.name for l in res]) result.extend([l.name for l in res])


return result return result



def get_contact_with_phone_number(number): def get_contact_with_phone_number(number):
if not number: return
if not number:
return


contacts = frappe.get_all('Contact Phone', filters=[
['phone', 'like', '%{0}'.format(number)]
], fields=["parent"], limit=1)
contacts = frappe.get_all(
"Contact Phone", filters=[["phone", "like", "%{0}".format(number)]], fields=["parent"], limit=1
)


return contacts[0].parent if contacts else None return contacts[0].parent if contacts else None



def get_contact_name(email_id): def get_contact_name(email_id):
contact = frappe.get_all("Contact Email", filters={"email_id": email_id}, fields=["parent"], limit=1)
contact = frappe.get_all(
"Contact Email", filters={"email_id": email_id}, fields=["parent"], limit=1
)
return contact[0].parent if contact else None return contact[0].parent if contact else None



def get_contacts_linking_to(doctype, docname, fields=None): def get_contacts_linking_to(doctype, docname, fields=None):
"""Return a list of contacts containing a link to the given document.""" """Return a list of contacts containing a link to the given document."""
return frappe.get_list('Contact', fields=fields, filters=[
['Dynamic Link', 'link_doctype', '=', doctype],
['Dynamic Link', 'link_name', '=', docname]
])
return frappe.get_list(
"Contact",
fields=fields,
filters=[
["Dynamic Link", "link_doctype", "=", doctype],
["Dynamic Link", "link_name", "=", docname],
],
)



def get_contacts_linked_from(doctype, docname, fields=None): def get_contacts_linked_from(doctype, docname, fields=None):
"""Return a list of contacts that are contained in (linked from) the given document.""" """Return a list of contacts that are contained in (linked from) the given document."""
link_fields = frappe.get_meta(doctype).get('fields', {
'fieldtype': 'Link',
'options': 'Contact'
})
link_fields = frappe.get_meta(doctype).get("fields", {"fieldtype": "Link", "options": "Contact"})
if not link_fields: if not link_fields:
return [] return []


@@ -285,6 +325,4 @@ def get_contacts_linked_from(doctype, docname, fields=None):
if not contact_names: if not contact_names:
return [] return []


return frappe.get_list('Contact', fields=fields, filters={
'name': ('in', contact_names)
})
return frappe.get_list("Contact", fields=fields, filters={"name": ("in", contact_names)})

+ 8
- 9
frappe/contacts/doctype/contact/test_contact.py 查看文件

@@ -1,13 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors # Copyright (c) 2017, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe
import unittest import unittest


test_dependencies = ['Contact', 'Salutation']
import frappe


class TestContact(unittest.TestCase):
test_dependencies = ["Contact", "Salutation"]



class TestContact(unittest.TestCase):
def test_check_default_email(self): def test_check_default_email(self):
emails = [ emails = [
{"email": "test1@example.com", "is_primary": 0}, {"email": "test1@example.com", "is_primary": 0},
@@ -32,13 +33,11 @@ class TestContact(unittest.TestCase):
self.assertEqual(contact.phone, "+91 0000000002") self.assertEqual(contact.phone, "+91 0000000002")
self.assertEqual(contact.mobile_no, "+91 0000000003") self.assertEqual(contact.mobile_no, "+91 0000000003")



def create_contact(name, salutation, emails=None, phones=None, save=True): def create_contact(name, salutation, emails=None, phones=None, save=True):
doc = frappe.get_doc({
"doctype": "Contact",
"first_name": name,
"status": "Open",
"salutation": salutation
})
doc = frappe.get_doc(
{"doctype": "Contact", "first_name": name, "status": "Open", "salutation": salutation}
)


if emails: if emails:
for d in emails: for d in emails:


+ 1
- 0
frappe/contacts/doctype/contact_email/contact_email.py 查看文件

@@ -5,5 +5,6 @@
# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document



class ContactEmail(Document): class ContactEmail(Document):
pass pass

+ 1
- 0
frappe/contacts/doctype/contact_phone/contact_phone.py 查看文件

@@ -5,5 +5,6 @@
# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document



class ContactPhone(Document): class ContactPhone(Document):
pass pass

+ 1
- 0
frappe/contacts/doctype/gender/gender.py 查看文件

@@ -4,5 +4,6 @@


from frappe.model.document import Document from frappe.model.document import Document



class Gender(Document): class Gender(Document):
pass pass

+ 1
- 0
frappe/contacts/doctype/gender/test_gender.py 查看文件

@@ -3,5 +3,6 @@
# License: MIT. See LICENSE # License: MIT. See LICENSE
import unittest import unittest



class TestGender(unittest.TestCase): class TestGender(unittest.TestCase):
pass pass

+ 1
- 0
frappe/contacts/doctype/salutation/salutation.py 查看文件

@@ -4,5 +4,6 @@


from frappe.model.document import Document from frappe.model.document import Document



class Salutation(Document): class Salutation(Document):
pass pass

+ 1
- 0
frappe/contacts/doctype/salutation/test_salutation.py 查看文件

@@ -3,5 +3,6 @@
# License: MIT. See LICENSE # License: MIT. See LICENSE
import unittest import unittest



class TestSalutation(unittest.TestCase): class TestSalutation(unittest.TestCase):
pass pass

+ 40
- 10
frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py 查看文件

@@ -4,17 +4,37 @@ import frappe
from frappe import _ from frappe import _


field_map = { field_map = {
"Contact": ["first_name", "last_name", "address", "phone", "mobile_no", "email_id", "is_primary_contact"],
"Address": ["address_line1", "address_line2", "city", "state", "pincode", "country", "is_primary_address"]
"Contact": [
"first_name",
"last_name",
"address",
"phone",
"mobile_no",
"email_id",
"is_primary_contact",
],
"Address": [
"address_line1",
"address_line2",
"city",
"state",
"pincode",
"country",
"is_primary_address",
],
} }



def execute(filters=None): def execute(filters=None):
columns, data = get_columns(filters), get_data(filters) columns, data = get_columns(filters), get_data(filters)
return columns, data return columns, data



def get_columns(filters): def get_columns(filters):
return [ return [
"{reference_doctype}:Link/{reference_doctype}".format(reference_doctype=filters.get("reference_doctype")),
"{reference_doctype}:Link/{reference_doctype}".format(
reference_doctype=filters.get("reference_doctype")
),
"Address Line 1", "Address Line 1",
"Address Line 2", "Address Line 2",
"City", "City",
@@ -27,9 +47,10 @@ def get_columns(filters):
"Address", "Address",
"Phone", "Phone",
"Email Id", "Email Id",
"Is Primary Contact:Check"
"Is Primary Contact:Check",
] ]



def get_data(filters): def get_data(filters):
data = [] data = []
reference_doctype = filters.get("reference_doctype") reference_doctype = filters.get("reference_doctype")
@@ -37,6 +58,7 @@ def get_data(filters):


return get_reference_addresses_and_contact(reference_doctype, reference_name) return get_reference_addresses_and_contact(reference_doctype, reference_name)



def get_reference_addresses_and_contact(reference_doctype, reference_name): def get_reference_addresses_and_contact(reference_doctype, reference_name):
data = [] data = []
filters = None filters = None
@@ -48,16 +70,22 @@ def get_reference_addresses_and_contact(reference_doctype, reference_name):
if reference_name: if reference_name:
filters = {"name": reference_name} filters = {"name": reference_name}


reference_list = [d[0] for d in frappe.get_list(reference_doctype, filters=filters, fields=["name"], as_list=True)]
reference_list = [
d[0] for d in frappe.get_list(reference_doctype, filters=filters, fields=["name"], as_list=True)
]


for d in reference_list: for d in reference_list:
reference_details.setdefault(d, frappe._dict()) reference_details.setdefault(d, frappe._dict())
reference_details = get_reference_details(reference_doctype, "Address", reference_list, reference_details)
reference_details = get_reference_details(reference_doctype, "Contact", reference_list, reference_details)
reference_details = get_reference_details(
reference_doctype, "Address", reference_list, reference_details
)
reference_details = get_reference_details(
reference_doctype, "Contact", reference_list, reference_details
)


for reference_name, details in reference_details.items(): for reference_name, details in reference_details.items():
addresses = details.get("address", []) addresses = details.get("address", [])
contacts = details.get("contact", [])
contacts = details.get("contact", [])
if not any([addresses, contacts]): if not any([addresses, contacts]):
result = [reference_name] result = [reference_name]
result.extend(add_blank_columns_for("Address")) result.extend(add_blank_columns_for("Address"))
@@ -78,10 +106,11 @@ def get_reference_addresses_and_contact(reference_doctype, reference_name):


return data return data



def get_reference_details(reference_doctype, doctype, reference_list, reference_details): def get_reference_details(reference_doctype, doctype, reference_list, reference_details):
filters = [
filters = [
["Dynamic Link", "link_doctype", "=", reference_doctype], ["Dynamic Link", "link_doctype", "=", reference_doctype],
["Dynamic Link", "link_name", "in", reference_list]
["Dynamic Link", "link_name", "in", reference_list],
] ]
fields = ["`tabDynamic Link`.link_name"] + field_map.get(doctype, []) fields = ["`tabDynamic Link`.link_name"] + field_map.get(doctype, [])


@@ -97,5 +126,6 @@ def get_reference_details(reference_doctype, doctype, reference_list, reference_
reference_details[reference_list[0]][frappe.scrub(doctype)] = temp_records reference_details[reference_list[0]][frappe.scrub(doctype)] = temp_records
return reference_details return reference_details



def add_blank_columns_for(doctype): def add_blank_columns_for(doctype):
return ["" for field in field_map.get(doctype, [])] return ["" for field in field_map.get(doctype, [])]

+ 66
- 58
frappe/contacts/report/addresses_and_contacts/test_addresses_and_contacts.py 查看文件

@@ -1,95 +1,87 @@
import unittest


import frappe import frappe
import frappe.defaults import frappe.defaults
import unittest

from frappe.contacts.report.addresses_and_contacts.addresses_and_contacts import get_data from frappe.contacts.report.addresses_and_contacts.addresses_and_contacts import get_data



def get_custom_linked_doctype(): def get_custom_linked_doctype():
if bool(frappe.get_all("DocType", filters={'name':'Test Custom Doctype'})):
if bool(frappe.get_all("DocType", filters={"name": "Test Custom Doctype"})):
return return


doc = frappe.get_doc({
"doctype": "DocType",
"module": "Core",
"custom": 1,
"fields": [{
"label": "Test Field",
"fieldname": "test_field",
"fieldtype": "Data"
},
{
"label": "Contact HTML",
"fieldname": "contact_html",
"fieldtype": "HTML"
},
doc = frappe.get_doc(
{ {
"label": "Address HTML",
"fieldname": "address_html",
"fieldtype": "HTML"
}],
"permissions": [{
"role": "System Manager",
"read": 1
}],
"name": "Test Custom Doctype",
})
"doctype": "DocType",
"module": "Core",
"custom": 1,
"fields": [
{"label": "Test Field", "fieldname": "test_field", "fieldtype": "Data"},
{"label": "Contact HTML", "fieldname": "contact_html", "fieldtype": "HTML"},
{"label": "Address HTML", "fieldname": "address_html", "fieldtype": "HTML"},
],
"permissions": [{"role": "System Manager", "read": 1}],
"name": "Test Custom Doctype",
}
)
doc.insert() doc.insert()



def get_custom_doc_for_address_and_contacts(): def get_custom_doc_for_address_and_contacts():
get_custom_linked_doctype() get_custom_linked_doctype()
linked_doc = frappe.get_doc({
"doctype": "Test Custom Doctype",
"test_field": "Hello",
}).insert()
linked_doc = frappe.get_doc(
{
"doctype": "Test Custom Doctype",
"test_field": "Hello",
}
).insert()
return linked_doc return linked_doc



def create_linked_address(link_list): def create_linked_address(link_list):
if frappe.flags.test_address_created: if frappe.flags.test_address_created:
return return


address = frappe.get_doc({
"doctype": "Address",
"address_title": "_Test Address",
"address_type": "Billing",
"address_line1": "test address line 1",
"address_line2": "test address line 2",
"city": "Milan",
"country": "Italy"
})
address = frappe.get_doc(
{
"doctype": "Address",
"address_title": "_Test Address",
"address_type": "Billing",
"address_line1": "test address line 1",
"address_line2": "test address line 2",
"city": "Milan",
"country": "Italy",
}
)


for name in link_list: for name in link_list:
address.append("links",{
'link_doctype': 'Test Custom Doctype',
'link_name': name
})
address.append("links", {"link_doctype": "Test Custom Doctype", "link_name": name})


address.insert() address.insert()
frappe.flags.test_address_created = True frappe.flags.test_address_created = True


return address.name return address.name



def create_linked_contact(link_list, address): def create_linked_contact(link_list, address):
if frappe.flags.test_contact_created: if frappe.flags.test_contact_created:
return return


contact = frappe.get_doc({
"doctype": "Contact",
"salutation": "Mr",
"first_name": "_Test First Name",
"last_name": "_Test Last Name",
"is_primary_contact": 1,
"address": address,
"status": "Open"
})
contact = frappe.get_doc(
{
"doctype": "Contact",
"salutation": "Mr",
"first_name": "_Test First Name",
"last_name": "_Test Last Name",
"is_primary_contact": 1,
"address": address,
"status": "Open",
}
)
contact.add_email("test_contact@example.com", is_primary=True) contact.add_email("test_contact@example.com", is_primary=True)
contact.add_phone("+91 0000000000", is_primary_phone=True) contact.add_phone("+91 0000000000", is_primary_phone=True)


for name in link_list: for name in link_list:
contact.append("links",{
'link_doctype': 'Test Custom Doctype',
'link_name': name
})
contact.append("links", {"link_doctype": "Test Custom Doctype", "link_name": name})


contact.insert(ignore_permissions=True) contact.insert(ignore_permissions=True)
frappe.flags.test_contact_created = True frappe.flags.test_contact_created = True
@@ -103,7 +95,23 @@ class TestAddressesAndContacts(unittest.TestCase):
create_linked_contact(links_list, d) create_linked_contact(links_list, d)
report_data = get_data({"reference_doctype": "Test Custom Doctype"}) report_data = get_data({"reference_doctype": "Test Custom Doctype"})
for idx, link in enumerate(links_list): for idx, link in enumerate(links_list):
test_item = [link, 'test address line 1', 'test address line 2', 'Milan', None, None, 'Italy', 0, '_Test First Name', '_Test Last Name', '_Test Address-Billing', '+91 0000000000', '', 'test_contact@example.com', 1]
test_item = [
link,
"test address line 1",
"test address line 2",
"Milan",
None,
None,
"Italy",
0,
"_Test First Name",
"_Test Last Name",
"_Test Address-Billing",
"+91 0000000000",
"",
"test_contact@example.com",
1,
]
self.assertListEqual(test_item, report_data[idx]) self.assertListEqual(test_item, report_data[idx])


def tearDown(self): def tearDown(self):


+ 0
- 1
frappe/core/doctype/__init__.py 查看文件

@@ -1,3 +1,2 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE


+ 26
- 18
frappe/core/doctype/access_log/access_log.py 查看文件

@@ -1,9 +1,10 @@
# Copyright (c) 2021, Frappe Technologies and contributors # Copyright (c) 2021, Frappe Technologies and contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe
from frappe.utils import cstr
from tenacity import retry, retry_if_exception_type, stop_after_attempt from tenacity import retry, retry_if_exception_type, stop_after_attempt

import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cstr




class AccessLog(Document): class AccessLog(Document):
@@ -22,14 +23,19 @@ def make_access_log(
columns=None, columns=None,
): ):
_make_access_log( _make_access_log(
doctype, document, method, file_type, report_name, filters, page, columns,
doctype,
document,
method,
file_type,
report_name,
filters,
page,
columns,
) )




@frappe.write_only() @frappe.write_only()
@retry(
stop=stop_after_attempt(3), retry=retry_if_exception_type(frappe.DuplicateEntryError)
)
@retry(stop=stop_after_attempt(3), retry=retry_if_exception_type(frappe.DuplicateEntryError))
def _make_access_log( def _make_access_log(
doctype=None, doctype=None,
document=None, document=None,
@@ -43,18 +49,20 @@ def _make_access_log(
user = frappe.session.user user = frappe.session.user
in_request = frappe.request and frappe.request.method == "GET" in_request = frappe.request and frappe.request.method == "GET"


frappe.get_doc({
"doctype": "Access Log",
"user": user,
"export_from": doctype,
"reference_document": document,
"file_type": file_type,
"report_name": report_name,
"page": page,
"method": method,
"filters": cstr(filters) or None,
"columns": columns,
}).db_insert()
frappe.get_doc(
{
"doctype": "Access Log",
"user": user,
"export_from": doctype,
"reference_document": document,
"file_type": file_type,
"report_name": report_name,
"page": page,
"method": method,
"filters": cstr(filters) or None,
"columns": columns,
}
).db_insert()


# `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview` # `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview`
# dont commit in test mode. It must be tempting to put this block along with the in_request in the # dont commit in test mode. It must be tempting to put this block along with the in_request in the


+ 32
- 30
frappe/core/doctype/access_log/test_access_log.py 查看文件

@@ -2,20 +2,21 @@
# Copyright (c) 2019, Frappe Technologies and Contributors # Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE


# imports - standard imports
import unittest
import base64 import base64
import os import os


# imports - standard imports
import unittest

# imports - third party imports
import requests

# imports - module imports # imports - module imports
import frappe import frappe
from frappe.core.doctype.access_log.access_log import make_access_log from frappe.core.doctype.access_log.access_log import make_access_log
from frappe.utils import cstr, get_site_url
from frappe.core.doctype.data_import.data_import import export_csv from frappe.core.doctype.data_import.data_import import export_csv
from frappe.core.doctype.user.user import generate_keys from frappe.core.doctype.user.user import generate_keys

# imports - third party imports
import requests
from frappe.utils import cstr, get_site_url




class TestAccessLog(unittest.TestCase): class TestAccessLog(unittest.TestCase):
@@ -23,8 +24,9 @@ class TestAccessLog(unittest.TestCase):
# generate keys for current user to send requests for the following tests # generate keys for current user to send requests for the following tests
generate_keys(frappe.session.user) generate_keys(frappe.session.user)
frappe.db.commit() frappe.db.commit()
generated_secret = frappe.utils.password.get_decrypted_password("User",
frappe.session.user, fieldname='api_secret')
generated_secret = frappe.utils.password.get_decrypted_password(
"User", frappe.session.user, fieldname="api_secret"
)
api_key = frappe.db.get_value("User", "Administrator", "api_key") api_key = frappe.db.get_value("User", "Administrator", "api_key")
self.header = {"Authorization": "token {}:{}".format(api_key, generated_secret)} self.header = {"Authorization": "token {}:{}".format(api_key, generated_secret)}


@@ -101,54 +103,55 @@ class TestAccessLog(unittest.TestCase):
"party": [], "party": [],
"group_by": "Group by Voucher (Consolidated)", "group_by": "Group by Voucher (Consolidated)",
"cost_center": [], "cost_center": [],
"project": []
"project": [],
} }


self.test_doctype = 'File'
self.test_document = 'Test Document'
self.test_report_name = 'General Ledger'
self.test_file_type = 'CSV'
self.test_method = 'Test Method'
self.file_name = frappe.utils.random_string(10) + '.txt'
self.test_doctype = "File"
self.test_document = "Test Document"
self.test_report_name = "General Ledger"
self.test_file_type = "CSV"
self.test_method = "Test Method"
self.file_name = frappe.utils.random_string(10) + ".txt"
self.test_content = frappe.utils.random_string(1024) self.test_content = frappe.utils.random_string(1024)



def test_make_full_access_log(self): def test_make_full_access_log(self):
self.maxDiff = None self.maxDiff = None


# test if all fields maintain data: html page and filters are converted? # test if all fields maintain data: html page and filters are converted?
make_access_log(doctype=self.test_doctype,
make_access_log(
doctype=self.test_doctype,
document=self.test_document, document=self.test_document,
report_name=self.test_report_name, report_name=self.test_report_name,
page=self.test_html_template, page=self.test_html_template,
file_type=self.test_file_type, file_type=self.test_file_type,
method=self.test_method, method=self.test_method,
filters=self.test_filters)
filters=self.test_filters,
)


last_doc = frappe.get_last_doc('Access Log')
last_doc = frappe.get_last_doc("Access Log")
self.assertEqual(last_doc.filters, cstr(self.test_filters)) self.assertEqual(last_doc.filters, cstr(self.test_filters))
self.assertEqual(self.test_doctype, last_doc.export_from) self.assertEqual(self.test_doctype, last_doc.export_from)
self.assertEqual(self.test_document, last_doc.reference_document) self.assertEqual(self.test_document, last_doc.reference_document)



def test_make_export_log(self): def test_make_export_log(self):
# export data and delete temp file generated on disk # export data and delete temp file generated on disk
export_csv(self.test_doctype, self.file_name) export_csv(self.test_doctype, self.file_name)
os.remove(self.file_name) os.remove(self.file_name)


# test if the exported data is logged # test if the exported data is logged
last_doc = frappe.get_last_doc('Access Log')
last_doc = frappe.get_last_doc("Access Log")
self.assertEqual(self.test_doctype, last_doc.export_from) self.assertEqual(self.test_doctype, last_doc.export_from)



def test_private_file_download(self): def test_private_file_download(self):
# create new private file # create new private file
new_private_file = frappe.get_doc({
'doctype': self.test_doctype,
'file_name': self.file_name,
'content': base64.b64encode(self.test_content.encode('utf-8')),
'is_private': 1,
})
new_private_file = frappe.get_doc(
{
"doctype": self.test_doctype,
"file_name": self.file_name,
"content": base64.b64encode(self.test_content.encode("utf-8")),
"is_private": 1,
}
)
new_private_file.insert() new_private_file.insert()


# access the created file # access the created file
@@ -156,7 +159,7 @@ class TestAccessLog(unittest.TestCase):


try: try:
request = requests.post(private_file_link, headers=self.header) request = requests.post(private_file_link, headers=self.header)
last_doc = frappe.get_last_doc('Access Log')
last_doc = frappe.get_last_doc("Access Log")


if request.ok: if request.ok:
# check for the access log of downloaded file # check for the access log of downloaded file
@@ -169,6 +172,5 @@ class TestAccessLog(unittest.TestCase):
# cleanup # cleanup
new_private_file.delete() new_private_file.delete()



def tearDown(self): def tearDown(self):
pass pass

+ 13
- 10
frappe/core/doctype/activity_log/activity_log.py 查看文件

@@ -26,20 +26,25 @@ class ActivityLog(Document):
if self.reference_doctype and self.reference_name: if self.reference_doctype and self.reference_name:
self.status = "Linked" self.status = "Linked"



def on_doctype_update(): def on_doctype_update():
"""Add indexes in `tabActivity Log`""" """Add indexes in `tabActivity Log`"""
frappe.db.add_index("Activity Log", ["reference_doctype", "reference_name"]) frappe.db.add_index("Activity Log", ["reference_doctype", "reference_name"])
frappe.db.add_index("Activity Log", ["timeline_doctype", "timeline_name"]) frappe.db.add_index("Activity Log", ["timeline_doctype", "timeline_name"])
frappe.db.add_index("Activity Log", ["link_doctype", "link_name"]) frappe.db.add_index("Activity Log", ["link_doctype", "link_name"])



def add_authentication_log(subject, user, operation="Login", status="Success"): def add_authentication_log(subject, user, operation="Login", status="Success"):
frappe.get_doc({
"doctype": "Activity Log",
"user": user,
"status": status,
"subject": subject,
"operation": operation,
}).insert(ignore_permissions=True, ignore_links=True)
frappe.get_doc(
{
"doctype": "Activity Log",
"user": user,
"status": status,
"subject": subject,
"operation": operation,
}
).insert(ignore_permissions=True, ignore_links=True)



def clear_activity_logs(days=None): def clear_activity_logs(days=None):
"""clear 90 day old authentication logs or configured in log settings""" """clear 90 day old authentication logs or configured in log settings"""
@@ -47,6 +52,4 @@ def clear_activity_logs(days=None):
if not days: if not days:
days = 90 days = 90
doctype = DocType("Activity Log") doctype = DocType("Activity Log")
frappe.db.delete(doctype, filters=(
doctype.creation < (Now() - Interval(days=days))
))
frappe.db.delete(doctype, filters=(doctype.creation < (Now() - Interval(days=days))))

+ 46
- 35
frappe/core/doctype/activity_log/feed.py 查看文件

@@ -3,15 +3,16 @@


import frappe import frappe
import frappe.permissions import frappe.permissions
from frappe.utils import get_fullname
from frappe import _ from frappe import _
from frappe.core.doctype.activity_log.activity_log import add_authentication_log from frappe.core.doctype.activity_log.activity_log import add_authentication_log
from frappe.utils import get_fullname



def update_feed(doc, method=None): def update_feed(doc, method=None):
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import: if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import:
return return


if doc._action!="save" or doc.flags.ignore_feed:
if doc._action != "save" or doc.flags.ignore_feed:
return return


if doc.doctype == "Activity Log" or doc.meta.issingle: if doc.doctype == "Activity Log" or doc.meta.issingle:
@@ -29,65 +30,75 @@ def update_feed(doc, method=None):
name = feed.name or doc.name name = feed.name or doc.name


# delete earlier feed # delete earlier feed
frappe.db.delete("Activity Log", {
"reference_doctype": doctype,
"reference_name": name,
"link_doctype": feed.link_doctype
})

frappe.get_doc({
"doctype": "Activity Log",
"reference_doctype": doctype,
"reference_name": name,
"subject": feed.subject,
"full_name": get_fullname(doc.owner),
"reference_owner": frappe.db.get_value(doctype, name, "owner"),
"link_doctype": feed.link_doctype,
"link_name": feed.link_name
}).insert(ignore_permissions=True)
frappe.db.delete(
"Activity Log",
{"reference_doctype": doctype, "reference_name": name, "link_doctype": feed.link_doctype},
)

frappe.get_doc(
{
"doctype": "Activity Log",
"reference_doctype": doctype,
"reference_name": name,
"subject": feed.subject,
"full_name": get_fullname(doc.owner),
"reference_owner": frappe.db.get_value(doctype, name, "owner"),
"link_doctype": feed.link_doctype,
"link_name": feed.link_name,
}
).insert(ignore_permissions=True)



def login_feed(login_manager): def login_feed(login_manager):
if login_manager.user != "Guest": if login_manager.user != "Guest":
subject = _("{0} logged in").format(get_fullname(login_manager.user)) subject = _("{0} logged in").format(get_fullname(login_manager.user))
add_authentication_log(subject, login_manager.user) add_authentication_log(subject, login_manager.user)



def logout_feed(user, reason): def logout_feed(user, reason):
if user and user != "Guest": if user and user != "Guest":
subject = _("{0} logged out: {1}").format(get_fullname(user), frappe.bold(reason)) subject = _("{0} logged out: {1}").format(get_fullname(user), frappe.bold(reason))
add_authentication_log(subject, user, operation="Logout") add_authentication_log(subject, user, operation="Logout")


def get_feed_match_conditions(user=None, doctype='Comment'):
if not user: user = frappe.session.user


conditions = ['`tab{doctype}`.owner={user} or `tab{doctype}`.reference_owner={user}'.format(
user = frappe.db.escape(user),
doctype = doctype
)]
def get_feed_match_conditions(user=None, doctype="Comment"):
if not user:
user = frappe.session.user

conditions = [
"`tab{doctype}`.owner={user} or `tab{doctype}`.reference_owner={user}".format(
user=frappe.db.escape(user), doctype=doctype
)
]


user_permissions = frappe.permissions.get_user_permissions(user) user_permissions = frappe.permissions.get_user_permissions(user)
can_read = frappe.get_user().get_can_read() can_read = frappe.get_user().get_can_read()


can_read_doctypes = ["'{}'".format(dt) for dt in
list(set(can_read) - set(list(user_permissions)))]
can_read_doctypes = [
"'{}'".format(dt) for dt in list(set(can_read) - set(list(user_permissions)))
]


if can_read_doctypes: if can_read_doctypes:
conditions += ["""(`tab{doctype}`.reference_doctype is null
conditions += [
"""(`tab{doctype}`.reference_doctype is null
or `tab{doctype}`.reference_doctype = '' or `tab{doctype}`.reference_doctype = ''
or `tab{doctype}`.reference_doctype or `tab{doctype}`.reference_doctype
in ({values}))""".format( in ({values}))""".format(
doctype = doctype,
values =", ".join(can_read_doctypes)
)]
doctype=doctype, values=", ".join(can_read_doctypes)
)
]


if user_permissions: if user_permissions:
can_read_docs = [] can_read_docs = []
for dt, obj in user_permissions.items(): for dt, obj in user_permissions.items():
for n in obj: for n in obj:
can_read_docs.append('{}|{}'.format(frappe.db.escape(dt), frappe.db.escape(n.get('doc', ''))))
can_read_docs.append("{}|{}".format(frappe.db.escape(dt), frappe.db.escape(n.get("doc", ""))))


if can_read_docs: if can_read_docs:
conditions.append("concat_ws('|', `tab{doctype}`.reference_doctype, `tab{doctype}`.reference_name) in ({values})".format(
doctype = doctype,
values = ", ".join(can_read_docs)))
conditions.append(
"concat_ws('|', `tab{doctype}`.reference_doctype, `tab{doctype}`.reference_name) in ({values})".format(
doctype=doctype, values=", ".join(can_read_docs)
)
)


return "(" + " or ".join(conditions) + ")"
return "(" + " or ".join(conditions) + ")"

+ 34
- 36
frappe/core/doctype/activity_log/test_activity_log.py 查看文件

@@ -1,77 +1,74 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors # Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe
import unittest
import time import time
from frappe.auth import LoginManager, CookieManager
import unittest

import frappe
from frappe.auth import CookieManager, LoginManager



class TestActivityLog(unittest.TestCase): class TestActivityLog(unittest.TestCase):
def test_activity_log(self): def test_activity_log(self):


# test user login log # test user login log
frappe.local.form_dict = frappe._dict({
'cmd': 'login',
'sid': 'Guest',
'pwd': 'admin',
'usr': 'Administrator'
})
frappe.local.form_dict = frappe._dict(
{"cmd": "login", "sid": "Guest", "pwd": "admin", "usr": "Administrator"}
)


frappe.local.cookie_manager = CookieManager() frappe.local.cookie_manager = CookieManager()
frappe.local.login_manager = LoginManager() frappe.local.login_manager = LoginManager()


auth_log = self.get_auth_log() auth_log = self.get_auth_log()
self.assertEqual(auth_log.status, 'Success')
self.assertEqual(auth_log.status, "Success")


# test user logout log # test user logout log
frappe.local.login_manager.logout() frappe.local.login_manager.logout()
auth_log = self.get_auth_log(operation='Logout')
self.assertEqual(auth_log.status, 'Success')
auth_log = self.get_auth_log(operation="Logout")
self.assertEqual(auth_log.status, "Success")


# test invalid login # test invalid login
frappe.form_dict.update({ 'pwd': 'password' })
frappe.form_dict.update({"pwd": "password"})
self.assertRaises(frappe.AuthenticationError, LoginManager) self.assertRaises(frappe.AuthenticationError, LoginManager)
auth_log = self.get_auth_log() auth_log = self.get_auth_log()
self.assertEqual(auth_log.status, 'Failed')
self.assertEqual(auth_log.status, "Failed")


frappe.local.form_dict = frappe._dict() frappe.local.form_dict = frappe._dict()


def get_auth_log(self, operation='Login'):
names = frappe.db.get_all('Activity Log', filters={
'user': 'Administrator',
'operation': operation,
}, order_by='`creation` DESC')
def get_auth_log(self, operation="Login"):
names = frappe.db.get_all(
"Activity Log",
filters={
"user": "Administrator",
"operation": operation,
},
order_by="`creation` DESC",
)


name = names[0] name = names[0]
auth_log = frappe.get_doc('Activity Log', name)
auth_log = frappe.get_doc("Activity Log", name)
return auth_log return auth_log


def test_brute_security(self): def test_brute_security(self):
update_system_settings({
'allow_consecutive_login_attempts': 3,
'allow_login_after_fail': 5
})

frappe.local.form_dict = frappe._dict({
'cmd': 'login',
'sid': 'Guest',
'pwd': 'admin',
'usr': 'Administrator'
})
update_system_settings({"allow_consecutive_login_attempts": 3, "allow_login_after_fail": 5})

frappe.local.form_dict = frappe._dict(
{"cmd": "login", "sid": "Guest", "pwd": "admin", "usr": "Administrator"}
)


frappe.local.cookie_manager = CookieManager() frappe.local.cookie_manager = CookieManager()
frappe.local.login_manager = LoginManager() frappe.local.login_manager = LoginManager()


auth_log = self.get_auth_log() auth_log = self.get_auth_log()
self.assertEqual(auth_log.status, 'Success')
self.assertEqual(auth_log.status, "Success")


# test user logout log # test user logout log
frappe.local.login_manager.logout() frappe.local.login_manager.logout()
auth_log = self.get_auth_log(operation='Logout')
self.assertEqual(auth_log.status, 'Success')
auth_log = self.get_auth_log(operation="Logout")
self.assertEqual(auth_log.status, "Success")


# test invalid login # test invalid login
frappe.form_dict.update({ 'pwd': 'password' })
frappe.form_dict.update({"pwd": "password"})
self.assertRaises(frappe.AuthenticationError, LoginManager) self.assertRaises(frappe.AuthenticationError, LoginManager)
self.assertRaises(frappe.AuthenticationError, LoginManager) self.assertRaises(frappe.AuthenticationError, LoginManager)
self.assertRaises(frappe.AuthenticationError, LoginManager) self.assertRaises(frappe.AuthenticationError, LoginManager)
@@ -85,8 +82,9 @@ class TestActivityLog(unittest.TestCase):


frappe.local.form_dict = frappe._dict() frappe.local.form_dict = frappe._dict()



def update_system_settings(args): def update_system_settings(args):
doc = frappe.get_doc('System Settings')
doc = frappe.get_doc("System Settings")
doc.update(args) doc.update(args)
doc.flags.ignore_mandatory = 1 doc.flags.ignore_mandatory = 1
doc.save() doc.save()

+ 1
- 0
frappe/core/doctype/block_module/block_module.py 查看文件

@@ -5,5 +5,6 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document



class BlockModule(Document): class BlockModule(Document):
pass pass

+ 79
- 55
frappe/core/doctype/comment/comment.py 查看文件

@@ -1,22 +1,27 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors # Copyright (c) 2019, Frappe Technologies and contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import json

import frappe import frappe
from frappe import _ from frappe import _
import json
from frappe.model.document import Document
from frappe.core.doctype.user.user import extract_mentions from frappe.core.doctype.user.user import extract_mentions
from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification,\
get_title, get_title_html
from frappe.utils import get_fullname
from frappe.website.utils import clear_cache
from frappe.database.schema import add_column from frappe.database.schema import add_column
from frappe.desk.doctype.notification_log.notification_log import (
enqueue_create_notification,
get_title,
get_title_html,
)
from frappe.exceptions import ImplicitCommitError from frappe.exceptions import ImplicitCommitError
from frappe.model.document import Document
from frappe.utils import get_fullname
from frappe.website.utils import clear_cache



class Comment(Document): class Comment(Document):
def after_insert(self): def after_insert(self):
self.notify_mentions() self.notify_mentions()
self.notify_change('add')
self.notify_change("add")


def validate(self): def validate(self):
if not self.comment_email: if not self.comment_email:
@@ -26,34 +31,35 @@ class Comment(Document):
def on_update(self): def on_update(self):
update_comment_in_doc(self) update_comment_in_doc(self)
if self.is_new(): if self.is_new():
self.notify_change('update')
self.notify_change("update")


def on_trash(self): def on_trash(self):
self.remove_comment_from_cache() self.remove_comment_from_cache()
self.notify_change('delete')
self.notify_change("delete")


def notify_change(self, action): def notify_change(self, action):
key_map = { key_map = {
'Like': 'like_logs',
'Assigned': 'assignment_logs',
'Assignment Completed': 'assignment_logs',
'Comment': 'comments',
'Attachment': 'attachment_logs',
'Attachment Removed': 'attachment_logs',
"Like": "like_logs",
"Assigned": "assignment_logs",
"Assignment Completed": "assignment_logs",
"Comment": "comments",
"Attachment": "attachment_logs",
"Attachment Removed": "attachment_logs",
} }
key = key_map.get(self.comment_type) key = key_map.get(self.comment_type)
if not key: return
if not key:
return


frappe.publish_realtime('update_docinfo_for_{}_{}'.format(self.reference_doctype, self.reference_name), {
'doc': self.as_dict(),
'key': key,
'action': action
}, after_commit=True)
frappe.publish_realtime(
"update_docinfo_for_{}_{}".format(self.reference_doctype, self.reference_name),
{"doc": self.as_dict(), "key": key, "action": action},
after_commit=True,
)


def remove_comment_from_cache(self): def remove_comment_from_cache(self):
_comments = get_comments_from_parent(self) _comments = get_comments_from_parent(self)
for c in _comments: for c in _comments:
if c.get("name")==self.name:
if c.get("name") == self.name:
_comments.remove(c) _comments.remove(c)


update_comments_in_parent(self.reference_doctype, self.reference_name, _comments) update_comments_in_parent(self.reference_doctype, self.reference_name, _comments)
@@ -68,19 +74,26 @@ class Comment(Document):
sender_fullname = get_fullname(frappe.session.user) sender_fullname = get_fullname(frappe.session.user)
title = get_title(self.reference_doctype, self.reference_name) title = get_title(self.reference_doctype, self.reference_name)


recipients = [frappe.db.get_value("User", {"enabled": 1, "name": name, "user_type": "System User", "allowed_in_mentions": 1}, "email")
for name in mentions]
recipients = [
frappe.db.get_value(
"User",
{"enabled": 1, "name": name, "user_type": "System User", "allowed_in_mentions": 1},
"email",
)
for name in mentions
]


notification_message = _('''{0} mentioned you in a comment in {1} {2}''')\
.format(frappe.bold(sender_fullname), frappe.bold(self.reference_doctype), get_title_html(title))
notification_message = _("""{0} mentioned you in a comment in {1} {2}""").format(
frappe.bold(sender_fullname), frappe.bold(self.reference_doctype), get_title_html(title)
)


notification_doc = { notification_doc = {
'type': 'Mention',
'document_type': self.reference_doctype,
'document_name': self.reference_name,
'subject': notification_message,
'from_user': frappe.session.user,
'email_content': self.content
"type": "Mention",
"document_type": self.reference_doctype,
"document_name": self.reference_name,
"subject": notification_message,
"from_user": frappe.session.user,
"email_content": self.content,
} }


enqueue_create_notification(recipients, notification_doc) enqueue_create_notification(recipients, notification_doc)
@@ -99,45 +112,46 @@ def update_comment_in_doc(doc):


`_comments` format `_comments` format


{
"comment": [String],
"by": [user],
"name": [Comment Document name]
}"""
{
"comment": [String],
"by": [user],
"name": [Comment Document name]
}"""


# only comments get updates, not likes, assignments etc. # only comments get updates, not likes, assignments etc.
if doc.doctype == 'Comment' and doc.comment_type != 'Comment':
if doc.doctype == "Comment" and doc.comment_type != "Comment":
return return


def get_truncated(content): def get_truncated(content):
return (content[:97] + '...') if len(content) > 100 else content
return (content[:97] + "...") if len(content) > 100 else content


if doc.reference_doctype and doc.reference_name and doc.content: if doc.reference_doctype and doc.reference_name and doc.content:
_comments = get_comments_from_parent(doc) _comments = get_comments_from_parent(doc)


updated = False updated = False
for c in _comments: for c in _comments:
if c.get("name")==doc.name:
if c.get("name") == doc.name:
c["comment"] = get_truncated(doc.content) c["comment"] = get_truncated(doc.content)
updated = True updated = True


if not updated: if not updated:
_comments.append({
"comment": get_truncated(doc.content),

# "comment_email" for Comment and "sender" for Communication
"by": getattr(doc, 'comment_email', None) or getattr(doc, 'sender', None) or doc.owner,
"name": doc.name
})
_comments.append(
{
"comment": get_truncated(doc.content),
# "comment_email" for Comment and "sender" for Communication
"by": getattr(doc, "comment_email", None) or getattr(doc, "sender", None) or doc.owner,
"name": doc.name,
}
)


update_comments_in_parent(doc.reference_doctype, doc.reference_name, _comments) update_comments_in_parent(doc.reference_doctype, doc.reference_name, _comments)




def get_comments_from_parent(doc): def get_comments_from_parent(doc):
'''
"""
get the list of comments cached in the document record in the column get the list of comments cached in the document record in the column
`_comments` `_comments`
'''
"""
try: try:
_comments = frappe.db.get_value(doc.reference_doctype, doc.reference_name, "_comments") or "[]" _comments = frappe.db.get_value(doc.reference_doctype, doc.reference_name, "_comments") or "[]"


@@ -153,23 +167,32 @@ def get_comments_from_parent(doc):
except ValueError: except ValueError:
return [] return []



def update_comments_in_parent(reference_doctype, reference_name, _comments): def update_comments_in_parent(reference_doctype, reference_name, _comments):
"""Updates `_comments` property in parent Document with given dict. """Updates `_comments` property in parent Document with given dict.


:param _comments: Dict of comments.""" :param _comments: Dict of comments."""
if not reference_doctype or not reference_name or frappe.db.get_value("DocType", reference_doctype, "issingle") or frappe.db.get_value("DocType", reference_doctype, "is_virtual"):
if (
not reference_doctype
or not reference_name
or frappe.db.get_value("DocType", reference_doctype, "issingle")
or frappe.db.get_value("DocType", reference_doctype, "is_virtual")
):
return return


try: try:
# use sql, so that we do not mess with the timestamp # use sql, so that we do not mess with the timestamp
frappe.db.sql("""update `tab{0}` set `_comments`=%s where name=%s""".format(reference_doctype), # nosec
(json.dumps(_comments[-100:]), reference_name))
frappe.db.sql(
"""update `tab{0}` set `_comments`=%s where name=%s""".format(reference_doctype), # nosec
(json.dumps(_comments[-100:]), reference_name),
)


except Exception as e: except Exception as e:
if frappe.db.is_column_missing(e) and getattr(frappe.local, 'request', None):
if frappe.db.is_column_missing(e) and getattr(frappe.local, "request", None):
# missing column and in request, add column and update after commit # missing column and in request, add column and update after commit
frappe.local._comments = (getattr(frappe.local, "_comments", [])
+ [(reference_doctype, reference_name, _comments)])
frappe.local._comments = getattr(frappe.local, "_comments", []) + [
(reference_doctype, reference_name, _comments)
]


elif frappe.db.is_data_too_long(e): elif frappe.db.is_data_too_long(e):
raise frappe.DataTooLongException raise frappe.DataTooLongException
@@ -183,6 +206,7 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments):
if getattr(reference_doc, "route", None): if getattr(reference_doc, "route", None):
clear_cache(reference_doc.route) clear_cache(reference_doc.route)



def update_comments_in_parent_after_request(): def update_comments_in_parent_after_request():
"""update _comments in parent if _comments column is missing""" """update _comments in parent if _comments column is missing"""
if hasattr(frappe.local, "_comments"): if hasattr(frappe.local, "_comments"):


+ 51
- 35
frappe/core/doctype/comment/test_comment.py 查看文件

@@ -1,9 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors # Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe, json
import json
import unittest import unittest


import frappe


class TestComment(unittest.TestCase): class TestComment(unittest.TestCase):
def tearDown(self): def tearDown(self):
frappe.form_dict.comment = None frappe.form_dict.comment = None
@@ -15,75 +18,88 @@ class TestComment(unittest.TestCase):
frappe.local.request_ip = None frappe.local.request_ip = None


def test_comment_creation(self): def test_comment_creation(self):
test_doc = frappe.get_doc(dict(doctype = 'ToDo', description = 'test'))
test_doc = frappe.get_doc(dict(doctype="ToDo", description="test"))
test_doc.insert() test_doc.insert()
comment = test_doc.add_comment('Comment', 'test comment')
comment = test_doc.add_comment("Comment", "test comment")


test_doc.reload() test_doc.reload()


# check if updated in _comments cache # check if updated in _comments cache
comments = json.loads(test_doc.get('_comments'))
self.assertEqual(comments[0].get('name'), comment.name)
self.assertEqual(comments[0].get('comment'), comment.content)
comments = json.loads(test_doc.get("_comments"))
self.assertEqual(comments[0].get("name"), comment.name)
self.assertEqual(comments[0].get("comment"), comment.content)


# check document creation # check document creation
comment_1 = frappe.get_all('Comment', fields = ['*'], filters = dict(
reference_doctype = test_doc.doctype,
reference_name = test_doc.name
))[0]
comment_1 = frappe.get_all(
"Comment",
fields=["*"],
filters=dict(reference_doctype=test_doc.doctype, reference_name=test_doc.name),
)[0]


self.assertEqual(comment_1.content, 'test comment')
self.assertEqual(comment_1.content, "test comment")


# test via blog # test via blog
def test_public_comment(self): def test_public_comment(self):
from frappe.website.doctype.blog_post.test_blog_post import make_test_blog from frappe.website.doctype.blog_post.test_blog_post import make_test_blog

test_blog = make_test_blog() test_blog = make_test_blog()


frappe.db.delete("Comment", {"reference_doctype": "Blog Post"}) frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})


from frappe.templates.includes.comments.comments import add_comment from frappe.templates.includes.comments.comments import add_comment


frappe.form_dict.comment = 'Good comment with 10 chars'
frappe.form_dict.comment_email = 'test@test.com'
frappe.form_dict.comment_by = 'Good Tester'
frappe.form_dict.reference_doctype = 'Blog Post'
frappe.form_dict.comment = "Good comment with 10 chars"
frappe.form_dict.comment_email = "test@test.com"
frappe.form_dict.comment_by = "Good Tester"
frappe.form_dict.reference_doctype = "Blog Post"
frappe.form_dict.reference_name = test_blog.name frappe.form_dict.reference_name = test_blog.name
frappe.form_dict.route = test_blog.route frappe.form_dict.route = test_blog.route
frappe.local.request_ip = '127.0.0.1'
frappe.local.request_ip = "127.0.0.1"


add_comment() add_comment()


self.assertEqual(frappe.get_all('Comment', fields = ['*'], filters = dict(
reference_doctype = test_blog.doctype,
reference_name = test_blog.name
))[0].published, 1)
self.assertEqual(
frappe.get_all(
"Comment",
fields=["*"],
filters=dict(reference_doctype=test_blog.doctype, reference_name=test_blog.name),
)[0].published,
1,
)


frappe.db.delete("Comment", {"reference_doctype": "Blog Post"}) frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})


frappe.form_dict.comment = 'pleez vizits my site http://mysite.com'
frappe.form_dict.comment_by = 'bad commentor'
frappe.form_dict.comment = "pleez vizits my site http://mysite.com"
frappe.form_dict.comment_by = "bad commentor"


add_comment() add_comment()


self.assertEqual(len(frappe.get_all('Comment', fields = ['*'], filters = dict(
reference_doctype = test_blog.doctype,
reference_name = test_blog.name
))), 0)
self.assertEqual(
len(
frappe.get_all(
"Comment",
fields=["*"],
filters=dict(reference_doctype=test_blog.doctype, reference_name=test_blog.name),
)
),
0,
)


# test for filtering html and css injection elements # test for filtering html and css injection elements
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"}) frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})


frappe.form_dict.comment = '<script>alert(1)</script>Comment'
frappe.form_dict.comment_by = 'hacker'
frappe.form_dict.comment = "<script>alert(1)</script>Comment"
frappe.form_dict.comment_by = "hacker"


add_comment() add_comment()


self.assertEqual(frappe.get_all('Comment', fields = ['content'], filters = dict(
reference_doctype = test_blog.doctype,
reference_name = test_blog.name
))[0]['content'], 'Comment')
self.assertEqual(
frappe.get_all(
"Comment",
fields=["content"],
filters=dict(reference_doctype=test_blog.doctype, reference_name=test_blog.name),
)[0]["content"],
"Comment",
)


test_blog.delete() test_blog.delete()




+ 0
- 1
frappe/core/doctype/communication/__init__.py 查看文件

@@ -1,3 +1,2 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE


+ 173
- 125
frappe/core/doctype/communication/communication.py 查看文件

@@ -2,49 +2,67 @@
# License: MIT. See LICENSE # License: MIT. See LICENSE


from collections import Counter from collections import Counter
from email.utils import getaddresses
from typing import List from typing import List
from urllib.parse import unquote

from parse import compile

import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document
from frappe.utils import validate_email_address, strip_html, cstr, time_diff_in_seconds
from frappe.automation.doctype.assignment_rule.assignment_rule import (
apply as apply_assignment_rule,
)
from frappe.contacts.doctype.contact.contact import get_contact_name
from frappe.core.doctype.comment.comment import update_comment_in_doc
from frappe.core.doctype.communication.email import validate_email from frappe.core.doctype.communication.email import validate_email
from frappe.core.doctype.communication.mixins import CommunicationEmailMixin from frappe.core.doctype.communication.mixins import CommunicationEmailMixin
from frappe.core.utils import get_parent_doc from frappe.core.utils import get_parent_doc
from frappe.utils import parse_addr, split_emails
from frappe.core.doctype.comment.comment import update_comment_in_doc
from email.utils import getaddresses
from urllib.parse import unquote
from frappe.model.document import Document
from frappe.utils import (
cstr,
parse_addr,
split_emails,
strip_html,
time_diff_in_seconds,
validate_email_address,
)
from frappe.utils.user import is_system_user from frappe.utils.user import is_system_user
from frappe.contacts.doctype.contact.contact import get_contact_name
from frappe.automation.doctype.assignment_rule.assignment_rule import apply as apply_assignment_rule
from parse import compile


exclude_from_linked_with = True exclude_from_linked_with = True



class Communication(Document, CommunicationEmailMixin): class Communication(Document, CommunicationEmailMixin):
"""Communication represents an external communication like Email.
"""
"""Communication represents an external communication like Email."""
no_feed_on_delete = True no_feed_on_delete = True
DOCTYPE = 'Communication'
DOCTYPE = "Communication"


def onload(self): def onload(self):
"""create email flag queue""" """create email flag queue"""
if self.communication_type == "Communication" and self.communication_medium == "Email" \
and self.sent_or_received == "Received" and self.uid and self.uid != -1:

email_flag_queue = frappe.db.get_value("Email Flag Queue", {
"communication": self.name,
"is_completed": 0})
if (
self.communication_type == "Communication"
and self.communication_medium == "Email"
and self.sent_or_received == "Received"
and self.uid
and self.uid != -1
):

email_flag_queue = frappe.db.get_value(
"Email Flag Queue", {"communication": self.name, "is_completed": 0}
)
if email_flag_queue: if email_flag_queue:
return return


frappe.get_doc({
"doctype": "Email Flag Queue",
"action": "Read",
"communication": self.name,
"uid": self.uid,
"email_account": self.email_account
}).insert(ignore_permissions=True)
frappe.get_doc(
{
"doctype": "Email Flag Queue",
"action": "Read",
"communication": self.name,
"uid": self.uid,
"email_account": self.email_account,
}
).insert(ignore_permissions=True)
frappe.db.commit() frappe.db.commit()


def validate(self): def validate(self):
@@ -74,25 +92,33 @@ class Communication(Document, CommunicationEmailMixin):
def validate_reference(self): def validate_reference(self):
if self.reference_doctype and self.reference_name: if self.reference_doctype and self.reference_name:
if not self.reference_owner: if not self.reference_owner:
self.reference_owner = frappe.db.get_value(self.reference_doctype, self.reference_name, "owner")
self.reference_owner = frappe.db.get_value(
self.reference_doctype, self.reference_name, "owner"
)


# prevent communication against a child table # prevent communication against a child table
if frappe.get_meta(self.reference_doctype).istable: if frappe.get_meta(self.reference_doctype).istable:
frappe.throw(_("Cannot create a {0} against a child document: {1}")
.format(_(self.communication_type), _(self.reference_doctype)))
frappe.throw(
_("Cannot create a {0} against a child document: {1}").format(
_(self.communication_type), _(self.reference_doctype)
)
)


# Prevent circular linking of Communication DocTypes # Prevent circular linking of Communication DocTypes
if self.reference_doctype == "Communication": if self.reference_doctype == "Communication":
circular_linking = False circular_linking = False
doc = get_parent_doc(self) doc = get_parent_doc(self)
while doc.reference_doctype == "Communication": while doc.reference_doctype == "Communication":
if get_parent_doc(doc).name==self.name:
if get_parent_doc(doc).name == self.name:
circular_linking = True circular_linking = True
break break
doc = get_parent_doc(doc) doc = get_parent_doc(doc)


if circular_linking: if circular_linking:
frappe.throw(_("Please make sure the Reference Communication Docs are not circularly linked."), frappe.CircularLinkingError)
frappe.throw(
_("Please make sure the Reference Communication Docs are not circularly linked."),
frappe.CircularLinkingError,
)


def after_insert(self): def after_insert(self):
if not (self.reference_doctype and self.reference_name): if not (self.reference_doctype and self.reference_name):
@@ -102,21 +128,21 @@ class Communication(Document, CommunicationEmailMixin):
frappe.db.set_value("Communication", self.reference_name, "status", "Replied") frappe.db.set_value("Communication", self.reference_name, "status", "Replied")


if self.communication_type == "Communication": if self.communication_type == "Communication":
self.notify_change('add')
self.notify_change("add")


elif self.communication_type in ("Chat", "Notification"): elif self.communication_type in ("Chat", "Notification"):
if self.reference_name == frappe.session.user: if self.reference_name == frappe.session.user:
message = self.as_dict() message = self.as_dict()
message['broadcast'] = True
frappe.publish_realtime('new_message', message, after_commit=True)
message["broadcast"] = True
frappe.publish_realtime("new_message", message, after_commit=True)
else: else:
# reference_name contains the user who is addressed in the messages' page comment # reference_name contains the user who is addressed in the messages' page comment
frappe.publish_realtime('new_message', self.as_dict(),
user=self.reference_name, after_commit=True)
frappe.publish_realtime(
"new_message", self.as_dict(), user=self.reference_name, after_commit=True
)


def set_signature_in_email_content(self): def set_signature_in_email_content(self):
"""Set sender's User.email_signature or default outgoing's EmailAccount.signature to the email
"""
"""Set sender's User.email_signature or default outgoing's EmailAccount.signature to the email"""
if not self.content: if not self.content:
return return


@@ -128,11 +154,15 @@ class Communication(Document, CommunicationEmailMixin):


email_body = email_body[0] email_body = email_body[0]


user_email_signature = frappe.db.get_value(
"User",
self.sender,
"email_signature",
) if self.sender else None
user_email_signature = (
frappe.db.get_value(
"User",
self.sender,
"email_signature",
)
if self.sender
else None
)


signature = user_email_signature or frappe.db.get_value( signature = user_email_signature or frappe.db.get_value(
"Email Account", "Email Account",
@@ -157,19 +187,19 @@ class Communication(Document, CommunicationEmailMixin):
# comments count for the list view # comments count for the list view
update_comment_in_doc(self) update_comment_in_doc(self)


if self.comment_type != 'Updated':
if self.comment_type != "Updated":
update_parent_document_on_communication(self) update_parent_document_on_communication(self)


def on_trash(self): def on_trash(self):
if self.communication_type == "Communication": if self.communication_type == "Communication":
self.notify_change('delete')
self.notify_change("delete")


@property @property
def sender_mailid(self): def sender_mailid(self):
return parse_addr(self.sender)[1] if self.sender else "" return parse_addr(self.sender)[1] if self.sender else ""


@staticmethod @staticmethod
def _get_emails_list(emails=None, exclude_displayname = False):
def _get_emails_list(emails=None, exclude_displayname=False):
"""Returns list of emails from given email string. """Returns list of emails from given email string.


* Removes duplicate mailids * Removes duplicate mailids
@@ -180,35 +210,32 @@ class Communication(Document, CommunicationEmailMixin):
return [email.lower() for email in set([parse_addr(email)[1] for email in emails]) if email] return [email.lower() for email in set([parse_addr(email)[1] for email in emails]) if email]
return [email.lower() for email in set(emails) if email] return [email.lower() for email in set(emails) if email]


def to_list(self, exclude_displayname = True):
"""Returns to list.
"""
def to_list(self, exclude_displayname=True):
"""Returns to list."""
return self._get_emails_list(self.recipients, exclude_displayname=exclude_displayname) return self._get_emails_list(self.recipients, exclude_displayname=exclude_displayname)


def cc_list(self, exclude_displayname = True):
"""Returns cc list.
"""
def cc_list(self, exclude_displayname=True):
"""Returns cc list."""
return self._get_emails_list(self.cc, exclude_displayname=exclude_displayname) return self._get_emails_list(self.cc, exclude_displayname=exclude_displayname)


def bcc_list(self, exclude_displayname = True):
"""Returns bcc list.
"""
def bcc_list(self, exclude_displayname=True):
"""Returns bcc list."""
return self._get_emails_list(self.bcc, exclude_displayname=exclude_displayname) return self._get_emails_list(self.bcc, exclude_displayname=exclude_displayname)


def get_attachments(self): def get_attachments(self):
attachments = frappe.get_all( attachments = frappe.get_all(
"File", "File",
fields=["name", "file_name", "file_url", "is_private"], fields=["name", "file_name", "file_url", "is_private"],
filters = {"attached_to_name": self.name, "attached_to_doctype": self.DOCTYPE}
filters={"attached_to_name": self.name, "attached_to_doctype": self.DOCTYPE},
) )
return attachments return attachments


def notify_change(self, action): def notify_change(self, action):
frappe.publish_realtime('update_docinfo_for_{}_{}'.format(self.reference_doctype, self.reference_name), {
'doc': self.as_dict(),
'key': 'communications',
'action': action
}, after_commit=True)
frappe.publish_realtime(
"update_docinfo_for_{}_{}".format(self.reference_doctype, self.reference_name),
{"doc": self.as_dict(), "key": "communications", "action": action},
after_commit=True,
)


def set_status(self): def set_status(self):
if not self.is_new(): if not self.is_new():
@@ -216,15 +243,19 @@ class Communication(Document, CommunicationEmailMixin):


if self.reference_doctype and self.reference_name: if self.reference_doctype and self.reference_name:
self.status = "Linked" self.status = "Linked"
elif self.communication_type=="Communication":
elif self.communication_type == "Communication":
self.status = "Open" self.status = "Open"
else: else:
self.status = "Closed" self.status = "Closed"


# set email status to spam # set email status to spam
email_rule = frappe.db.get_value("Email Rule", { "email_id": self.sender, "is_spam":1 })
if self.communication_type == "Communication" and self.communication_medium == "Email" \
and self.sent_or_received == "Sent" and email_rule:
email_rule = frappe.db.get_value("Email Rule", {"email_id": self.sender, "is_spam": 1})
if (
self.communication_type == "Communication"
and self.communication_medium == "Email"
and self.sent_or_received == "Sent"
and email_rule
):


self.email_status = "Spam" self.email_status = "Spam"


@@ -254,7 +285,7 @@ class Communication(Document, CommunicationEmailMixin):
self.sender_full_name = self.sender self.sender_full_name = self.sender
self.sender = None self.sender = None
else: else:
if self.sent_or_received=='Sent':
if self.sent_or_received == "Sent":
validate_email_address(self.sender, throw=True) validate_email_address(self.sender, throw=True)
sender_name, sender_email = parse_addr(self.sender) sender_name, sender_email = parse_addr(self.sender)
if sender_name == sender_email: if sender_name == sender_email:
@@ -264,40 +295,41 @@ class Communication(Document, CommunicationEmailMixin):
self.sender_full_name = sender_name self.sender_full_name = sender_name


if not self.sender_full_name: if not self.sender_full_name:
self.sender_full_name = frappe.db.get_value('User', self.sender, 'full_name')
self.sender_full_name = frappe.db.get_value("User", self.sender, "full_name")


if not self.sender_full_name: if not self.sender_full_name:
first_name, last_name = frappe.db.get_value('Contact',
filters={'email_id': sender_email},
fieldname=['first_name', 'last_name']
first_name, last_name = frappe.db.get_value(
"Contact", filters={"email_id": sender_email}, fieldname=["first_name", "last_name"]
) or [None, None] ) or [None, None]
self.sender_full_name = (first_name or '') + (last_name or '')
self.sender_full_name = (first_name or "") + (last_name or "")


if not self.sender_full_name: if not self.sender_full_name:
self.sender_full_name = sender_email self.sender_full_name = sender_email


def set_delivery_status(self, commit=False): def set_delivery_status(self, commit=False):
'''Look into the status of Email Queue linked to this Communication and set the Delivery Status of this Communication'''
"""Look into the status of Email Queue linked to this Communication and set the Delivery Status of this Communication"""
delivery_status = None delivery_status = None
status_counts = Counter(frappe.get_all("Email Queue", pluck="status", filters={"communication": self.name}))
status_counts = Counter(
frappe.get_all("Email Queue", pluck="status", filters={"communication": self.name})
)
if self.sent_or_received == "Received": if self.sent_or_received == "Received":
return return


if status_counts.get('Not Sent') or status_counts.get('Sending'):
delivery_status = 'Sending'
if status_counts.get("Not Sent") or status_counts.get("Sending"):
delivery_status = "Sending"


elif status_counts.get('Error'):
delivery_status = 'Error'
elif status_counts.get("Error"):
delivery_status = "Error"


elif status_counts.get('Expired'):
delivery_status = 'Expired'
elif status_counts.get("Expired"):
delivery_status = "Expired"


elif status_counts.get('Sent'):
delivery_status = 'Sent'
elif status_counts.get("Sent"):
delivery_status = "Sent"


if delivery_status: if delivery_status:
self.db_set('delivery_status', delivery_status)
self.notify_change('update')
self.db_set("delivery_status", delivery_status)
self.notify_change("update")


# for list views and forms # for list views and forms
self.notify_update() self.notify_update()
@@ -311,13 +343,17 @@ class Communication(Document, CommunicationEmailMixin):
# Timeline Links # Timeline Links
def set_timeline_links(self): def set_timeline_links(self):
contacts = [] contacts = []
create_contact_enabled = self.email_account and frappe.db.get_value("Email Account", self.email_account, "create_contact")
contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc], auto_create_contact=create_contact_enabled)
create_contact_enabled = self.email_account and frappe.db.get_value(
"Email Account", self.email_account, "create_contact"
)
contacts = get_contacts(
[self.sender, self.recipients, self.cc, self.bcc], auto_create_contact=create_contact_enabled
)


for contact_name in contacts: for contact_name in contacts:
self.add_link('Contact', contact_name)
self.add_link("Contact", contact_name)


#link contact's dynamic links to communication
# link contact's dynamic links to communication
add_contact_links_to_communication(self, contact_name) add_contact_links_to_communication(self, contact_name)


def deduplicate_timeline_links(self): def deduplicate_timeline_links(self):
@@ -332,17 +368,12 @@ class Communication(Document, CommunicationEmailMixin):
duplicate = True duplicate = True


if duplicate: if duplicate:
del self.timeline_links[:] # make it python 2 compatible as list.clear() is python 3 only
del self.timeline_links[:] # make it python 2 compatible as list.clear() is python 3 only
for l in links: for l in links:
self.add_link(link_doctype=l[0], link_name=l[1]) self.add_link(link_doctype=l[0], link_name=l[1])


def add_link(self, link_doctype, link_name, autosave=False): def add_link(self, link_doctype, link_name, autosave=False):
self.append("timeline_links",
{
"link_doctype": link_doctype,
"link_name": link_name
}
)
self.append("timeline_links", {"link_doctype": link_doctype, "link_name": link_name})


if autosave: if autosave:
self.save(ignore_permissions=True) self.save(ignore_permissions=True)
@@ -358,13 +389,15 @@ class Communication(Document, CommunicationEmailMixin):
if autosave: if autosave:
self.save(ignore_permissions=ignore_permissions) self.save(ignore_permissions=ignore_permissions)



def on_doctype_update(): def on_doctype_update():
"""Add indexes in `tabCommunication`""" """Add indexes in `tabCommunication`"""
frappe.db.add_index("Communication", ["reference_doctype", "reference_name"]) frappe.db.add_index("Communication", ["reference_doctype", "reference_name"])
frappe.db.add_index("Communication", ["status", "communication_type"]) frappe.db.add_index("Communication", ["status", "communication_type"])



def has_permission(doc, ptype, user): def has_permission(doc, ptype, user):
if ptype=="read":
if ptype == "read":
if doc.reference_doctype == "Communication" and doc.reference_name == doc.name: if doc.reference_doctype == "Communication" and doc.reference_name == doc.name:
return return


@@ -372,24 +405,28 @@ def has_permission(doc, ptype, user):
if frappe.has_permission(doc.reference_doctype, ptype="read", doc=doc.reference_name): if frappe.has_permission(doc.reference_doctype, ptype="read", doc=doc.reference_name):
return True return True



def get_permission_query_conditions_for_communication(user): def get_permission_query_conditions_for_communication(user):
if not user: user = frappe.session.user
if not user:
user = frappe.session.user


roles = frappe.get_roles(user) roles = frappe.get_roles(user)


if "Super Email User" in roles or "System Manager" in roles: if "Super Email User" in roles or "System Manager" in roles:
return None return None
else: else:
accounts = frappe.get_all("User Email", filters={ "parent": user },
fields=["email_account"],
distinct=True, order_by="idx")
accounts = frappe.get_all(
"User Email", filters={"parent": user}, fields=["email_account"], distinct=True, order_by="idx"
)


if not accounts: if not accounts:
return """`tabCommunication`.communication_medium!='Email'""" return """`tabCommunication`.communication_medium!='Email'"""


email_accounts = [ '"%s"'%account.get("email_account") for account in accounts ]
return """`tabCommunication`.email_account in ({email_accounts})"""\
.format(email_accounts=','.join(email_accounts))
email_accounts = ['"%s"' % account.get("email_account") for account in accounts]
return """`tabCommunication`.email_account in ({email_accounts})""".format(
email_accounts=",".join(email_accounts)
)



def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[str]: def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[str]:
email_addrs = get_emails(email_strings) email_addrs = get_emails(email_strings)
@@ -403,12 +440,12 @@ def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[st
first_name = frappe.unscrub(email_parts[0]) first_name = frappe.unscrub(email_parts[0])


try: try:
contact_name = '{0}-{1}'.format(first_name, email_parts[1]) if first_name == 'Contact' else first_name
contact = frappe.get_doc({
"doctype": "Contact",
"first_name": contact_name,
"name": contact_name
})
contact_name = (
"{0}-{1}".format(first_name, email_parts[1]) if first_name == "Contact" else first_name
)
contact = frappe.get_doc(
{"doctype": "Contact", "first_name": contact_name, "name": contact_name}
)
contact.add_email(email_id=email, is_primary=True) contact.add_email(email_id=email, is_primary=True)
contact.insert(ignore_permissions=True) contact.insert(ignore_permissions=True)
contact_name = contact.name contact_name = contact.name
@@ -421,6 +458,7 @@ def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[st


return contacts return contacts



def get_emails(email_strings: List[str]) -> List[str]: def get_emails(email_strings: List[str]) -> List[str]:
email_addrs = [] email_addrs = []


@@ -432,22 +470,25 @@ def get_emails(email_strings: List[str]) -> List[str]:


return email_addrs return email_addrs



def add_contact_links_to_communication(communication, contact_name): def add_contact_links_to_communication(communication, contact_name):
contact_links = frappe.get_all("Dynamic Link", filters={
"parenttype": "Contact",
"parent": contact_name
}, fields=["link_doctype", "link_name"])
contact_links = frappe.get_all(
"Dynamic Link",
filters={"parenttype": "Contact", "parent": contact_name},
fields=["link_doctype", "link_name"],
)


if contact_links: if contact_links:
for contact_link in contact_links: for contact_link in contact_links:
communication.add_link(contact_link.link_doctype, contact_link.link_name) communication.add_link(contact_link.link_doctype, contact_link.link_name)



def parse_email(communication, email_strings): def parse_email(communication, email_strings):
""" """
Parse email to add timeline links.
When automatic email linking is enabled, an email from email_strings can contain
a doctype and docname ie in the format `admin+doctype+docname@example.com`,
the email is parsed and doctype and docname is extracted and timeline link is added.
Parse email to add timeline links.
When automatic email linking is enabled, an email from email_strings can contain
a doctype and docname ie in the format `admin+doctype+docname@example.com`,
the email is parsed and doctype and docname is extracted and timeline link is added.
""" """
if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}): if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}):
return return
@@ -469,10 +510,11 @@ def parse_email(communication, email_strings):
if doctype and docname and frappe.db.exists(doctype, docname): if doctype and docname and frappe.db.exists(doctype, docname):
communication.add_link(doctype, docname) communication.add_link(doctype, docname)



def get_email_without_link(email): def get_email_without_link(email):
""" """
returns email address without doctype links
returns admin@example.com for email admin+doctype+docname@example.com
returns email address without doctype links
returns admin@example.com for email admin+doctype+docname@example.com
""" """
if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}): if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}):
return email return email
@@ -486,6 +528,7 @@ def get_email_without_link(email):


return "{0}@{1}".format(email_id, email_host) return "{0}@{1}".format(email_id, email_host)



def update_parent_document_on_communication(doc): def update_parent_document_on_communication(doc):
"""Update mins_to_first_communication of parent document based on who is replying.""" """Update mins_to_first_communication of parent document based on who is replying."""


@@ -516,6 +559,7 @@ def update_parent_document_on_communication(doc):
parent.run_method("notify_communication", doc) parent.run_method("notify_communication", doc)
parent.notify_update() parent.notify_update()



def update_first_response_time(parent, communication): def update_first_response_time(parent, communication):
if parent.meta.has_field("first_response_time") and not parent.get("first_response_time"): if parent.meta.has_field("first_response_time") and not parent.get("first_response_time"):
if is_system_user(communication.sender): if is_system_user(communication.sender):
@@ -526,25 +570,29 @@ def update_first_response_time(parent, communication):
first_response_time = round(time_diff_in_seconds(first_responded_on, parent.creation), 2) first_response_time = round(time_diff_in_seconds(first_responded_on, parent.creation), 2)
parent.db_set("first_response_time", first_response_time) parent.db_set("first_response_time", first_response_time)



def set_avg_response_time(parent, communication): def set_avg_response_time(parent, communication):
if parent.meta.has_field("avg_response_time") and communication.sent_or_received == "Sent": if parent.meta.has_field("avg_response_time") and communication.sent_or_received == "Sent":
# avg response time for all the responses # avg response time for all the responses
communications = frappe.get_list("Communication", filters={
"reference_doctype": parent.doctype,
"reference_name": parent.name
},
communications = frappe.get_list(
"Communication",
filters={"reference_doctype": parent.doctype, "reference_name": parent.name},
fields=["sent_or_received", "name", "creation"], fields=["sent_or_received", "name", "creation"],
order_by="creation"
order_by="creation",
) )


if len(communications): if len(communications):
response_times = [] response_times = []
for i in range(len(communications)): for i in range(len(communications)):
if communications[i].sent_or_received == "Sent" and communications[i-1].sent_or_received == "Received":
response_time = round(time_diff_in_seconds(communications[i].creation, communications[i-1].creation), 2)
if (
communications[i].sent_or_received == "Sent"
and communications[i - 1].sent_or_received == "Received"
):
response_time = round(
time_diff_in_seconds(communications[i].creation, communications[i - 1].creation), 2
)
if response_time > 0: if response_time > 0:
response_times.append(response_time) response_times.append(response_time)
if response_times: if response_times:
avg_response_time = sum(response_times) / len(response_times) avg_response_time = sum(response_times) / len(response_times)
parent.db_set("avg_response_time", avg_response_time) parent.db_set("avg_response_time", avg_response_time)


+ 83
- 67
frappe/core/doctype/communication/email.py 查看文件

@@ -8,17 +8,25 @@ import frappe
import frappe.email.smtp import frappe.email.smtp
from frappe import _ from frappe import _
from frappe.email.email_body import get_message_id from frappe.email.email_body import get_message_id
from frappe.utils import (cint, get_datetime, get_formatted_email,
list_to_str, split_emails, validate_email_address)
from frappe.utils import (
cint,
get_datetime,
get_formatted_email,
list_to_str,
split_emails,
validate_email_address,
)


if TYPE_CHECKING: if TYPE_CHECKING:
from frappe.core.doctype.communication.communication import Communication from frappe.core.doctype.communication.communication import Communication




OUTGOING_EMAIL_ACCOUNT_MISSING = _("""
OUTGOING_EMAIL_ACCOUNT_MISSING = _(
"""
Unable to send mail because of a missing email account. Unable to send mail because of a missing email account.
Please setup default Email Account from Setup > Email > Email Account Please setup default Email Account from Setup > Email > Email Account
""")
"""
)




@frappe.whitelist() @frappe.whitelist()
@@ -64,16 +72,15 @@ def make(
""" """
if kwargs: if kwargs:
from frappe.utils.commands import warn from frappe.utils.commands import warn

warn( warn(
f"Options {kwargs} used in frappe.core.doctype.communication.email.make " f"Options {kwargs} used in frappe.core.doctype.communication.email.make "
"are deprecated or unsupported", "are deprecated or unsupported",
category=DeprecationWarning
category=DeprecationWarning,
) )


if doctype and name and not frappe.has_permission(doctype=doctype, ptype="email", doc=name): if doctype and name and not frappe.has_permission(doctype=doctype, ptype="email", doc=name):
raise frappe.PermissionError(
f"You are not allowed to send emails related to: {doctype} {name}"
)
raise frappe.PermissionError(f"You are not allowed to send emails related to: {doctype} {name}")


return _make( return _make(
doctype=doctype, doctype=doctype,
@@ -123,33 +130,34 @@ def _make(
communication_type=None, communication_type=None,
add_signature=True, add_signature=True,
) -> Dict[str, str]: ) -> Dict[str, str]:
"""Internal method to make a new communication that ignores Permission checks.
"""
"""Internal method to make a new communication that ignores Permission checks."""


sender = sender or get_formatted_email(frappe.session.user) sender = sender or get_formatted_email(frappe.session.user)
recipients = list_to_str(recipients) if isinstance(recipients, list) else recipients recipients = list_to_str(recipients) if isinstance(recipients, list) else recipients
cc = list_to_str(cc) if isinstance(cc, list) else cc cc = list_to_str(cc) if isinstance(cc, list) else cc
bcc = list_to_str(bcc) if isinstance(bcc, list) else bcc bcc = list_to_str(bcc) if isinstance(bcc, list) else bcc


comm: "Communication" = frappe.get_doc({
"doctype":"Communication",
"subject": subject,
"content": content,
"sender": sender,
"sender_full_name":sender_full_name,
"recipients": recipients,
"cc": cc or None,
"bcc": bcc or None,
"communication_medium": communication_medium,
"sent_or_received": sent_or_received,
"reference_doctype": doctype,
"reference_name": name,
"email_template": email_template,
"message_id":get_message_id().strip(" <>"),
"read_receipt":read_receipt,
"has_attachment": 1 if attachments else 0,
"communication_type": communication_type,
})
comm: "Communication" = frappe.get_doc(
{
"doctype": "Communication",
"subject": subject,
"content": content,
"sender": sender,
"sender_full_name": sender_full_name,
"recipients": recipients,
"cc": cc or None,
"bcc": bcc or None,
"communication_medium": communication_medium,
"sent_or_received": sent_or_received,
"reference_doctype": doctype,
"reference_name": name,
"email_template": email_template,
"message_id": get_message_id().strip(" <>"),
"read_receipt": read_receipt,
"has_attachment": 1 if attachments else 0,
"communication_type": communication_type,
}
)
comm.flags.skip_add_signature = not add_signature comm.flags.skip_add_signature = not add_signature
comm.insert(ignore_permissions=True) comm.insert(ignore_permissions=True)


@@ -161,9 +169,7 @@ def _make(


if cint(send_email): if cint(send_email):
if not comm.get_outgoing_email_account(): if not comm.get_outgoing_email_account():
frappe.throw(
msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError
)
frappe.throw(msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError)


comm.send_email( comm.send_email(
print_html=print_html, print_html=print_html,
@@ -179,7 +185,10 @@ def _make(


def validate_email(doc: "Communication") -> None: def validate_email(doc: "Communication") -> None:
"""Validate Email Addresses of Recipients and CC""" """Validate Email Addresses of Recipients and CC"""
if not (doc.communication_type=="Communication" and doc.communication_medium == "Email") or doc.flags.in_receive:
if (
not (doc.communication_type == "Communication" and doc.communication_medium == "Email")
or doc.flags.in_receive
):
return return


# validate recipients # validate recipients
@@ -193,36 +202,45 @@ def validate_email(doc: "Communication") -> None:
for email in split_emails(doc.bcc): for email in split_emails(doc.bcc):
validate_email_address(email, throw=True) validate_email_address(email, throw=True)



def set_incoming_outgoing_accounts(doc): def set_incoming_outgoing_accounts(doc):
from frappe.email.doctype.email_account.email_account import EmailAccount from frappe.email.doctype.email_account.email_account import EmailAccount

incoming_email_account = EmailAccount.find_incoming( incoming_email_account = EmailAccount.find_incoming(
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype)
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype
)
doc.incoming_email_account = incoming_email_account.email_id if incoming_email_account else None doc.incoming_email_account = incoming_email_account.email_id if incoming_email_account else None


doc.outgoing_email_account = EmailAccount.find_outgoing( doc.outgoing_email_account = EmailAccount.find_outgoing(
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype)
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype
)


if doc.sent_or_received == "Sent": if doc.sent_or_received == "Sent":
doc.db_set("email_account", doc.outgoing_email_account.name) doc.db_set("email_account", doc.outgoing_email_account.name)



def add_attachments(name, attachments): def add_attachments(name, attachments):
'''Add attachments to the given Communication'''
"""Add attachments to the given Communication"""
# loop through attachments # loop through attachments
for a in attachments: for a in attachments:
if isinstance(a, str): if isinstance(a, str):
attach = frappe.db.get_value("File", {"name":a},
["file_name", "file_url", "is_private"], as_dict=1)
attach = frappe.db.get_value(
"File", {"name": a}, ["file_name", "file_url", "is_private"], as_dict=1
)
# save attachments to new doc # save attachments to new doc
_file = frappe.get_doc({
"doctype": "File",
"file_url": attach.file_url,
"attached_to_doctype": "Communication",
"attached_to_name": name,
"folder": "Home/Attachments",
"is_private": attach.is_private
})
_file = frappe.get_doc(
{
"doctype": "File",
"file_url": attach.file_url,
"attached_to_doctype": "Communication",
"attached_to_name": name,
"folder": "Home/Attachments",
"is_private": attach.is_private,
}
)
_file.save(ignore_permissions=True) _file.save(ignore_permissions=True)



@frappe.whitelist(allow_guest=True, methods=("GET",)) @frappe.whitelist(allow_guest=True, methods=("GET",))
def mark_email_as_seen(name: str = None): def mark_email_as_seen(name: str = None):
try: try:
@@ -233,33 +251,31 @@ def mark_email_as_seen(name: str = None):
frappe.log_error(frappe.get_traceback()) frappe.log_error(frappe.get_traceback())


finally: finally:
frappe.response.update({
"type": "binary",
"filename": "imaginary_pixel.png",
"filecontent": (
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00"
b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\r"
b"IDATx\x9cc\xf8\xff\xff?\x03\x00\x08\xfc\x02\xfe\xa7\x9a\xa0"
b"\xa0\x00\x00\x00\x00IEND\xaeB`\x82"
)
})
frappe.response.update(
{
"type": "binary",
"filename": "imaginary_pixel.png",
"filecontent": (
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00"
b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\r"
b"IDATx\x9cc\xf8\xff\xff?\x03\x00\x08\xfc\x02\xfe\xa7\x9a\xa0"
b"\xa0\x00\x00\x00\x00IEND\xaeB`\x82"
),
}
)



def update_communication_as_read(name): def update_communication_as_read(name):
if not name or not isinstance(name, str): if not name or not isinstance(name, str):
return return


communication = frappe.db.get_value(
"Communication",
name,
"read_by_recipient",
as_dict=True
)
communication = frappe.db.get_value("Communication", name, "read_by_recipient", as_dict=True)


if not communication or communication.read_by_recipient: if not communication or communication.read_by_recipient:
return return


frappe.db.set_value("Communication", name, {
"read_by_recipient": 1,
"delivery_status": "Read",
"read_by_recipient_on": get_datetime()
})
frappe.db.set_value(
"Communication",
name,
{"read_by_recipient": 1, "delivery_status": "Read", "read_by_recipient_on": get_datetime()},
)

+ 85
- 65
frappe/core/doctype/communication/mixins.py 查看文件

@@ -1,33 +1,34 @@
from typing import List from typing import List

import frappe import frappe
from frappe import _ from frappe import _
from frappe.core.utils import get_parent_doc from frappe.core.utils import get_parent_doc
from frappe.utils import parse_addr, get_formatted_email, get_url
from frappe.email.doctype.email_account.email_account import EmailAccount
from frappe.desk.doctype.todo.todo import ToDo from frappe.desk.doctype.todo.todo import ToDo
from frappe.email.doctype.email_account.email_account import EmailAccount
from frappe.utils import get_formatted_email, get_url, parse_addr



class CommunicationEmailMixin: class CommunicationEmailMixin:
"""Mixin class to handle communication mails.
"""
"""Mixin class to handle communication mails."""
def is_email_communication(self): def is_email_communication(self):
return self.communication_type=="Communication" and self.communication_medium == "Email"
return self.communication_type == "Communication" and self.communication_medium == "Email"


def get_owner(self): def get_owner(self):
"""Get owner of the communication docs parent.
"""
"""Get owner of the communication docs parent."""
parent_doc = get_parent_doc(self) parent_doc = get_parent_doc(self)
return parent_doc.owner if parent_doc else None return parent_doc.owner if parent_doc else None


def get_all_email_addresses(self, exclude_displayname=False): def get_all_email_addresses(self, exclude_displayname=False):
"""Get all Email addresses mentioned in the doc along with display name.
"""
return self.to_list(exclude_displayname=exclude_displayname) + \
self.cc_list(exclude_displayname=exclude_displayname) + \
self.bcc_list(exclude_displayname=exclude_displayname)
"""Get all Email addresses mentioned in the doc along with display name."""
return (
self.to_list(exclude_displayname=exclude_displayname)
+ self.cc_list(exclude_displayname=exclude_displayname)
+ self.bcc_list(exclude_displayname=exclude_displayname)
)


def get_email_with_displayname(self, email_address): def get_email_with_displayname(self, email_address):
"""Returns email address after adding displayname.
"""
"""Returns email address after adding displayname."""
display_name, email = parse_addr(email_address) display_name, email = parse_addr(email_address)
if display_name and display_name != email: if display_name and display_name != email:
return email_address return email_address
@@ -37,26 +38,24 @@ class CommunicationEmailMixin:
return email_map.get(email, email) return email_map.get(email, email)


def mail_recipients(self, is_inbound_mail_communcation=False): def mail_recipients(self, is_inbound_mail_communcation=False):
"""Build to(recipient) list to send an email.
"""
"""Build to(recipient) list to send an email."""
# Incase of inbound mail, recipients already received the mail, no need to send again. # Incase of inbound mail, recipients already received the mail, no need to send again.
if is_inbound_mail_communcation: if is_inbound_mail_communcation:
return [] return []


if hasattr(self, '_final_recipients'):
if hasattr(self, "_final_recipients"):
return self._final_recipients return self._final_recipients


to = self.to_list() to = self.to_list()
self._final_recipients = list(filter(lambda id: id != 'Administrator', to))
self._final_recipients = list(filter(lambda id: id != "Administrator", to))
return self._final_recipients return self._final_recipients


def get_mail_recipients_with_displayname(self, is_inbound_mail_communcation=False): def get_mail_recipients_with_displayname(self, is_inbound_mail_communcation=False):
"""Build to(recipient) list to send an email including displayname in email.
"""
"""Build to(recipient) list to send an email including displayname in email."""
to_list = self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation) to_list = self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation)
return [self.get_email_with_displayname(email) for email in to_list] return [self.get_email_with_displayname(email) for email in to_list]


def mail_cc(self, is_inbound_mail_communcation=False, include_sender = False):
def mail_cc(self, is_inbound_mail_communcation=False, include_sender=False):
"""Build cc list to send an email. """Build cc list to send an email.


* if email copy is requested by sender, then add sender to CC. * if email copy is requested by sender, then add sender to CC.
@@ -67,7 +66,7 @@ class CommunicationEmailMixin:


* FixMe: Removed adding TODO owners to cc list. Check if that is needed. * FixMe: Removed adding TODO owners to cc list. Check if that is needed.
""" """
if hasattr(self, '_final_cc'):
if hasattr(self, "_final_cc"):
return self._final_cc return self._final_cc


cc = self.cc_list() cc = self.cc_list()
@@ -88,11 +87,13 @@ class CommunicationEmailMixin:
if is_inbound_mail_communcation: if is_inbound_mail_communcation:
cc = cc - set(self.cc_list() + self.to_list()) cc = cc - set(self.cc_list() + self.to_list())


self._final_cc = list(filter(lambda id: id != 'Administrator', cc))
self._final_cc = list(filter(lambda id: id != "Administrator", cc))
return self._final_cc return self._final_cc


def get_mail_cc_with_displayname(self, is_inbound_mail_communcation=False, include_sender = False):
cc_list = self.mail_cc(is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender = include_sender)
def get_mail_cc_with_displayname(self, is_inbound_mail_communcation=False, include_sender=False):
cc_list = self.mail_cc(
is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=include_sender
)
return [self.get_email_with_displayname(email) for email in cc_list] return [self.get_email_with_displayname(email) for email in cc_list]


def mail_bcc(self, is_inbound_mail_communcation=False): def mail_bcc(self, is_inbound_mail_communcation=False):
@@ -102,7 +103,7 @@ class CommunicationEmailMixin:
* User must be enabled in the system * User must be enabled in the system
* remove_administrator_from_email_list * remove_administrator_from_email_list
""" """
if hasattr(self, '_final_bcc'):
if hasattr(self, "_final_bcc"):
return self._final_bcc return self._final_bcc


bcc = set(self.bcc_list()) bcc = set(self.bcc_list())
@@ -116,7 +117,7 @@ class CommunicationEmailMixin:
if is_inbound_mail_communcation: if is_inbound_mail_communcation:
bcc = bcc - set(self.bcc_list() + self.to_list()) bcc = bcc - set(self.bcc_list() + self.to_list())


self._final_bcc = list(filter(lambda id: id != 'Administrator', bcc))
self._final_bcc = list(filter(lambda id: id != "Administrator", bcc))
return self._final_bcc return self._final_bcc


def get_mail_bcc_with_displayname(self, is_inbound_mail_communcation=False): def get_mail_bcc_with_displayname(self, is_inbound_mail_communcation=False):
@@ -145,22 +146,23 @@ class CommunicationEmailMixin:


def get_attach_link(self, print_format): def get_attach_link(self, print_format):
"""Returns public link for the attachment via `templates/emails/print_link.html`.""" """Returns public link for the attachment via `templates/emails/print_link.html`."""
return frappe.get_template("templates/emails/print_link.html").render({
"url": get_url(),
"doctype": self.reference_doctype,
"name": self.reference_name,
"print_format": print_format,
"key": get_parent_doc(self).get_signature()
})
return frappe.get_template("templates/emails/print_link.html").render(
{
"url": get_url(),
"doctype": self.reference_doctype,
"name": self.reference_name,
"print_format": print_format,
"key": get_parent_doc(self).get_signature(),
}
)


def get_outgoing_email_account(self): def get_outgoing_email_account(self):
if not hasattr(self, '_outgoing_email_account'):
if not hasattr(self, "_outgoing_email_account"):
if self.email_account: if self.email_account:
self._outgoing_email_account = EmailAccount.find(self.email_account) self._outgoing_email_account = EmailAccount.find(self.email_account)
else: else:
self._outgoing_email_account = EmailAccount.find_outgoing( self._outgoing_email_account = EmailAccount.find_outgoing(
match_by_email=self.sender_mailid,
match_by_doctype=self.reference_doctype
match_by_email=self.sender_mailid, match_by_doctype=self.reference_doctype
) )


if self.sent_or_received == "Sent" and self._outgoing_email_account: if self.sent_or_received == "Sent" and self._outgoing_email_account:
@@ -169,10 +171,9 @@ class CommunicationEmailMixin:
return self._outgoing_email_account return self._outgoing_email_account


def get_incoming_email_account(self): def get_incoming_email_account(self):
if not hasattr(self, '_incoming_email_account'):
if not hasattr(self, "_incoming_email_account"):
self._incoming_email_account = EmailAccount.find_incoming( self._incoming_email_account = EmailAccount.find_incoming(
match_by_email=self.sender_mailid,
match_by_doctype=self.reference_doctype
match_by_email=self.sender_mailid, match_by_doctype=self.reference_doctype
) )
return self._incoming_email_account return self._incoming_email_account


@@ -180,12 +181,17 @@ class CommunicationEmailMixin:
final_attachments = [] final_attachments = []


if print_format or print_html: if print_format or print_html:
d = {'print_format': print_format, 'html': print_html, 'print_format_attachment': 1,
'doctype': self.reference_doctype, 'name': self.reference_name}
d = {
"print_format": print_format,
"html": print_html,
"print_format_attachment": 1,
"doctype": self.reference_doctype,
"name": self.reference_name,
}
final_attachments.append(d) final_attachments.append(d)


for a in self.get_attachments() or []: for a in self.get_attachments() or []:
final_attachments.append({"fid": a['name']})
final_attachments.append({"fid": a["name"]})


return final_attachments return final_attachments


@@ -193,48 +199,57 @@ class CommunicationEmailMixin:
email_account = self.get_outgoing_email_account() email_account = self.get_outgoing_email_account()
if email_account and email_account.send_unsubscribe_message: if email_account and email_account.send_unsubscribe_message:
return _("Leave this conversation") return _("Leave this conversation")
return ''
return ""


def exclude_emails_list(self, is_inbound_mail_communcation=False, include_sender=False) -> List: def exclude_emails_list(self, is_inbound_mail_communcation=False, include_sender=False) -> List:
"""List of mail id's excluded while sending mail.
"""
"""List of mail id's excluded while sending mail."""
all_ids = self.get_all_email_addresses(exclude_displayname=True) all_ids = self.get_all_email_addresses(exclude_displayname=True)


final_ids = ( final_ids = (
self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation) self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation)
+ self.mail_bcc(is_inbound_mail_communcation=is_inbound_mail_communcation) + self.mail_bcc(is_inbound_mail_communcation=is_inbound_mail_communcation)
+ self.mail_cc(is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=include_sender)
+ self.mail_cc(
is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=include_sender
)
) )


return list(set(all_ids) - set(final_ids)) return list(set(all_ids) - set(final_ids))


def get_assignees(self): def get_assignees(self):
"""Get owners of the reference document.
"""
filters = {'status': 'Open', 'reference_name': self.reference_name,
'reference_type': self.reference_doctype}
"""Get owners of the reference document."""
filters = {
"status": "Open",
"reference_name": self.reference_name,
"reference_type": self.reference_doctype,
}
return ToDo.get_owners(filters) return ToDo.get_owners(filters)


@staticmethod @staticmethod
def filter_thread_notification_disbled_users(emails): def filter_thread_notification_disbled_users(emails):
"""Filter users based on notifications for email threads setting is disabled.
"""
"""Filter users based on notifications for email threads setting is disabled."""
if not emails: if not emails:
return [] return []


return frappe.get_all("User", pluck="email", filters={"email": ["in", emails], "thread_notify": 0})
return frappe.get_all(
"User", pluck="email", filters={"email": ["in", emails], "thread_notify": 0}
)


@staticmethod @staticmethod
def filter_disabled_users(emails): def filter_disabled_users(emails):
"""
"""
""" """
if not emails: if not emails:
return [] return []


return frappe.get_all("User", pluck="email", filters={"email": ["in", emails], "enabled": 0}) return frappe.get_all("User", pluck="email", filters={"email": ["in", emails], "enabled": 0})


def sendmail_input_dict(self, print_html=None, print_format=None,
send_me_a_copy=None, print_letterhead=None, is_inbound_mail_communcation=None):
def sendmail_input_dict(
self,
print_html=None,
print_format=None,
send_me_a_copy=None,
print_letterhead=None,
is_inbound_mail_communcation=None,
):


outgoing_email_account = self.get_outgoing_email_account() outgoing_email_account = self.get_outgoing_email_account()
if not outgoing_email_account: if not outgoing_email_account:
@@ -244,8 +259,7 @@ class CommunicationEmailMixin:
is_inbound_mail_communcation=is_inbound_mail_communcation is_inbound_mail_communcation=is_inbound_mail_communcation
) )
cc = self.get_mail_cc_with_displayname( cc = self.get_mail_cc_with_displayname(
is_inbound_mail_communcation=is_inbound_mail_communcation,
include_sender = send_me_a_copy
is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=send_me_a_copy
) )
bcc = self.get_mail_bcc_with_displayname( bcc = self.get_mail_bcc_with_displayname(
is_inbound_mail_communcation=is_inbound_mail_communcation is_inbound_mail_communcation=is_inbound_mail_communcation
@@ -273,18 +287,24 @@ class CommunicationEmailMixin:
"delayed": True, "delayed": True,
"communication": self.name, "communication": self.name,
"read_receipt": self.read_receipt, "read_receipt": self.read_receipt,
"is_notification": (self.sent_or_received =="Received" and True) or False,
"print_letterhead": print_letterhead
"is_notification": (self.sent_or_received == "Received" and True) or False,
"print_letterhead": print_letterhead,
} }


def send_email(self, print_html=None, print_format=None,
send_me_a_copy=None, print_letterhead=None, is_inbound_mail_communcation=None):
def send_email(
self,
print_html=None,
print_format=None,
send_me_a_copy=None,
print_letterhead=None,
is_inbound_mail_communcation=None,
):
input_dict = self.sendmail_input_dict( input_dict = self.sendmail_input_dict(
print_html=print_html, print_html=print_html,
print_format=print_format, print_format=print_format,
send_me_a_copy=send_me_a_copy, send_me_a_copy=send_me_a_copy,
print_letterhead=print_letterhead, print_letterhead=print_letterhead,
is_inbound_mail_communcation=is_inbound_mail_communcation
is_inbound_mail_communcation=is_inbound_mail_communcation,
) )


if input_dict: if input_dict:


+ 212
- 159
frappe/core/doctype/communication/test_communication.py 查看文件

@@ -7,20 +7,30 @@ import frappe
from frappe.core.doctype.communication.communication import get_emails from frappe.core.doctype.communication.communication import get_emails
from frappe.email.doctype.email_queue.email_queue import EmailQueue from frappe.email.doctype.email_queue.email_queue import EmailQueue


test_records = frappe.get_test_records('Communication')
test_records = frappe.get_test_records("Communication")


class TestCommunication(unittest.TestCase):


class TestCommunication(unittest.TestCase):
def test_email(self): def test_email(self):
valid_email_list = ["Full Name <full@example.com>",
'"Full Name with quotes and <weird@chars.com>" <weird@example.com>',
"Surname, Name <name.surname@domain.com>",
"Purchase@ABC <purchase@abc.com>", "xyz@abc2.com <xyz@abc.com>",
"Name [something else] <name@domain.com>"]

invalid_email_list = ["[invalid!email]", "invalid-email",
"tes2", "e", "rrrrrrrr", "manas","[[[sample]]]",
"[invalid!email].com"]
valid_email_list = [
"Full Name <full@example.com>",
'"Full Name with quotes and <weird@chars.com>" <weird@example.com>',
"Surname, Name <name.surname@domain.com>",
"Purchase@ABC <purchase@abc.com>",
"xyz@abc2.com <xyz@abc.com>",
"Name [something else] <name@domain.com>",
]

invalid_email_list = [
"[invalid!email]",
"invalid-email",
"tes2",
"e",
"rrrrrrrr",
"manas",
"[[[sample]]]",
"[invalid!email].com",
]


for x in valid_email_list: for x in valid_email_list:
self.assertTrue(frappe.utils.parse_addr(x)[1]) self.assertTrue(frappe.utils.parse_addr(x)[1])
@@ -29,15 +39,25 @@ class TestCommunication(unittest.TestCase):
self.assertFalse(frappe.utils.parse_addr(x)[0]) self.assertFalse(frappe.utils.parse_addr(x)[0])


def test_name(self): def test_name(self):
valid_email_list = ["Full Name <full@example.com>",
'"Full Name with quotes and <weird@chars.com>" <weird@example.com>',
"Surname, Name <name.surname@domain.com>",
"Purchase@ABC <purchase@abc.com>", "xyz@abc2.com <xyz@abc.com>",
"Name [something else] <name@domain.com>"]

invalid_email_list = ["[invalid!email]", "invalid-email",
"tes2", "e", "rrrrrrrr", "manas","[[[sample]]]",
"[invalid!email].com"]
valid_email_list = [
"Full Name <full@example.com>",
'"Full Name with quotes and <weird@chars.com>" <weird@example.com>',
"Surname, Name <name.surname@domain.com>",
"Purchase@ABC <purchase@abc.com>",
"xyz@abc2.com <xyz@abc.com>",
"Name [something else] <name@domain.com>",
]

invalid_email_list = [
"[invalid!email]",
"invalid-email",
"tes2",
"e",
"rrrrrrrr",
"manas",
"[[[sample]]]",
"[invalid!email].com",
]


for x in valid_email_list: for x in valid_email_list:
self.assertTrue(frappe.utils.parse_addr(x)[0]) self.assertTrue(frappe.utils.parse_addr(x)[0])
@@ -46,27 +66,33 @@ class TestCommunication(unittest.TestCase):
self.assertFalse(frappe.utils.parse_addr(x)[0]) self.assertFalse(frappe.utils.parse_addr(x)[0])


def test_circular_linking(self): def test_circular_linking(self):
a = frappe.get_doc({
"doctype": "Communication",
"communication_type": "Communication",
"content": "This was created to test circular linking: Communication A",
}).insert(ignore_permissions=True)

b = frappe.get_doc({
"doctype": "Communication",
"communication_type": "Communication",
"content": "This was created to test circular linking: Communication B",
"reference_doctype": "Communication",
"reference_name": a.name
}).insert(ignore_permissions=True)

c = frappe.get_doc({
"doctype": "Communication",
"communication_type": "Communication",
"content": "This was created to test circular linking: Communication C",
"reference_doctype": "Communication",
"reference_name": b.name
}).insert(ignore_permissions=True)
a = frappe.get_doc(
{
"doctype": "Communication",
"communication_type": "Communication",
"content": "This was created to test circular linking: Communication A",
}
).insert(ignore_permissions=True)

b = frappe.get_doc(
{
"doctype": "Communication",
"communication_type": "Communication",
"content": "This was created to test circular linking: Communication B",
"reference_doctype": "Communication",
"reference_name": a.name,
}
).insert(ignore_permissions=True)

c = frappe.get_doc(
{
"doctype": "Communication",
"communication_type": "Communication",
"content": "This was created to test circular linking: Communication C",
"reference_doctype": "Communication",
"reference_name": b.name,
}
).insert(ignore_permissions=True)


a = frappe.get_doc("Communication", a.name) a = frappe.get_doc("Communication", a.name)
a.reference_doctype = "Communication" a.reference_doctype = "Communication"
@@ -77,20 +103,24 @@ class TestCommunication(unittest.TestCase):
def test_deduplication_timeline_links(self): def test_deduplication_timeline_links(self):
frappe.delete_doc_if_exists("Note", "deduplication timeline links") frappe.delete_doc_if_exists("Note", "deduplication timeline links")


note = frappe.get_doc({
"doctype": "Note",
"title": "deduplication timeline links",
"content": "deduplication timeline links"
}).insert(ignore_permissions=True)

comm = frappe.get_doc({
"doctype": "Communication",
"communication_type": "Communication",
"content": "Deduplication of Links",
"communication_medium": "Email"
}).insert(ignore_permissions=True)

#adding same link twice
note = frappe.get_doc(
{
"doctype": "Note",
"title": "deduplication timeline links",
"content": "deduplication timeline links",
}
).insert(ignore_permissions=True)

comm = frappe.get_doc(
{
"doctype": "Communication",
"communication_type": "Communication",
"content": "Deduplication of Links",
"communication_medium": "Email",
}
).insert(ignore_permissions=True)

# adding same link twice
comm.add_link(link_doctype="Note", link_name=note.name, autosave=True) comm.add_link(link_doctype="Note", link_name=note.name, autosave=True)
comm.add_link(link_doctype="Note", link_name=note.name, autosave=True) comm.add_link(link_doctype="Note", link_name=note.name, autosave=True)


@@ -99,35 +129,43 @@ class TestCommunication(unittest.TestCase):
self.assertNotEqual(2, len(comm.timeline_links)) self.assertNotEqual(2, len(comm.timeline_links))


def test_contacts_attached(self): def test_contacts_attached(self):
contact_sender = frappe.get_doc({
"doctype": "Contact",
"first_name": "contact_sender",
})
contact_sender = frappe.get_doc(
{
"doctype": "Contact",
"first_name": "contact_sender",
}
)
contact_sender.add_email("comm_sender@example.com") contact_sender.add_email("comm_sender@example.com")
contact_sender.insert(ignore_permissions=True) contact_sender.insert(ignore_permissions=True)


contact_recipient = frappe.get_doc({
"doctype": "Contact",
"first_name": "contact_recipient",
})
contact_recipient = frappe.get_doc(
{
"doctype": "Contact",
"first_name": "contact_recipient",
}
)
contact_recipient.add_email("comm_recipient@example.com") contact_recipient.add_email("comm_recipient@example.com")
contact_recipient.insert(ignore_permissions=True) contact_recipient.insert(ignore_permissions=True)


contact_cc = frappe.get_doc({
"doctype": "Contact",
"first_name": "contact_cc",
})
contact_cc = frappe.get_doc(
{
"doctype": "Contact",
"first_name": "contact_cc",
}
)
contact_cc.add_email("comm_cc@example.com") contact_cc.add_email("comm_cc@example.com")
contact_cc.insert(ignore_permissions=True) contact_cc.insert(ignore_permissions=True)


comm = frappe.get_doc({
"doctype": "Communication",
"communication_medium": "Email",
"subject": "Contacts Attached Test",
"sender": "comm_sender@example.com",
"recipients": "comm_recipient@example.com",
"cc": "comm_cc@example.com"
}).insert(ignore_permissions=True)
comm = frappe.get_doc(
{
"doctype": "Communication",
"communication_medium": "Email",
"subject": "Contacts Attached Test",
"sender": "comm_sender@example.com",
"recipients": "comm_recipient@example.com",
"cc": "comm_cc@example.com",
}
).insert(ignore_permissions=True)


comm = frappe.get_doc("Communication", comm.name) comm = frappe.get_doc("Communication", comm.name)


@@ -144,27 +182,29 @@ class TestCommunication(unittest.TestCase):


frappe.delete_doc_if_exists("Note", "get communication data") frappe.delete_doc_if_exists("Note", "get communication data")


note = frappe.get_doc({
"doctype": "Note",
"title": "get communication data",
"content": "get communication data"
}).insert(ignore_permissions=True)
note = frappe.get_doc(
{"doctype": "Note", "title": "get communication data", "content": "get communication data"}
).insert(ignore_permissions=True)


comm_note_1 = frappe.get_doc({
"doctype": "Communication",
"communication_type": "Communication",
"content": "Test Get Communication Data 1",
"communication_medium": "Email"
}).insert(ignore_permissions=True)
comm_note_1 = frappe.get_doc(
{
"doctype": "Communication",
"communication_type": "Communication",
"content": "Test Get Communication Data 1",
"communication_medium": "Email",
}
).insert(ignore_permissions=True)


comm_note_1.add_link(link_doctype="Note", link_name=note.name, autosave=True) comm_note_1.add_link(link_doctype="Note", link_name=note.name, autosave=True)


comm_note_2 = frappe.get_doc({
"doctype": "Communication",
"communication_type": "Communication",
"content": "Test Get Communication Data 2",
"communication_medium": "Email"
}).insert(ignore_permissions=True)
comm_note_2 = frappe.get_doc(
{
"doctype": "Communication",
"communication_type": "Communication",
"content": "Test Get Communication Data 2",
"communication_medium": "Email",
}
).insert(ignore_permissions=True)


comm_note_2.add_link(link_doctype="Note", link_name=note.name, autosave=True) comm_note_2.add_link(link_doctype="Note", link_name=note.name, autosave=True)


@@ -182,19 +222,23 @@ class TestCommunication(unittest.TestCase):


create_email_account() create_email_account()


note = frappe.get_doc({
"doctype": "Note",
"title": "test document link in email",
"content": "test document link in email"
}).insert(ignore_permissions=True)

comm = frappe.get_doc({
"doctype": "Communication",
"communication_medium": "Email",
"subject": "Document Link in Email",
"sender": "comm_sender@example.com",
"recipients": "comm_recipient+{0}+{1}@example.com".format(quote("Note"), quote(note.name)),
}).insert(ignore_permissions=True)
note = frappe.get_doc(
{
"doctype": "Note",
"title": "test document link in email",
"content": "test document link in email",
}
).insert(ignore_permissions=True)

comm = frappe.get_doc(
{
"doctype": "Communication",
"communication_medium": "Email",
"subject": "Document Link in Email",
"sender": "comm_sender@example.com",
"recipients": "comm_recipient+{0}+{1}@example.com".format(quote("Note"), quote(note.name)),
}
).insert(ignore_permissions=True)


doc_links = [] doc_links = []
for timeline_link in comm.timeline_links: for timeline_link in comm.timeline_links:
@@ -205,9 +249,9 @@ class TestCommunication(unittest.TestCase):
def test_parse_emails(self): def test_parse_emails(self):
emails = get_emails( emails = get_emails(
[ [
'comm_recipient+DocType+DocName@example.com',
"comm_recipient+DocType+DocName@example.com",
'"First, LastName" <first.lastname@email.com>', '"First, LastName" <first.lastname@email.com>',
'test@user.com'
"test@user.com",
] ]
) )


@@ -215,99 +259,108 @@ class TestCommunication(unittest.TestCase):
self.assertEqual(emails[1], "first.lastname@email.com") self.assertEqual(emails[1], "first.lastname@email.com")
self.assertEqual(emails[2], "test@user.com") self.assertEqual(emails[2], "test@user.com")



class TestCommunicationEmailMixin(unittest.TestCase): class TestCommunicationEmailMixin(unittest.TestCase):
def new_communication(self, recipients=None, cc=None, bcc=None): def new_communication(self, recipients=None, cc=None, bcc=None):
recipients = ', '.join(recipients or [])
cc = ', '.join(cc or [])
bcc = ', '.join(bcc or [])

comm = frappe.get_doc({
"doctype": "Communication",
"communication_type": "Communication",
"communication_medium": "Email",
"content": "Test content",
"recipients": recipients,
"cc": cc,
"bcc": bcc
}).insert(ignore_permissions=True)
recipients = ", ".join(recipients or [])
cc = ", ".join(cc or [])
bcc = ", ".join(bcc or [])

comm = frappe.get_doc(
{
"doctype": "Communication",
"communication_type": "Communication",
"communication_medium": "Email",
"content": "Test content",
"recipients": recipients,
"cc": cc,
"bcc": bcc,
}
).insert(ignore_permissions=True)
return comm return comm


def new_user(self, email, **user_data): def new_user(self, email, **user_data):
user_data.setdefault('first_name', 'first_name')
user = frappe.new_doc('User')
user_data.setdefault("first_name", "first_name")
user = frappe.new_doc("User")
user.email = email user.email = email
user.update(user_data) user.update(user_data)
user.insert(ignore_permissions=True, ignore_if_duplicate=True) user.insert(ignore_permissions=True, ignore_if_duplicate=True)
return user return user


def test_recipients(self): def test_recipients(self):
to_list = ['to@test.com', 'receiver <to+1@test.com>', 'to@test.com']
comm = self.new_communication(recipients = to_list)
to_list = ["to@test.com", "receiver <to+1@test.com>", "to@test.com"]
comm = self.new_communication(recipients=to_list)
res = comm.get_mail_recipients_with_displayname() res = comm.get_mail_recipients_with_displayname()
self.assertCountEqual(res, ['to@test.com', 'receiver <to+1@test.com>'])
self.assertCountEqual(res, ["to@test.com", "receiver <to+1@test.com>"])
comm.delete() comm.delete()


def test_cc(self): def test_cc(self):
to_list = ['to@test.com']
cc_list = ['cc+1@test.com', 'cc <cc+2@test.com>', 'to@test.com']
user = self.new_user(email='cc+1@test.com', thread_notify=0)
to_list = ["to@test.com"]
cc_list = ["cc+1@test.com", "cc <cc+2@test.com>", "to@test.com"]
user = self.new_user(email="cc+1@test.com", thread_notify=0)
comm = self.new_communication(recipients=to_list, cc=cc_list) comm = self.new_communication(recipients=to_list, cc=cc_list)
res = comm.get_mail_cc_with_displayname() res = comm.get_mail_cc_with_displayname()
self.assertCountEqual(res, ['cc <cc+2@test.com>'])
self.assertCountEqual(res, ["cc <cc+2@test.com>"])
user.delete() user.delete()
comm.delete() comm.delete()


def test_bcc(self): def test_bcc(self):
bcc_list = ['bcc+1@test.com', 'cc <bcc+2@test.com>', ]
user = self.new_user(email='bcc+2@test.com', enabled=0)
bcc_list = [
"bcc+1@test.com",
"cc <bcc+2@test.com>",
]
user = self.new_user(email="bcc+2@test.com", enabled=0)
comm = self.new_communication(bcc=bcc_list) comm = self.new_communication(bcc=bcc_list)
res = comm.get_mail_bcc_with_displayname() res = comm.get_mail_bcc_with_displayname()
self.assertCountEqual(res, ['bcc+1@test.com'])
self.assertCountEqual(res, ["bcc+1@test.com"])
user.delete() user.delete()
comm.delete() comm.delete()


def test_sendmail(self): def test_sendmail(self):
to_list = ['to <to@test.com>']
cc_list = ['cc <cc+1@test.com>', 'cc <cc+2@test.com>']
to_list = ["to <to@test.com>"]
cc_list = ["cc <cc+1@test.com>", "cc <cc+2@test.com>"]


comm = self.new_communication(recipients=to_list, cc=cc_list) comm = self.new_communication(recipients=to_list, cc=cc_list)
comm.send_email() comm.send_email()
doc = EmailQueue.find_one_by_filters(communication=comm.name) doc = EmailQueue.find_one_by_filters(communication=comm.name)
mail_receivers = [each.recipient for each in doc.recipients] mail_receivers = [each.recipient for each in doc.recipients]
self.assertIsNotNone(doc) self.assertIsNotNone(doc)
self.assertCountEqual(to_list+cc_list, mail_receivers)
self.assertCountEqual(to_list + cc_list, mail_receivers)
doc.delete() doc.delete()
comm.delete() comm.delete()



def create_email_account(): def create_email_account():
frappe.delete_doc_if_exists("Email Account", "_Test Comm Account 1") frappe.delete_doc_if_exists("Email Account", "_Test Comm Account 1")


frappe.flags.mute_emails = False frappe.flags.mute_emails = False
frappe.flags.sent_mail = None frappe.flags.sent_mail = None


email_account = frappe.get_doc({
"is_default": 1,
"is_global": 1,
"doctype": "Email Account",
"domain":"example.com",
"append_to": "ToDo",
"email_account_name": "_Test Comm Account 1",
"enable_outgoing": 1,
"smtp_server": "test.example.com",
"email_id": "test_comm@example.com",
"password": "password",
"add_signature": 1,
"signature": "\nBest Wishes\nTest Signature",
"enable_auto_reply": 1,
"auto_reply_message": "",
"enable_incoming": 1,
"notify_if_unreplied": 1,
"unreplied_for_mins": 20,
"send_notification_to": "test_comm@example.com",
"pop3_server": "pop.test.example.com",
"imap_folder": [{"folder_name": "INBOX", "append_to": "ToDo"}],
"no_remaining":"0",
"enable_automatic_linking": 1
}).insert(ignore_permissions=True)
email_account = frappe.get_doc(
{
"is_default": 1,
"is_global": 1,
"doctype": "Email Account",
"domain": "example.com",
"append_to": "ToDo",
"email_account_name": "_Test Comm Account 1",
"enable_outgoing": 1,
"smtp_server": "test.example.com",
"email_id": "test_comm@example.com",
"password": "password",
"add_signature": 1,
"signature": "\nBest Wishes\nTest Signature",
"enable_auto_reply": 1,
"auto_reply_message": "",
"enable_incoming": 1,
"notify_if_unreplied": 1,
"unreplied_for_mins": 20,
"send_notification_to": "test_comm@example.com",
"pop3_server": "pop.test.example.com",
"imap_folder": [{"folder_name": "INBOX", "append_to": "ToDo"}],
"no_remaining": "0",
"enable_automatic_linking": 1,
}
).insert(ignore_permissions=True)


return email_account return email_account

+ 3
- 1
frappe/core/doctype/communication_link/communication_link.py 查看文件

@@ -5,8 +5,10 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document



class CommunicationLink(Document): class CommunicationLink(Document):
pass pass



def on_doctype_update(): def on_doctype_update():
frappe.db.add_index("Communication Link", ["link_doctype", "link_name"])
frappe.db.add_index("Communication Link", ["link_doctype", "link_name"])

+ 2
- 1
frappe/core/doctype/custom_docperm/custom_docperm.py 查看文件

@@ -5,6 +5,7 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document



class CustomDocPerm(Document): class CustomDocPerm(Document):
def on_update(self): def on_update(self):
frappe.clear_cache(doctype = self.parent)
frappe.clear_cache(doctype=self.parent)

+ 3
- 1
frappe/core/doctype/custom_docperm/test_custom_docperm.py 查看文件

@@ -1,10 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors # Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe
import unittest import unittest


import frappe

# test_records = frappe.get_test_records('Custom DocPerm') # test_records = frappe.get_test_records('Custom DocPerm')



class TestCustomDocPerm(unittest.TestCase): class TestCustomDocPerm(unittest.TestCase):
pass pass

+ 6
- 4
frappe/core/doctype/custom_role/custom_role.py 查看文件

@@ -5,16 +5,18 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document



class CustomRole(Document): class CustomRole(Document):
def validate(self): def validate(self):
if self.report and not self.ref_doctype: if self.report and not self.ref_doctype:
self.ref_doctype = frappe.db.get_value('Report', self.report, 'ref_doctype')
self.ref_doctype = frappe.db.get_value("Report", self.report, "ref_doctype")



def get_custom_allowed_roles(field, name): def get_custom_allowed_roles(field, name):
allowed_roles = [] allowed_roles = []
custom_role = frappe.db.get_value('Custom Role', {field: name}, 'name')
custom_role = frappe.db.get_value("Custom Role", {field: name}, "name")
if custom_role: if custom_role:
custom_role_doc = frappe.get_doc('Custom Role', custom_role)
custom_role_doc = frappe.get_doc("Custom Role", custom_role)
allowed_roles = [d.role for d in custom_role_doc.roles] allowed_roles = [d.role for d in custom_role_doc.roles]


return allowed_roles
return allowed_roles

+ 3
- 1
frappe/core/doctype/custom_role/test_custom_role.py 查看文件

@@ -1,10 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors # Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe
import unittest import unittest


import frappe

# test_records = frappe.get_test_records('Custom Role') # test_records = frappe.get_test_records('Custom Role')



class TestCustomRole(unittest.TestCase): class TestCustomRole(unittest.TestCase):
pass pass

+ 1
- 0
frappe/core/doctype/data_export/data_export.py 查看文件

@@ -4,5 +4,6 @@


from frappe.model.document import Document from frappe.model.document import Document



class DataExport(Document): class DataExport(Document):
pass pass

+ 168
- 100
frappe/core/doctype/data_export/exporter.py 查看文件

@@ -1,47 +1,78 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE


import csv
import os
import re

import frappe import frappe
from frappe import _
import frappe.permissions import frappe.permissions
import re, csv, os
from frappe.utils.csvutils import UnicodeWriter
from frappe.utils import cstr, formatdate, format_datetime, parse_json, cint, format_duration
from frappe import _
from frappe.core.doctype.access_log.access_log import make_access_log from frappe.core.doctype.access_log.access_log import make_access_log
from frappe.utils import cint, cstr, format_datetime, format_duration, formatdate, parse_json
from frappe.utils.csvutils import UnicodeWriter

reflags = {"I": re.I, "L": re.L, "M": re.M, "U": re.U, "S": re.S, "X": re.X, "D": re.DEBUG}


reflags = {
"I":re.I,
"L":re.L,
"M":re.M,
"U":re.U,
"S":re.S,
"X":re.X,
"D": re.DEBUG
}


def get_data_keys(): def get_data_keys():
return frappe._dict({
"data_separator": _('Start entering data below this line'),
"main_table": _("Table") + ":",
"parent_table": _("Parent Table") + ":",
"columns": _("Column Name") + ":",
"doctype": _("DocType") + ":"
})
return frappe._dict(
{
"data_separator": _("Start entering data below this line"),
"main_table": _("Table") + ":",
"parent_table": _("Parent Table") + ":",
"columns": _("Column Name") + ":",
"doctype": _("DocType") + ":",
}
)



@frappe.whitelist() @frappe.whitelist()
def export_data(doctype=None, parent_doctype=None, all_doctypes=True, with_data=False,
select_columns=None, file_type='CSV', template=False, filters=None):
def export_data(
doctype=None,
parent_doctype=None,
all_doctypes=True,
with_data=False,
select_columns=None,
file_type="CSV",
template=False,
filters=None,
):
_doctype = doctype _doctype = doctype
if isinstance(_doctype, list): if isinstance(_doctype, list):
_doctype = _doctype[0] _doctype = _doctype[0]
make_access_log(doctype=_doctype, file_type=file_type, columns=select_columns, filters=filters, method=parent_doctype)
exporter = DataExporter(doctype=doctype, parent_doctype=parent_doctype, all_doctypes=all_doctypes, with_data=with_data,
select_columns=select_columns, file_type=file_type, template=template, filters=filters)
make_access_log(
doctype=_doctype,
file_type=file_type,
columns=select_columns,
filters=filters,
method=parent_doctype,
)
exporter = DataExporter(
doctype=doctype,
parent_doctype=parent_doctype,
all_doctypes=all_doctypes,
with_data=with_data,
select_columns=select_columns,
file_type=file_type,
template=template,
filters=filters,
)
exporter.build_response() exporter.build_response()



class DataExporter: class DataExporter:
def __init__(self, doctype=None, parent_doctype=None, all_doctypes=True, with_data=False,
select_columns=None, file_type='CSV', template=False, filters=None):
def __init__(
self,
doctype=None,
parent_doctype=None,
all_doctypes=True,
with_data=False,
select_columns=None,
file_type="CSV",
template=False,
filters=None,
):
self.doctype = doctype self.doctype = doctype
self.parent_doctype = parent_doctype self.parent_doctype = parent_doctype
self.all_doctypes = all_doctypes self.all_doctypes = all_doctypes
@@ -81,18 +112,18 @@ class DataExporter:


def build_response(self): def build_response(self):
self.writer = UnicodeWriter() self.writer = UnicodeWriter()
self.name_field = 'parent' if self.parent_doctype != self.doctype else 'name'
self.name_field = "parent" if self.parent_doctype != self.doctype else "name"


if self.template: if self.template:
self.add_main_header() self.add_main_header()


self.writer.writerow([''])
self.writer.writerow([""])
self.tablerow = [self.data_keys.doctype] self.tablerow = [self.data_keys.doctype]
self.labelrow = [_("Column Labels:")] self.labelrow = [_("Column Labels:")]
self.fieldrow = [self.data_keys.columns] self.fieldrow = [self.data_keys.columns]
self.mandatoryrow = [_("Mandatory:")] self.mandatoryrow = [_("Mandatory:")]
self.typerow = [_('Type:')]
self.inforow = [_('Info:')]
self.typerow = [_("Type:")]
self.inforow = [_("Info:")]
self.columns = [] self.columns = []


self.build_field_columns(self.doctype) self.build_field_columns(self.doctype)
@@ -100,74 +131,99 @@ class DataExporter:
if self.all_doctypes: if self.all_doctypes:
for d in self.child_doctypes: for d in self.child_doctypes:
self.append_empty_field_column() self.append_empty_field_column()
if (self.select_columns and self.select_columns.get(d['doctype'], None)) or not self.select_columns:
if (
self.select_columns and self.select_columns.get(d["doctype"], None)
) or not self.select_columns:
# if atleast one column is selected for this doctype # if atleast one column is selected for this doctype
self.build_field_columns(d['doctype'], d['parentfield'])
self.build_field_columns(d["doctype"], d["parentfield"])


self.add_field_headings() self.add_field_headings()
self.add_data() self.add_data()
if self.with_data and not self.data: if self.with_data and not self.data:
frappe.respond_as_web_page(_('No Data'), _('There is no data to be exported'), indicator_color='orange')
frappe.respond_as_web_page(
_("No Data"), _("There is no data to be exported"), indicator_color="orange"
)


if self.file_type == 'Excel':
if self.file_type == "Excel":
self.build_response_as_excel() self.build_response_as_excel()
else: else:
# write out response as a type csv # write out response as a type csv
frappe.response['result'] = cstr(self.writer.getvalue())
frappe.response['type'] = 'csv'
frappe.response['doctype'] = self.doctype
frappe.response["result"] = cstr(self.writer.getvalue())
frappe.response["type"] = "csv"
frappe.response["doctype"] = self.doctype


def add_main_header(self): def add_main_header(self):
self.writer.writerow([_('Data Import Template')])
self.writer.writerow([_("Data Import Template")])
self.writer.writerow([self.data_keys.main_table, self.doctype]) self.writer.writerow([self.data_keys.main_table, self.doctype])


if self.parent_doctype != self.doctype: if self.parent_doctype != self.doctype:
self.writer.writerow([self.data_keys.parent_table, self.parent_doctype]) self.writer.writerow([self.data_keys.parent_table, self.parent_doctype])
else: else:
self.writer.writerow([''])

self.writer.writerow([''])
self.writer.writerow([_('Notes:')])
self.writer.writerow([_('Please do not change the template headings.')])
self.writer.writerow([_('First data column must be blank.')])
self.writer.writerow([_('If you are uploading new records, leave the "name" (ID) column blank.')])
self.writer.writerow([_('If you are uploading new records, "Naming Series" becomes mandatory, if present.')])
self.writer.writerow([_('Only mandatory fields are necessary for new records. You can delete non-mandatory columns if you wish.')])
self.writer.writerow([_('For updating, you can update only selective columns.')])
self.writer.writerow([_('You can only upload upto 5000 records in one go. (may be less in some cases)')])
self.writer.writerow([""])

self.writer.writerow([""])
self.writer.writerow([_("Notes:")])
self.writer.writerow([_("Please do not change the template headings.")])
self.writer.writerow([_("First data column must be blank.")])
self.writer.writerow(
[_('If you are uploading new records, leave the "name" (ID) column blank.')]
)
self.writer.writerow(
[_('If you are uploading new records, "Naming Series" becomes mandatory, if present.')]
)
self.writer.writerow(
[
_(
"Only mandatory fields are necessary for new records. You can delete non-mandatory columns if you wish."
)
]
)
self.writer.writerow([_("For updating, you can update only selective columns.")])
self.writer.writerow(
[_("You can only upload upto 5000 records in one go. (may be less in some cases)")]
)
if self.name_field == "parent": if self.name_field == "parent":
self.writer.writerow([_('"Parent" signifies the parent table in which this row must be added')]) self.writer.writerow([_('"Parent" signifies the parent table in which this row must be added')])
self.writer.writerow([_('If you are updating, please select "Overwrite" else existing rows will not be deleted.')])
self.writer.writerow(
[_('If you are updating, please select "Overwrite" else existing rows will not be deleted.')]
)


def build_field_columns(self, dt, parentfield=None): def build_field_columns(self, dt, parentfield=None):
meta = frappe.get_meta(dt) meta = frappe.get_meta(dt)


# build list of valid docfields # build list of valid docfields
tablecolumns = [] tablecolumns = []
table_name = 'tab' + dt
table_name = "tab" + dt
for f in frappe.db.get_table_columns_description(table_name): for f in frappe.db.get_table_columns_description(table_name):
field = meta.get_field(f.name) field = meta.get_field(f.name)
if field and ((self.select_columns and f.name in self.select_columns[dt]) or not self.select_columns):
if field and (
(self.select_columns and f.name in self.select_columns[dt]) or not self.select_columns
):
tablecolumns.append(field) tablecolumns.append(field)


tablecolumns.sort(key = lambda a: int(a.idx))
tablecolumns.sort(key=lambda a: int(a.idx))


_column_start_end = frappe._dict(start=0) _column_start_end = frappe._dict(start=0)


if dt==self.doctype:
if (meta.get('autoname') and meta.get('autoname').lower()=='prompt') or (self.with_data):
if dt == self.doctype:
if (meta.get("autoname") and meta.get("autoname").lower() == "prompt") or (self.with_data):
self._append_name_column() self._append_name_column()


# if importing only child table for new record, add parent field # if importing only child table for new record, add parent field
if meta.get('istable') and not self.with_data:
self.append_field_column(frappe._dict({
"fieldname": "parent",
"parent": "",
"label": "Parent",
"fieldtype": "Data",
"reqd": 1,
"info": _("Parent is the name of the document to which the data will get added to.")
}), True)
if meta.get("istable") and not self.with_data:
self.append_field_column(
frappe._dict(
{
"fieldname": "parent",
"parent": "",
"label": "Parent",
"fieldtype": "Data",
"reqd": 1,
"info": _("Parent is the name of the document to which the data will get added to."),
}
),
True,
)


_column_start_end = frappe._dict(start=0) _column_start_end = frappe._dict(start=0)
else: else:
@@ -184,7 +240,7 @@ class DataExporter:
self.append_field_column(docfield, False) self.append_field_column(docfield, False)


# if there is one column, add a blank column (?) # if there is one column, add a blank column (?)
if len(self.columns)-_column_start_end.start == 1:
if len(self.columns) - _column_start_end.start == 1:
self.append_empty_field_column() self.append_empty_field_column()


# append DocType name # append DocType name
@@ -204,18 +260,21 @@ class DataExporter:
return return
if not for_mandatory and docfield.reqd: if not for_mandatory and docfield.reqd:
return return
if docfield.fieldname in ('parenttype', 'trash_reason'):
if docfield.fieldname in ("parenttype", "trash_reason"):
return return
if docfield.hidden: if docfield.hidden:
return return
if self.select_columns and docfield.fieldname not in self.select_columns.get(docfield.parent, []) \
and docfield.fieldname!="name":
if (
self.select_columns
and docfield.fieldname not in self.select_columns.get(docfield.parent, [])
and docfield.fieldname != "name"
):
return return


self.tablerow.append("") self.tablerow.append("")
self.fieldrow.append(docfield.fieldname) self.fieldrow.append(docfield.fieldname)
self.labelrow.append(_(docfield.label)) self.labelrow.append(_(docfield.label))
self.mandatoryrow.append(docfield.reqd and 'Yes' or 'No')
self.mandatoryrow.append(docfield.reqd and "Yes" or "No")
self.typerow.append(docfield.fieldtype) self.typerow.append(docfield.fieldtype)
self.inforow.append(self.getinforow(docfield)) self.inforow.append(self.getinforow(docfield))
self.columns.append(docfield.fieldname) self.columns.append(docfield.fieldname)
@@ -232,15 +291,15 @@ class DataExporter:
@staticmethod @staticmethod
def getinforow(docfield): def getinforow(docfield):
"""make info comment for options, links etc.""" """make info comment for options, links etc."""
if docfield.fieldtype == 'Select':
if docfield.fieldtype == "Select":
if not docfield.options: if not docfield.options:
return ''
return ""
else: else:
return _("One of") + ': %s' % ', '.join(filter(None, docfield.options.split('\n')))
elif docfield.fieldtype == 'Link':
return 'Valid %s' % docfield.options
elif docfield.fieldtype == 'Int':
return 'Integer'
return _("One of") + ": %s" % ", ".join(filter(None, docfield.options.split("\n")))
elif docfield.fieldtype == "Link":
return "Valid %s" % docfield.options
elif docfield.fieldtype == "Int":
return "Integer"
elif docfield.fieldtype == "Check": elif docfield.fieldtype == "Check":
return "0 or 1" return "0 or 1"
elif docfield.fieldtype in ["Date", "Datetime"]: elif docfield.fieldtype in ["Date", "Datetime"]:
@@ -248,7 +307,7 @@ class DataExporter:
elif hasattr(docfield, "info"): elif hasattr(docfield, "info"):
return docfield.info return docfield.info
else: else:
return ''
return ""


def add_field_headings(self): def add_field_headings(self):
self.writer.writerow(self.tablerow) self.writer.writerow(self.tablerow)
@@ -262,6 +321,7 @@ class DataExporter:


def add_data(self): def add_data(self):
from frappe.query_builder import DocType from frappe.query_builder import DocType

if self.template and not self.with_data: if self.template and not self.with_data:
return return


@@ -270,26 +330,28 @@ class DataExporter:
# sort nested set doctypes by `lft asc` # sort nested set doctypes by `lft asc`
order_by = None order_by = None
table_columns = frappe.db.get_table_columns(self.parent_doctype) table_columns = frappe.db.get_table_columns(self.parent_doctype)
if 'lft' in table_columns and 'rgt' in table_columns:
order_by = '`tab{doctype}`.`lft` asc'.format(doctype=self.parent_doctype)
if "lft" in table_columns and "rgt" in table_columns:
order_by = "`tab{doctype}`.`lft` asc".format(doctype=self.parent_doctype)
# get permitted data only # get permitted data only
self.data = frappe.get_list(self.doctype, fields=["*"], filters=self.filters, limit_page_length=None, order_by=order_by)
self.data = frappe.get_list(
self.doctype, fields=["*"], filters=self.filters, limit_page_length=None, order_by=order_by
)


for doc in self.data: for doc in self.data:
op = self.docs_to_export.get("op") op = self.docs_to_export.get("op")
names = self.docs_to_export.get("name") names = self.docs_to_export.get("name")


if names and op: if names and op:
if op == '=' and doc.name not in names:
if op == "=" and doc.name not in names:
continue continue
elif op == '!=' and doc.name in names:
elif op == "!=" and doc.name in names:
continue continue
elif names: elif names:
try: try:
sflags = self.docs_to_export.get("flags", "I,U").upper() sflags = self.docs_to_export.get("flags", "I,U").upper()
flags = 0 flags = 0
for a in re.split(r'\W+', sflags):
flags = flags | reflags.get(a,0)
for a in re.split(r"\W+", sflags):
flags = flags | reflags.get(a, 0)


c = re.compile(names, flags) c = re.compile(names, flags)
m = c.match(doc.name) m = c.match(doc.name)
@@ -315,7 +377,7 @@ class DataExporter:
.orderby(child_doctype_table.idx) .orderby(child_doctype_table.idx)
) )
for ci, child in enumerate(data_row.run(as_dict=True)): for ci, child in enumerate(data_row.run(as_dict=True)):
self.add_data_row(rows, c['doctype'], c['parentfield'], child, ci)
self.add_data_row(rows, c["doctype"], c["parentfield"], child, ci)


for row in rows: for row in rows:
self.writer.writerow(row) self.writer.writerow(row)
@@ -333,7 +395,7 @@ class DataExporter:
_column_start_end = self.column_start_end.get((dt, parentfield)) _column_start_end = self.column_start_end.get((dt, parentfield))


if _column_start_end: if _column_start_end:
for i, c in enumerate(self.columns[_column_start_end.start:_column_start_end.end]):
for i, c in enumerate(self.columns[_column_start_end.start : _column_start_end.end]):
df = meta.get_field(c) df = meta.get_field(c)
fieldtype = df.fieldtype if df else "Data" fieldtype = df.fieldtype if df else "Data"
value = d.get(c, "") value = d.get(c, "")
@@ -349,27 +411,33 @@ class DataExporter:


def build_response_as_excel(self): def build_response_as_excel(self):
filename = frappe.generate_hash("", 10) filename = frappe.generate_hash("", 10)
with open(filename, 'wb') as f:
f.write(cstr(self.writer.getvalue()).encode('utf-8'))
with open(filename, "wb") as f:
f.write(cstr(self.writer.getvalue()).encode("utf-8"))
f = open(filename) f = open(filename)
reader = csv.reader(f) reader = csv.reader(f)


from frappe.utils.xlsxutils import make_xlsx from frappe.utils.xlsxutils import make_xlsx
xlsx_file = make_xlsx(reader, "Data Import Template" if self.template else 'Data Export')

xlsx_file = make_xlsx(reader, "Data Import Template" if self.template else "Data Export")


f.close() f.close()
os.remove(filename) os.remove(filename)


# write out response as a xlsx type # write out response as a xlsx type
frappe.response['filename'] = self.doctype + '.xlsx'
frappe.response['filecontent'] = xlsx_file.getvalue()
frappe.response['type'] = 'binary'
frappe.response["filename"] = self.doctype + ".xlsx"
frappe.response["filecontent"] = xlsx_file.getvalue()
frappe.response["type"] = "binary"


def _append_name_column(self, dt=None): def _append_name_column(self, dt=None):
self.append_field_column(frappe._dict({
"fieldname": "name" if dt else self.name_field,
"parent": dt or "",
"label": "ID",
"fieldtype": "Data",
"reqd": 1,
}), True)
self.append_field_column(
frappe._dict(
{
"fieldname": "name" if dt else self.name_field,
"parent": dt or "",
"label": "ID",
"fieldtype": "Data",
"reqd": 1,
}
),
True,
)

+ 59
- 49
frappe/core/doctype/data_export/test_data_exporter.py 查看文件

@@ -2,13 +2,15 @@
# Copyright (c) 2019, Frappe Technologies and Contributors # Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import unittest import unittest

import frappe import frappe
from frappe.core.doctype.data_export.exporter import DataExporter from frappe.core.doctype.data_export.exporter import DataExporter



class TestDataExporter(unittest.TestCase): class TestDataExporter(unittest.TestCase):
def setUp(self): def setUp(self):
self.doctype_name = 'Test DocType for Export Tool'
self.doc_name = 'Test Data for Export Tool'
self.doctype_name = "Test DocType for Export Tool"
self.doc_name = "Test Data for Export Tool"
self.create_doctype_if_not_exists(doctype_name=self.doctype_name) self.create_doctype_if_not_exists(doctype_name=self.doctype_name)
self.create_test_data() self.create_test_data()


@@ -17,42 +19,49 @@ class TestDataExporter(unittest.TestCase):
Helper Function for setting up doctypes Helper Function for setting up doctypes
""" """
if force: if force:
frappe.delete_doc_if_exists('DocType', doctype_name)
frappe.delete_doc_if_exists('DocType', 'Child 1 of ' + doctype_name)
frappe.delete_doc_if_exists("DocType", doctype_name)
frappe.delete_doc_if_exists("DocType", "Child 1 of " + doctype_name)


if frappe.db.exists('DocType', doctype_name):
if frappe.db.exists("DocType", doctype_name):
return return


# Child Table 1 # Child Table 1
table_1_name = 'Child 1 of ' + doctype_name
frappe.get_doc({
'doctype': 'DocType',
'name': table_1_name,
'module': 'Custom',
'custom': 1,
'istable': 1,
'fields': [
{'label': 'Child Title', 'fieldname': 'child_title', 'reqd': 1, 'fieldtype': 'Data'},
{'label': 'Child Number', 'fieldname': 'child_number', 'fieldtype': 'Int'},
]
}).insert()
table_1_name = "Child 1 of " + doctype_name
frappe.get_doc(
{
"doctype": "DocType",
"name": table_1_name,
"module": "Custom",
"custom": 1,
"istable": 1,
"fields": [
{"label": "Child Title", "fieldname": "child_title", "reqd": 1, "fieldtype": "Data"},
{"label": "Child Number", "fieldname": "child_number", "fieldtype": "Int"},
],
}
).insert()


# Main Table # Main Table
frappe.get_doc({
'doctype': 'DocType',
'name': doctype_name,
'module': 'Custom',
'custom': 1,
'autoname': 'field:title',
'fields': [
{'label': 'Title', 'fieldname': 'title', 'reqd': 1, 'fieldtype': 'Data'},
{'label': 'Number', 'fieldname': 'number', 'fieldtype': 'Int'},
{'label': 'Table Field 1', 'fieldname': 'table_field_1', 'fieldtype': 'Table', 'options': table_1_name},
],
'permissions': [
{'role': 'System Manager'}
]
}).insert()
frappe.get_doc(
{
"doctype": "DocType",
"name": doctype_name,
"module": "Custom",
"custom": 1,
"autoname": "field:title",
"fields": [
{"label": "Title", "fieldname": "title", "reqd": 1, "fieldtype": "Data"},
{"label": "Number", "fieldname": "number", "fieldtype": "Int"},
{
"label": "Table Field 1",
"fieldname": "table_field_1",
"fieldtype": "Table",
"options": table_1_name,
},
],
"permissions": [{"role": "System Manager"}],
}
).insert()


def create_test_data(self, force=False): def create_test_data(self, force=False):
""" """
@@ -69,37 +78,38 @@ class TestDataExporter(unittest.TestCase):
table_field_1=[ table_field_1=[
{"child_title": "Child Title 1", "child_number": "50"}, {"child_title": "Child Title 1", "child_number": "50"},
{"child_title": "Child Title 2", "child_number": "51"}, {"child_title": "Child Title 2", "child_number": "51"},
]
],
).insert() ).insert()
else: else:
self.doc = frappe.get_doc(self.doctype_name, self.doc_name) self.doc = frappe.get_doc(self.doctype_name, self.doc_name)


def test_export_content(self): def test_export_content(self):
exp = DataExporter(doctype=self.doctype_name, file_type='CSV')
exp = DataExporter(doctype=self.doctype_name, file_type="CSV")
exp.build_response() exp.build_response()


self.assertEqual(frappe.response['type'],'csv')
self.assertEqual(frappe.response['doctype'], self.doctype_name)
self.assertTrue(frappe.response['result'])
self.assertIn('Child Title 1\",50',frappe.response['result'])
self.assertIn('Child Title 2\",51',frappe.response['result'])
self.assertEqual(frappe.response["type"], "csv")
self.assertEqual(frappe.response["doctype"], self.doctype_name)
self.assertTrue(frappe.response["result"])
self.assertIn('Child Title 1",50', frappe.response["result"])
self.assertIn('Child Title 2",51', frappe.response["result"])


def test_export_type(self): def test_export_type(self):
for type in ['csv', 'Excel']:
for type in ["csv", "Excel"]:
with self.subTest(type=type): with self.subTest(type=type):
exp = DataExporter(doctype=self.doctype_name, file_type=type) exp = DataExporter(doctype=self.doctype_name, file_type=type)
exp.build_response() exp.build_response()


self.assertEqual(frappe.response['doctype'], self.doctype_name)
self.assertTrue(frappe.response['result'])
self.assertEqual(frappe.response["doctype"], self.doctype_name)
self.assertTrue(frappe.response["result"])


if type == 'csv':
self.assertEqual(frappe.response['type'],'csv')
elif type == 'Excel':
self.assertEqual(frappe.response['type'],'binary')
self.assertEqual(frappe.response['filename'], self.doctype_name+'.xlsx') # 'Test DocType for Export Tool.xlsx')
self.assertTrue(frappe.response['filecontent'])
if type == "csv":
self.assertEqual(frappe.response["type"], "csv")
elif type == "Excel":
self.assertEqual(frappe.response["type"], "binary")
self.assertEqual(
frappe.response["filename"], self.doctype_name + ".xlsx"
) # 'Test DocType for Export Tool.xlsx')
self.assertTrue(frappe.response["filecontent"])


def tearDown(self): def tearDown(self):
pass pass


+ 25
- 30
frappe/core/doctype/data_import/data_import.py 查看文件

@@ -64,9 +64,7 @@ class DataImport(Document):
from frappe.utils.scheduler import is_scheduler_inactive from frappe.utils.scheduler import is_scheduler_inactive


if is_scheduler_inactive() and not frappe.flags.in_test: if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(
_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive")
)
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))


enqueued_jobs = [d.get("job_name") for d in get_info()] enqueued_jobs = [d.get("job_name") for d in get_info()]


@@ -100,6 +98,7 @@ def get_preview_from_template(data_import, import_file=None, google_sheets_url=N
import_file, google_sheets_url import_file, google_sheets_url
) )



@frappe.whitelist() @frappe.whitelist()
def form_start_import(data_import): def form_start_import(data_import):
return frappe.get_doc("Data Import", data_import).start_import() return frappe.get_doc("Data Import", data_import).start_import()
@@ -127,11 +126,11 @@ def download_template(
): ):
""" """
Download template from Exporter Download template from Exporter
:param doctype: Document Type
:param export_fields=None: Fields to export as dict {'Sales Invoice': ['name', 'customer'], 'Sales Invoice Item': ['item_code']}
:param export_records=None: One of 'all', 'by_filter', 'blank_template'
:param export_filters: Filter dict
:param file_type: File type to export into
:param doctype: Document Type
:param export_fields=None: Fields to export as dict {'Sales Invoice': ['name', 'customer'], 'Sales Invoice Item': ['item_code']}
:param export_records=None: One of 'all', 'by_filter', 'blank_template'
:param export_filters: Filter dict
:param file_type: File type to export into
""" """


export_fields = frappe.parse_json(export_fields) export_fields = frappe.parse_json(export_fields)
@@ -154,34 +153,38 @@ def download_errored_template(data_import_name):
data_import = frappe.get_doc("Data Import", data_import_name) data_import = frappe.get_doc("Data Import", data_import_name)
data_import.export_errored_rows() data_import.export_errored_rows()



@frappe.whitelist() @frappe.whitelist()
def download_import_log(data_import_name): def download_import_log(data_import_name):
data_import = frappe.get_doc("Data Import", data_import_name) data_import = frappe.get_doc("Data Import", data_import_name)
data_import.download_import_log() data_import.download_import_log()



@frappe.whitelist() @frappe.whitelist()
def get_import_status(data_import_name): def get_import_status(data_import_name):
import_status = {} import_status = {}


logs = frappe.get_all('Data Import Log', fields=['count(*) as count', 'success'],
filters={'data_import': data_import_name},
group_by='success')
logs = frappe.get_all(
"Data Import Log",
fields=["count(*) as count", "success"],
filters={"data_import": data_import_name},
group_by="success",
)


total_payload_count = frappe.db.get_value('Data Import', data_import_name, 'payload_count')
total_payload_count = frappe.db.get_value("Data Import", data_import_name, "payload_count")


for log in logs: for log in logs:
if log.get('success'):
import_status['success'] = log.get('count')
if log.get("success"):
import_status["success"] = log.get("count")
else: else:
import_status['failed'] = log.get('count')
import_status["failed"] = log.get("count")


import_status['total_records'] = total_payload_count
import_status["total_records"] = total_payload_count


return import_status return import_status


def import_file(
doctype, file_path, import_type, submit_after_import=False, console=False
):

def import_file(doctype, file_path, import_type, submit_after_import=False, console=False):
""" """
Import documents in from CSV or XLSX using data import. Import documents in from CSV or XLSX using data import.


@@ -198,9 +201,7 @@ def import_file(
"Insert New Records" if import_type.lower() == "insert" else "Update Existing Records" "Insert New Records" if import_type.lower() == "insert" else "Update Existing Records"
) )


i = Importer(
doctype=doctype, file_path=file_path, data_import=data_import, console=console
)
i = Importer(doctype=doctype, file_path=file_path, data_import=data_import, console=console)
i.import_data() i.import_data()




@@ -214,11 +215,7 @@ def import_doc(path, pre_process=None):
if f.endswith(".json"): if f.endswith(".json"):
frappe.flags.mute_emails = True frappe.flags.mute_emails = True
import_file_by_path( import_file_by_path(
f,
data_import=True,
force=True,
pre_process=pre_process,
reset_permissions=True
f, data_import=True, force=True, pre_process=pre_process, reset_permissions=True
) )
frappe.flags.mute_emails = False frappe.flags.mute_emails = False
frappe.db.commit() frappe.db.commit()
@@ -226,9 +223,7 @@ def import_doc(path, pre_process=None):
raise NotImplementedError("Only .json files can be imported") raise NotImplementedError("Only .json files can be imported")




def export_json(
doctype, path, filters=None, or_filters=None, name=None, order_by="creation asc"
):
def export_json(doctype, path, filters=None, or_filters=None, name=None, order_by="creation asc"):
def post_process(out): def post_process(out):
# Note on Tree DocTypes: # Note on Tree DocTypes:
# The tree structure is maintained in the database via the fields "lft" # The tree structure is maintained in the database via the fields "lft"


+ 10
- 17
frappe/core/doctype/data_import/exporter.py 查看文件

@@ -6,11 +6,8 @@ import typing


import frappe import frappe
from frappe import _ from frappe import _
from frappe.model import (
display_fieldtypes,
no_value_fields,
table_fields as table_fieldtypes,
)
from frappe.model import display_fieldtypes, no_value_fields
from frappe.model import table_fields as table_fieldtypes
from frappe.utils import flt, format_duration, groupby_metric from frappe.utils import flt, format_duration, groupby_metric
from frappe.utils.csvutils import build_csv_response from frappe.utils.csvutils import build_csv_response
from frappe.utils.xlsxutils import build_xlsx_response from frappe.utils.xlsxutils import build_xlsx_response
@@ -28,11 +25,11 @@ class Exporter:
): ):
""" """
Exports records of a DocType for use with Importer Exports records of a DocType for use with Importer
:param doctype: Document Type to export
:param export_fields=None: One of 'All', 'Mandatory' or {'DocType': ['field1', 'field2'], 'Child DocType': ['childfield1']}
:param export_data=False: Whether to export data as well
:param export_filters=None: The filters (dict or list) which is used to query the records
:param file_type: One of 'Excel' or 'CSV'
:param doctype: Document Type to export
:param export_fields=None: One of 'All', 'Mandatory' or {'DocType': ['field1', 'field2'], 'Child DocType': ['childfield1']}
:param export_data=False: Whether to export data as well
:param export_filters=None: The filters (dict or list) which is used to query the records
:param file_type: One of 'Excel' or 'CSV'
""" """
self.doctype = doctype self.doctype = doctype
self.meta = frappe.get_meta(doctype) self.meta = frappe.get_meta(doctype)
@@ -168,9 +165,7 @@ class Exporter:
else: else:
order_by = "`tab{0}`.`creation` DESC".format(self.doctype) order_by = "`tab{0}`.`creation` DESC".format(self.doctype)


parent_fields = [
format_column_name(df) for df in self.fields if df.parent == self.doctype
]
parent_fields = [format_column_name(df) for df in self.fields if df.parent == self.doctype]
parent_data = frappe.db.get_list( parent_data = frappe.db.get_list(
self.doctype, self.doctype,
filters=filters, filters=filters,
@@ -188,9 +183,7 @@ class Exporter:
child_table_df = self.meta.get_field(key) child_table_df = self.meta.get_field(key)
child_table_doctype = child_table_df.options child_table_doctype = child_table_df.options
child_fields = ["name", "idx", "parent", "parentfield"] + list( child_fields = ["name", "idx", "parent", "parentfield"] + list(
set(
[format_column_name(df) for df in self.fields if df.parent == child_table_doctype]
)
set([format_column_name(df) for df in self.fields if df.parent == child_table_doctype])
) )
data = frappe.db.get_all( data = frappe.db.get_all(
child_table_doctype, child_table_doctype,
@@ -261,4 +254,4 @@ class Exporter:
build_xlsx_response(self.get_csv_array_for_export(), _(self.doctype)) build_xlsx_response(self.get_csv_array_for_export(), _(self.doctype))


def group_children_data_by_parent(self, children_data: typing.Dict[str, list]): def group_children_data_by_parent(self, children_data: typing.Dict[str, list]):
return groupby_metric(children_data, key='parent')
return groupby_metric(children_data, key="parent")

+ 136
- 115
frappe/core/doctype/data_import/importer.py 查看文件

@@ -1,21 +1,23 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE


import os
import io import io
import frappe
import timeit
import json import json
from datetime import datetime, date
import os
import timeit
from datetime import date, datetime

import frappe
from frappe import _ from frappe import _
from frappe.utils import cint, flt, update_progress_bar, cstr, duration_to_seconds
from frappe.utils.csvutils import read_csv_content, get_csv_content_from_google_sheets
from frappe.core.doctype.version.version import get_diff
from frappe.model import no_value_fields
from frappe.model import table_fields as table_fieldtypes
from frappe.utils import cint, cstr, duration_to_seconds, flt, update_progress_bar
from frappe.utils.csvutils import get_csv_content_from_google_sheets, read_csv_content
from frappe.utils.xlsxutils import ( from frappe.utils.xlsxutils import (
read_xlsx_file_from_attached_file,
read_xls_file_from_attached_file, read_xls_file_from_attached_file,
read_xlsx_file_from_attached_file,
) )
from frappe.model import no_value_fields, table_fields as table_fieldtypes
from frappe.core.doctype.version.version import get_diff


INVALID_VALUES = ("", None) INVALID_VALUES = ("", None)
MAX_ROWS_IN_PREVIEW = 10 MAX_ROWS_IN_PREVIEW = 10
@@ -24,9 +26,7 @@ UPDATE = "Update Existing Records"




class Importer: class Importer:
def __init__(
self, doctype, data_import=None, file_path=None, import_type=None, console=False
):
def __init__(self, doctype, data_import=None, file_path=None, import_type=None, console=False):
self.doctype = doctype self.doctype = doctype
self.console = console self.console = console


@@ -49,9 +49,13 @@ class Importer:
def get_data_for_import_preview(self): def get_data_for_import_preview(self):
out = self.import_file.get_data_for_import_preview() out = self.import_file.get_data_for_import_preview()


out.import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success"],
out.import_log = frappe.db.get_all(
"Data Import Log",
fields=["row_indexes", "success"],
filters={"data_import": self.data_import.name}, filters={"data_import": self.data_import.name},
order_by="log_index", limit=10)
order_by="log_index",
limit=10,
)


return out return out


@@ -84,14 +88,23 @@ class Importer:
return return


# setup import log # setup import log
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "log_index"],
filters={"data_import": self.data_import.name},
order_by="log_index") or []
import_log = (
frappe.db.get_all(
"Data Import Log",
fields=["row_indexes", "success", "log_index"],
filters={"data_import": self.data_import.name},
order_by="log_index",
)
or []
)


log_index = 0 log_index = 0


# Do not remove rows in case of retry after an error or pending data import # Do not remove rows in case of retry after an error or pending data import
if self.data_import.status == "Partial Success" and len(import_log) >= self.data_import.payload_count:
if (
self.data_import.status == "Partial Success"
and len(import_log) >= self.data_import.payload_count
):
# remove previous failures from import log only in case of retry after partial success # remove previous failures from import log only in case of retry after partial success
import_log = [log for log in import_log if log.get("success")] import_log = [log for log in import_log if log.get("success")]


@@ -108,9 +121,7 @@ class Importer:
total_payload_count = len(payloads) total_payload_count = len(payloads)
batch_size = frappe.conf.data_import_batch_size or 1000 batch_size = frappe.conf.data_import_batch_size or 1000


for batch_index, batched_payloads in enumerate(
frappe.utils.create_batch(payloads, batch_size)
):
for batch_index, batched_payloads in enumerate(frappe.utils.create_batch(payloads, batch_size)):
for i, payload in enumerate(batched_payloads): for i, payload in enumerate(batched_payloads):
doc = payload.doc doc = payload.doc
row_indexes = [row.row_number for row in payload.rows] row_indexes = [row.row_number for row in payload.rows]
@@ -156,11 +167,11 @@ class Importer:
}, },
) )


create_import_log(self.data_import.name, log_index, {
'success': True,
'docname': doc.name,
'row_indexes': row_indexes
})
create_import_log(
self.data_import.name,
log_index,
{"success": True, "docname": doc.name, "row_indexes": row_indexes},
)


log_index += 1 log_index += 1


@@ -177,19 +188,29 @@ class Importer:
# rollback if exception # rollback if exception
frappe.db.rollback() frappe.db.rollback()


create_import_log(self.data_import.name, log_index, {
'success': False,
'exception': frappe.get_traceback(),
'messages': messages,
'row_indexes': row_indexes
})
create_import_log(
self.data_import.name,
log_index,
{
"success": False,
"exception": frappe.get_traceback(),
"messages": messages,
"row_indexes": row_indexes,
},
)


log_index += 1 log_index += 1


# Logs are db inserted directly so will have to be fetched again # Logs are db inserted directly so will have to be fetched again
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "log_index"],
filters={"data_import": self.data_import.name},
order_by="log_index") or []
import_log = (
frappe.db.get_all(
"Data Import Log",
fields=["row_indexes", "success", "log_index"],
filters={"data_import": self.data_import.name},
order_by="log_index",
)
or []
)


# set status # set status
failures = [log for log in import_log if not log.get("success")] failures = [log for log in import_log if not log.get("success")]
@@ -274,9 +295,15 @@ class Importer:
if not self.data_import: if not self.data_import:
return return


import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success"],
filters={"data_import": self.data_import.name},
order_by="log_index") or []
import_log = (
frappe.db.get_all(
"Data Import Log",
fields=["row_indexes", "success"],
filters={"data_import": self.data_import.name},
order_by="log_index",
)
or []
)


failures = [log for log in import_log if not log.get("success")] failures = [log for log in import_log if not log.get("success")]
row_indexes = [] row_indexes = []
@@ -299,9 +326,12 @@ class Importer:
if not self.data_import: if not self.data_import:
return return


import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "messages", "exception", "docname"],
import_log = frappe.db.get_all(
"Data Import Log",
fields=["row_indexes", "success", "messages", "exception", "docname"],
filters={"data_import": self.data_import.name}, filters={"data_import": self.data_import.name},
order_by="log_index")
order_by="log_index",
)


header_row = ["Row Numbers", "Status", "Message", "Exception"] header_row = ["Row Numbers", "Status", "Message", "Exception"]


@@ -309,10 +339,13 @@ class Importer:


for log in import_log: for log in import_log:
row_number = json.loads(log.get("row_indexes"))[0] row_number = json.loads(log.get("row_indexes"))[0]
status = "Success" if log.get('success') else "Failure"
message = "Successfully Imported {0}".format(log.get('docname')) if log.get('success') else \
log.get("messages")
exception = frappe.utils.cstr(log.get("exception", ''))
status = "Success" if log.get("success") else "Failure"
message = (
"Successfully Imported {0}".format(log.get("docname"))
if log.get("success")
else log.get("messages")
)
exception = frappe.utils.cstr(log.get("exception", ""))
rows += [[row_number, status, message, exception]] rows += [[row_number, status, message, exception]]


build_csv_response(rows, self.doctype) build_csv_response(rows, self.doctype)
@@ -324,9 +357,7 @@ class Importer:
if successful_records: if successful_records:
print() print()
print( print(
"Successfully imported {0} records out of {1}".format(
len(successful_records), len(import_log)
)
"Successfully imported {0} records out of {1}".format(len(successful_records), len(import_log))
) )


if failed_records: if failed_records:
@@ -363,9 +394,7 @@ class Importer:
class ImportFile: class ImportFile:
def __init__(self, doctype, file, template_options=None, import_type=None): def __init__(self, doctype, file, template_options=None, import_type=None):
self.doctype = doctype self.doctype = doctype
self.template_options = template_options or frappe._dict(
column_to_field_map=frappe._dict()
)
self.template_options = template_options or frappe._dict(column_to_field_map=frappe._dict())
self.column_to_field_map = self.template_options.column_to_field_map self.column_to_field_map = self.template_options.column_to_field_map
self.import_type = import_type self.import_type = import_type
self.warnings = [] self.warnings = []
@@ -556,9 +585,7 @@ class ImportFile:
def read_content(self, content, extension): def read_content(self, content, extension):
error_title = _("Template Error") error_title = _("Template Error")
if extension not in ("csv", "xlsx", "xls"): if extension not in ("csv", "xlsx", "xls"):
frappe.throw(
_("Import template should be of type .csv, .xlsx or .xls"), title=error_title
)
frappe.throw(_("Import template should be of type .csv, .xlsx or .xls"), title=error_title)


if extension == "csv": if extension == "csv":
data = read_csv_content(content) data = read_csv_content(content)
@@ -587,12 +614,13 @@ class Row:
if len_row != len_columns: if len_row != len_columns:
less_than_columns = len_row < len_columns less_than_columns = len_row < len_columns
message = ( message = (
"Row has less values than columns"
if less_than_columns
else "Row has more values than columns"
"Row has less values than columns" if less_than_columns else "Row has more values than columns"
) )
self.warnings.append( self.warnings.append(
{"row": self.row_number, "message": message,}
{
"row": self.row_number,
"message": message,
}
) )


def parse_doc(self, doctype, parent_doc=None, table_df=None): def parse_doc(self, doctype, parent_doc=None, table_df=None):
@@ -662,18 +690,24 @@ class Row:
options_string = ", ".join(frappe.bold(d) for d in select_options) options_string = ", ".join(frappe.bold(d) for d in select_options)
msg = _("Value must be one of {0}").format(options_string) msg = _("Value must be one of {0}").format(options_string)
self.warnings.append( self.warnings.append(
{"row": self.row_number, "field": df_as_json(df), "message": msg,}
{
"row": self.row_number,
"field": df_as_json(df),
"message": msg,
}
) )
return return


elif df.fieldtype == "Link": elif df.fieldtype == "Link":
exists = self.link_exists(value, df) exists = self.link_exists(value, df)
if not exists: if not exists:
msg = _("Value {0} missing for {1}").format(
frappe.bold(value), frappe.bold(df.options)
)
msg = _("Value {0} missing for {1}").format(frappe.bold(value), frappe.bold(df.options))
self.warnings.append( self.warnings.append(
{"row": self.row_number, "field": df_as_json(df), "message": msg,}
{
"row": self.row_number,
"field": df_as_json(df),
"message": msg,
}
) )
return return
elif df.fieldtype in ["Date", "Datetime"]: elif df.fieldtype in ["Date", "Datetime"]:
@@ -693,6 +727,7 @@ class Row:
return return
elif df.fieldtype == "Duration": elif df.fieldtype == "Duration":
import re import re

is_valid_duration = re.match(r"^(?:(\d+d)?((^|\s)\d+h)?((^|\s)\d+m)?((^|\s)\d+s)?)$", value) is_valid_duration = re.match(r"^(?:(\d+d)?((^|\s)\d+h)?((^|\s)\d+m)?((^|\s)\d+s)?)$", value)
if not is_valid_duration: if not is_valid_duration:
self.warnings.append( self.warnings.append(
@@ -702,7 +737,7 @@ class Row:
"field": df_as_json(df), "field": df_as_json(df),
"message": _("Value {0} must be in the valid duration format: d h m s").format( "message": _("Value {0} must be in the valid duration format: d h m s").format(
frappe.bold(value) frappe.bold(value)
)
),
} }
) )


@@ -789,9 +824,7 @@ class Header(Row):
else: else:
doctypes.append((col.df.parent, col.df.child_table_df)) doctypes.append((col.df.parent, col.df.child_table_df))


self.doctypes = sorted(
list(set(doctypes)), key=lambda x: -1 if x[0] == self.doctype else 1
)
self.doctypes = sorted(list(set(doctypes)), key=lambda x: -1 if x[0] == self.doctype else 1)


def get_column_indexes(self, doctype, tablefield=None): def get_column_indexes(self, doctype, tablefield=None):
def is_table_field(df): def is_table_field(df):
@@ -802,10 +835,7 @@ class Header(Row):
return [ return [
col.index col.index
for col in self.columns for col in self.columns
if not col.skip_import
and col.df
and col.df.parent == doctype
and is_table_field(col.df)
if not col.skip_import and col.df and col.df.parent == doctype and is_table_field(col.df)
] ]


def get_columns(self, indexes): def get_columns(self, indexes):
@@ -893,9 +923,7 @@ class Column:
self.warnings.append( self.warnings.append(
{ {
"col": column_number, "col": column_number,
"message": _("Cannot match column {0} with any field").format(
frappe.bold(header_title)
),
"message": _("Cannot match column {0} with any field").format(frappe.bold(header_title)),
"type": "info", "type": "info",
} }
) )
@@ -958,9 +986,7 @@ class Column:
if self.df.fieldtype == "Link": if self.df.fieldtype == "Link":
# find all values that dont exist # find all values that dont exist
values = list({cstr(v) for v in self.column_values[1:] if v}) values = list({cstr(v) for v in self.column_values[1:] if v})
exists = [
d.name for d in frappe.db.get_all(self.df.options, filters={"name": ("in", values)})
]
exists = [d.name for d in frappe.db.get_all(self.df.options, filters={"name": ("in", values)})]
not_exists = list(set(values) - set(exists)) not_exists = list(set(values) - set(exists))
if not_exists: if not_exists:
missing_values = ", ".join(not_exists) missing_values = ", ".join(not_exists)
@@ -968,9 +994,7 @@ class Column:
{ {
"col": self.column_number, "col": self.column_number,
"message": ( "message": (
"The following values do not exist for {}: {}".format(
self.df.options, missing_values
)
"The following values do not exist for {}: {}".format(self.df.options, missing_values)
), ),
"type": "warning", "type": "warning",
} }
@@ -983,7 +1007,9 @@ class Column:
self.warnings.append( self.warnings.append(
{ {
"col": self.column_number, "col": self.column_number,
"message": _("Date format could not be determined from the values in this column. Defaulting to yyyy-mm-dd."),
"message": _(
"Date format could not be determined from the values in this column. Defaulting to yyyy-mm-dd."
),
"type": "info", "type": "info",
} }
) )
@@ -1027,12 +1053,12 @@ def build_fields_dict_for_column_matching(parent_doctype):
Build a dict with various keys to match with column headers and value as docfield Build a dict with various keys to match with column headers and value as docfield
The keys can be label or fieldname The keys can be label or fieldname
{ {
'Customer': df1,
'customer': df1,
'Due Date': df2,
'due_date': df2,
'Item Code (Sales Invoice Item)': df3,
'Sales Invoice Item:item_code': df3,
'Customer': df1,
'customer': df1,
'Due Date': df2,
'due_date': df2,
'Item Code (Sales Invoice Item)': df3,
'Sales Invoice Item:item_code': df3,
} }
""" """


@@ -1062,9 +1088,7 @@ def build_fields_dict_for_column_matching(parent_doctype):
out = {} out = {}


# doctypes and fieldname if it is a child doctype # doctypes and fieldname if it is a child doctype
doctypes = [(parent_doctype, None)] + [
(df.options, df) for df in parent_meta.get_table_fields()
]
doctypes = [(parent_doctype, None)] + [(df.options, df) for df in parent_meta.get_table_fields()]


for doctype, table_df in doctypes: for doctype, table_df in doctypes:
translated_table_label = _(table_df.label) if table_df else None translated_table_label = _(table_df.label) if table_df else None
@@ -1082,15 +1106,15 @@ def build_fields_dict_for_column_matching(parent_doctype):


if doctype == parent_doctype: if doctype == parent_doctype:
name_headers = ( name_headers = (
"name", # fieldname
"ID", # label
_("ID"), # translated label
"name", # fieldname
"ID", # label
_("ID"), # translated label
) )
else: else:
name_headers = ( name_headers = (
"{0}.name".format(table_df.fieldname), # fieldname
"ID ({0})".format(table_df.label), # label
"{0} ({1})".format(_("ID"), translated_table_label), # translated label
"{0}.name".format(table_df.fieldname), # fieldname
"ID ({0})".format(table_df.label), # label
"{0} ({1})".format(_("ID"), translated_table_label), # translated label
) )


name_df.is_child_table_field = True name_df.is_child_table_field = True
@@ -1122,7 +1146,7 @@ def build_fields_dict_for_column_matching(parent_doctype):
for header in ( for header in (
df.fieldname, df.fieldname,
f"{label} ({df.fieldname})", f"{label} ({df.fieldname})",
f"{translated_label} ({df.fieldname})"
f"{translated_label} ({df.fieldname})",
): ):
out[header] = df out[header] = df


@@ -1155,9 +1179,8 @@ def build_fields_dict_for_column_matching(parent_doctype):
autoname_field = get_autoname_field(parent_doctype) autoname_field = get_autoname_field(parent_doctype)
if autoname_field: if autoname_field:
for header in ( for header in (
"ID ({})".format(autoname_field.label), # label
"{0} ({1})".format(_("ID"), _(autoname_field.label)), # translated label

"ID ({})".format(autoname_field.label), # label
"{0} ({1})".format(_("ID"), _(autoname_field.label)), # translated label
# ID field should also map to the autoname field # ID field should also map to the autoname field
"ID", "ID",
_("ID"), _("ID"),
@@ -1205,10 +1228,7 @@ def get_item_at_index(_list, i, default=None):


def get_user_format(date_format): def get_user_format(date_format):
return ( return (
date_format.replace("%Y", "yyyy")
.replace("%y", "yy")
.replace("%m", "mm")
.replace("%d", "dd")
date_format.replace("%Y", "yyyy").replace("%y", "yy").replace("%m", "mm").replace("%d", "dd")
) )




@@ -1226,16 +1246,17 @@ def df_as_json(df):
def get_select_options(df): def get_select_options(df):
return [d for d in (df.options or "").split("\n") if d] return [d for d in (df.options or "").split("\n") if d]


def create_import_log(data_import, log_index, log_details):
frappe.get_doc({
'doctype': 'Data Import Log',
'log_index': log_index,
'success': log_details.get('success'),
'data_import': data_import,
'row_indexes': json.dumps(log_details.get('row_indexes')),
'docname': log_details.get('docname'),
'messages': json.dumps(log_details.get('messages', '[]')),
'exception': log_details.get('exception')
}).db_insert()



def create_import_log(data_import, log_index, log_details):
frappe.get_doc(
{
"doctype": "Data Import Log",
"log_index": log_index,
"success": log_details.get("success"),
"data_import": data_import,
"row_indexes": json.dumps(log_details.get("row_indexes")),
"docname": log_details.get("docname"),
"messages": json.dumps(log_details.get("messages", "[]")),
"exception": log_details.get("exception"),
}
).db_insert()

+ 1
- 0
frappe/core/doctype/data_import/test_data_import.py 查看文件

@@ -4,5 +4,6 @@
# import frappe # import frappe
import unittest import unittest



class TestDataImport(unittest.TestCase): class TestDataImport(unittest.TestCase):
pass pass

+ 8
- 8
frappe/core/doctype/data_import/test_exporter.py 查看文件

@@ -2,13 +2,13 @@
# Copyright (c) 2019, Frappe Technologies and Contributors # Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import unittest import unittest

import frappe import frappe
from frappe.core.doctype.data_import.exporter import Exporter from frappe.core.doctype.data_import.exporter import Exporter
from frappe.core.doctype.data_import.test_importer import (
create_doctype_if_not_exists,
)
from frappe.core.doctype.data_import.test_importer import create_doctype_if_not_exists
doctype_name = "DocType for Export"


doctype_name = 'DocType for Export'


class TestExporter(unittest.TestCase): class TestExporter(unittest.TestCase):
def setUp(self): def setUp(self):
@@ -93,10 +93,10 @@ class TestExporter(unittest.TestCase):
doctype_name, doctype_name,
export_fields={doctype_name: ["title", "description"]}, export_fields={doctype_name: ["title", "description"]},
export_data=True, export_data=True,
file_type="CSV"
file_type="CSV",
) )
e.build_response() e.build_response()


self.assertTrue(frappe.response['result'])
self.assertEqual(frappe.response['doctype'], doctype_name)
self.assertEqual(frappe.response['type'], "csv")
self.assertTrue(frappe.response["result"])
self.assertEqual(frappe.response["doctype"], doctype_name)
self.assertEqual(frappe.response["type"], "csv")

+ 144
- 105
frappe/core/doctype/data_import/test_importer.py 查看文件

@@ -2,53 +2,57 @@
# Copyright (c) 2019, Frappe Technologies and Contributors # Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import unittest import unittest

import frappe import frappe
from frappe.core.doctype.data_import.importer import Importer from frappe.core.doctype.data_import.importer import Importer
from frappe.tests.test_query_builder import db_type_is, run_only_if from frappe.tests.test_query_builder import db_type_is, run_only_if
from frappe.utils import getdate, format_duration
from frappe.utils import format_duration, getdate

doctype_name = "DocType for Import"


doctype_name = 'DocType for Import'


class TestImporter(unittest.TestCase): class TestImporter(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
create_doctype_if_not_exists(doctype_name,)
create_doctype_if_not_exists(
doctype_name,
)


def test_data_import_from_file(self): def test_data_import_from_file(self):
import_file = get_import_file('sample_import_file')
import_file = get_import_file("sample_import_file")
data_import = self.get_importer(doctype_name, import_file) data_import = self.get_importer(doctype_name, import_file)
data_import.start_import() data_import.start_import()


doc1 = frappe.get_doc(doctype_name, 'Test')
doc2 = frappe.get_doc(doctype_name, 'Test 2')
doc3 = frappe.get_doc(doctype_name, 'Test 3')
doc1 = frappe.get_doc(doctype_name, "Test")
doc2 = frappe.get_doc(doctype_name, "Test 2")
doc3 = frappe.get_doc(doctype_name, "Test 3")


self.assertEqual(doc1.description, 'test description')
self.assertEqual(doc1.description, "test description")
self.assertEqual(doc1.number, 1) self.assertEqual(doc1.number, 1)
self.assertEqual(format_duration(doc1.duration), '3h')
self.assertEqual(format_duration(doc1.duration), "3h")


self.assertEqual(doc1.table_field_1[0].child_title, 'child title')
self.assertEqual(doc1.table_field_1[0].child_description, 'child description')
self.assertEqual(doc1.table_field_1[0].child_title, "child title")
self.assertEqual(doc1.table_field_1[0].child_description, "child description")


self.assertEqual(doc1.table_field_1[1].child_title, 'child title 2')
self.assertEqual(doc1.table_field_1[1].child_description, 'child description 2')
self.assertEqual(doc1.table_field_1[1].child_title, "child title 2")
self.assertEqual(doc1.table_field_1[1].child_description, "child description 2")


self.assertEqual(doc1.table_field_2[1].child_2_title, 'title child')
self.assertEqual(doc1.table_field_2[1].child_2_date, getdate('2019-10-30'))
self.assertEqual(doc1.table_field_2[1].child_2_title, "title child")
self.assertEqual(doc1.table_field_2[1].child_2_date, getdate("2019-10-30"))
self.assertEqual(doc1.table_field_2[1].child_2_another_number, 5) self.assertEqual(doc1.table_field_2[1].child_2_another_number, 5)


self.assertEqual(doc1.table_field_1_again[0].child_title, 'child title again')
self.assertEqual(doc1.table_field_1_again[1].child_title, 'child title again 2')
self.assertEqual(doc1.table_field_1_again[1].child_date, getdate('2021-09-22'))
self.assertEqual(doc1.table_field_1_again[0].child_title, "child title again")
self.assertEqual(doc1.table_field_1_again[1].child_title, "child title again 2")
self.assertEqual(doc1.table_field_1_again[1].child_date, getdate("2021-09-22"))


self.assertEqual(doc2.description, 'test description 2')
self.assertEqual(format_duration(doc2.duration), '4d 3h')
self.assertEqual(doc2.description, "test description 2")
self.assertEqual(format_duration(doc2.duration), "4d 3h")


self.assertEqual(doc3.another_number, 5) self.assertEqual(doc3.another_number, 5)
self.assertEqual(format_duration(doc3.duration), '5d 5h 45m')
self.assertEqual(format_duration(doc3.duration), "5d 5h 45m")


def test_data_import_preview(self): def test_data_import_preview(self):
import_file = get_import_file('sample_import_file')
import_file = get_import_file("sample_import_file")
data_import = self.get_importer(doctype_name, import_file) data_import = self.get_importer(doctype_name, import_file)
preview = data_import.get_preview_from_template() preview = data_import.get_preview_from_template()


@@ -58,35 +62,49 @@ class TestImporter(unittest.TestCase):
# ignored on postgres because myisam doesn't exist on pg # ignored on postgres because myisam doesn't exist on pg
@run_only_if(db_type_is.MARIADB) @run_only_if(db_type_is.MARIADB)
def test_data_import_without_mandatory_values(self): def test_data_import_without_mandatory_values(self):
import_file = get_import_file('sample_import_file_without_mandatory')
import_file = get_import_file("sample_import_file_without_mandatory")
data_import = self.get_importer(doctype_name, import_file) data_import = self.get_importer(doctype_name, import_file)
frappe.local.message_log = [] frappe.local.message_log = []
data_import.start_import() data_import.start_import()
data_import.reload() data_import.reload()


import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "messages", "exception", "docname"],
import_log = frappe.db.get_all(
"Data Import Log",
fields=["row_indexes", "success", "messages", "exception", "docname"],
filters={"data_import": data_import.name}, filters={"data_import": data_import.name},
order_by="log_index")
order_by="log_index",
)


self.assertEqual(frappe.parse_json(import_log[0]['row_indexes']), [2,3])
expected_error = "Error: <strong>Child 1 of DocType for Import</strong> Row #1: Value missing for: Child Title"
self.assertEqual(frappe.parse_json(frappe.parse_json(import_log[0]['messages'])[0])['message'], expected_error)
expected_error = "Error: <strong>Child 1 of DocType for Import</strong> Row #2: Value missing for: Child Title"
self.assertEqual(frappe.parse_json(frappe.parse_json(import_log[0]['messages'])[1])['message'], expected_error)
self.assertEqual(frappe.parse_json(import_log[0]["row_indexes"]), [2, 3])
expected_error = (
"Error: <strong>Child 1 of DocType for Import</strong> Row #1: Value missing for: Child Title"
)
self.assertEqual(
frappe.parse_json(frappe.parse_json(import_log[0]["messages"])[0])["message"], expected_error
)
expected_error = (
"Error: <strong>Child 1 of DocType for Import</strong> Row #2: Value missing for: Child Title"
)
self.assertEqual(
frappe.parse_json(frappe.parse_json(import_log[0]["messages"])[1])["message"], expected_error
)


self.assertEqual(frappe.parse_json(import_log[1]['row_indexes']), [4])
self.assertEqual(frappe.parse_json(frappe.parse_json(import_log[1]['messages'])[0])['message'], "Title is required")
self.assertEqual(frappe.parse_json(import_log[1]["row_indexes"]), [4])
self.assertEqual(
frappe.parse_json(frappe.parse_json(import_log[1]["messages"])[0])["message"],
"Title is required",
)


def test_data_import_update(self): def test_data_import_update(self):
existing_doc = frappe.get_doc( existing_doc = frappe.get_doc(
doctype=doctype_name, doctype=doctype_name,
title=frappe.generate_hash(doctype_name, 8), title=frappe.generate_hash(doctype_name, 8),
table_field_1=[{'child_title': 'child title to update'}]
table_field_1=[{"child_title": "child title to update"}],
) )
existing_doc.save() existing_doc.save()
frappe.db.commit() frappe.db.commit()


import_file = get_import_file('sample_import_file_for_update')
import_file = get_import_file("sample_import_file_for_update")
data_import = self.get_importer(doctype_name, import_file, update=True) data_import = self.get_importer(doctype_name, import_file, update=True)
i = Importer(data_import.reference_doctype, data_import=data_import) i = Importer(data_import.reference_doctype, data_import=data_import)


@@ -104,15 +122,15 @@ class TestImporter(unittest.TestCase):


updated_doc = frappe.get_doc(doctype_name, existing_doc.name) updated_doc = frappe.get_doc(doctype_name, existing_doc.name)
self.assertEqual(existing_doc.title, updated_doc.title) self.assertEqual(existing_doc.title, updated_doc.title)
self.assertEqual(updated_doc.description, 'test description')
self.assertEqual(updated_doc.table_field_1[0].child_title, 'child title')
self.assertEqual(updated_doc.description, "test description")
self.assertEqual(updated_doc.table_field_1[0].child_title, "child title")
self.assertEqual(updated_doc.table_field_1[0].name, existing_doc.table_field_1[0].name) self.assertEqual(updated_doc.table_field_1[0].name, existing_doc.table_field_1[0].name)
self.assertEqual(updated_doc.table_field_1[0].child_description, 'child description')
self.assertEqual(updated_doc.table_field_1_again[0].child_title, 'child title again')
self.assertEqual(updated_doc.table_field_1[0].child_description, "child description")
self.assertEqual(updated_doc.table_field_1_again[0].child_title, "child title again")


def get_importer(self, doctype, import_file, update=False): def get_importer(self, doctype, import_file, update=False):
data_import = frappe.new_doc('Data Import')
data_import.import_type = 'Insert New Records' if not update else 'Update Existing Records'
data_import = frappe.new_doc("Data Import")
data_import.import_type = "Insert New Records" if not update else "Update Existing Records"
data_import.reference_doctype = doctype data_import.reference_doctype = doctype
data_import.import_file = import_file.file_url data_import.import_file = import_file.file_url
data_import.insert() data_import.insert()
@@ -121,88 +139,109 @@ class TestImporter(unittest.TestCase):


return data_import return data_import



def create_doctype_if_not_exists(doctype_name, force=False): def create_doctype_if_not_exists(doctype_name, force=False):
if force: if force:
frappe.delete_doc_if_exists('DocType', doctype_name)
frappe.delete_doc_if_exists('DocType', 'Child 1 of ' + doctype_name)
frappe.delete_doc_if_exists('DocType', 'Child 2 of ' + doctype_name)
frappe.delete_doc_if_exists("DocType", doctype_name)
frappe.delete_doc_if_exists("DocType", "Child 1 of " + doctype_name)
frappe.delete_doc_if_exists("DocType", "Child 2 of " + doctype_name)


if frappe.db.exists('DocType', doctype_name):
if frappe.db.exists("DocType", doctype_name):
return return


# Child Table 1 # Child Table 1
table_1_name = 'Child 1 of ' + doctype_name
frappe.get_doc({
'doctype': 'DocType',
'name': table_1_name,
'module': 'Custom',
'custom': 1,
'istable': 1,
'fields': [
{'label': 'Child Title', 'fieldname': 'child_title', 'reqd': 1, 'fieldtype': 'Data'},
{'label': 'Child Description', 'fieldname': 'child_description', 'fieldtype': 'Small Text'},
{'label': 'Child Date', 'fieldname': 'child_date', 'fieldtype': 'Date'},
{'label': 'Child Number', 'fieldname': 'child_number', 'fieldtype': 'Int'},
{'label': 'Child Number', 'fieldname': 'child_another_number', 'fieldtype': 'Int'},
]
}).insert()
table_1_name = "Child 1 of " + doctype_name
frappe.get_doc(
{
"doctype": "DocType",
"name": table_1_name,
"module": "Custom",
"custom": 1,
"istable": 1,
"fields": [
{"label": "Child Title", "fieldname": "child_title", "reqd": 1, "fieldtype": "Data"},
{"label": "Child Description", "fieldname": "child_description", "fieldtype": "Small Text"},
{"label": "Child Date", "fieldname": "child_date", "fieldtype": "Date"},
{"label": "Child Number", "fieldname": "child_number", "fieldtype": "Int"},
{"label": "Child Number", "fieldname": "child_another_number", "fieldtype": "Int"},
],
}
).insert()


# Child Table 2 # Child Table 2
table_2_name = 'Child 2 of ' + doctype_name
frappe.get_doc({
'doctype': 'DocType',
'name': table_2_name,
'module': 'Custom',
'custom': 1,
'istable': 1,
'fields': [
{'label': 'Child 2 Title', 'fieldname': 'child_2_title', 'reqd': 1, 'fieldtype': 'Data'},
{'label': 'Child 2 Description', 'fieldname': 'child_2_description', 'fieldtype': 'Small Text'},
{'label': 'Child 2 Date', 'fieldname': 'child_2_date', 'fieldtype': 'Date'},
{'label': 'Child 2 Number', 'fieldname': 'child_2_number', 'fieldtype': 'Int'},
{'label': 'Child 2 Number', 'fieldname': 'child_2_another_number', 'fieldtype': 'Int'},
]
}).insert()
table_2_name = "Child 2 of " + doctype_name
frappe.get_doc(
{
"doctype": "DocType",
"name": table_2_name,
"module": "Custom",
"custom": 1,
"istable": 1,
"fields": [
{"label": "Child 2 Title", "fieldname": "child_2_title", "reqd": 1, "fieldtype": "Data"},
{
"label": "Child 2 Description",
"fieldname": "child_2_description",
"fieldtype": "Small Text",
},
{"label": "Child 2 Date", "fieldname": "child_2_date", "fieldtype": "Date"},
{"label": "Child 2 Number", "fieldname": "child_2_number", "fieldtype": "Int"},
{"label": "Child 2 Number", "fieldname": "child_2_another_number", "fieldtype": "Int"},
],
}
).insert()


# Main Table # Main Table
frappe.get_doc({
'doctype': 'DocType',
'name': doctype_name,
'module': 'Custom',
'custom': 1,
'autoname': 'field:title',
'fields': [
{'label': 'Title', 'fieldname': 'title', 'reqd': 1, 'fieldtype': 'Data'},
{'label': 'Description', 'fieldname': 'description', 'fieldtype': 'Small Text'},
{'label': 'Date', 'fieldname': 'date', 'fieldtype': 'Date'},
{'label': 'Duration', 'fieldname': 'duration', 'fieldtype': 'Duration'},
{'label': 'Number', 'fieldname': 'number', 'fieldtype': 'Int'},
{'label': 'Number', 'fieldname': 'another_number', 'fieldtype': 'Int'},
{'label': 'Table Field 1', 'fieldname': 'table_field_1', 'fieldtype': 'Table', 'options': table_1_name},
{'label': 'Table Field 2', 'fieldname': 'table_field_2', 'fieldtype': 'Table', 'options': table_2_name},
{'label': 'Table Field 1 Again', 'fieldname': 'table_field_1_again', 'fieldtype': 'Table', 'options': table_1_name},
],
'permissions': [
{'role': 'System Manager'}
]
}).insert()
frappe.get_doc(
{
"doctype": "DocType",
"name": doctype_name,
"module": "Custom",
"custom": 1,
"autoname": "field:title",
"fields": [
{"label": "Title", "fieldname": "title", "reqd": 1, "fieldtype": "Data"},
{"label": "Description", "fieldname": "description", "fieldtype": "Small Text"},
{"label": "Date", "fieldname": "date", "fieldtype": "Date"},
{"label": "Duration", "fieldname": "duration", "fieldtype": "Duration"},
{"label": "Number", "fieldname": "number", "fieldtype": "Int"},
{"label": "Number", "fieldname": "another_number", "fieldtype": "Int"},
{
"label": "Table Field 1",
"fieldname": "table_field_1",
"fieldtype": "Table",
"options": table_1_name,
},
{
"label": "Table Field 2",
"fieldname": "table_field_2",
"fieldtype": "Table",
"options": table_2_name,
},
{
"label": "Table Field 1 Again",
"fieldname": "table_field_1_again",
"fieldtype": "Table",
"options": table_1_name,
},
],
"permissions": [{"role": "System Manager"}],
}
).insert()




def get_import_file(csv_file_name, force=False): def get_import_file(csv_file_name, force=False):
file_name = csv_file_name + '.csv'
_file = frappe.db.exists('File', {'file_name': file_name})
file_name = csv_file_name + ".csv"
_file = frappe.db.exists("File", {"file_name": file_name})
if force and _file: if force and _file:
frappe.delete_doc_if_exists('File', _file)
frappe.delete_doc_if_exists("File", _file)


if frappe.db.exists('File', {'file_name': file_name}):
f = frappe.get_doc('File', {'file_name': file_name})
if frappe.db.exists("File", {"file_name": file_name}):
f = frappe.get_doc("File", {"file_name": file_name})
else: else:
full_path = get_csv_file_path(file_name) full_path = get_csv_file_path(file_name)
f = frappe.get_doc( f = frappe.get_doc(
doctype='File',
content=frappe.read_file(full_path),
file_name=file_name,
is_private=1
doctype="File", content=frappe.read_file(full_path), file_name=file_name, is_private=1
) )
f.save(ignore_permissions=True) f.save(ignore_permissions=True)


@@ -210,4 +249,4 @@ def get_import_file(csv_file_name, force=False):




def get_csv_file_path(file_name): def get_csv_file_path(file_name):
return frappe.get_app_path('frappe', 'core', 'doctype', 'data_import', 'fixtures', file_name)
return frappe.get_app_path("frappe", "core", "doctype", "data_import", "fixtures", file_name)

+ 1
- 0
frappe/core/doctype/data_import_log/data_import_log.py 查看文件

@@ -4,5 +4,6 @@
# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document



class DataImportLog(Document): class DataImportLog(Document):
pass pass

+ 1
- 0
frappe/core/doctype/data_import_log/test_data_import_log.py 查看文件

@@ -4,5 +4,6 @@
# import frappe # import frappe
import unittest import unittest



class TestDataImportLog(unittest.TestCase): class TestDataImportLog(unittest.TestCase):
pass pass

+ 0
- 1
frappe/core/doctype/defaultvalue/__init__.py 查看文件

@@ -1,3 +1,2 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE


+ 12
- 7
frappe/core/doctype/defaultvalue/defaultvalue.py 查看文件

@@ -2,19 +2,24 @@
# License: MIT. See LICENSE # License: MIT. See LICENSE


import frappe import frappe

from frappe.model.document import Document from frappe.model.document import Document



class DefaultValue(Document): class DefaultValue(Document):
pass pass



def on_doctype_update(): def on_doctype_update():
"""Create indexes for `tabDefaultValue` on `(parent, defkey)`""" """Create indexes for `tabDefaultValue` on `(parent, defkey)`"""
frappe.db.commit() frappe.db.commit()
frappe.db.add_index(doctype='DefaultValue',
fields=['parent', 'defkey'],
index_name='defaultvalue_parent_defkey_index')
frappe.db.add_index(
doctype="DefaultValue",
fields=["parent", "defkey"],
index_name="defaultvalue_parent_defkey_index",
)


frappe.db.add_index(doctype='DefaultValue',
fields=['parent', 'parenttype'],
index_name='defaultvalue_parent_parenttype_index')
frappe.db.add_index(
doctype="DefaultValue",
fields=["parent", "parenttype"],
index_name="defaultvalue_parent_parenttype_index",
)

+ 8
- 11
frappe/core/doctype/deleted_document/deleted_document.py 查看文件

@@ -2,11 +2,12 @@
# Copyright (c) 2015, Frappe Technologies and contributors # Copyright (c) 2015, Frappe Technologies and contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE


import frappe
import json import json

import frappe
from frappe import _
from frappe.desk.doctype.bulk_update.bulk_update import show_progress from frappe.desk.doctype.bulk_update.bulk_update import show_progress
from frappe.model.document import Document from frappe.model.document import Document
from frappe import _




class DeletedDocument(Document): class DeletedDocument(Document):
@@ -15,7 +16,7 @@ class DeletedDocument(Document):


@frappe.whitelist() @frappe.whitelist()
def restore(name, alert=True): def restore(name, alert=True):
deleted = frappe.get_doc('Deleted Document', name)
deleted = frappe.get_doc("Deleted Document", name)


if deleted.restored: if deleted.restored:
frappe.throw(_("Document {0} Already Restored").format(name), exc=frappe.DocumentAlreadyRestored) frappe.throw(_("Document {0} Already Restored").format(name), exc=frappe.DocumentAlreadyRestored)
@@ -29,20 +30,20 @@ def restore(name, alert=True):
doc.docstatus = 0 doc.docstatus = 0
doc.insert() doc.insert()


doc.add_comment('Edit', _('restored {0} as {1}').format(deleted.deleted_name, doc.name))
doc.add_comment("Edit", _("restored {0} as {1}").format(deleted.deleted_name, doc.name))


deleted.new_name = doc.name deleted.new_name = doc.name
deleted.restored = 1 deleted.restored = 1
deleted.db_update() deleted.db_update()


if alert: if alert:
frappe.msgprint(_('Document Restored'))
frappe.msgprint(_("Document Restored"))




@frappe.whitelist() @frappe.whitelist()
def bulk_restore(docnames): def bulk_restore(docnames):
docnames = frappe.parse_json(docnames) docnames = frappe.parse_json(docnames)
message = _('Restoring Deleted Document')
message = _("Restoring Deleted Document")
restored, invalid, failed = [], [], [] restored, invalid, failed = [], [], []


for i, d in enumerate(docnames): for i, d in enumerate(docnames):
@@ -61,8 +62,4 @@ def bulk_restore(docnames):
failed.append(d) failed.append(d)
frappe.db.rollback() frappe.db.rollback()


return {
"restored": restored,
"invalid": invalid,
"failed": failed
}
return {"restored": restored, "invalid": invalid, "failed": failed}

+ 3
- 1
frappe/core/doctype/deleted_document/test_deleted_document.py 查看文件

@@ -1,10 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors # Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe
import unittest import unittest


import frappe

# test_records = frappe.get_test_records('Deleted Document') # test_records = frappe.get_test_records('Deleted Document')



class TestDeletedDocument(unittest.TestCase): class TestDeletedDocument(unittest.TestCase):
pass pass

+ 0
- 1
frappe/core/doctype/docfield/__init__.py 查看文件

@@ -1,3 +1,2 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE


+ 13
- 13
frappe/core/doctype/docfield/docfield.py 查看文件

@@ -4,28 +4,28 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document



class DocField(Document): class DocField(Document):
def get_link_doctype(self): def get_link_doctype(self):
'''Returns the Link doctype for the docfield (if applicable)
"""Returns the Link doctype for the docfield (if applicable)
if fieldtype is Link: Returns "options" if fieldtype is Link: Returns "options"
if fieldtype is Table MultiSelect: Returns "options" of the Link field in the Child Table if fieldtype is Table MultiSelect: Returns "options" of the Link field in the Child Table
'''
if self.fieldtype == 'Link':
"""
if self.fieldtype == "Link":
return self.options return self.options


if self.fieldtype == 'Table MultiSelect':
if self.fieldtype == "Table MultiSelect":
table_doctype = self.options table_doctype = self.options


link_doctype = frappe.db.get_value('DocField', {
'fieldtype': 'Link',
'parenttype': 'DocType',
'parent': table_doctype,
'in_list_view': 1
}, 'options')
link_doctype = frappe.db.get_value(
"DocField",
{"fieldtype": "Link", "parenttype": "DocType", "parent": table_doctype, "in_list_view": 1},
"options",
)


return link_doctype return link_doctype


def get_select_options(self): def get_select_options(self):
if self.fieldtype == 'Select':
options = self.options or ''
return [d for d in options.split('\n') if d]
if self.fieldtype == "Select":
options = self.options or ""
return [d for d in options.split("\n") if d]

+ 0
- 1
frappe/core/doctype/docperm/__init__.py 查看文件

@@ -1,3 +1,2 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE


+ 1
- 1
frappe/core/doctype/docperm/docperm.py 查看文件

@@ -2,8 +2,8 @@
# License: MIT. See LICENSE # License: MIT. See LICENSE


import frappe import frappe

from frappe.model.document import Document from frappe.model.document import Document



class DocPerm(Document): class DocPerm(Document):
pass pass

+ 24
- 10
frappe/core/doctype/docshare/docshare.py 查看文件

@@ -2,12 +2,13 @@
# License: MIT. See LICENSE # License: MIT. See LICENSE


import frappe import frappe
from frappe.model.document import Document
from frappe import _ from frappe import _
from frappe.utils import get_fullname, cint
from frappe.model.document import Document
from frappe.utils import cint, get_fullname


exclude_from_linked_with = True exclude_from_linked_with = True



class DocShare(Document): class DocShare(Document):
no_feed_on_delete = True no_feed_on_delete = True


@@ -36,15 +37,21 @@ class DocShare(Document):
frappe.throw(_("User is mandatory for Share"), frappe.MandatoryError) frappe.throw(_("User is mandatory for Share"), frappe.MandatoryError)


def check_share_permission(self): def check_share_permission(self):
if (not self.flags.ignore_share_permission and
not frappe.has_permission(self.share_doctype, "share", self.get_doc())):
if not self.flags.ignore_share_permission and not frappe.has_permission(
self.share_doctype, "share", self.get_doc()
):


frappe.throw(_('You need to have "Share" permission'), frappe.PermissionError) frappe.throw(_('You need to have "Share" permission'), frappe.PermissionError)


def check_is_submittable(self): def check_is_submittable(self):
if self.submit and not cint(frappe.db.get_value("DocType", self.share_doctype, "is_submittable")):
frappe.throw(_("Cannot share {0} with submit permission as the doctype {1} is not submittable").format(
frappe.bold(self.share_name), frappe.bold(self.share_doctype)))
if self.submit and not cint(
frappe.db.get_value("DocType", self.share_doctype, "is_submittable")
):
frappe.throw(
_("Cannot share {0} with submit permission as the doctype {1} is not submittable").format(
frappe.bold(self.share_name), frappe.bold(self.share_doctype)
)
)


def after_insert(self): def after_insert(self):
doc = self.get_doc() doc = self.get_doc()
@@ -53,14 +60,21 @@ class DocShare(Document):
if self.everyone: if self.everyone:
doc.add_comment("Shared", _("{0} shared this document with everyone").format(owner)) doc.add_comment("Shared", _("{0} shared this document with everyone").format(owner))
else: else:
doc.add_comment("Shared", _("{0} shared this document with {1}").format(owner, get_fullname(self.user)))
doc.add_comment(
"Shared", _("{0} shared this document with {1}").format(owner, get_fullname(self.user))
)


def on_trash(self): def on_trash(self):
if not self.flags.ignore_share_permission: if not self.flags.ignore_share_permission:
self.check_share_permission() self.check_share_permission()


self.get_doc().add_comment("Unshared",
_("{0} un-shared this document with {1}").format(get_fullname(self.owner), get_fullname(self.user)))
self.get_doc().add_comment(
"Unshared",
_("{0} un-shared this document with {1}").format(
get_fullname(self.owner), get_fullname(self.user)
),
)



def on_doctype_update(): def on_doctype_update():
"""Add index in `tabDocShare` for `(user, share_doctype)`""" """Add index in `tabDocShare` for `(user, share_doctype)`"""


+ 21
- 9
frappe/core/doctype/docshare/test_docshare.py 查看文件

@@ -1,20 +1,26 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE


import unittest

import frappe import frappe
import frappe.share import frappe.share
import unittest
from frappe.automation.doctype.auto_repeat.test_auto_repeat import create_submittable_doctype from frappe.automation.doctype.auto_repeat.test_auto_repeat import create_submittable_doctype


test_dependencies = ['User']
test_dependencies = ["User"]



class TestDocShare(unittest.TestCase): class TestDocShare(unittest.TestCase):
def setUp(self): def setUp(self):
self.user = "test@example.com" self.user = "test@example.com"
self.event = frappe.get_doc({"doctype": "Event",
"subject": "test share event",
"starts_on": "2015-01-01 10:00:00",
"event_type": "Private"}).insert()
self.event = frappe.get_doc(
{
"doctype": "Event",
"subject": "test share event",
"starts_on": "2015-01-01 10:00:00",
"event_type": "Private",
}
).insert()


def tearDown(self): def tearDown(self):
frappe.set_user("Administrator") frappe.set_user("Administrator")
@@ -98,7 +104,9 @@ class TestDocShare(unittest.TestCase):
doctype = "Test DocShare with Submit" doctype = "Test DocShare with Submit"
create_submittable_doctype(doctype, submit_perms=0) create_submittable_doctype(doctype, submit_perms=0)


submittable_doc = frappe.get_doc(dict(doctype=doctype, test="test docshare with submit")).insert()
submittable_doc = frappe.get_doc(
dict(doctype=doctype, test="test docshare with submit")
).insert()


frappe.set_user(self.user) frappe.set_user(self.user)
self.assertFalse(frappe.has_permission(doctype, "submit", user=self.user)) self.assertFalse(frappe.has_permission(doctype, "submit", user=self.user))
@@ -107,10 +115,14 @@ class TestDocShare(unittest.TestCase):
frappe.share.add(doctype, submittable_doc.name, self.user, submit=1) frappe.share.add(doctype, submittable_doc.name, self.user, submit=1)


frappe.set_user(self.user) frappe.set_user(self.user)
self.assertTrue(frappe.has_permission(doctype, "submit", doc=submittable_doc.name, user=self.user))
self.assertTrue(
frappe.has_permission(doctype, "submit", doc=submittable_doc.name, user=self.user)
)


# test cascade # test cascade
self.assertTrue(frappe.has_permission(doctype, "read", doc=submittable_doc.name, user=self.user)) self.assertTrue(frappe.has_permission(doctype, "read", doc=submittable_doc.name, user=self.user))
self.assertTrue(frappe.has_permission(doctype, "write", doc=submittable_doc.name, user=self.user))
self.assertTrue(
frappe.has_permission(doctype, "write", doc=submittable_doc.name, user=self.user)
)


frappe.share.remove(doctype, submittable_doc.name, self.user) frappe.share.remove(doctype, submittable_doc.name, self.user)

+ 0
- 1
frappe/core/doctype/doctype/__init__.py 查看文件

@@ -1,3 +1,2 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE


+ 574
- 322
frappe/core/doctype/doctype/doctype.py
文件差异内容过多而无法显示
查看文件


+ 4
- 3
frappe/core/doctype/doctype/patches/set_route.py 查看文件

@@ -1,7 +1,8 @@
import frappe import frappe
from frappe.desk.utils import slug from frappe.desk.utils import slug



def execute(): def execute():
for doctype in frappe.get_all('DocType', ['name', 'route'], dict(istable=0)):
if not doctype.route:
frappe.db.set_value('DocType', doctype.name, 'route', slug(doctype.name), update_modified = False)
for doctype in frappe.get_all("DocType", ["name", "route"], dict(istable=0)):
if not doctype.route:
frappe.db.set_value("DocType", doctype.name, "route", slug(doctype.name), update_modified=False)

+ 220
- 197
frappe/core/doctype/doctype/test_doctype.py 查看文件

@@ -1,21 +1,24 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe
import unittest import unittest
from frappe.core.doctype.doctype.doctype import (UniqueFieldnameError,
IllegalMandatoryError,

import frappe
from frappe.core.doctype.doctype.doctype import (
CannotIndexedError,
DoctypeLinkError, DoctypeLinkError,
WrongOptionsDoctypeLinkError,
HiddenAndMandatoryWithoutDefaultError, HiddenAndMandatoryWithoutDefaultError,
CannotIndexedError,
IllegalMandatoryError,
InvalidFieldNameError, InvalidFieldNameError,
validate_links_table_fieldnames)
UniqueFieldnameError,
WrongOptionsDoctypeLinkError,
validate_links_table_fieldnames,
)


# test_records = frappe.get_test_records('DocType') # test_records = frappe.get_test_records('DocType')


class TestDocType(unittest.TestCase):


class TestDocType(unittest.TestCase):
def tearDown(self): def tearDown(self):
frappe.db.rollback() frappe.db.rollback()


@@ -23,7 +26,10 @@ class TestDocType(unittest.TestCase):
self.assertRaises(frappe.NameError, new_doctype("_Some DocType").insert) self.assertRaises(frappe.NameError, new_doctype("_Some DocType").insert)
self.assertRaises(frappe.NameError, new_doctype("8Some DocType").insert) self.assertRaises(frappe.NameError, new_doctype("8Some DocType").insert)
self.assertRaises(frappe.NameError, new_doctype("Some (DocType)").insert) self.assertRaises(frappe.NameError, new_doctype("Some (DocType)").insert)
self.assertRaises(frappe.NameError, new_doctype("Some Doctype with a name whose length is more than 61 characters").insert)
self.assertRaises(
frappe.NameError,
new_doctype("Some Doctype with a name whose length is more than 61 characters").insert,
)
for name in ("Some DocType", "Some_DocType", "Some-DocType"): for name in ("Some DocType", "Some_DocType", "Some-DocType"):
if frappe.db.exists("DocType", name): if frappe.db.exists("DocType", name):
frappe.delete_doc("DocType", name) frappe.delete_doc("DocType", name)
@@ -86,19 +92,33 @@ class TestDocType(unittest.TestCase):
def test_all_depends_on_fields_conditions(self): def test_all_depends_on_fields_conditions(self):
import re import re


docfields = frappe.get_all("DocField",
or_filters={
"ifnull(depends_on, '')": ("!=", ''),
"ifnull(collapsible_depends_on, '')": ("!=", ''),
"ifnull(mandatory_depends_on, '')": ("!=", ''),
"ifnull(read_only_depends_on, '')": ("!=", '')
docfields = frappe.get_all(
"DocField",
or_filters={
"ifnull(depends_on, '')": ("!=", ""),
"ifnull(collapsible_depends_on, '')": ("!=", ""),
"ifnull(mandatory_depends_on, '')": ("!=", ""),
"ifnull(read_only_depends_on, '')": ("!=", ""),
}, },
fields=["parent", "depends_on", "collapsible_depends_on", "mandatory_depends_on",\
"read_only_depends_on", "fieldname", "fieldtype"])
fields=[
"parent",
"depends_on",
"collapsible_depends_on",
"mandatory_depends_on",
"read_only_depends_on",
"fieldname",
"fieldtype",
],
)


pattern = r'[\w\.:_]+\s*={1}\s*[\w\.@\'"]+' pattern = r'[\w\.:_]+\s*={1}\s*[\w\.@\'"]+'
for field in docfields: for field in docfields:
for depends_on in ["depends_on", "collapsible_depends_on", "mandatory_depends_on", "read_only_depends_on"]:
for depends_on in [
"depends_on",
"collapsible_depends_on",
"mandatory_depends_on",
"read_only_depends_on",
]:
condition = field.get(depends_on) condition = field.get(depends_on)
if condition: if condition:
self.assertFalse(re.match(pattern, condition)) self.assertFalse(re.match(pattern, condition))
@@ -108,18 +128,18 @@ class TestDocType(unittest.TestCase):
valid_data_field_options = frappe.model.data_field_options + ("",) valid_data_field_options = frappe.model.data_field_options + ("",)
invalid_data_field_options = ("Invalid Option 1", frappe.utils.random_string(5)) invalid_data_field_options = ("Invalid Option 1", frappe.utils.random_string(5))


for field_option in (valid_data_field_options + invalid_data_field_options):
test_doctype = frappe.get_doc({
"doctype": "DocType",
"name": doctype_name,
"module": "Core",
"custom": 1,
"fields": [{
"fieldname": "{0}_field".format(field_option),
"fieldtype": "Data",
"options": field_option
}]
})
for field_option in valid_data_field_options + invalid_data_field_options:
test_doctype = frappe.get_doc(
{
"doctype": "DocType",
"name": doctype_name,
"module": "Core",
"custom": 1,
"fields": [
{"fieldname": "{0}_field".format(field_option), "fieldtype": "Data", "options": field_option}
],
}
)


if field_option in invalid_data_field_options: if field_option in invalid_data_field_options:
# assert that only data options in frappe.model.data_field_options are valid # assert that only data options in frappe.model.data_field_options are valid
@@ -130,45 +150,29 @@ class TestDocType(unittest.TestCase):
test_doctype.delete() test_doctype.delete()


def test_sync_field_order(self): def test_sync_field_order(self):
from frappe.modules.import_file import get_file_path
import os import os


from frappe.modules.import_file import get_file_path

# create test doctype # create test doctype
test_doctype = frappe.get_doc({
"doctype": "DocType",
"module": "Core",
"fields": [
{
"label": "Field 1",
"fieldname": "field_1",
"fieldtype": "Data"
},
{
"label": "Field 2",
"fieldname": "field_2",
"fieldtype": "Data"
},
{
"label": "Field 3",
"fieldname": "field_3",
"fieldtype": "Data"
},
{
"label": "Field 4",
"fieldname": "field_4",
"fieldtype": "Data"
}
],
"permissions": [{
"role": "System Manager",
"read": 1
}],
"name": "Test Field Order DocType",
"__islocal": 1
})
test_doctype = frappe.get_doc(
{
"doctype": "DocType",
"module": "Core",
"fields": [
{"label": "Field 1", "fieldname": "field_1", "fieldtype": "Data"},
{"label": "Field 2", "fieldname": "field_2", "fieldtype": "Data"},
{"label": "Field 3", "fieldname": "field_3", "fieldtype": "Data"},
{"label": "Field 4", "fieldname": "field_4", "fieldtype": "Data"},
],
"permissions": [{"role": "System Manager", "read": 1}],
"name": "Test Field Order DocType",
"__islocal": 1,
}
)


path = get_file_path(test_doctype.module, test_doctype.doctype, test_doctype.name) path = get_file_path(test_doctype.module, test_doctype.doctype, test_doctype.name)
initial_fields_order = ['field_1', 'field_2', 'field_3', 'field_4']
initial_fields_order = ["field_1", "field_2", "field_3", "field_4"]


frappe.delete_doc_if_exists("DocType", "Test Field Order DocType") frappe.delete_doc_if_exists("DocType", "Test Field Order DocType")
if os.path.isfile(path): if os.path.isfile(path):
@@ -181,14 +185,18 @@ class TestDocType(unittest.TestCase):
# assert that field_order list is being created with the default order # assert that field_order list is being created with the default order
test_doctype_json = frappe.get_file_json(path) test_doctype_json = frappe.get_file_json(path)
self.assertTrue(test_doctype_json.get("field_order")) self.assertTrue(test_doctype_json.get("field_order"))
self.assertEqual(len(test_doctype_json['fields']), len(test_doctype_json['field_order']))
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], test_doctype_json['field_order'])
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], initial_fields_order)
self.assertListEqual(test_doctype_json['field_order'], initial_fields_order)
self.assertEqual(len(test_doctype_json["fields"]), len(test_doctype_json["field_order"]))
self.assertListEqual(
[f["fieldname"] for f in test_doctype_json["fields"]], test_doctype_json["field_order"]
)
self.assertListEqual(
[f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order
)
self.assertListEqual(test_doctype_json["field_order"], initial_fields_order)


# remove field_order to test reload_doc/sync/migrate is backwards compatible without field_order # remove field_order to test reload_doc/sync/migrate is backwards compatible without field_order
del test_doctype_json['field_order']
with open(path, 'w+') as txtfile:
del test_doctype_json["field_order"]
with open(path, "w+") as txtfile:
txtfile.write(frappe.as_json(test_doctype_json)) txtfile.write(frappe.as_json(test_doctype_json))


# assert that field_order is actually removed from the json file # assert that field_order is actually removed from the json file
@@ -203,10 +211,14 @@ class TestDocType(unittest.TestCase):
test_doctype.save() test_doctype.save()
test_doctype_json = frappe.get_file_json(path) test_doctype_json = frappe.get_file_json(path)
self.assertTrue(test_doctype_json.get("field_order")) self.assertTrue(test_doctype_json.get("field_order"))
self.assertEqual(len(test_doctype_json['fields']), len(test_doctype_json['field_order']))
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], test_doctype_json['field_order'])
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], initial_fields_order)
self.assertListEqual(test_doctype_json['field_order'], initial_fields_order)
self.assertEqual(len(test_doctype_json["fields"]), len(test_doctype_json["field_order"]))
self.assertListEqual(
[f["fieldname"] for f in test_doctype_json["fields"]], test_doctype_json["field_order"]
)
self.assertListEqual(
[f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order
)
self.assertListEqual(test_doctype_json["field_order"], initial_fields_order)


# reorder fields: swap row 1 and 3 # reorder fields: swap row 1 and 3
test_doctype.fields[0], test_doctype.fields[2] = test_doctype.fields[2], test_doctype.fields[0] test_doctype.fields[0], test_doctype.fields[2] = test_doctype.fields[2], test_doctype.fields[0]
@@ -216,25 +228,30 @@ class TestDocType(unittest.TestCase):
# assert that reordering fields only affects `field_order` rather than `fields` attr # assert that reordering fields only affects `field_order` rather than `fields` attr
test_doctype.save() test_doctype.save()
test_doctype_json = frappe.get_file_json(path) test_doctype_json = frappe.get_file_json(path)
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], initial_fields_order)
self.assertListEqual(test_doctype_json['field_order'], ['field_3', 'field_2', 'field_1', 'field_4'])
self.assertListEqual(
[f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order
)
self.assertListEqual(
test_doctype_json["field_order"], ["field_3", "field_2", "field_1", "field_4"]
)


# reorder `field_order` in the json file: swap row 2 and 4 # reorder `field_order` in the json file: swap row 2 and 4
test_doctype_json['field_order'][1], test_doctype_json['field_order'][3] = test_doctype_json['field_order'][3], test_doctype_json['field_order'][1]
with open(path, 'w+') as txtfile:
test_doctype_json["field_order"][1], test_doctype_json["field_order"][3] = (
test_doctype_json["field_order"][3],
test_doctype_json["field_order"][1],
)
with open(path, "w+") as txtfile:
txtfile.write(frappe.as_json(test_doctype_json)) txtfile.write(frappe.as_json(test_doctype_json))


# assert that reordering `field_order` from json file is reflected in DocType upon migrate/sync # assert that reordering `field_order` from json file is reflected in DocType upon migrate/sync
frappe.reload_doctype(test_doctype.name, force=True) frappe.reload_doctype(test_doctype.name, force=True)
test_doctype.reload() test_doctype.reload()
self.assertListEqual([f.fieldname for f in test_doctype.fields], ['field_3', 'field_4', 'field_1', 'field_2'])
self.assertListEqual(
[f.fieldname for f in test_doctype.fields], ["field_3", "field_4", "field_1", "field_2"]
)


# insert row in the middle and remove first row (field 3) # insert row in the middle and remove first row (field 3)
test_doctype.append("fields", {
"label": "Field 5",
"fieldname": "field_5",
"fieldtype": "Data"
})
test_doctype.append("fields", {"label": "Field 5", "fieldname": "field_5", "fieldtype": "Data"})
test_doctype.fields[4], test_doctype.fields[3] = test_doctype.fields[3], test_doctype.fields[4] test_doctype.fields[4], test_doctype.fields[3] = test_doctype.fields[3], test_doctype.fields[4]
test_doctype.fields[3], test_doctype.fields[2] = test_doctype.fields[2], test_doctype.fields[3] test_doctype.fields[3], test_doctype.fields[2] = test_doctype.fields[2], test_doctype.fields[3]
test_doctype.remove(test_doctype.fields[0]) test_doctype.remove(test_doctype.fields[0])
@@ -243,115 +260,121 @@ class TestDocType(unittest.TestCase):


test_doctype.save() test_doctype.save()
test_doctype_json = frappe.get_file_json(path) test_doctype_json = frappe.get_file_json(path)
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], ['field_1', 'field_2', 'field_4', 'field_5'])
self.assertListEqual(test_doctype_json['field_order'], ['field_4', 'field_5', 'field_1', 'field_2'])
self.assertListEqual(
[f["fieldname"] for f in test_doctype_json["fields"]],
["field_1", "field_2", "field_4", "field_5"],
)
self.assertListEqual(
test_doctype_json["field_order"], ["field_4", "field_5", "field_1", "field_2"]
)
except: except:
raise raise
finally: finally:
frappe.flags.allow_doctype_export = 0 frappe.flags.allow_doctype_export = 0


def test_unique_field_name_for_two_fields(self): def test_unique_field_name_for_two_fields(self):
doc = new_doctype('Test Unique Field')
field_1 = doc.append('fields', {})
field_1.fieldname = 'some_fieldname_1'
field_1.fieldtype = 'Data'
doc = new_doctype("Test Unique Field")
field_1 = doc.append("fields", {})
field_1.fieldname = "some_fieldname_1"
field_1.fieldtype = "Data"


field_2 = doc.append('fields', {})
field_2.fieldname = 'some_fieldname_1'
field_2.fieldtype = 'Data'
field_2 = doc.append("fields", {})
field_2.fieldname = "some_fieldname_1"
field_2.fieldtype = "Data"


self.assertRaises(UniqueFieldnameError, doc.insert) self.assertRaises(UniqueFieldnameError, doc.insert)


def test_fieldname_is_not_name(self): def test_fieldname_is_not_name(self):
doc = new_doctype('Test Name Field')
field_1 = doc.append('fields', {})
field_1.label = 'Name'
field_1.fieldtype = 'Data'
doc = new_doctype("Test Name Field")
field_1 = doc.append("fields", {})
field_1.label = "Name"
field_1.fieldtype = "Data"
doc.insert() doc.insert()
self.assertEqual(doc.fields[1].fieldname, "name1") self.assertEqual(doc.fields[1].fieldname, "name1")
doc.fields[1].fieldname = 'name'
doc.fields[1].fieldname = "name"
self.assertRaises(InvalidFieldNameError, doc.save) self.assertRaises(InvalidFieldNameError, doc.save)


def test_illegal_mandatory_validation(self): def test_illegal_mandatory_validation(self):
doc = new_doctype('Test Illegal mandatory')
field_1 = doc.append('fields', {})
field_1.fieldname = 'some_fieldname_1'
field_1.fieldtype = 'Section Break'
doc = new_doctype("Test Illegal mandatory")
field_1 = doc.append("fields", {})
field_1.fieldname = "some_fieldname_1"
field_1.fieldtype = "Section Break"
field_1.reqd = 1 field_1.reqd = 1


self.assertRaises(IllegalMandatoryError, doc.insert) self.assertRaises(IllegalMandatoryError, doc.insert)


def test_link_with_wrong_and_no_options(self): def test_link_with_wrong_and_no_options(self):
doc = new_doctype('Test link')
field_1 = doc.append('fields', {})
field_1.fieldname = 'some_fieldname_1'
field_1.fieldtype = 'Link'
doc = new_doctype("Test link")
field_1 = doc.append("fields", {})
field_1.fieldname = "some_fieldname_1"
field_1.fieldtype = "Link"


self.assertRaises(DoctypeLinkError, doc.insert) self.assertRaises(DoctypeLinkError, doc.insert)


field_1.options = 'wrongdoctype'
field_1.options = "wrongdoctype"


self.assertRaises(WrongOptionsDoctypeLinkError, doc.insert) self.assertRaises(WrongOptionsDoctypeLinkError, doc.insert)


def test_hidden_and_mandatory_without_default(self): def test_hidden_and_mandatory_without_default(self):
doc = new_doctype('Test hidden and mandatory')
field_1 = doc.append('fields', {})
field_1.fieldname = 'some_fieldname_1'
field_1.fieldtype = 'Data'
doc = new_doctype("Test hidden and mandatory")
field_1 = doc.append("fields", {})
field_1.fieldname = "some_fieldname_1"
field_1.fieldtype = "Data"
field_1.reqd = 1 field_1.reqd = 1
field_1.hidden = 1 field_1.hidden = 1


self.assertRaises(HiddenAndMandatoryWithoutDefaultError, doc.insert) self.assertRaises(HiddenAndMandatoryWithoutDefaultError, doc.insert)


def test_field_can_not_be_indexed_validation(self): def test_field_can_not_be_indexed_validation(self):
doc = new_doctype('Test index')
field_1 = doc.append('fields', {})
field_1.fieldname = 'some_fieldname_1'
field_1.fieldtype = 'Long Text'
doc = new_doctype("Test index")
field_1 = doc.append("fields", {})
field_1.fieldname = "some_fieldname_1"
field_1.fieldtype = "Long Text"
field_1.search_index = 1 field_1.search_index = 1


self.assertRaises(CannotIndexedError, doc.insert) self.assertRaises(CannotIndexedError, doc.insert)


def test_cancel_link_doctype(self): def test_cancel_link_doctype(self):
import json import json
from frappe.desk.form.linked_with import get_submitted_linked_docs, cancel_all_linked_docs


#create doctype
link_doc = new_doctype('Test Linked Doctype')
from frappe.desk.form.linked_with import cancel_all_linked_docs, get_submitted_linked_docs

# create doctype
link_doc = new_doctype("Test Linked Doctype")
link_doc.is_submittable = 1 link_doc.is_submittable = 1
for data in link_doc.get('permissions'):
for data in link_doc.get("permissions"):
data.submit = 1 data.submit = 1
data.cancel = 1 data.cancel = 1
link_doc.insert() link_doc.insert()


doc = new_doctype('Test Doctype')
doc = new_doctype("Test Doctype")
doc.is_submittable = 1 doc.is_submittable = 1
field_2 = doc.append('fields', {})
field_2.label = 'Test Linked Doctype'
field_2.fieldname = 'test_linked_doctype'
field_2.fieldtype = 'Link'
field_2.options = 'Test Linked Doctype'
for data in link_doc.get('permissions'):
field_2 = doc.append("fields", {})
field_2.label = "Test Linked Doctype"
field_2.fieldname = "test_linked_doctype"
field_2.fieldtype = "Link"
field_2.options = "Test Linked Doctype"
for data in link_doc.get("permissions"):
data.submit = 1 data.submit = 1
data.cancel = 1 data.cancel = 1
doc.insert() doc.insert()


# create doctype data # create doctype data
data_link_doc = frappe.new_doc('Test Linked Doctype')
data_link_doc.some_fieldname = 'Data1'
data_link_doc = frappe.new_doc("Test Linked Doctype")
data_link_doc.some_fieldname = "Data1"
data_link_doc.insert() data_link_doc.insert()
data_link_doc.save() data_link_doc.save()
data_link_doc.submit() data_link_doc.submit()


data_doc = frappe.new_doc('Test Doctype')
data_doc.some_fieldname = 'Data1'
data_doc = frappe.new_doc("Test Doctype")
data_doc.some_fieldname = "Data1"
data_doc.test_linked_doctype = data_link_doc.name data_doc.test_linked_doctype = data_link_doc.name
data_doc.insert() data_doc.insert()
data_doc.save() data_doc.save()
data_doc.submit() data_doc.submit()


docs = get_submitted_linked_docs(link_doc.name, data_link_doc.name) docs = get_submitted_linked_docs(link_doc.name, data_link_doc.name)
dump_docs = json.dumps(docs.get('docs'))
dump_docs = json.dumps(docs.get("docs"))
cancel_all_linked_docs(dump_docs) cancel_all_linked_docs(dump_docs)
data_link_doc.cancel() data_link_doc.cancel()
data_doc.load_from_db() data_doc.load_from_db()
@@ -369,69 +392,70 @@ class TestDocType(unittest.TestCase):


def test_ignore_cancelation_of_linked_doctype_during_cancel(self): def test_ignore_cancelation_of_linked_doctype_during_cancel(self):
import json import json
from frappe.desk.form.linked_with import get_submitted_linked_docs, cancel_all_linked_docs


#create linked doctype
link_doc = new_doctype('Test Linked Doctype 1')
from frappe.desk.form.linked_with import cancel_all_linked_docs, get_submitted_linked_docs

# create linked doctype
link_doc = new_doctype("Test Linked Doctype 1")
link_doc.is_submittable = 1 link_doc.is_submittable = 1
for data in link_doc.get('permissions'):
for data in link_doc.get("permissions"):
data.submit = 1 data.submit = 1
data.cancel = 1 data.cancel = 1
link_doc.insert() link_doc.insert()


#create first parent doctype
test_doc_1 = new_doctype('Test Doctype 1')
# create first parent doctype
test_doc_1 = new_doctype("Test Doctype 1")
test_doc_1.is_submittable = 1 test_doc_1.is_submittable = 1


field_2 = test_doc_1.append('fields', {})
field_2.label = 'Test Linked Doctype 1'
field_2.fieldname = 'test_linked_doctype_a'
field_2.fieldtype = 'Link'
field_2.options = 'Test Linked Doctype 1'
field_2 = test_doc_1.append("fields", {})
field_2.label = "Test Linked Doctype 1"
field_2.fieldname = "test_linked_doctype_a"
field_2.fieldtype = "Link"
field_2.options = "Test Linked Doctype 1"


for data in test_doc_1.get('permissions'):
for data in test_doc_1.get("permissions"):
data.submit = 1 data.submit = 1
data.cancel = 1 data.cancel = 1
test_doc_1.insert() test_doc_1.insert()


#crete second parent doctype
doc = new_doctype('Test Doctype 2')
# crete second parent doctype
doc = new_doctype("Test Doctype 2")
doc.is_submittable = 1 doc.is_submittable = 1


field_2 = doc.append('fields', {})
field_2.label = 'Test Linked Doctype 1'
field_2.fieldname = 'test_linked_doctype_a'
field_2.fieldtype = 'Link'
field_2.options = 'Test Linked Doctype 1'
field_2 = doc.append("fields", {})
field_2.label = "Test Linked Doctype 1"
field_2.fieldname = "test_linked_doctype_a"
field_2.fieldtype = "Link"
field_2.options = "Test Linked Doctype 1"


for data in link_doc.get('permissions'):
for data in link_doc.get("permissions"):
data.submit = 1 data.submit = 1
data.cancel = 1 data.cancel = 1
doc.insert() doc.insert()


# create doctype data # create doctype data
data_link_doc_1 = frappe.new_doc('Test Linked Doctype 1')
data_link_doc_1.some_fieldname = 'Data1'
data_link_doc_1 = frappe.new_doc("Test Linked Doctype 1")
data_link_doc_1.some_fieldname = "Data1"
data_link_doc_1.insert() data_link_doc_1.insert()
data_link_doc_1.save() data_link_doc_1.save()
data_link_doc_1.submit() data_link_doc_1.submit()


data_doc_2 = frappe.new_doc('Test Doctype 1')
data_doc_2.some_fieldname = 'Data1'
data_doc_2 = frappe.new_doc("Test Doctype 1")
data_doc_2.some_fieldname = "Data1"
data_doc_2.test_linked_doctype_a = data_link_doc_1.name data_doc_2.test_linked_doctype_a = data_link_doc_1.name
data_doc_2.insert() data_doc_2.insert()
data_doc_2.save() data_doc_2.save()
data_doc_2.submit() data_doc_2.submit()


data_doc = frappe.new_doc('Test Doctype 2')
data_doc.some_fieldname = 'Data1'
data_doc = frappe.new_doc("Test Doctype 2")
data_doc.some_fieldname = "Data1"
data_doc.test_linked_doctype_a = data_link_doc_1.name data_doc.test_linked_doctype_a = data_link_doc_1.name
data_doc.insert() data_doc.insert()
data_doc.save() data_doc.save()
data_doc.submit() data_doc.submit()


docs = get_submitted_linked_docs(link_doc.name, data_link_doc_1.name) docs = get_submitted_linked_docs(link_doc.name, data_link_doc_1.name)
dump_docs = json.dumps(docs.get('docs'))
dump_docs = json.dumps(docs.get("docs"))


cancel_all_linked_docs(dump_docs, ignore_doctypes_on_cancel_all=["Test Doctype 2"]) cancel_all_linked_docs(dump_docs, ignore_doctypes_on_cancel_all=["Test Doctype 2"])


@@ -442,10 +466,10 @@ class TestDocType(unittest.TestCase):
data_doc_2.load_from_db() data_doc_2.load_from_db()
self.assertEqual(data_link_doc_1.docstatus, 2) self.assertEqual(data_link_doc_1.docstatus, 2)


#linked doc is canceled
# linked doc is canceled
self.assertEqual(data_doc_2.docstatus, 2) self.assertEqual(data_doc_2.docstatus, 2)


#ignored doctype 2 during cancel
# ignored doctype 2 during cancel
self.assertEqual(data_doc.docstatus, 1) self.assertEqual(data_doc.docstatus, 1)


# delete doctype record # delete doctype record
@@ -464,42 +488,35 @@ class TestDocType(unittest.TestCase):
doc = new_doctype("Test Links Table Validation") doc = new_doctype("Test Links Table Validation")


# check valid data # check valid data
doc.append("links", {
'link_doctype': "User",
'link_fieldname': "first_name"
})
validate_links_table_fieldnames(doc) # no error
doc.links = [] # reset links table
doc.append("links", {"link_doctype": "User", "link_fieldname": "first_name"})
validate_links_table_fieldnames(doc) # no error
doc.links = [] # reset links table


# check invalid doctype # check invalid doctype
doc.append("links", {
'link_doctype': "User2",
'link_fieldname': "first_name"
})
doc.append("links", {"link_doctype": "User2", "link_fieldname": "first_name"})
self.assertRaises(frappe.DoesNotExistError, validate_links_table_fieldnames, doc) self.assertRaises(frappe.DoesNotExistError, validate_links_table_fieldnames, doc)
doc.links = [] # reset links table
doc.links = [] # reset links table


# check invalid fieldname # check invalid fieldname
doc.append("links", {
'link_doctype': "User",
'link_fieldname': "a_field_that_does_not_exists"
})
doc.append("links", {"link_doctype": "User", "link_fieldname": "a_field_that_does_not_exists"})


self.assertRaises(InvalidFieldNameError, validate_links_table_fieldnames, doc) self.assertRaises(InvalidFieldNameError, validate_links_table_fieldnames, doc)


def test_create_virtual_doctype(self): def test_create_virtual_doctype(self):
"""Test virtual DOcTYpe.""" """Test virtual DOcTYpe."""
virtual_doc = new_doctype('Test Virtual Doctype')
virtual_doc = new_doctype("Test Virtual Doctype")
virtual_doc.is_virtual = 1 virtual_doc.is_virtual = 1
virtual_doc.insert() virtual_doc.insert()
virtual_doc.save() virtual_doc.save()
doc = frappe.get_doc("DocType", "Test Virtual Doctype") doc = frappe.get_doc("DocType", "Test Virtual Doctype")


self.assertEqual(doc.is_virtual, 1) self.assertEqual(doc.is_virtual, 1)
self.assertFalse(frappe.db.table_exists('Test Virtual Doctype'))
self.assertFalse(frappe.db.table_exists("Test Virtual Doctype"))


def test_default_fieldname(self): def test_default_fieldname(self):
fields = [{"label": "title", "fieldname": "title", "fieldtype": "Data", "default": "{some_fieldname}"}]
fields = [
{"label": "title", "fieldname": "title", "fieldtype": "Data", "default": "{some_fieldname}"}
]
dt = new_doctype("DT with default field", fields=fields) dt = new_doctype("DT with default field", fields=fields)
dt.insert() dt.insert()


@@ -521,28 +538,34 @@ class TestDocType(unittest.TestCase):
dt.delete(ignore_permissions=True) dt.delete(ignore_permissions=True)




def new_doctype(name, unique=0, depends_on='', fields=None, autoincremented=False):
doc = frappe.get_doc({
"doctype": "DocType",
"module": "Core",
"custom": 1,
"fields": [{
"label": "Some Field",
"fieldname": "some_fieldname",
"fieldtype": "Data",
"unique": unique,
"depends_on": depends_on,
}],
"permissions": [{
"role": "System Manager",
"read": 1,
}],
"name": name,
"autoname": "autoincrement" if autoincremented else ""
})
def new_doctype(name, unique=0, depends_on="", fields=None, autoincremented=False):
doc = frappe.get_doc(
{
"doctype": "DocType",
"module": "Core",
"custom": 1,
"fields": [
{
"label": "Some Field",
"fieldname": "some_fieldname",
"fieldtype": "Data",
"unique": unique,
"depends_on": depends_on,
}
],
"permissions": [
{
"role": "System Manager",
"read": 1,
}
],
"name": name,
"autoname": "autoincrement" if autoincremented else "",
}
)


if fields: if fields:
for f in fields: for f in fields:
doc.append('fields', f)
doc.append("fields", f)


return doc return doc

+ 1
- 0
frappe/core/doctype/doctype_action/doctype_action.py 查看文件

@@ -5,5 +5,6 @@
# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document



class DocTypeAction(Document): class DocTypeAction(Document):
pass pass

+ 1
- 0
frappe/core/doctype/doctype_link/doctype_link.py 查看文件

@@ -5,5 +5,6 @@
# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document



class DocTypeLink(Document): class DocTypeLink(Document):
pass pass

+ 1
- 0
frappe/core/doctype/doctype_state/doctype_state.py 查看文件

@@ -4,5 +4,6 @@
# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document



class DocTypeState(Document): class DocTypeState(Document):
pass pass

+ 19
- 11
frappe/core/doctype/document_naming_rule/document_naming_rule.py 查看文件

@@ -3,10 +3,11 @@
# License: MIT. See LICENSE # License: MIT. See LICENSE


import frappe import frappe
from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils.data import evaluate_filters
from frappe.model.naming import parse_naming_series from frappe.model.naming import parse_naming_series
from frappe import _
from frappe.utils.data import evaluate_filters



class DocumentNamingRule(Document): class DocumentNamingRule(Document):
def validate(self): def validate(self):
@@ -17,23 +18,30 @@ class DocumentNamingRule(Document):
docfields = [x.fieldname for x in frappe.get_meta(self.document_type).fields] docfields = [x.fieldname for x in frappe.get_meta(self.document_type).fields]
for condition in self.conditions: for condition in self.conditions:
if condition.field not in docfields: if condition.field not in docfields:
frappe.throw(_("{0} is not a field of doctype {1}").format(frappe.bold(condition.field), frappe.bold(self.document_type)))
frappe.throw(
_("{0} is not a field of doctype {1}").format(
frappe.bold(condition.field), frappe.bold(self.document_type)
)
)


def apply(self, doc): def apply(self, doc):
'''
"""
Apply naming rules for the given document. Will set `name` if the rule is matched. Apply naming rules for the given document. Will set `name` if the rule is matched.
'''
"""
if self.conditions: if self.conditions:
if not evaluate_filters(doc, [(self.document_type, d.field, d.condition, d.value) for d in self.conditions]):
if not evaluate_filters(
doc, [(self.document_type, d.field, d.condition, d.value) for d in self.conditions]
):
return return


counter = frappe.db.get_value(self.doctype, self.name, 'counter', for_update=True) or 0
counter = frappe.db.get_value(self.doctype, self.name, "counter", for_update=True) or 0
naming_series = parse_naming_series(self.prefix, doc=doc) naming_series = parse_naming_series(self.prefix, doc=doc)


doc.name = naming_series + ('%0'+str(self.prefix_digits)+'d') % (counter + 1)
frappe.db.set_value(self.doctype, self.name, 'counter', counter + 1)
doc.name = naming_series + ("%0" + str(self.prefix_digits) + "d") % (counter + 1)
frappe.db.set_value(self.doctype, self.name, "counter", counter + 1)



@frappe.whitelist() @frappe.whitelist()
def update_current(name, new_counter): def update_current(name, new_counter):
frappe.only_for('System Manager')
frappe.db.set_value('Document Naming Rule', name, 'counter', new_counter)
frappe.only_for("System Manager")
frappe.db.set_value("Document Naming Rule", name, "counter", new_counter)

+ 35
- 46
frappe/core/doctype/document_naming_rule/test_document_naming_rule.py 查看文件

@@ -1,79 +1,68 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors # Copyright (c) 2020, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe
import unittest import unittest


import frappe


class TestDocumentNamingRule(unittest.TestCase): class TestDocumentNamingRule(unittest.TestCase):
def test_naming_rule_by_series(self): def test_naming_rule_by_series(self):
naming_rule = frappe.get_doc(dict(
doctype = 'Document Naming Rule',
document_type = 'ToDo',
prefix = 'test-todo-',
prefix_digits = 5
)).insert()
naming_rule = frappe.get_doc(
dict(doctype="Document Naming Rule", document_type="ToDo", prefix="test-todo-", prefix_digits=5)
).insert()


todo = frappe.get_doc(dict(
doctype = 'ToDo',
description = 'Is this my name ' + frappe.generate_hash()
)).insert()
todo = frappe.get_doc(
dict(doctype="ToDo", description="Is this my name " + frappe.generate_hash())
).insert()


self.assertEqual(todo.name, 'test-todo-00001')
self.assertEqual(todo.name, "test-todo-00001")


naming_rule.delete() naming_rule.delete()
todo.delete() todo.delete()


def test_naming_rule_by_condition(self): def test_naming_rule_by_condition(self):
naming_rule = frappe.get_doc(dict(
doctype = 'Document Naming Rule',
document_type = 'ToDo',
prefix = 'test-high-',
prefix_digits = 5,
priority = 10,
conditions = [dict(
field = 'priority',
condition = '=',
value = 'High'
)]
)).insert()
naming_rule = frappe.get_doc(
dict(
doctype="Document Naming Rule",
document_type="ToDo",
prefix="test-high-",
prefix_digits=5,
priority=10,
conditions=[dict(field="priority", condition="=", value="High")],
)
).insert()


# another rule # another rule
naming_rule_1 = frappe.copy_doc(naming_rule) naming_rule_1 = frappe.copy_doc(naming_rule)
naming_rule_1.prefix = 'test-medium-'
naming_rule_1.conditions[0].value = 'Medium'
naming_rule_1.prefix = "test-medium-"
naming_rule_1.conditions[0].value = "Medium"
naming_rule_1.insert() naming_rule_1.insert()


# default rule with low priority - should not get applied for rules # default rule with low priority - should not get applied for rules
# with higher priority # with higher priority
naming_rule_2 = frappe.copy_doc(naming_rule) naming_rule_2 = frappe.copy_doc(naming_rule)
naming_rule_2.prefix = 'test-low-'
naming_rule_2.prefix = "test-low-"
naming_rule_2.priority = 0 naming_rule_2.priority = 0
naming_rule_2.conditions = [] naming_rule_2.conditions = []
naming_rule_2.insert() naming_rule_2.insert()


todo = frappe.get_doc(
dict(doctype="ToDo", priority="High", description="Is this my name " + frappe.generate_hash())
).insert()


todo = frappe.get_doc(dict(
doctype = 'ToDo',
priority = 'High',
description = 'Is this my name ' + frappe.generate_hash()
)).insert()

todo_1 = frappe.get_doc(dict(
doctype = 'ToDo',
priority = 'Medium',
description = 'Is this my name ' + frappe.generate_hash()
)).insert()
todo_1 = frappe.get_doc(
dict(doctype="ToDo", priority="Medium", description="Is this my name " + frappe.generate_hash())
).insert()


todo_2 = frappe.get_doc(dict(
doctype = 'ToDo',
priority = 'Low',
description = 'Is this my name ' + frappe.generate_hash()
)).insert()
todo_2 = frappe.get_doc(
dict(doctype="ToDo", priority="Low", description="Is this my name " + frappe.generate_hash())
).insert()


try: try:
self.assertEqual(todo.name, 'test-high-00001')
self.assertEqual(todo_1.name, 'test-medium-00001')
self.assertEqual(todo_2.name, 'test-low-00001')
self.assertEqual(todo.name, "test-high-00001")
self.assertEqual(todo_1.name, "test-medium-00001")
self.assertEqual(todo_2.name, "test-low-00001")
finally: finally:
naming_rule.delete() naming_rule.delete()
naming_rule_1.delete() naming_rule_1.delete()


+ 1
- 0
frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py 查看文件

@@ -5,5 +5,6 @@
# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document



class DocumentNamingRuleCondition(Document): class DocumentNamingRuleCondition(Document):
pass pass

+ 1
- 0
frappe/core/doctype/document_naming_rule_condition/test_document_naming_rule_condition.py 查看文件

@@ -4,5 +4,6 @@
# import frappe # import frappe
import unittest import unittest



class TestDocumentNamingRuleCondition(unittest.TestCase): class TestDocumentNamingRuleCondition(unittest.TestCase):
pass pass

+ 41
- 31
frappe/core/doctype/domain/domain.py 查看文件

@@ -3,16 +3,17 @@
# License: MIT. See LICENSE # License: MIT. See LICENSE


import frappe import frappe

from frappe.model.document import Document
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.model.document import Document



class Domain(Document): class Domain(Document):
'''Domain documents are created automatically when DocTypes
"""Domain documents are created automatically when DocTypes
with "Restricted" domains are imported during with "Restricted" domains are imported during
installation or migration'''
installation or migration"""

def setup_domain(self): def setup_domain(self):
'''Setup domain icons, permissions, custom fields etc.'''
"""Setup domain icons, permissions, custom fields etc."""
self.setup_data() self.setup_data()
self.setup_roles() self.setup_roles()
self.setup_properties() self.setup_properties()
@@ -31,20 +32,20 @@ class Domain(Document):
frappe.get_attr(self.data.on_setup)() frappe.get_attr(self.data.on_setup)()


def remove_domain(self): def remove_domain(self):
'''Unset domain settings'''
"""Unset domain settings"""
self.setup_data() self.setup_data()


if self.data.restricted_roles: if self.data.restricted_roles:
for role_name in self.data.restricted_roles: for role_name in self.data.restricted_roles:
if frappe.db.exists('Role', role_name):
role = frappe.get_doc('Role', role_name)
if frappe.db.exists("Role", role_name):
role = frappe.get_doc("Role", role_name)
role.disabled = 1 role.disabled = 1
role.save() role.save()


self.remove_custom_field() self.remove_custom_field()


def remove_custom_field(self): def remove_custom_field(self):
'''Remove custom_fields when disabling domain'''
"""Remove custom_fields when disabling domain"""
if self.data.custom_fields: if self.data.custom_fields:
for doctype in self.data.custom_fields: for doctype in self.data.custom_fields:
custom_fields = self.data.custom_fields[doctype] custom_fields = self.data.custom_fields[doctype]
@@ -54,47 +55,48 @@ class Domain(Document):
custom_fields = [custom_fields] custom_fields = [custom_fields]


for custom_field_detail in custom_fields: for custom_field_detail in custom_fields:
custom_field_name = frappe.db.get_value('Custom Field',
dict(dt=doctype, fieldname=custom_field_detail.get('fieldname')))
custom_field_name = frappe.db.get_value(
"Custom Field", dict(dt=doctype, fieldname=custom_field_detail.get("fieldname"))
)
if custom_field_name: if custom_field_name:
frappe.delete_doc('Custom Field', custom_field_name)
frappe.delete_doc("Custom Field", custom_field_name)


def setup_roles(self): def setup_roles(self):
'''Enable roles that are restricted to this domain'''
"""Enable roles that are restricted to this domain"""
if self.data.restricted_roles: if self.data.restricted_roles:
user = frappe.get_doc("User", frappe.session.user) user = frappe.get_doc("User", frappe.session.user)
for role_name in self.data.restricted_roles: for role_name in self.data.restricted_roles:
user.append("roles", {"role": role_name}) user.append("roles", {"role": role_name})
if not frappe.db.get_value('Role', role_name):
frappe.get_doc(dict(doctype='Role', role_name=role_name)).insert()
if not frappe.db.get_value("Role", role_name):
frappe.get_doc(dict(doctype="Role", role_name=role_name)).insert()
continue continue


role = frappe.get_doc('Role', role_name)
role = frappe.get_doc("Role", role_name)
role.disabled = 0 role.disabled = 0
role.save() role.save()
user.save() user.save()


def setup_data(self, domain=None): def setup_data(self, domain=None):
'''Load domain info via hooks'''
"""Load domain info via hooks"""
self.data = frappe.get_domain_data(self.name) self.data = frappe.get_domain_data(self.name)


def get_domain_data(self, module): def get_domain_data(self, module):
return frappe.get_attr(frappe.get_hooks('domains')[self.name] + '.data')
return frappe.get_attr(frappe.get_hooks("domains")[self.name] + ".data")


def set_default_portal_role(self): def set_default_portal_role(self):
'''Set default portal role based on domain'''
if self.data.get('default_portal_role'):
frappe.db.set_value('Portal Settings', None, 'default_role',
self.data.get('default_portal_role'))
"""Set default portal role based on domain"""
if self.data.get("default_portal_role"):
frappe.db.set_value(
"Portal Settings", None, "default_role", self.data.get("default_portal_role")
)


def setup_properties(self): def setup_properties(self):
if self.data.properties: if self.data.properties:
for args in self.data.properties: for args in self.data.properties:
frappe.make_property_setter(args) frappe.make_property_setter(args)



def set_values(self): def set_values(self):
'''set values based on `data.set_value`'''
"""set values based on `data.set_value`"""
if self.data.set_value: if self.data.set_value:
for args in self.data.set_value: for args in self.data.set_value:
frappe.reload_doctype(args[0]) frappe.reload_doctype(args[0])
@@ -103,19 +105,27 @@ class Domain(Document):
doc.save() doc.save()


def setup_sidebar_items(self): def setup_sidebar_items(self):
'''Enable / disable sidebar items'''
"""Enable / disable sidebar items"""
if self.data.allow_sidebar_items: if self.data.allow_sidebar_items:
# disable all # disable all
frappe.db.sql('update `tabPortal Menu Item` set enabled=0')
frappe.db.sql("update `tabPortal Menu Item` set enabled=0")


# enable # enable
frappe.db.sql('''update `tabPortal Menu Item` set enabled=1
where route in ({0})'''.format(', '.join('"{0}"'.format(d) for d in self.data.allow_sidebar_items)))
frappe.db.sql(
"""update `tabPortal Menu Item` set enabled=1
where route in ({0})""".format(
", ".join('"{0}"'.format(d) for d in self.data.allow_sidebar_items)
)
)


if self.data.remove_sidebar_items: if self.data.remove_sidebar_items:
# disable all # disable all
frappe.db.sql('update `tabPortal Menu Item` set enabled=1')
frappe.db.sql("update `tabPortal Menu Item` set enabled=1")


# enable # enable
frappe.db.sql('''update `tabPortal Menu Item` set enabled=0
where route in ({0})'''.format(', '.join('"{0}"'.format(d) for d in self.data.remove_sidebar_items)))
frappe.db.sql(
"""update `tabPortal Menu Item` set enabled=0
where route in ({0})""".format(
", ".join('"{0}"'.format(d) for d in self.data.remove_sidebar_items)
)
)

+ 3
- 1
frappe/core/doctype/domain/test_domain.py 查看文件

@@ -1,8 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors # Copyright (c) 2017, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe
import unittest import unittest


import frappe


class TestDomain(unittest.TestCase): class TestDomain(unittest.TestCase):
pass pass

+ 29
- 23
frappe/core/doctype/domain_settings/domain_settings.py 查看文件

@@ -5,13 +5,14 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document



class DomainSettings(Document): class DomainSettings(Document):
def set_active_domains(self, domains): def set_active_domains(self, domains):
active_domains = [d.domain for d in self.active_domains] active_domains = [d.domain for d in self.active_domains]
added = False added = False
for d in domains: for d in domains:
if not d in active_domains: if not d in active_domains:
self.append('active_domains', dict(domain=d))
self.append("active_domains", dict(domain=d))
added = True added = True


if added: if added:
@@ -22,49 +23,52 @@ class DomainSettings(Document):
# set the flag to update the the desktop icons of all domains # set the flag to update the the desktop icons of all domains
if i >= 1: if i >= 1:
frappe.flags.keep_desktop_icons = True frappe.flags.keep_desktop_icons = True
domain = frappe.get_doc('Domain', d.domain)
domain = frappe.get_doc("Domain", d.domain)
domain.setup_domain() domain.setup_domain()


self.restrict_roles_and_modules() self.restrict_roles_and_modules()
frappe.clear_cache() frappe.clear_cache()


def restrict_roles_and_modules(self): def restrict_roles_and_modules(self):
'''Disable all restricted roles and set `restrict_to_domain` property in Module Def'''
"""Disable all restricted roles and set `restrict_to_domain` property in Module Def"""
active_domains = frappe.get_active_domains() active_domains = frappe.get_active_domains()
all_domains = list((frappe.get_hooks('domains') or {}))
all_domains = list((frappe.get_hooks("domains") or {}))


def remove_role(role): def remove_role(role):
frappe.db.delete("Has Role", {"role": role}) frappe.db.delete("Has Role", {"role": role})
frappe.set_value('Role', role, 'disabled', 1)
frappe.set_value("Role", role, "disabled", 1)


for domain in all_domains: for domain in all_domains:
data = frappe.get_domain_data(domain) data = frappe.get_domain_data(domain)
if not frappe.db.get_value('Domain', domain):
frappe.get_doc(dict(doctype='Domain', domain=domain)).insert()
if 'modules' in data:
for module in data.get('modules'):
frappe.db.set_value('Module Def', module, 'restrict_to_domain', domain)
if 'restricted_roles' in data:
for role in data['restricted_roles']:
if not frappe.db.get_value('Role', role):
frappe.get_doc(dict(doctype='Role', role_name=role)).insert()
frappe.db.set_value('Role', role, 'restrict_to_domain', domain)
if not frappe.db.get_value("Domain", domain):
frappe.get_doc(dict(doctype="Domain", domain=domain)).insert()
if "modules" in data:
for module in data.get("modules"):
frappe.db.set_value("Module Def", module, "restrict_to_domain", domain)
if "restricted_roles" in data:
for role in data["restricted_roles"]:
if not frappe.db.get_value("Role", role):
frappe.get_doc(dict(doctype="Role", role_name=role)).insert()
frappe.db.set_value("Role", role, "restrict_to_domain", domain)


if domain not in active_domains: if domain not in active_domains:
remove_role(role) remove_role(role)


if 'custom_fields' in data:
if "custom_fields" in data:
if domain not in active_domains: if domain not in active_domains:
inactive_domain = frappe.get_doc("Domain", domain) inactive_domain = frappe.get_doc("Domain", domain)
inactive_domain.setup_data() inactive_domain.setup_data()
inactive_domain.remove_custom_field() inactive_domain.remove_custom_field()



def get_active_domains(): def get_active_domains():
""" get the domains set in the Domain Settings as active domain """
"""get the domains set in the Domain Settings as active domain"""

def _get_active_domains(): def _get_active_domains():
domains = frappe.get_all("Has Domain", filters={ "parent": "Domain Settings" },
fields=["domain"], distinct=True)
domains = frappe.get_all(
"Has Domain", filters={"parent": "Domain Settings"}, fields=["domain"], distinct=True
)


active_domains = [row.get("domain") for row in domains] active_domains = [row.get("domain") for row in domains]
active_domains.append("") active_domains.append("")
@@ -72,14 +76,16 @@ def get_active_domains():


return frappe.cache().get_value("active_domains", _get_active_domains) return frappe.cache().get_value("active_domains", _get_active_domains)



def get_active_modules(): def get_active_modules():
""" get the active modules from Module Def"""
"""get the active modules from Module Def"""

def _get_active_modules(): def _get_active_modules():
active_modules = [] active_modules = []
active_domains = get_active_domains() active_domains = get_active_domains()
for m in frappe.get_all("Module Def", fields=['name', 'restrict_to_domain']):
for m in frappe.get_all("Module Def", fields=["name", "restrict_to_domain"]):
if (not m.restrict_to_domain) or (m.restrict_to_domain in active_domains): if (not m.restrict_to_domain) or (m.restrict_to_domain in active_domains):
active_modules.append(m.name) active_modules.append(m.name)
return active_modules return active_modules


return frappe.cache().get_value('active_modules', _get_active_modules)
return frappe.cache().get_value("active_modules", _get_active_modules)

+ 4
- 1
frappe/core/doctype/dynamic_link/dynamic_link.py 查看文件

@@ -5,12 +5,15 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document



class DynamicLink(Document): class DynamicLink(Document):
pass pass



def on_doctype_update(): def on_doctype_update():
frappe.db.add_index("Dynamic Link", ["link_doctype", "link_name"]) frappe.db.add_index("Dynamic Link", ["link_doctype", "link_name"])



def deduplicate_dynamic_links(doc): def deduplicate_dynamic_links(doc):
links, duplicate = [], False links, duplicate = [], False
for l in doc.links or []: for l in doc.links or []:
@@ -23,4 +26,4 @@ def deduplicate_dynamic_links(doc):
if duplicate: if duplicate:
doc.links = [] doc.links = []
for l in links: for l in links:
doc.append('links', dict(link_doctype=l[0], link_name=l[1]))
doc.append("links", dict(link_doctype=l[0], link_name=l[1]))

+ 10
- 5
frappe/core/doctype/error_log/error_log.py 查看文件

@@ -5,19 +5,24 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document



class ErrorLog(Document): class ErrorLog(Document):
def onload(self): def onload(self):
if not self.seen: if not self.seen:
self.db_set('seen', 1, update_modified=0)
self.db_set("seen", 1, update_modified=0)
frappe.db.commit() frappe.db.commit()



def set_old_logs_as_seen(): def set_old_logs_as_seen():
# set logs as seen # set logs as seen
frappe.db.sql("""UPDATE `tabError Log` SET `seen`=1
WHERE `seen`=0 AND `creation` < (NOW() - INTERVAL '7' DAY)""")
frappe.db.sql(
"""UPDATE `tabError Log` SET `seen`=1
WHERE `seen`=0 AND `creation` < (NOW() - INTERVAL '7' DAY)"""
)



@frappe.whitelist() @frappe.whitelist()
def clear_error_logs(): def clear_error_logs():
'''Flush all Error Logs'''
frappe.only_for('System Manager')
"""Flush all Error Logs"""
frappe.only_for("System Manager")
frappe.db.truncate("Error Log") frappe.db.truncate("Error Log")

+ 3
- 1
frappe/core/doctype/error_log/test_error_log.py 查看文件

@@ -1,10 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors # Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe
import unittest import unittest


import frappe

# test_records = frappe.get_test_records('Error Log') # test_records = frappe.get_test_records('Error Log')



class TestErrorLog(unittest.TestCase): class TestErrorLog(unittest.TestCase):
pass pass

部分文件因为文件数量过多而无法显示

正在加载...
取消
保存