瀏覽代碼

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 Key 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
b2fc959307c7c79f5584625569d5aed04133ba13

# Format codebase and sort imports
cb6f68e8c106ee2d037dd4b39dbb6d7c68caf1c8

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

@@ -16,6 +16,17 @@ repos:
- id: check-merge-conflict
- 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:
autoupdate_schedule: weekly


+ 640
- 305
frappe/__init__.py
文件差異過大導致無法顯示
查看文件


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

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


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

`/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
`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
"""

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

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

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

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

if frappe.local.request.method=="GET":
if frappe.local.request.method == "GET":
if not doc.has_permission("read"):
frappe.throw(_("Not permitted"), frappe.PermissionError)
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"):
frappe.throw(_("Not permitted"), frappe.PermissionError)

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

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

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

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
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
if doc.get("parenttype"):
@@ -183,7 +181,7 @@ def validate_oauth(authorization_header):
Authenticate request using OAuth and set session user

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
@@ -194,7 +192,9 @@ def validate_oauth(authorization_header):
req = frappe.request
parsed_url = urlparse(req.url)
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
headers = req.headers
body = req.get_data()
@@ -202,8 +202,12 @@ def validate_oauth(authorization_header):
body = None

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:
frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user"))
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

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:
auth_type, auth_token = authorization_header
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(":")
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(":")
validate_api_key_secret(api_key, api_secret, authorization_source)
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):
pass


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"""
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
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 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:
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.local.form_dict = form_dict


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)()

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

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

import os
import logging
import os

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

import frappe
import frappe.handler
import frappe.auth
import frappe.api
import frappe.auth
import frappe.handler
import frappe.monitor
import frappe.rate_limiter
import frappe.recorder
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.website.serve import get_response
from frappe.utils import get_site_name, sanitize_html
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])

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

class RequestContext(object):

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

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


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

elif request.path.startswith('/backups'):
elif request.path.startswith("/backups"):
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)

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

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

return response


def init_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)

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

if frappe.local.conf.get('maintenance_mode'):
if frappe.local.conf.get("maintenance_mode"):
frappe.connect()
raise frappe.SessionStopped('Session Stopped')
raise frappe.SessionStopped("Session Stopped")
else:
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)

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


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):
@@ -145,19 +150,20 @@ def process_response(response):
return

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

# rate limiter headers
if hasattr(frappe.local, 'rate_limiter'):
if hasattr(frappe.local, "rate_limiter"):
response.headers.extend(frappe.local.rate_limiter.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)


def set_cors_headers(response):
origin = frappe.request.headers.get('Origin')
origin = frappe.request.headers.get("Origin")
allow_cors = frappe.conf.allow_cors
if not (origin and allow_cors):
return
@@ -169,20 +175,25 @@ def set_cors_headers(response):
if origin not in allow_cors:
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):
import json

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)
else:
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
frappe.local.form_dict.pop("_")


def handle_exception(e):
response = None
http_status_code = getattr(e, "http_status_code", 500)
return_as_message = False
accept_header = frappe.get_request_header("Accept") or ""
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
print(frappe.get_traceback())

@@ -220,27 +230,38 @@ def handle_exception(e):
# if the request is ajax, send back the trace or error message
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 (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."),
http_status_code=http_status_code, indicator_color='red')
http_status_code=http_status_code,
indicator_color="red",
)
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"),
http_status_code=http_status_code, indicator_color='red')
http_status_code=http_status_code,
indicator_color="red",
)
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"),
http_status_code=http_status_code, indicator_color='red')
http_status_code=http_status_code,
indicator_color="red",
)
return_as_message = True

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:
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

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

return response


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

return rollback


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
_site = site
_sites_path = sites_path

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.config = {
'SERVER_NAME': 'localhost:8000'
}
application.config = {"SERVER_NAME": "localhost:8000"}

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

in_test_env = os.environ.get('CI')
in_test_env = os.environ.get("CI")
if in_test_env:
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_debugger=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.sessions import Session, clear_sessions, delete_session
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.password import check_password
from frappe.website.utils import get_home_page
@@ -47,20 +52,20 @@ class HTTPRequest:
def domain(self):
if not getattr(self, "_domain", None):
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:]

return self._domain

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:
frappe.local.request_ip = '127.0.0.1'
frappe.local.request_ip = "127.0.0.1"

def set_cookies(self):
frappe.local.cookie_manager = CookieManager()
@@ -75,7 +80,7 @@ class HTTPRequest:
if (
not frappe.local.session.data.csrf_token
or frappe.local.session.data.device == "mobile"
or frappe.conf.get('ignore_csrf', None)
or frappe.conf.get("ignore_csrf", None)
):
# not via boot
return
@@ -99,10 +104,10 @@ class HTTPRequest:
def connect(self):
"""connect to db, from ac_name or db_name"""
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:
def __init__(self):
self.user = None
@@ -110,13 +115,15 @@ class LoginManager:
self.full_name = 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:
return
self.resume = False

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

def login(self):
# 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()
self.authenticate(user=user, pwd=pwd)
if self.force_user_to_reset_password():
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"
return False

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

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

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

@@ -170,28 +180,27 @@ class LoginManager:
# set sid again
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")
if not resume:
frappe.local.response["message"] = "No App"
frappe.local.response["home_page"] = '/' + get_home_page()
frappe.local.response["home_page"] = "/" + get_home_page()
else:
frappe.local.cookie_manager.set_cookie("system_user", "yes")
if not resume:
frappe.local.response['message'] = 'Logged In'
frappe.local.response["message"] = "Logged In"
frappe.local.response["home_page"] = "/app"

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

# 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:
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("user_id", self.user)
@@ -202,8 +211,9 @@ class LoginManager:

def make_session(self, resume=False):
# 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
self.user = frappe.local.session_obj.user
@@ -212,7 +222,10 @@ class LoginManager:

def clear_active_sessions(self):
"""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

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

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):
self.fail(_('Incomplete login details'), user=user)
self.fail(_("Incomplete login details"), user=user)

user = User.find_by_credentials(user, pwd)

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

# 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)
# 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)

if not user.is_authenticated:
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()
self.fail('User disabled or missing', user=user.name)
self.fail("User disabled or missing", user=user.name)
else:
tracker and tracker.add_success_attempt()
self.user = user.name
@@ -254,12 +267,14 @@ class LoginManager:
if self.user in frappe.STANDARD_USERS:
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:
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)

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

def fail(self, message, user=None):
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")
frappe.db.commit()
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, []):
frappe.call(frappe.get_attr(method), login_manager=self)

def validate_hour(self):
"""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):
return

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:
frappe.throw(_("Login not allowed at this time"), frappe.AuthenticationError)
@@ -311,9 +327,10 @@ class LoginManager:
self.user = user
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:
delete_session(frappe.session.sid, user=user, reason="User Manually Logged Out")
@@ -324,13 +341,15 @@ class LoginManager:
def clear_cookies(self):
clear_cookies()


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

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
expires = datetime.datetime.now() + datetime.timedelta(days=3)
@@ -340,7 +359,7 @@ class CookieManager:
self.set_cookie("country", frappe.session.session_country)

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"

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

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

def flush_cookies(self, response):
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"),
secure=opts.get("secure"),
httponly=opts.get("httponly"),
samesite=opts.get("samesite"))
samesite=opts.get("samesite"),
)

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


def clear_cookies():
if hasattr(frappe.local, "session"):
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):
"""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()
if not ip_list:
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
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)


def get_login_attempt_tracker(user_name: str, raise_locked_exception: bool = True):
"""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
"""
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 = {}

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)

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


@@ -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.
"""
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 max_consecutive_login_attempts: Maximum allowed consecutive failed login attempts
@@ -446,15 +486,15 @@ class LoginAttemptTracker(object):

@property
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
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
def login_failed_count(self):
frappe.cache().hdel('login_failed_count', self.user_name)
frappe.cache().hdel("login_failed_count", self.user_name)

@property
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.
"""
return frappe.cache().hget('login_failed_time', self.user_name)
return frappe.cache().hget("login_failed_time", self.user_name)

@login_failed_time.setter
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
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):
""" 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.
"""
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()

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

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

@@ -507,6 +546,10 @@ class LoginAttemptTracker(object):
login_failed_count = self.login_failed_count or 0
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 True

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

@@ -24,9 +24,7 @@ class AssignmentRule(Document):
def validate_document_types(self):
if self.document_type == "ToDo":
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):
@@ -38,70 +36,70 @@ class AssignmentRule(Document):

frappe.throw(
_("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):
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 False

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

def do_assignment(self, doc):
# 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)

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
self.db_set('last_user', user)
self.db_set("last_user", user)
return True

return False

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):
'''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):
'''
"""
Get the next user for assignment
'''
if self.rule == 'Round Robin':
"""
if self.rule == "Round Robin":
return self.get_user_round_robin()
elif self.rule == 'Load Balancing':
elif self.rule == "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)

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

# first time, or last in list, pick the first
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
for i, d in enumerate(self.users):
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
return self.users[0].user

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 = []
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
sorted_counts = sorted(counts, key = lambda k: k['count'])
sorted_counts = sorted(counts, key=lambda k: k["count"])

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

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

def safe_eval(self, fieldname, doc):
@@ -145,12 +144,12 @@ class AssignmentRule(Document):
except Exception as e:
# when assignment fails, don't block the document as it may be
# 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

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):
today = frappe.flags.assignment_day or frappe.utils.get_weekday()
@@ -159,11 +158,14 @@ class AssignmentRule(Document):


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()
@@ -173,21 +175,30 @@ def bulk_apply(doctype, docnames):

for name in docnames:
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:
apply(doctype=doctype, name=name)


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:
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)

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:
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
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:
@@ -224,8 +238,8 @@ def apply(doc=None, method=None, doctype=None, name=None):
doc = doc.as_dict()
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:
# first unassign
@@ -260,14 +274,18 @@ def apply(doc=None, method=None, doctype=None, name=None):

if not new_apply:
# 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:
# 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:
_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):
"""Run on_update on every Document (via hooks.py)
"""
"""Run on_update on every Document (via hooks.py)"""
skip_document_update = (
frappe.flags.in_migrate
or frappe.flags.in_patch
@@ -306,7 +323,7 @@ def update_due_date(doc, state=None):
"due_date_based_on": ["is", "set"],
"document_type": doc.doctype,
"disabled": 0,
}
},
)

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

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:
todo_doc = frappe.get_doc('ToDo', todo)
todo_doc = frappe.get_doc("ToDo", todo)
todo_doc.date = doc.get(due_date_field)
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)



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

@@ -20,13 +20,13 @@ class TestAutoAssign(unittest.TestCase):
def setUp(self):
make_test_records("User")
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.assignment_rule = get_assignment_rule([days, days])
@@ -36,20 +36,22 @@ class TestAutoAssign(unittest.TestCase):
note = make_note(dict(public=1))

# 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))

# 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()

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

# check if auto assigned to third user, even if
# 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
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):
self.assignment_rule.rule = 'Load Balancing'
self.assignment_rule.rule = "Load Balancing"
self.assignment_rule.save()

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

# 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
# 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})

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

# 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):
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()

frappe.set_user('test1@example.com')
frappe.set_user("test1@example.com")
note = make_note(dict(public=1))
# 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))
# 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):
# check condition
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):
note = make_note(dict(public=1))

# 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
note.public = 0
@@ -151,99 +162,101 @@ class TestAutoAssign(unittest.TestCase):
todo.load_from_db()

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

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

# 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()

todo.load_from_db()

# check if todo is closed
self.assertEqual(todo.status, 'Closed')
self.assertEqual(todo.status, "Closed")
# 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):
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)
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):
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"
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"
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):
frappe.db.delete("Assignment Rule")

# Add expiry_date 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)
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)

# 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)

# 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.assertEqual(frappe.utils.get_date_str(note2_todo.date), expiry_date)
assignment_rule.delete()


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


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:
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
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


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:
note.update(values)


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

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


class AssignmentRuleDay(Document):
pass

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

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


class AssignmentRuleUser(Document):
pass

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

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

from datetime import timedelta

from dateutil.relativedelta import relativedelta

import frappe
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.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.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.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):
def validate(self):
@@ -46,7 +68,7 @@ class AutoRepeat(Document):
frappe.get_doc(self.reference_doctype, self.reference_document).notify_update()

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()

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)

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):
if frappe.flags.in_test or frappe.flags.in_patch:
return
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):
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):
if frappe.flags.in_patch:
return

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:
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):
if self.notify_by_email:
@@ -100,17 +129,17 @@ class AutoRepeat(Document):

frappe.throw(
_("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):
#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")
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:
frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", self.name)

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

if self.end_date:
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 = {
"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)
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):
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)
new_doc.insert(ignore_permissions = True)
new_doc.insert(ignore_permissions=True)

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

def update_doc(self, new_doc, reference_doc):
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):
new_doc.set(fieldname, reference_doc.get(fieldname))

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)

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):
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:
return

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))
else:
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):
"""
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):
month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1
@@ -295,60 +335,75 @@ class AutoRepeat(Document):
return 7

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):
"""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:
subject = _("New {0}: {1}").format(new_doc.doctype, new_doc.name)
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

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:
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 += _("{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:
message = error_string
elif not self.message:
message = _("Please find attached {0}: {1}").format(new_doc.doctype, new_doc.name)
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()
def fetch_linked_contacts(self):
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}
if not email_ids:
frappe.msgprint(_('No contacts linked to document'), alert=True)
frappe.msgprint(_("No contacts linked to document"), alert=True)
else:
self.recipients = ', '.join(email_ids)
self.recipients = ", ".join(email_ids)

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):
recipients = list(get_system_managers(only_name=True))
@@ -356,20 +411,17 @@ class AutoRepeat(Document):
subject = _("Auto Repeat Document Creation Failed")

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(
recipients=recipients,
subject=subject,
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):
days = list(week_map.keys())
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:
days = days[(current_schedule_day + 1):]
days = days[(current_schedule_day + 1) :]

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


#called through hooks
# called through hooks
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()

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):
for d in data:
doc = frappe.get_doc('Auto Repeat', d.name)
doc = frappe.get_doc("Auto Repeat", d.name)

current_date = getdate(today())
schedule_date = getdate(doc.next_schedule_date)
@@ -413,33 +465,32 @@ def create_repeated_entries(data):
doc.create_documents()
schedule_date = doc.get_next_schedule_date(schedule_date=schedule_date)
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):
if not date:
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():
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:
doc = frappe.get_doc("Auto Repeat", entry.name)
if doc.is_completed():
doc.status = 'Completed'
doc.status = "Completed"
doc.save()


@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:
start_date = getdate(today())
doc = frappe.new_doc('Auto Repeat')
doc = frappe.new_doc("Auto Repeat")
doc.reference_doctype = doctype
doc.reference_document = docname
doc.frequency = frequency
@@ -449,24 +500,34 @@ def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, e
doc.save()
return doc


# method for reference_doctype filter
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
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]

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 = set(list(docs))

return [[d] for d in docs]


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


@frappe.whitelist()
def generate_message_preview(reference_dt, reference_doc, message=None, subject=None):
frappe.has_permission("Auto Repeat", "write", throw=True)
doc = frappe.get_doc(reference_dt, reference_doc)
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:
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 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.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():
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):
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()

def test_daily_auto_repeat(self):
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)
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)
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):
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())
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)
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):
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())
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())
data = get_auto_repeat_entries(getdate(today()))
@@ -90,136 +116,173 @@ class TestAutoRepeat(unittest.TestCase):
end_date = add_months(start_date, 12)

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):
diff = (12 * end.year + end.month) - (12 * start.year + start.month)
return diff + 1

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()

data = get_auto_repeat_entries(getdate(today()))
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)

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()))
data = get_auto_repeat_entries(getdate(today()))
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)

def test_notification_is_attached(self):
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()))
create_repeated_entries(data)
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)

def test_next_schedule_date(self):
current_date = getdate(today())
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
# it should not be a previous month's date
self.assertTrue((doc.next_schedule_date >= current_date))

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)

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

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()
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)
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)


def make_auto_repeat(**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


def create_submittable_doctype(doctype, submit_perms=1):
if frappe.db.exists('DocType', doctype):
if frappe.db.exists("DocType", doctype):
return
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.save()
doc.save()

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

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


class AutoRepeatDay(Document):
pass

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

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


class Milestone(Document):
pass


def on_doctype_update():
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 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE
#import frappe
# import frappe
import unittest


class TestMilestone(unittest.TestCase):
pass

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

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

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


class MilestoneTracker(Document):
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):
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):
before_save = doc.get_doc_before_save()
from_value = before_save and before_save.get(self.track_field) or None
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):
if (frappe.flags.in_install
if (
frappe.flags.in_install
or frappe.flags.in_migrate
or frappe.flags.in_setup_wizard
or doc.doctype in log_types):
or doc.doctype in log_types
):
return

# track milestones related to this 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 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE
import unittest

import frappe
import frappe.cache_manager
import unittest

class TestMilestoneTracker(unittest.TestCase):
def test_milestone(self):
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(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()

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(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
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.defaults
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.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.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.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.functions import Count
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():
@@ -38,9 +40,9 @@ def get_bootinfo():
bootinfo.sysdefaults = frappe.defaults.get_defaults()
bootinfo.server_date = frappe.utils.nowdate()

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

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

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)
bootinfo.page_info = get_allowed_pages()
load_translations(bootinfo)
@@ -66,8 +70,8 @@ def get_bootinfo():
set_time_zone(bootinfo)

# 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
bootinfo.docs = doclist
@@ -77,7 +81,7 @@ def get_bootinfo():

if 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.calendars = sorted(frappe.get_hooks("calendars"))
@@ -97,37 +101,47 @@ def get_bootinfo():

return bootinfo


def get_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


def load_conf_settings(bootinfo):
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):
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.dashboards = frappe.get_all("Dashboard")


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):
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):
_cache = frappe.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:
return has_role

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

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

customRole = DocType("Custom 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
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(
(hasRole.parent == customRole.name)
& (parentTable.name == customRole[parent.lower()])
& (customRole[parent.lower()].isnotnull())
& (hasRole.role.isin(roles)))
& (hasRole.role.isin(roles))
)
).run(as_dict=True)

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 = (
frappe.qb.from_(customRole).select(customRole[parent.lower()])
frappe.qb.from_(customRole)
.select(customRole[parent.lower()])
.where(customRole[parent.lower()].isnotnull())
)

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

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

for p in pages_with_standard_roles:
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":
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
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)
).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}

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

# 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


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

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

# 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


def get_user_info():
# get info for current user
user_info = frappe._dict()
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

return user_info


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


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

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


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

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


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


def load_print_css(bootinfo, print_settings):
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():
note = DocType("Note")
nsb = DocType("Note Seen By").as_("nsb")

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(
(note.notify_on_every_login == 1)
& (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():
return frappe.get_all("Success Action", fields=["*"])


def get_link_preview_doctypes():
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:
@@ -309,22 +352,23 @@ def get_link_preview_doctypes():

return link_preview_doctypes


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

return filter_config


def add_layouts(bootinfo):
# 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():
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 = {}

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

return desk_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()
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]


def set_time_zone(bootinfo):
bootinfo.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
# License: MIT. See LICENSE
import os
import shutil
import re
import shutil
import subprocess
from distutils.spawn import find_executable
from subprocess import getoutput
@@ -25,6 +25,7 @@ sites_path = os.path.abspath(os.getcwd())
class AssetsNotDownloadedError(Exception):
pass


class AssetsDontExistError(HTTPError):
pass

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


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 = []
current_asset_files = []
@@ -60,7 +61,7 @@ def build_missing_files():
assets_json = frappe.parse_json(assets_json)

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

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:
tag = getoutput(
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:
@@ -111,6 +111,7 @@ def fetch_assets(url, frappe_head):

def setup_assets(assets_archive):
import tarfile

directories_created = set()

click.secho("\nExtracting assets...\n", fg="yellow")
@@ -221,7 +222,16 @@ def setup():
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"""
setup()
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"

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

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

@@ -253,9 +263,7 @@ def watch(apps=None):
if 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:
command += " --live-reload"
@@ -266,8 +274,8 @@ def watch(apps=None):


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:
click.echo(f"{warn} Please update your node version to 14")
if not find_executable("yarn"):
@@ -276,9 +284,7 @@ def check_node_executable():


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


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


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)
try:
max_str = os.get_terminal_size().columns
@@ -367,7 +372,9 @@ def make_asset_dirs(hard_link=False):
symlinks = generate_assets_map()

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}")

# 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
content = re.sub(r"(<!--.*?-->)", "", content)

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


def html_to_js_template(path, content):
"""returns HTML template content as Javascript code, adding it to `frappe.templates`"""
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
# 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.desk.notifications import (delete_notification_count_for,
clear_notifications)

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):
cache = frappe.cache()
@@ -47,11 +89,13 @@ def clear_user_cache(user=None):
clear_defaults_cache()
clear_global_cache()


def clear_domain_cache(user=None):
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)


def clear_global_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.setup_module_map()


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


def clear_doctype_cache(doctype=None):
clear_controller_cache(doctype)
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]

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

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

# 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 all notifications
@@ -101,6 +148,7 @@ def clear_doctype_cache(doctype=None):
for name in doctype_cache_keys:
cache.delete_value(name)


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


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

if doctype_map is not None:
@@ -121,7 +170,7 @@ def get_doctype_map(doctype, name, filters=None, order_by=None):
else:
# non cached, build cache
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))
except frappe.db.TableMissingError:
# executed from inside patch, ignore
@@ -129,15 +178,19 @@ def get_doctype_map(doctype, name, filters=None, order_by=None):

return items


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():
if (frappe.flags.in_patch
if (
frappe.flags.in_patch
or frappe.flags.in_install
or frappe.flags.in_migrate
or frappe.flags.in_import
or frappe.flags.in_setup_wizard):
or frappe.flags.in_setup_wizard
):
return

_cache = frappe.cache()
@@ -145,39 +198,45 @@ def build_table_count_cache():
table_rows = frappe.qb.Field("table_rows").as_("count")
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)

return counts


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_migrate
or frappe.flags.in_import
or frappe.flags.in_setup_wizard):
or frappe.flags.in_setup_wizard
):
return
_cache = frappe.cache()
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]
_cache.set_value("domain_restricted_doctypes", doctypes)

return doctypes


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_migrate
or frappe.flags.in_import
or frappe.flags.in_setup_wizard):
or frappe.flags.in_setup_wizard
):
return
_cache = frappe.cache()
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]
_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
# License: MIT. See LICENSE
import json
import os

import frappe
from frappe import _
import frappe.model
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.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.

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


@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 fields: fields to be returned. Default is `name`
:param filters: filter list by this dict
:param order_by: Order by this fieldname
: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):
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_page_length=limit_page_length,
debug=debug,
as_list=not as_dict
as_list=not as_dict,
)

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


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


@frappe.whitelist()
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 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):
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()


@frappe.whitelist()
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 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):
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:
value = frappe.db.get_values_from_single(fields, filters, doctype, as_dict=as_dict, debug=debug)
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:
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]


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

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

@frappe.whitelist(methods=["POST", "PUT"])
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 name: name of the document
: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):
frappe.throw(_("Cannot edit standard fields"))
@@ -137,7 +162,7 @@ def set_value(doctype, name, fieldname, value=None):
try:
values = json.loads(fieldname)
except ValueError:
values = {fieldname: ''}
values = {fieldname: ""}
else:
values = {fieldname: value}

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

return doc.as_dict()

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

@frappe.whitelist(methods=["POST", "PUT"])
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):
doc = json.loads(doc)

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

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

@frappe.whitelist(methods=["POST", "PUT"])
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):
docs = json.loads(docs)

out = []

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:
if doc.get("parenttype"):
@@ -199,11 +226,12 @@ def insert_many(docs=None):

return out

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

@frappe.whitelist(methods=["POST", "PUT"])
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):
doc = json.loads(doc)

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

return doc.as_dict()

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

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

:param doctype: DocType 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)
return new_name

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

@frappe.whitelist(methods=["POST", "PUT"])
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):
doc = json.loads(doc)

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

return doc.as_dict()

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

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

: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.cancel()

return wrapper.as_dict()

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

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

: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.whitelist(methods=['POST', 'PUT'])

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


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


@frappe.whitelist(methods=['POST', 'PUT'])
@frappe.whitelist(methods=["POST", "PUT"])
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):
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):
'''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)
failed_docs = []
for doc in docs:
@@ -290,41 +325,40 @@ def bulk_update(docs):
existing_doc.update(doc)
existing_doc.save()
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()
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 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
return {"has_permission": frappe.has_permission(doctype, perm_type.lower(), docname)}


@frappe.whitelist()
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 name: `name` of the document that holds the password
:param fieldname: `fieldname` of the password property
'''
"""
frappe.only_for("System Manager")
return frappe.get_doc(doctype, name).get_password(fieldname)


@frappe.whitelist()
def get_js(items):
'''Load JS code files. Will also append translations
"""Load JS code files. Will also append translations
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)
out = []
for src in items:
@@ -346,14 +380,25 @@ def get_js(items):

return out


@frappe.whitelist(allow_guest=True)
def get_time_zone():
'''Returns default time zone'''
"""Returns default 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 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 decode_base64: decode filedata from base64 encode, default is False
: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")

@@ -374,16 +419,19 @@ def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder
if not doc.has_permission():
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()

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

return _file.as_dict()


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


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

return False


@frappe.whitelist()
def validate_link(doctype: str, docname: str, fields=None):
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"))

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(
_("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()
@@ -438,14 +485,11 @@ def validate_link(doctype: str, docname: str, fields=None):
except frappe.PermissionError:
frappe.clear_last_message()
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"),
indicator="orange"
indicator="orange",
)

return values

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

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

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

import click

import frappe
import frappe.utils

click.disable_unicode_literals_warning = True


def pass_context(f):
@wraps(f)
def _func(ctx, *args, **kwargs):
profile = ctx.obj['profile']
profile = ctx.obj["profile"]
if profile:
pr = cProfile.Profile()
pr.enable()
@@ -25,18 +28,17 @@ def pass_context(f):
try:
ret = f(frappe._dict(ctx.obj), *args, **kwargs)
except frappe.exceptions.SiteNotSpecifiedError as e:
click.secho(str(e), fg='yellow')
click.secho(str(e), fg="yellow")
sys.exit(1)
except frappe.exceptions.IncorrectSitePath:
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)

if profile:
pr.disable()
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()

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

return click.pass_context(_func)


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


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:
env = dict(environ, **env)

def set_low_prio():
import psutil

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

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

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

return return_


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


def get_commands():
# prevent circular imports
from .redis_utils import commands as redis_commands
from .scheduler import commands as scheduler_commands
from .site import commands as site_commands
from .translate import commands as translate_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 = (
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:


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

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

import frappe
from frappe.utils.redis_queue import RedisQueue
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):
"""Create Redis Queue users and add to acl and app configs.

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.
"""
acl_file_path = os.path.abspath('../config/redis_queue.acl')
acl_file_path = os.path.abspath("../config/redis_queue.acl")

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()
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:
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('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 click

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.utils import cint


def _is_scheduler_enabled():
enable_scheduler = False
try:
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:
pass
finally:
@@ -44,11 +49,12 @@ def trigger_scheduler_event(context, event):
sys.exit(exit_code)


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

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

@click.command('disable-scheduler')

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

for site in context.sites:
try:
frappe.init(site=site)
@@ -79,13 +87,13 @@ def disable_scheduler(context):
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
def scheduler(context, state, site=None):
from frappe.installer import update_site_config
import frappe.utils.scheduler
from frappe.installer import update_site_config

if not site:
site = get_site(context)
@@ -93,58 +101,64 @@ def scheduler(context, state, site=None):
try:
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.utils.scheduler.disable_scheduler()
frappe.db.commit()
elif state == 'enable':
elif state == "enable":
frappe.connect()
frappe.utils.scheduler.enable_scheduler()
frappe.db.commit()

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

finally:
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
def set_maintenance_mode(context, state, site=None):
from frappe.installer import update_site_config

if not site:
site = get_site(context)

try:
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:
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
def doctor(context, site=None):
"Get diagnostic info about background workers"
from frappe.utils.doctor import doctor as _doctor

if not site:
site = get_site(context, raise_err=False)
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
def show_pending_jobs(context, site=None):
"Get diagnostic info about background jobs"
from frappe.utils.doctor import pending_jobs as _pending_jobs

if not site:
site = get_site(context)

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

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):
"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
frappe.init(site or '')

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

@click.command('schedule')

@click.command("schedule")
def start_scheduler():
from frappe.utils.scheduler import 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
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
def ready_for_migration(context, site=None):
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)

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)

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

finally:
frappe.destroy()


commands = [
disable_scheduler,
doctor,


+ 424
- 238
frappe/commands/site.py
文件差異過大導致無法顯示
查看文件


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

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

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


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

for site in context.sites:
try:
frappe.init(site=site)
@@ -18,32 +21,41 @@ def build_message_files(context):
if not context.sites:
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
@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):
"""Create lang-code.csv for given app"""
import frappe.translate

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

# init site
frappe.connect(site=context['sites'][0])
frappe.connect(site=context["sites"][0])
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
def get_untranslated(context, lang, untranslated_file, all=None):
"Get untranslated strings for language"
import frappe.translate

site = get_site(context)
try:
frappe.init(site=site)
@@ -52,14 +64,16 @@ def get_untranslated(context, lang, untranslated_file, all=None):
finally:
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
def update_translations(context, lang, untranslated_file, translated_file):
"Update translated strings"
import frappe.translate

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

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

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

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


commands = [
build_message_files,
get_untranslated,


+ 405
- 221
frappe/commands/utils.py
文件差異過大導致無法顯示
查看文件


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

@@ -1,14 +1,20 @@
import frappe
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):
if not user:
user = frappe.session.user

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
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

# 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


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


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():
table_rows = frappe.qb.Field("table_rows")
table_name = frappe.qb.Field("table_name")
information_schema = frappe.qb.Schema("information_schema")

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()

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]
return empty_tables_by_module


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


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
# License: MIT. See LICENSE

import frappe

from frappe import _
import functools
import re

import frappe
from frappe import _


def load_address_and_contact(doc, key=None):
"""Loads address list and contact list in `__onload`"""
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 = [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 = []
filters = [
@@ -37,29 +41,38 @@ def load_address_and_contact(doc, key=None):
contact_list = frappe.get_list("Contact", filters=filters, fields=["*"])

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",
"parent": contact.name,
"is_primary_phone": 0,
"is_primary_mobile_no": 0
}, fields=["phone"])
"is_primary_mobile_no": 0,
},
fields=["phone"],
)

if contact.address:
address = frappe.get_doc("Address", contact.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):
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
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
name = doc.get(df.fieldname)
names.append(name)
@@ -81,12 +94,15 @@ def has_permission(doc, ptype, user):
return True
return False


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


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


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

@@ -100,7 +116,9 @@ def get_permission_query_conditions(doctype):
# when everything is not permitted
for df in links.get("not_permitted_links"):
# 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) + " )"

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

for df in links.get("permitted_links"):
# 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) + " )"


def get_permitted_and_not_permitted_links(doctype):
permitted_links = []
not_permitted_links = []
@@ -129,40 +150,40 @@ def get_permitted_and_not_permitted_links(doctype):
else:
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):
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""",
(parenttype, doctype, docname))
(parenttype, doctype, docname),
)

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


@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
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]
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


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


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

@@ -2,16 +2,15 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# 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 jinja2 import TemplateSyntaxError
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):
@@ -24,10 +23,11 @@ class Address(Document):
self.address_title = self.links[0].link_name

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):
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:
throw(_("Address Title is mandatory."))

@@ -42,15 +42,15 @@ class Address(Document):
if not self.links:
contact_name = frappe.db.get_value("Contact", {"email_id": self.owner})
if contact_name:
contact = frappe.get_cached_doc('Contact', contact_name)
contact = frappe.get_cached_doc("Contact", contact_name)
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 False

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:
if self.get(field):
@@ -76,9 +76,11 @@ class Address(Document):

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
FROM
`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.link_name = %s and ifnull(addr.disabled, 0) = 0 and
%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:
return address[0].name

return


@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

out = frappe.db.sql(""" SELECT
out = frappe.db.sql(
""" SELECT
addr.name, addr.%s
FROM
`tabAddress` addr, `tabDynamic Link` dl
WHERE
dl.parent = addr.name and dl.link_doctype = %s and
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:
for contact in out:
@@ -150,84 +162,96 @@ def get_territory_from_address(address):

return territory


def get_list_context(context=None):
return {
"title": _("Addresses"),
"get_list": get_address_list,
"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

user = frappe.session.user
ignore_permissions = True

if not filters: filters = []
if not filters:
filters = []
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):
"""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})

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

return False


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:
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:
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:
return result


def get_company_address(company):
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)

return ret


@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def address_query(doctype, txt, searchfield, start, page_len, filters):
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 = ""
meta = frappe.get_meta("Address")
for fieldname, value in filters.items():
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()

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)

search_condition = ''
search_condition = ""
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:
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
from
`tabAddress`, `tabDynamic Link`
@@ -245,19 +269,24 @@ def address_query(doctype, txt, searchfield, start, page_len, filters):
limit %(start)s, %(page_len)s """.format(
mcond=get_match_cond(doctype),
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):
fields = ["address_title", "address_line1", "address_line2", "city", "county", "state", "country"]
return ", ".join(doc.get(d) for d in fields if doc.get(d))


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 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE
import frappe, unittest
import unittest

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


class TestAddress(unittest.TestCase):
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
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

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

class AddressTemplate(Document):
def validate(self):
if not self.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.defaults:
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"))

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


@frappe.whitelist()
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>
{% 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 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 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE
import frappe, unittest
import unittest

import frappe


class TestAddressTemplate(unittest.TestCase):
def setUp(self):
@@ -27,17 +30,12 @@ class TestAddressTemplate(unittest.TestCase):
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 -%}"""

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
# License: MIT. See LICENSE
import frappe
from frappe.utils import cstr, has_gravatar
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.model.document import Document
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):
def autoname(self):
# 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):
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
for link in self.links:
self.name = self.name + '-' + link.link_name.strip()
self.name = self.name + "-" + link.link_name.strip()
break

def validate(self):
@@ -45,7 +46,7 @@ class Contact(Document):
self.user = frappe.db.get_value("User", {"email": self.email_id})

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:
if link.link_doctype == link_doctype:
return link.link_name
@@ -65,21 +66,21 @@ class Contact(Document):

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}):
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:
self.save(ignore_permissions=True)

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}):
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:
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)]

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
for d in self.phone_nos:
@@ -125,9 +128,11 @@ class Contact(Document):
if not primary_number_exists:
setattr(self, fieldname, "")


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)
as is_primary_contact
from
@@ -135,7 +140,10 @@ def get_default_contact(doctype, name):
where
dl.link_doctype=%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:
for contact in out:
@@ -145,6 +153,7 @@ def get_default_contact(doctype, name):
else:
return None


@frappe.whitelist()
def invite_user(contact):
contact = frappe.get_doc("Contact", contact)
@@ -153,34 +162,39 @@ def invite_user(contact):
frappe.throw(_("Please set Email Address"))

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


@frappe.whitelist()
def get_contact_details(contact):
contact = frappe.get_doc("Contact", contact)
out = {
"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_mobile": contact.get("mobile_no"),
"contact_phone": contact.get("phone"),
"contact_designation": contact.get("designation"),
"contact_department": contact.get("department")
"contact_department": contact.get("department"),
}
return out


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})
if contact_name:
contact = frappe.get_doc("Contact", contact_name)
@@ -190,19 +204,23 @@ def update_contact(doc, method):
contact.flags.ignore_mandatory = True
contact.save(ignore_permissions=True)


@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def contact_query(doctype, txt, searchfield, start, page_len, filters):
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 []

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
from
`tabContact`, `tabDynamic Link`
@@ -216,68 +234,90 @@ def contact_query(doctype, txt, searchfield, start, page_len, filters):
order by
if(locate(%(_txt)s, `tabContact`.name), locate(%(_txt)s, `tabContact`.name), 99999),
`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()
def address_query(links):
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 = []

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

res = frappe.db.sql("""
res = frappe.db.sql(
"""
SELECT `tabAddress`.name
FROM `tabAddress`, `tabDynamic Link`
WHERE `tabDynamic Link`.parenttype='Address'
AND `tabDynamic Link`.parent=`tabAddress`.name
AND `tabDynamic Link`.link_doctype = %(link_doctype)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])

return result


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


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


def get_contacts_linking_to(doctype, docname, fields=None):
"""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):
"""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:
return []

@@ -285,6 +325,4 @@ def get_contacts_linked_from(doctype, docname, fields=None):
if not contact_names:
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 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# License: MIT. See LICENSE
import frappe
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):
emails = [
{"email": "test1@example.com", "is_primary": 0},
@@ -32,13 +33,11 @@ class TestContact(unittest.TestCase):
self.assertEqual(contact.phone, "+91 0000000002")
self.assertEqual(contact.mobile_no, "+91 0000000003")


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:
for d in emails:


+ 1
- 0
frappe/contacts/doctype/contact_email/contact_email.py 查看文件

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


class ContactEmail(Document):
pass

+ 1
- 0
frappe/contacts/doctype/contact_phone/contact_phone.py 查看文件

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


class ContactPhone(Document):
pass

+ 1
- 0
frappe/contacts/doctype/gender/gender.py 查看文件

@@ -4,5 +4,6 @@

from frappe.model.document import Document


class Gender(Document):
pass

+ 1
- 0
frappe/contacts/doctype/gender/test_gender.py 查看文件

@@ -3,5 +3,6 @@
# License: MIT. See LICENSE
import unittest


class TestGender(unittest.TestCase):
pass

+ 1
- 0
frappe/contacts/doctype/salutation/salutation.py 查看文件

@@ -4,5 +4,6 @@

from frappe.model.document import Document


class Salutation(Document):
pass

+ 1
- 0
frappe/contacts/doctype/salutation/test_salutation.py 查看文件

@@ -3,5 +3,6 @@
# License: MIT. See LICENSE
import unittest


class TestSalutation(unittest.TestCase):
pass

+ 40
- 10
frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py 查看文件

@@ -4,17 +4,37 @@ import frappe
from frappe import _

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):
columns, data = get_columns(filters), get_data(filters)
return columns, data


def get_columns(filters):
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 2",
"City",
@@ -27,9 +47,10 @@ def get_columns(filters):
"Address",
"Phone",
"Email Id",
"Is Primary Contact:Check"
"Is Primary Contact:Check",
]


def get_data(filters):
data = []
reference_doctype = filters.get("reference_doctype")
@@ -37,6 +58,7 @@ def get_data(filters):

return get_reference_addresses_and_contact(reference_doctype, reference_name)


def get_reference_addresses_and_contact(reference_doctype, reference_name):
data = []
filters = None
@@ -48,16 +70,22 @@ def get_reference_addresses_and_contact(reference_doctype, reference_name):
if 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:
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():
addresses = details.get("address", [])
contacts = details.get("contact", [])
contacts = details.get("contact", [])
if not any([addresses, contacts]):
result = [reference_name]
result.extend(add_blank_columns_for("Address"))
@@ -78,10 +106,11 @@ def get_reference_addresses_and_contact(reference_doctype, reference_name):

return data


def get_reference_details(reference_doctype, doctype, reference_list, reference_details):
filters = [
filters = [
["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, [])

@@ -97,5 +126,6 @@ def get_reference_details(reference_doctype, doctype, reference_list, reference_
reference_details[reference_list[0]][frappe.scrub(doctype)] = temp_records
return reference_details


def add_blank_columns_for(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.defaults
import unittest

from frappe.contacts.report.addresses_and_contacts.addresses_and_contacts import get_data


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

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()


def get_custom_doc_for_address_and_contacts():
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


def create_linked_address(link_list):
if frappe.flags.test_address_created:
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:
address.append("links",{
'link_doctype': 'Test Custom Doctype',
'link_name': name
})
address.append("links", {"link_doctype": "Test Custom Doctype", "link_name": name})

address.insert()
frappe.flags.test_address_created = True

return address.name


def create_linked_contact(link_list, address):
if frappe.flags.test_contact_created:
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_phone("+91 0000000000", is_primary_phone=True)

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)
frappe.flags.test_contact_created = True
@@ -103,7 +95,23 @@ class TestAddressesAndContacts(unittest.TestCase):
create_linked_contact(links_list, d)
report_data = get_data({"reference_doctype": "Test Custom Doctype"})
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])

def tearDown(self):


+ 0
- 1
frappe/core/doctype/__init__.py 查看文件

@@ -1,3 +1,2 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# 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
# License: MIT. See LICENSE
import frappe
from frappe.utils import cstr
from tenacity import retry, retry_if_exception_type, stop_after_attempt

import frappe
from frappe.model.document import Document
from frappe.utils import cstr


class AccessLog(Document):
@@ -22,14 +23,19 @@ def make_access_log(
columns=None,
):
_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()
@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(
doctype=None,
document=None,
@@ -43,18 +49,20 @@ def _make_access_log(
user = frappe.session.user
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`
# 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
# License: MIT. See LICENSE

# imports - standard imports
import unittest
import base64
import os

# imports - standard imports
import unittest

# imports - third party imports
import requests

# imports - module imports
import frappe
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.user.user import generate_keys

# imports - third party imports
import requests
from frappe.utils import cstr, get_site_url


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(frappe.session.user)
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")
self.header = {"Authorization": "token {}:{}".format(api_key, generated_secret)}

@@ -101,54 +103,55 @@ class TestAccessLog(unittest.TestCase):
"party": [],
"group_by": "Group by Voucher (Consolidated)",
"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)


def test_make_full_access_log(self):
self.maxDiff = None

# 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,
report_name=self.test_report_name,
page=self.test_html_template,
file_type=self.test_file_type,
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(self.test_doctype, last_doc.export_from)
self.assertEqual(self.test_document, last_doc.reference_document)


def test_make_export_log(self):
# export data and delete temp file generated on disk
export_csv(self.test_doctype, self.file_name)
os.remove(self.file_name)

# 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)


def test_private_file_download(self):
# 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()

# access the created file
@@ -156,7 +159,7 @@ class TestAccessLog(unittest.TestCase):

try:
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:
# check for the access log of downloaded file
@@ -169,6 +172,5 @@ class TestAccessLog(unittest.TestCase):
# cleanup
new_private_file.delete()


def tearDown(self):
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:
self.status = "Linked"


def on_doctype_update():
"""Add indexes in `tabActivity Log`"""
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", ["link_doctype", "link_name"])


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):
"""clear 90 day old authentication logs or configured in log settings"""
@@ -47,6 +52,4 @@ def clear_activity_logs(days=None):
if not days:
days = 90
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.permissions
from frappe.utils import get_fullname
from frappe import _
from frappe.core.doctype.activity_log.activity_log import add_authentication_log
from frappe.utils import get_fullname


def update_feed(doc, method=None):
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import:
return

if doc._action!="save" or doc.flags.ignore_feed:
if doc._action != "save" or doc.flags.ignore_feed:
return

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

# 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):
if login_manager.user != "Guest":
subject = _("{0} logged in").format(get_fullname(login_manager.user))
add_authentication_log(subject, login_manager.user)


def logout_feed(user, reason):
if user and user != "Guest":
subject = _("{0} logged out: {1}").format(get_fullname(user), frappe.bold(reason))
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)
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:
conditions += ["""(`tab{doctype}`.reference_doctype is null
conditions += [
"""(`tab{doctype}`.reference_doctype is null
or `tab{doctype}`.reference_doctype = ''
or `tab{doctype}`.reference_doctype
in ({values}))""".format(
doctype = doctype,
values =", ".join(can_read_doctypes)
)]
doctype=doctype, values=", ".join(can_read_doctypes)
)
]

if user_permissions:
can_read_docs = []
for dt, obj in user_permissions.items():
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:
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 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE
import frappe
import unittest
import time
from frappe.auth import LoginManager, CookieManager
import unittest

import frappe
from frappe.auth import CookieManager, LoginManager


class TestActivityLog(unittest.TestCase):
def test_activity_log(self):

# 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.login_manager = LoginManager()

auth_log = self.get_auth_log()
self.assertEqual(auth_log.status, 'Success')
self.assertEqual(auth_log.status, "Success")

# test user logout log
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
frappe.form_dict.update({ 'pwd': 'password' })
frappe.form_dict.update({"pwd": "password"})
self.assertRaises(frappe.AuthenticationError, LoginManager)
auth_log = self.get_auth_log()
self.assertEqual(auth_log.status, 'Failed')
self.assertEqual(auth_log.status, "Failed")

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]
auth_log = frappe.get_doc('Activity Log', name)
auth_log = frappe.get_doc("Activity Log", name)
return auth_log

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.login_manager = LoginManager()

auth_log = self.get_auth_log()
self.assertEqual(auth_log.status, 'Success')
self.assertEqual(auth_log.status, "Success")

# test user logout log
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
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)
@@ -85,8 +82,9 @@ class TestActivityLog(unittest.TestCase):

frappe.local.form_dict = frappe._dict()


def update_system_settings(args):
doc = frappe.get_doc('System Settings')
doc = frappe.get_doc("System Settings")
doc.update(args)
doc.flags.ignore_mandatory = 1
doc.save()

+ 1
- 0
frappe/core/doctype/block_module/block_module.py 查看文件

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


class BlockModule(Document):
pass

+ 79
- 55
frappe/core/doctype/comment/comment.py 查看文件

@@ -1,22 +1,27 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# License: MIT. See LICENSE
import json

import frappe
from frappe import _
import json
from frappe.model.document import Document
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.desk.doctype.notification_log.notification_log import (
enqueue_create_notification,
get_title,
get_title_html,
)
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):
def after_insert(self):
self.notify_mentions()
self.notify_change('add')
self.notify_change("add")

def validate(self):
if not self.comment_email:
@@ -26,34 +31,35 @@ class Comment(Document):
def on_update(self):
update_comment_in_doc(self)
if self.is_new():
self.notify_change('update')
self.notify_change("update")

def on_trash(self):
self.remove_comment_from_cache()
self.notify_change('delete')
self.notify_change("delete")

def notify_change(self, action):
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)
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):
_comments = get_comments_from_parent(self)
for c in _comments:
if c.get("name")==self.name:
if c.get("name") == self.name:
_comments.remove(c)

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)
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 = {
'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)
@@ -99,45 +112,46 @@ def update_comment_in_doc(doc):

`_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.
if doc.doctype == 'Comment' and doc.comment_type != 'Comment':
if doc.doctype == "Comment" and doc.comment_type != "Comment":
return

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:
_comments = get_comments_from_parent(doc)

updated = False
for c in _comments:
if c.get("name")==doc.name:
if c.get("name") == doc.name:
c["comment"] = get_truncated(doc.content)
updated = True

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)


def get_comments_from_parent(doc):
'''
"""
get the list of comments cached in the document record in the column
`_comments`
'''
"""
try:
_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:
return []


def update_comments_in_parent(reference_doctype, reference_name, _comments):
"""Updates `_comments` property in parent Document with given dict.

: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

try:
# 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:
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
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):
raise frappe.DataTooLongException
@@ -183,6 +206,7 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments):
if getattr(reference_doc, "route", None):
clear_cache(reference_doc.route)


def update_comments_in_parent_after_request():
"""update _comments in parent if _comments column is missing"""
if hasattr(frappe.local, "_comments"):


+ 51
- 35
frappe/core/doctype/comment/test_comment.py 查看文件

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

import frappe


class TestComment(unittest.TestCase):
def tearDown(self):
frappe.form_dict.comment = None
@@ -15,75 +18,88 @@ class TestComment(unittest.TestCase):
frappe.local.request_ip = None

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()
comment = test_doc.add_comment('Comment', 'test comment')
comment = test_doc.add_comment("Comment", "test comment")

test_doc.reload()

# 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
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
def test_public_comment(self):
from frappe.website.doctype.blog_post.test_blog_post import make_test_blog

test_blog = make_test_blog()

frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})

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.route = test_blog.route
frappe.local.request_ip = '127.0.0.1'
frappe.local.request_ip = "127.0.0.1"

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.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()

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
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()

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()




+ 0
- 1
frappe/core/doctype/communication/__init__.py 查看文件

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


+ 173
- 125
frappe/core/doctype/communication/communication.py 查看文件

@@ -2,49 +2,67 @@
# License: MIT. See LICENSE

from collections import Counter
from email.utils import getaddresses
from typing import List
from urllib.parse import unquote

from parse import compile

import frappe
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.mixins import CommunicationEmailMixin
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.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


class Communication(Document, CommunicationEmailMixin):
"""Communication represents an external communication like Email.
"""
"""Communication represents an external communication like Email."""
no_feed_on_delete = True
DOCTYPE = 'Communication'
DOCTYPE = "Communication"

def onload(self):
"""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:
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()

def validate(self):
@@ -74,25 +92,33 @@ class Communication(Document, CommunicationEmailMixin):
def validate_reference(self):
if self.reference_doctype and self.reference_name:
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
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
if self.reference_doctype == "Communication":
circular_linking = False
doc = get_parent_doc(self)
while doc.reference_doctype == "Communication":
if get_parent_doc(doc).name==self.name:
if get_parent_doc(doc).name == self.name:
circular_linking = True
break
doc = get_parent_doc(doc)

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):
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")

if self.communication_type == "Communication":
self.notify_change('add')
self.notify_change("add")

elif self.communication_type in ("Chat", "Notification"):
if self.reference_name == frappe.session.user:
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:
# 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):
"""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:
return

@@ -128,11 +154,15 @@ class Communication(Document, CommunicationEmailMixin):

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(
"Email Account",
@@ -157,19 +187,19 @@ class Communication(Document, CommunicationEmailMixin):
# comments count for the list view
update_comment_in_doc(self)

if self.comment_type != 'Updated':
if self.comment_type != "Updated":
update_parent_document_on_communication(self)

def on_trash(self):
if self.communication_type == "Communication":
self.notify_change('delete')
self.notify_change("delete")

@property
def sender_mailid(self):
return parse_addr(self.sender)[1] if self.sender else ""

@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.

* 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(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)

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)

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)

def get_attachments(self):
attachments = frappe.get_all(
"File",
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

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):
if not self.is_new():
@@ -216,15 +243,19 @@ class Communication(Document, CommunicationEmailMixin):

if self.reference_doctype and self.reference_name:
self.status = "Linked"
elif self.communication_type=="Communication":
elif self.communication_type == "Communication":
self.status = "Open"
else:
self.status = "Closed"

# 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"

@@ -254,7 +285,7 @@ class Communication(Document, CommunicationEmailMixin):
self.sender_full_name = self.sender
self.sender = None
else:
if self.sent_or_received=='Sent':
if self.sent_or_received == "Sent":
validate_email_address(self.sender, throw=True)
sender_name, sender_email = parse_addr(self.sender)
if sender_name == sender_email:
@@ -264,40 +295,41 @@ class Communication(Document, CommunicationEmailMixin):
self.sender_full_name = sender_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:
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]
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:
self.sender_full_name = sender_email

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
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":
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:
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
self.notify_update()
@@ -311,13 +343,17 @@ class Communication(Document, CommunicationEmailMixin):
# Timeline Links
def set_timeline_links(self):
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:
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)

def deduplicate_timeline_links(self):
@@ -332,17 +368,12 @@ class Communication(Document, CommunicationEmailMixin):
duplicate = True

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:
self.add_link(link_doctype=l[0], link_name=l[1])

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:
self.save(ignore_permissions=True)
@@ -358,13 +389,15 @@ class Communication(Document, CommunicationEmailMixin):
if autosave:
self.save(ignore_permissions=ignore_permissions)


def on_doctype_update():
"""Add indexes in `tabCommunication`"""
frappe.db.add_index("Communication", ["reference_doctype", "reference_name"])
frappe.db.add_index("Communication", ["status", "communication_type"])


def has_permission(doc, ptype, user):
if ptype=="read":
if ptype == "read":
if doc.reference_doctype == "Communication" and doc.reference_name == doc.name:
return

@@ -372,24 +405,28 @@ def has_permission(doc, ptype, user):
if frappe.has_permission(doc.reference_doctype, ptype="read", doc=doc.reference_name):
return True


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)

if "Super Email User" in roles or "System Manager" in roles:
return None
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:
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]:
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])

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.insert(ignore_permissions=True)
contact_name = contact.name
@@ -421,6 +458,7 @@ def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[st

return contacts


def get_emails(email_strings: List[str]) -> List[str]:
email_addrs = []

@@ -432,22 +470,25 @@ def get_emails(email_strings: List[str]) -> List[str]:

return email_addrs


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:
for contact_link in contact_links:
communication.add_link(contact_link.link_doctype, contact_link.link_name)


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}):
return
@@ -469,10 +510,11 @@ def parse_email(communication, email_strings):
if doctype and docname and frappe.db.exists(doctype, docname):
communication.add_link(doctype, docname)


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}):
return email
@@ -486,6 +528,7 @@ def get_email_without_link(email):

return "{0}@{1}".format(email_id, email_host)


def update_parent_document_on_communication(doc):
"""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.notify_update()


def update_first_response_time(parent, communication):
if parent.meta.has_field("first_response_time") and not parent.get("first_response_time"):
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)
parent.db_set("first_response_time", first_response_time)


def set_avg_response_time(parent, communication):
if parent.meta.has_field("avg_response_time") and communication.sent_or_received == "Sent":
# 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"],
order_by="creation"
order_by="creation",
)

if len(communications):
response_times = []
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:
response_times.append(response_time)
if response_times:
avg_response_time = sum(response_times) / len(response_times)
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
from frappe import _
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:
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.
Please setup default Email Account from Setup > Email > Email Account
""")
"""
)


@frappe.whitelist()
@@ -64,16 +72,15 @@ def make(
"""
if kwargs:
from frappe.utils.commands import warn

warn(
f"Options {kwargs} used in frappe.core.doctype.communication.email.make "
"are deprecated or unsupported",
category=DeprecationWarning
category=DeprecationWarning,
)

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(
doctype=doctype,
@@ -123,33 +130,34 @@ def _make(
communication_type=None,
add_signature=True,
) -> 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)
recipients = list_to_str(recipients) if isinstance(recipients, list) else recipients
cc = list_to_str(cc) if isinstance(cc, list) else cc
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.insert(ignore_permissions=True)

@@ -161,9 +169,7 @@ def _make(

if cint(send_email):
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(
print_html=print_html,
@@ -179,7 +185,10 @@ def _make(

def validate_email(doc: "Communication") -> None:
"""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

# validate recipients
@@ -193,36 +202,45 @@ def validate_email(doc: "Communication") -> None:
for email in split_emails(doc.bcc):
validate_email_address(email, throw=True)


def set_incoming_outgoing_accounts(doc):
from frappe.email.doctype.email_account.email_account import EmailAccount

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.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":
doc.db_set("email_account", doc.outgoing_email_account.name)


def add_attachments(name, attachments):
'''Add attachments to the given Communication'''
"""Add attachments to the given Communication"""
# loop through attachments
for a in attachments:
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
_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)


@frappe.whitelist(allow_guest=True, methods=("GET",))
def mark_email_as_seen(name: str = None):
try:
@@ -233,33 +251,31 @@ def mark_email_as_seen(name: str = None):
frappe.log_error(frappe.get_traceback())

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):
if not name or not isinstance(name, str):
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:
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

import frappe
from frappe import _
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.email.doctype.email_account.email_account import EmailAccount
from frappe.utils import get_formatted_email, get_url, parse_addr


class CommunicationEmailMixin:
"""Mixin class to handle communication mails.
"""
"""Mixin class to handle communication mails."""
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):
"""Get owner of the communication docs parent.
"""
"""Get owner of the communication docs parent."""
parent_doc = get_parent_doc(self)
return parent_doc.owner if parent_doc else None

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):
"""Returns email address after adding displayname.
"""
"""Returns email address after adding displayname."""
display_name, email = parse_addr(email_address)
if display_name and display_name != email:
return email_address
@@ -37,26 +38,24 @@ class CommunicationEmailMixin:
return email_map.get(email, email)

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.
if is_inbound_mail_communcation:
return []

if hasattr(self, '_final_recipients'):
if hasattr(self, "_final_recipients"):
return self._final_recipients

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

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)
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.

* 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.
"""
if hasattr(self, '_final_cc'):
if hasattr(self, "_final_cc"):
return self._final_cc

cc = self.cc_list()
@@ -88,11 +87,13 @@ class CommunicationEmailMixin:
if is_inbound_mail_communcation:
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

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]

def mail_bcc(self, is_inbound_mail_communcation=False):
@@ -102,7 +103,7 @@ class CommunicationEmailMixin:
* User must be enabled in the system
* remove_administrator_from_email_list
"""
if hasattr(self, '_final_bcc'):
if hasattr(self, "_final_bcc"):
return self._final_bcc

bcc = set(self.bcc_list())
@@ -116,7 +117,7 @@ class CommunicationEmailMixin:
if is_inbound_mail_communcation:
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

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):
"""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):
if not hasattr(self, '_outgoing_email_account'):
if not hasattr(self, "_outgoing_email_account"):
if self.email_account:
self._outgoing_email_account = EmailAccount.find(self.email_account)
else:
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:
@@ -169,10 +171,9 @@ class CommunicationEmailMixin:
return self._outgoing_email_account

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(
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

@@ -180,12 +181,17 @@ class CommunicationEmailMixin:
final_attachments = []

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)

for a in self.get_attachments() or []:
final_attachments.append({"fid": a['name']})
final_attachments.append({"fid": a["name"]})

return final_attachments

@@ -193,48 +199,57 @@ class CommunicationEmailMixin:
email_account = self.get_outgoing_email_account()
if email_account and email_account.send_unsubscribe_message:
return _("Leave this conversation")
return ''
return ""

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)

final_ids = (
self.mail_recipients(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))

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)

@staticmethod
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:
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
def filter_disabled_users(emails):
"""
"""
""" """
if not emails:
return []

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()
if not outgoing_email_account:
@@ -244,8 +259,7 @@ class CommunicationEmailMixin:
is_inbound_mail_communcation=is_inbound_mail_communcation
)
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(
is_inbound_mail_communcation=is_inbound_mail_communcation
@@ -273,18 +287,24 @@ class CommunicationEmailMixin:
"delayed": True,
"communication": self.name,
"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(
print_html=print_html,
print_format=print_format,
send_me_a_copy=send_me_a_copy,
print_letterhead=print_letterhead,
is_inbound_mail_communcation=is_inbound_mail_communcation
is_inbound_mail_communcation=is_inbound_mail_communcation,
)

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.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):
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:
self.assertTrue(frappe.utils.parse_addr(x)[1])
@@ -29,15 +39,25 @@ class TestCommunication(unittest.TestCase):
self.assertFalse(frappe.utils.parse_addr(x)[0])

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:
self.assertTrue(frappe.utils.parse_addr(x)[0])
@@ -46,27 +66,33 @@ class TestCommunication(unittest.TestCase):
self.assertFalse(frappe.utils.parse_addr(x)[0])

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.reference_doctype = "Communication"
@@ -77,20 +103,24 @@ class TestCommunication(unittest.TestCase):
def test_deduplication_timeline_links(self):
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)

@@ -99,35 +129,43 @@ class TestCommunication(unittest.TestCase):
self.assertNotEqual(2, len(comm.timeline_links))

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.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.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.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)

@@ -144,27 +182,29 @@ class TestCommunication(unittest.TestCase):

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_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)

@@ -182,19 +222,23 @@ class TestCommunication(unittest.TestCase):

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 = []
for timeline_link in comm.timeline_links:
@@ -205,9 +249,9 @@ class TestCommunication(unittest.TestCase):
def test_parse_emails(self):
emails = get_emails(
[
'comm_recipient+DocType+DocName@example.com',
"comm_recipient+DocType+DocName@example.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[2], "test@user.com")


class TestCommunicationEmailMixin(unittest.TestCase):
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

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.update(user_data)
user.insert(ignore_permissions=True, ignore_if_duplicate=True)
return user

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()
self.assertCountEqual(res, ['to@test.com', 'receiver <to+1@test.com>'])
self.assertCountEqual(res, ["to@test.com", "receiver <to+1@test.com>"])
comm.delete()

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)
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()
comm.delete()

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)
res = comm.get_mail_bcc_with_displayname()
self.assertCountEqual(res, ['bcc+1@test.com'])
self.assertCountEqual(res, ["bcc+1@test.com"])
user.delete()
comm.delete()

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.send_email()
doc = EmailQueue.find_one_by_filters(communication=comm.name)
mail_receivers = [each.recipient for each in doc.recipients]
self.assertIsNotNone(doc)
self.assertCountEqual(to_list+cc_list, mail_receivers)
self.assertCountEqual(to_list + cc_list, mail_receivers)
doc.delete()
comm.delete()


def create_email_account():
frappe.delete_doc_if_exists("Email Account", "_Test Comm Account 1")

frappe.flags.mute_emails = False
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

+ 3
- 1
frappe/core/doctype/communication_link/communication_link.py 查看文件

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


class CommunicationLink(Document):
pass


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
from frappe.model.document import Document


class CustomDocPerm(Document):
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 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE
import frappe
import unittest

import frappe

# test_records = frappe.get_test_records('Custom DocPerm')


class TestCustomDocPerm(unittest.TestCase):
pass

+ 6
- 4
frappe/core/doctype/custom_role/custom_role.py 查看文件

@@ -5,16 +5,18 @@
import frappe
from frappe.model.document import Document


class CustomRole(Document):
def validate(self):
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):
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:
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]

return allowed_roles
return allowed_roles

+ 3
- 1
frappe/core/doctype/custom_role/test_custom_role.py 查看文件

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

import frappe

# test_records = frappe.get_test_records('Custom Role')


class TestCustomRole(unittest.TestCase):
pass

+ 1
- 0
frappe/core/doctype/data_export/data_export.py 查看文件

@@ -4,5 +4,6 @@

from frappe.model.document import Document


class DataExport(Document):
pass

+ 168
- 100
frappe/core/doctype/data_export/exporter.py 查看文件

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

import csv
import os
import re

import frappe
from frappe import _
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.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():
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()
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
if isinstance(_doctype, list):
_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()


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.parent_doctype = parent_doctype
self.all_doctypes = all_doctypes
@@ -81,18 +112,18 @@ class DataExporter:

def build_response(self):
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:
self.add_main_header()

self.writer.writerow([''])
self.writer.writerow([""])
self.tablerow = [self.data_keys.doctype]
self.labelrow = [_("Column Labels:")]
self.fieldrow = [self.data_keys.columns]
self.mandatoryrow = [_("Mandatory:")]
self.typerow = [_('Type:')]
self.inforow = [_('Info:')]
self.typerow = [_("Type:")]
self.inforow = [_("Info:")]
self.columns = []

self.build_field_columns(self.doctype)
@@ -100,74 +131,99 @@ class DataExporter:
if self.all_doctypes:
for d in self.child_doctypes:
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
self.build_field_columns(d['doctype'], d['parentfield'])
self.build_field_columns(d["doctype"], d["parentfield"])

self.add_field_headings()
self.add_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()
else:
# 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):
self.writer.writerow([_('Data Import Template')])
self.writer.writerow([_("Data Import Template")])
self.writer.writerow([self.data_keys.main_table, self.doctype])

if self.parent_doctype != self.doctype:
self.writer.writerow([self.data_keys.parent_table, self.parent_doctype])
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":
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):
meta = frappe.get_meta(dt)

# build list of valid docfields
tablecolumns = []
table_name = 'tab' + dt
table_name = "tab" + dt
for f in frappe.db.get_table_columns_description(table_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.sort(key = lambda a: int(a.idx))
tablecolumns.sort(key=lambda a: int(a.idx))

_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()

# 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)
else:
@@ -184,7 +240,7 @@ class DataExporter:
self.append_field_column(docfield, False)

# 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()

# append DocType name
@@ -204,18 +260,21 @@ class DataExporter:
return
if not for_mandatory and docfield.reqd:
return
if docfield.fieldname in ('parenttype', 'trash_reason'):
if docfield.fieldname in ("parenttype", "trash_reason"):
return
if docfield.hidden:
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

self.tablerow.append("")
self.fieldrow.append(docfield.fieldname)
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.inforow.append(self.getinforow(docfield))
self.columns.append(docfield.fieldname)
@@ -232,15 +291,15 @@ class DataExporter:
@staticmethod
def getinforow(docfield):
"""make info comment for options, links etc."""
if docfield.fieldtype == 'Select':
if docfield.fieldtype == "Select":
if not docfield.options:
return ''
return ""
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":
return "0 or 1"
elif docfield.fieldtype in ["Date", "Datetime"]:
@@ -248,7 +307,7 @@ class DataExporter:
elif hasattr(docfield, "info"):
return docfield.info
else:
return ''
return ""

def add_field_headings(self):
self.writer.writerow(self.tablerow)
@@ -262,6 +321,7 @@ class DataExporter:

def add_data(self):
from frappe.query_builder import DocType

if self.template and not self.with_data:
return

@@ -270,26 +330,28 @@ class DataExporter:
# sort nested set doctypes by `lft asc`
order_by = None
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
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:
op = self.docs_to_export.get("op")
names = self.docs_to_export.get("name")

if names and op:
if op == '=' and doc.name not in names:
if op == "=" and doc.name not in names:
continue
elif op == '!=' and doc.name in names:
elif op == "!=" and doc.name in names:
continue
elif names:
try:
sflags = self.docs_to_export.get("flags", "I,U").upper()
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)
m = c.match(doc.name)
@@ -315,7 +377,7 @@ class DataExporter:
.orderby(child_doctype_table.idx)
)
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:
self.writer.writerow(row)
@@ -333,7 +395,7 @@ class DataExporter:
_column_start_end = self.column_start_end.get((dt, parentfield))

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)
fieldtype = df.fieldtype if df else "Data"
value = d.get(c, "")
@@ -349,27 +411,33 @@ class DataExporter:

def build_response_as_excel(self):
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)
reader = csv.reader(f)

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()
os.remove(filename)

# 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):
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
# License: MIT. See LICENSE
import unittest

import frappe
from frappe.core.doctype.data_export.exporter import DataExporter


class TestDataExporter(unittest.TestCase):
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_test_data()

@@ -17,42 +19,49 @@ class TestDataExporter(unittest.TestCase):
Helper Function for setting up doctypes
"""
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

# 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
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):
"""
@@ -69,37 +78,38 @@ class TestDataExporter(unittest.TestCase):
table_field_1=[
{"child_title": "Child Title 1", "child_number": "50"},
{"child_title": "Child Title 2", "child_number": "51"},
]
],
).insert()
else:
self.doc = frappe.get_doc(self.doctype_name, self.doc_name)

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()

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):
for type in ['csv', 'Excel']:
for type in ["csv", "Excel"]:
with self.subTest(type=type):
exp = DataExporter(doctype=self.doctype_name, file_type=type)
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):
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

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()]

@@ -100,6 +98,7 @@ def get_preview_from_template(data_import, import_file=None, google_sheets_url=N
import_file, google_sheets_url
)


@frappe.whitelist()
def form_start_import(data_import):
return frappe.get_doc("Data Import", data_import).start_import()
@@ -127,11 +126,11 @@ def download_template(
):
"""
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)
@@ -154,34 +153,38 @@ def download_errored_template(data_import_name):
data_import = frappe.get_doc("Data Import", data_import_name)
data_import.export_errored_rows()


@frappe.whitelist()
def download_import_log(data_import_name):
data_import = frappe.get_doc("Data Import", data_import_name)
data_import.download_import_log()


@frappe.whitelist()
def get_import_status(data_import_name):
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:
if log.get('success'):
import_status['success'] = log.get('count')
if log.get("success"):
import_status["success"] = log.get("count")
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

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.

@@ -198,9 +201,7 @@ def import_file(
"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()


@@ -214,11 +215,7 @@ def import_doc(path, pre_process=None):
if f.endswith(".json"):
frappe.flags.mute_emails = True
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.db.commit()
@@ -226,9 +223,7 @@ def import_doc(path, pre_process=None):
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):
# Note on Tree DocTypes:
# 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
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.csvutils import build_csv_response
from frappe.utils.xlsxutils import build_xlsx_response
@@ -28,11 +25,11 @@ class Exporter:
):
"""
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.meta = frappe.get_meta(doctype)
@@ -168,9 +165,7 @@ class Exporter:
else:
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(
self.doctype,
filters=filters,
@@ -188,9 +183,7 @@ class Exporter:
child_table_df = self.meta.get_field(key)
child_table_doctype = child_table_df.options
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(
child_table_doctype,
@@ -261,4 +254,4 @@ class Exporter:
build_xlsx_response(self.get_csv_array_for_export(), _(self.doctype))

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
# License: MIT. See LICENSE

import os
import io
import frappe
import timeit
import json
from datetime import datetime, date
import os
import timeit
from datetime import date, datetime

import frappe
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 (
read_xlsx_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)
MAX_ROWS_IN_PREVIEW = 10
@@ -24,9 +26,7 @@ UPDATE = "Update Existing Records"


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.console = console

@@ -49,9 +49,13 @@ class Importer:
def get_data_for_import_preview(self):
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},
order_by="log_index", limit=10)
order_by="log_index",
limit=10,
)

return out

@@ -84,14 +88,23 @@ class Importer:
return

# 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

# 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
import_log = [log for log in import_log if log.get("success")]

@@ -108,9 +121,7 @@ class Importer:
total_payload_count = len(payloads)
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):
doc = payload.doc
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

@@ -177,19 +188,29 @@ class Importer:
# rollback if exception
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

# 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
failures = [log for log in import_log if not log.get("success")]
@@ -274,9 +295,15 @@ class Importer:
if not self.data_import:
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")]
row_indexes = []
@@ -299,9 +326,12 @@ class Importer:
if not self.data_import:
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},
order_by="log_index")
order_by="log_index",
)

header_row = ["Row Numbers", "Status", "Message", "Exception"]

@@ -309,10 +339,13 @@ class Importer:

for log in import_log:
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]]

build_csv_response(rows, self.doctype)
@@ -324,9 +357,7 @@ class Importer:
if successful_records:
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:
@@ -363,9 +394,7 @@ class Importer:
class ImportFile:
def __init__(self, doctype, file, template_options=None, import_type=None):
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.import_type = import_type
self.warnings = []
@@ -556,9 +585,7 @@ class ImportFile:
def read_content(self, content, extension):
error_title = _("Template Error")
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":
data = read_csv_content(content)
@@ -587,12 +614,13 @@ class Row:
if len_row != len_columns:
less_than_columns = len_row < len_columns
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(
{"row": self.row_number, "message": message,}
{
"row": self.row_number,
"message": message,
}
)

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)
msg = _("Value must be one of {0}").format(options_string)
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

elif df.fieldtype == "Link":
exists = self.link_exists(value, df)
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(
{"row": self.row_number, "field": df_as_json(df), "message": msg,}
{
"row": self.row_number,
"field": df_as_json(df),
"message": msg,
}
)
return
elif df.fieldtype in ["Date", "Datetime"]:
@@ -693,6 +727,7 @@ class Row:
return
elif df.fieldtype == "Duration":
import re

is_valid_duration = re.match(r"^(?:(\d+d)?((^|\s)\d+h)?((^|\s)\d+m)?((^|\s)\d+s)?)$", value)
if not is_valid_duration:
self.warnings.append(
@@ -702,7 +737,7 @@ class Row:
"field": df_as_json(df),
"message": _("Value {0} must be in the valid duration format: d h m s").format(
frappe.bold(value)
)
),
}
)

@@ -789,9 +824,7 @@ class Header(Row):
else:
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 is_table_field(df):
@@ -802,10 +835,7 @@ class Header(Row):
return [
col.index
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):
@@ -893,9 +923,7 @@ class Column:
self.warnings.append(
{
"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",
}
)
@@ -958,9 +986,7 @@ class Column:
if self.df.fieldtype == "Link":
# find all values that dont exist
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))
if not_exists:
missing_values = ", ".join(not_exists)
@@ -968,9 +994,7 @@ class Column:
{
"col": self.column_number,
"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",
}
@@ -983,7 +1007,9 @@ class Column:
self.warnings.append(
{
"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",
}
)
@@ -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
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 = {}

# 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:
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:
name_headers = (
"name", # fieldname
"ID", # label
_("ID"), # translated label
"name", # fieldname
"ID", # label
_("ID"), # translated label
)
else:
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
@@ -1122,7 +1146,7 @@ def build_fields_dict_for_column_matching(parent_doctype):
for header in (
df.fieldname,
f"{label} ({df.fieldname})",
f"{translated_label} ({df.fieldname})"
f"{translated_label} ({df.fieldname})",
):
out[header] = df

@@ -1155,9 +1179,8 @@ def build_fields_dict_for_column_matching(parent_doctype):
autoname_field = get_autoname_field(parent_doctype)
if autoname_field:
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",
_("ID"),
@@ -1205,10 +1228,7 @@ def get_item_at_index(_list, i, default=None):

def get_user_format(date_format):
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):
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 unittest


class TestDataImport(unittest.TestCase):
pass

+ 8
- 8
frappe/core/doctype/data_import/test_exporter.py 查看文件

@@ -2,13 +2,13 @@
# Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE
import unittest

import frappe
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):
def setUp(self):
@@ -93,10 +93,10 @@ class TestExporter(unittest.TestCase):
doctype_name,
export_fields={doctype_name: ["title", "description"]},
export_data=True,
file_type="CSV"
file_type="CSV",
)
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
# License: MIT. See LICENSE
import unittest

import frappe
from frappe.core.doctype.data_import.importer import Importer
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):
@classmethod
def setUpClass(cls):
create_doctype_if_not_exists(doctype_name,)
create_doctype_if_not_exists(
doctype_name,
)

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.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(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_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(format_duration(doc3.duration), '5d 5h 45m')
self.assertEqual(format_duration(doc3.duration), "5d 5h 45m")

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)
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
@run_only_if(db_type_is.MARIADB)
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)
frappe.local.message_log = []
data_import.start_import()
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},
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):
existing_doc = frappe.get_doc(
doctype=doctype_name,
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()
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)
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)
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].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):
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.import_file = import_file.file_url
data_import.insert()
@@ -121,88 +139,109 @@ class TestImporter(unittest.TestCase):

return data_import


def create_doctype_if_not_exists(doctype_name, force=False):
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

# 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
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
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):
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:
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:
full_path = get_csv_file_path(file_name)
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)

@@ -210,4 +249,4 @@ def get_import_file(csv_file_name, force=False):


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
from frappe.model.document import Document


class DataImportLog(Document):
pass

+ 1
- 0
frappe/core/doctype/data_import_log/test_data_import_log.py 查看文件

@@ -4,5 +4,6 @@
# import frappe
import unittest


class TestDataImportLog(unittest.TestCase):
pass

+ 0
- 1
frappe/core/doctype/defaultvalue/__init__.py 查看文件

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


+ 12
- 7
frappe/core/doctype/defaultvalue/defaultvalue.py 查看文件

@@ -2,19 +2,24 @@
# License: MIT. See LICENSE

import frappe

from frappe.model.document import Document


class DefaultValue(Document):
pass


def on_doctype_update():
"""Create indexes for `tabDefaultValue` on `(parent, defkey)`"""
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
# License: MIT. See LICENSE

import frappe
import json

import frappe
from frappe import _
from frappe.desk.doctype.bulk_update.bulk_update import show_progress
from frappe.model.document import Document
from frappe import _


class DeletedDocument(Document):
@@ -15,7 +16,7 @@ class DeletedDocument(Document):

@frappe.whitelist()
def restore(name, alert=True):
deleted = frappe.get_doc('Deleted Document', name)
deleted = frappe.get_doc("Deleted Document", name)

if deleted.restored:
frappe.throw(_("Document {0} Already Restored").format(name), exc=frappe.DocumentAlreadyRestored)
@@ -29,20 +30,20 @@ def restore(name, alert=True):
doc.docstatus = 0
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.restored = 1
deleted.db_update()

if alert:
frappe.msgprint(_('Document Restored'))
frappe.msgprint(_("Document Restored"))


@frappe.whitelist()
def bulk_restore(docnames):
docnames = frappe.parse_json(docnames)
message = _('Restoring Deleted Document')
message = _("Restoring Deleted Document")
restored, invalid, failed = [], [], []

for i, d in enumerate(docnames):
@@ -61,8 +62,4 @@ def bulk_restore(docnames):
failed.append(d)
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 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE
import frappe
import unittest

import frappe

# test_records = frappe.get_test_records('Deleted Document')


class TestDeletedDocument(unittest.TestCase):
pass

+ 0
- 1
frappe/core/doctype/docfield/__init__.py 查看文件

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


+ 13
- 13
frappe/core/doctype/docfield/docfield.py 查看文件

@@ -4,28 +4,28 @@
import frappe
from frappe.model.document import Document


class DocField(Document):
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 Table MultiSelect: Returns "options" of the Link field in the Child Table
'''
if self.fieldtype == 'Link':
"""
if self.fieldtype == "Link":
return self.options

if self.fieldtype == 'Table MultiSelect':
if self.fieldtype == "Table MultiSelect":
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

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
# License: MIT. See LICENSE


+ 1
- 1
frappe/core/doctype/docperm/docperm.py 查看文件

@@ -2,8 +2,8 @@
# License: MIT. See LICENSE

import frappe

from frappe.model.document import Document


class DocPerm(Document):
pass

+ 24
- 10
frappe/core/doctype/docshare/docshare.py 查看文件

@@ -2,12 +2,13 @@
# License: MIT. See LICENSE

import frappe
from frappe.model.document import Document
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


class DocShare(Document):
no_feed_on_delete = True

@@ -36,15 +37,21 @@ class DocShare(Document):
frappe.throw(_("User is mandatory for Share"), frappe.MandatoryError)

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)

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):
doc = self.get_doc()
@@ -53,14 +60,21 @@ class DocShare(Document):
if self.everyone:
doc.add_comment("Shared", _("{0} shared this document with everyone").format(owner))
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):
if not self.flags.ignore_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():
"""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
# License: MIT. See LICENSE

import unittest

import frappe
import frappe.share
import unittest
from frappe.automation.doctype.auto_repeat.test_auto_repeat import create_submittable_doctype

test_dependencies = ['User']
test_dependencies = ["User"]


class TestDocShare(unittest.TestCase):
def setUp(self):
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):
frappe.set_user("Administrator")
@@ -98,7 +104,9 @@ class TestDocShare(unittest.TestCase):
doctype = "Test DocShare with Submit"
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)
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.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
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)

+ 0
- 1
frappe/core/doctype/doctype/__init__.py 查看文件

@@ -1,3 +1,2 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# 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
from frappe.desk.utils import slug


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 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe
import unittest
from frappe.core.doctype.doctype.doctype import (UniqueFieldnameError,
IllegalMandatoryError,

import frappe
from frappe.core.doctype.doctype.doctype import (
CannotIndexedError,
DoctypeLinkError,
WrongOptionsDoctypeLinkError,
HiddenAndMandatoryWithoutDefaultError,
CannotIndexedError,
IllegalMandatoryError,
InvalidFieldNameError,
validate_links_table_fieldnames)
UniqueFieldnameError,
WrongOptionsDoctypeLinkError,
validate_links_table_fieldnames,
)

# test_records = frappe.get_test_records('DocType')

class TestDocType(unittest.TestCase):

class TestDocType(unittest.TestCase):
def tearDown(self):
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("8Some 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"):
if frappe.db.exists("DocType", name):
frappe.delete_doc("DocType", name)
@@ -86,19 +92,33 @@ class TestDocType(unittest.TestCase):
def test_all_depends_on_fields_conditions(self):
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\.@\'"]+'
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)
if condition:
self.assertFalse(re.match(pattern, condition))
@@ -108,18 +128,18 @@ class TestDocType(unittest.TestCase):
valid_data_field_options = frappe.model.data_field_options + ("",)
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:
# assert that only data options in frappe.model.data_field_options are valid
@@ -130,45 +150,29 @@ class TestDocType(unittest.TestCase):
test_doctype.delete()

def test_sync_field_order(self):
from frappe.modules.import_file import get_file_path
import os

from frappe.modules.import_file import get_file_path

# 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)
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")
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
test_doctype_json = frappe.get_file_json(path)
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
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))

# assert that field_order is actually removed from the json file
@@ -203,10 +211,14 @@ class TestDocType(unittest.TestCase):
test_doctype.save()
test_doctype_json = frappe.get_file_json(path)
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
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
test_doctype.save()
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
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))

# assert that reordering `field_order` from json file is reflected in DocType upon migrate/sync
frappe.reload_doctype(test_doctype.name, force=True)
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)
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[3], test_doctype.fields[2] = test_doctype.fields[2], test_doctype.fields[3]
test_doctype.remove(test_doctype.fields[0])
@@ -243,115 +260,121 @@ class TestDocType(unittest.TestCase):

test_doctype.save()
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:
raise
finally:
frappe.flags.allow_doctype_export = 0

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)

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()
self.assertEqual(doc.fields[1].fieldname, "name1")
doc.fields[1].fieldname = 'name'
doc.fields[1].fieldname = "name"
self.assertRaises(InvalidFieldNameError, doc.save)

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

self.assertRaises(IllegalMandatoryError, doc.insert)

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)

field_1.options = 'wrongdoctype'
field_1.options = "wrongdoctype"

self.assertRaises(WrongOptionsDoctypeLinkError, doc.insert)

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.hidden = 1

self.assertRaises(HiddenAndMandatoryWithoutDefaultError, doc.insert)

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

self.assertRaises(CannotIndexedError, doc.insert)

def test_cancel_link_doctype(self):
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
for data in link_doc.get('permissions'):
for data in link_doc.get("permissions"):
data.submit = 1
data.cancel = 1
link_doc.insert()

doc = new_doctype('Test Doctype')
doc = new_doctype("Test Doctype")
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.cancel = 1
doc.insert()

# 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.save()
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.insert()
data_doc.save()
data_doc.submit()

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)
data_link_doc.cancel()
data_doc.load_from_db()
@@ -369,69 +392,70 @@ class TestDocType(unittest.TestCase):

def test_ignore_cancelation_of_linked_doctype_during_cancel(self):
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
for data in link_doc.get('permissions'):
for data in link_doc.get("permissions"):
data.submit = 1
data.cancel = 1
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

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.cancel = 1
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

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.cancel = 1
doc.insert()

# 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.save()
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.insert()
data_doc_2.save()
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.insert()
data_doc.save()
data_doc.submit()

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"])

@@ -442,10 +466,10 @@ class TestDocType(unittest.TestCase):
data_doc_2.load_from_db()
self.assertEqual(data_link_doc_1.docstatus, 2)

#linked doc is canceled
# linked doc is canceled
self.assertEqual(data_doc_2.docstatus, 2)

#ignored doctype 2 during cancel
# ignored doctype 2 during cancel
self.assertEqual(data_doc.docstatus, 1)

# delete doctype record
@@ -464,42 +488,35 @@ class TestDocType(unittest.TestCase):
doc = new_doctype("Test Links Table Validation")

# 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
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)
doc.links = [] # reset links table
doc.links = [] # reset links table

# 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)

def test_create_virtual_doctype(self):
"""Test virtual DOcTYpe."""
virtual_doc = new_doctype('Test Virtual Doctype')
virtual_doc = new_doctype("Test Virtual Doctype")
virtual_doc.is_virtual = 1
virtual_doc.insert()
virtual_doc.save()
doc = frappe.get_doc("DocType", "Test Virtual Doctype")

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):
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.insert()

@@ -521,28 +538,34 @@ class TestDocType(unittest.TestCase):
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:
for f in fields:
doc.append('fields', f)
doc.append("fields", f)

return doc

+ 1
- 0
frappe/core/doctype/doctype_action/doctype_action.py 查看文件

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


class DocTypeAction(Document):
pass

+ 1
- 0
frappe/core/doctype/doctype_link/doctype_link.py 查看文件

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


class DocTypeLink(Document):
pass

+ 1
- 0
frappe/core/doctype/doctype_state/doctype_state.py 查看文件

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


class DocTypeState(Document):
pass

+ 19
- 11
frappe/core/doctype/document_naming_rule/document_naming_rule.py 查看文件

@@ -3,10 +3,11 @@
# License: MIT. See LICENSE

import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils.data import evaluate_filters
from frappe.model.naming import parse_naming_series
from frappe import _
from frappe.utils.data import evaluate_filters


class DocumentNamingRule(Document):
def validate(self):
@@ -17,23 +18,30 @@ class DocumentNamingRule(Document):
docfields = [x.fieldname for x in frappe.get_meta(self.document_type).fields]
for condition in self.conditions:
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):
'''
"""
Apply naming rules for the given document. Will set `name` if the rule is matched.
'''
"""
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

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)

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()
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 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# License: MIT. See LICENSE
import frappe
import unittest

import frappe


class TestDocumentNamingRule(unittest.TestCase):
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()
todo.delete()

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
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()

# default rule with low priority - should not get applied for rules
# with higher priority
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.conditions = []
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:
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:
naming_rule.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
from frappe.model.document import Document


class DocumentNamingRuleCondition(Document):
pass

+ 1
- 0
frappe/core/doctype/document_naming_rule_condition/test_document_naming_rule_condition.py 查看文件

@@ -4,5 +4,6 @@
# import frappe
import unittest


class TestDocumentNamingRuleCondition(unittest.TestCase):
pass

+ 41
- 31
frappe/core/doctype/domain/domain.py 查看文件

@@ -3,16 +3,17 @@
# License: MIT. See LICENSE

import frappe

from frappe.model.document import Document
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.model.document import Document


class Domain(Document):
'''Domain documents are created automatically when DocTypes
"""Domain documents are created automatically when DocTypes
with "Restricted" domains are imported during
installation or migration'''
installation or migration"""

def setup_domain(self):
'''Setup domain icons, permissions, custom fields etc.'''
"""Setup domain icons, permissions, custom fields etc."""
self.setup_data()
self.setup_roles()
self.setup_properties()
@@ -31,20 +32,20 @@ class Domain(Document):
frappe.get_attr(self.data.on_setup)()

def remove_domain(self):
'''Unset domain settings'''
"""Unset domain settings"""
self.setup_data()

if 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.save()

self.remove_custom_field()

def remove_custom_field(self):
'''Remove custom_fields when disabling domain'''
"""Remove custom_fields when disabling domain"""
if self.data.custom_fields:
for doctype in self.data.custom_fields:
custom_fields = self.data.custom_fields[doctype]
@@ -54,47 +55,48 @@ class Domain(Document):
custom_fields = [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:
frappe.delete_doc('Custom Field', custom_field_name)
frappe.delete_doc("Custom Field", custom_field_name)

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:
user = frappe.get_doc("User", frappe.session.user)
for role_name in self.data.restricted_roles:
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

role = frappe.get_doc('Role', role_name)
role = frappe.get_doc("Role", role_name)
role.disabled = 0
role.save()
user.save()

def setup_data(self, domain=None):
'''Load domain info via hooks'''
"""Load domain info via hooks"""
self.data = frappe.get_domain_data(self.name)

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):
'''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):
if self.data.properties:
for args in self.data.properties:
frappe.make_property_setter(args)


def set_values(self):
'''set values based on `data.set_value`'''
"""set values based on `data.set_value`"""
if self.data.set_value:
for args in self.data.set_value:
frappe.reload_doctype(args[0])
@@ -103,19 +105,27 @@ class Domain(Document):
doc.save()

def setup_sidebar_items(self):
'''Enable / disable sidebar items'''
"""Enable / disable sidebar items"""
if self.data.allow_sidebar_items:
# disable all
frappe.db.sql('update `tabPortal Menu Item` set enabled=0')
frappe.db.sql("update `tabPortal Menu Item` set enabled=0")

# 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:
# disable all
frappe.db.sql('update `tabPortal Menu Item` set enabled=1')
frappe.db.sql("update `tabPortal Menu Item` set enabled=1")

# 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 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# License: MIT. See LICENSE
import frappe
import unittest

import frappe


class TestDomain(unittest.TestCase):
pass

+ 29
- 23
frappe/core/doctype/domain_settings/domain_settings.py 查看文件

@@ -5,13 +5,14 @@
import frappe
from frappe.model.document import Document


class DomainSettings(Document):
def set_active_domains(self, domains):
active_domains = [d.domain for d in self.active_domains]
added = False
for d in domains:
if not d in active_domains:
self.append('active_domains', dict(domain=d))
self.append("active_domains", dict(domain=d))
added = True

if added:
@@ -22,49 +23,52 @@ class DomainSettings(Document):
# set the flag to update the the desktop icons of all domains
if i >= 1:
frappe.flags.keep_desktop_icons = True
domain = frappe.get_doc('Domain', d.domain)
domain = frappe.get_doc("Domain", d.domain)
domain.setup_domain()

self.restrict_roles_and_modules()
frappe.clear_cache()

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()
all_domains = list((frappe.get_hooks('domains') or {}))
all_domains = list((frappe.get_hooks("domains") or {}))

def remove_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:
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:
remove_role(role)

if 'custom_fields' in data:
if "custom_fields" in data:
if domain not in active_domains:
inactive_domain = frappe.get_doc("Domain", domain)
inactive_domain.setup_data()
inactive_domain.remove_custom_field()


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():
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.append("")
@@ -72,14 +76,16 @@ def get_active_domains():

return frappe.cache().get_value("active_domains", _get_active_domains)


def get_active_modules():
""" get the active modules from Module Def"""
"""get the active modules from Module Def"""

def _get_active_modules():
active_modules = []
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):
active_modules.append(m.name)
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
from frappe.model.document import Document


class DynamicLink(Document):
pass


def on_doctype_update():
frappe.db.add_index("Dynamic Link", ["link_doctype", "link_name"])


def deduplicate_dynamic_links(doc):
links, duplicate = [], False
for l in doc.links or []:
@@ -23,4 +26,4 @@ def deduplicate_dynamic_links(doc):
if duplicate:
doc.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
from frappe.model.document import Document


class ErrorLog(Document):
def onload(self):
if not self.seen:
self.db_set('seen', 1, update_modified=0)
self.db_set("seen", 1, update_modified=0)
frappe.db.commit()


def set_old_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()
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")

+ 3
- 1
frappe/core/doctype/error_log/test_error_log.py 查看文件

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

import frappe

# test_records = frappe.get_test_records('Error Log')


class TestErrorLog(unittest.TestCase):
pass

部分文件因文件數量過多而無法顯示

Loading…
取消
儲存