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

Co-authored-by: Frappe Bot <developers@frappe.io>
This commit is contained in:
Suraj Shetty 2022-04-12 10:59:25 +05:30 committed by GitHub
parent 5c8856d66e
commit c0c5b2ebdd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
895 changed files with 35785 additions and 25448 deletions

View file

@ -19,3 +19,6 @@ fe20515c23a3ac41f1092bf0eaf0a0a452ec2e85
# Clean up whitespace # Clean up whitespace
b2fc959307c7c79f5584625569d5aed04133ba13 b2fc959307c7c79f5584625569d5aed04133ba13
# Format codebase and sort imports
cb6f68e8c106ee2d037dd4b39dbb6d7c68caf1c8

View file

@ -16,6 +16,17 @@ repos:
- id: check-merge-conflict - id: check-merge-conflict
- id: check-ast - id: check-ast
- repo: https://github.com/adityahase/black
rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119
hooks:
- id: black
additional_dependencies: ['click==8.0.4']
- repo: https://github.com/timothycrosley/isort
rev: 5.9.1
hooks:
- id: isort
exclude: ".*setup.py$"
ci: ci:
autoupdate_schedule: weekly autoupdate_schedule: weekly

File diff suppressed because it is too large Load diff

View file

@ -9,8 +9,8 @@ import frappe
import frappe.client import frappe.client
import frappe.handler import frappe.handler
from frappe import _ from frappe import _
from frappe.utils.response import build_response
from frappe.utils.data import sbool from frappe.utils.data import sbool
from frappe.utils.response import build_response
def handle(): def handle():
@ -22,22 +22,22 @@ def handle():
`/api/method/{methodname}` will call a whitelisted method `/api/method/{methodname}` will call a whitelisted method
`/api/resource/{doctype}` will query a table `/api/resource/{doctype}` will query a table
examples: examples:
- `?fields=["name", "owner"]` - `?fields=["name", "owner"]`
- `?filters=[["Task", "name", "like", "%005"]]` - `?filters=[["Task", "name", "like", "%005"]]`
- `?limit_start=0` - `?limit_start=0`
- `?limit_page_length=20` - `?limit_page_length=20`
`/api/resource/{doctype}/{name}` will point to a resource `/api/resource/{doctype}/{name}` will point to a resource
`GET` will return doclist `GET` will return doclist
`POST` will insert `POST` will insert
`PUT` will update `PUT` will update
`DELETE` will delete `DELETE` will delete
`/api/resource/{doctype}/{name}?run_method={method}` will run a whitelisted controller method `/api/resource/{doctype}/{name}?run_method={method}` will run a whitelisted controller method
""" """
parts = frappe.request.path[1:].split("/",3) parts = frappe.request.path[1:].split("/", 3)
call = doctype = name = None call = doctype = name = None
if len(parts) > 1: if len(parts) > 1:
@ -49,22 +49,22 @@ def handle():
if len(parts) > 3: if len(parts) > 3:
name = parts[3] name = parts[3]
if call=="method": if call == "method":
frappe.local.form_dict.cmd = doctype frappe.local.form_dict.cmd = doctype
return frappe.handler.handle() return frappe.handler.handle()
elif call=="resource": elif call == "resource":
if "run_method" in frappe.local.form_dict: if "run_method" in frappe.local.form_dict:
method = frappe.local.form_dict.pop("run_method") method = frappe.local.form_dict.pop("run_method")
doc = frappe.get_doc(doctype, name) doc = frappe.get_doc(doctype, name)
doc.is_whitelisted(method) doc.is_whitelisted(method)
if frappe.local.request.method=="GET": if frappe.local.request.method == "GET":
if not doc.has_permission("read"): if not doc.has_permission("read"):
frappe.throw(_("Not permitted"), frappe.PermissionError) frappe.throw(_("Not permitted"), frappe.PermissionError)
frappe.local.response.update({"data": doc.run_method(method, **frappe.local.form_dict)}) frappe.local.response.update({"data": doc.run_method(method, **frappe.local.form_dict)})
if frappe.local.request.method=="POST": if frappe.local.request.method == "POST":
if not doc.has_permission("write"): if not doc.has_permission("write"):
frappe.throw(_("Not permitted"), frappe.PermissionError) frappe.throw(_("Not permitted"), frappe.PermissionError)
@ -73,13 +73,13 @@ def handle():
else: else:
if name: if name:
if frappe.local.request.method=="GET": if frappe.local.request.method == "GET":
doc = frappe.get_doc(doctype, name) doc = frappe.get_doc(doctype, name)
if not doc.has_permission("read"): if not doc.has_permission("read"):
raise frappe.PermissionError raise frappe.PermissionError
frappe.local.response.update({"data": doc}) frappe.local.response.update({"data": doc})
if frappe.local.request.method=="PUT": if frappe.local.request.method == "PUT":
data = get_request_form_data() data = get_request_form_data()
doc = frappe.get_doc(doctype, name, for_update=True) doc = frappe.get_doc(doctype, name, for_update=True)
@ -90,9 +90,7 @@ def handle():
# Not checking permissions here because it's checked in doc.save # Not checking permissions here because it's checked in doc.save
doc.update(data) doc.update(data)
frappe.local.response.update({ frappe.local.response.update({"data": doc.save().as_dict()})
"data": doc.save().as_dict()
})
# check for child table doctype # check for child table doctype
if doc.get("parenttype"): if doc.get("parenttype"):
@ -183,7 +181,7 @@ def validate_oauth(authorization_header):
Authenticate request using OAuth and set session user Authenticate request using OAuth and set session user
Args: Args:
authorization_header (list of str): The 'Authorization' header containing the prefix and token authorization_header (list of str): The 'Authorization' header containing the prefix and token
""" """
from frappe.integrations.oauth2 import get_oauth_server from frappe.integrations.oauth2 import get_oauth_server
@ -194,7 +192,9 @@ def validate_oauth(authorization_header):
req = frappe.request req = frappe.request
parsed_url = urlparse(req.url) parsed_url = urlparse(req.url)
access_token = {"access_token": token} access_token = {"access_token": token}
uri = parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + "?" + urlencode(access_token) uri = (
parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + "?" + urlencode(access_token)
)
http_method = req.method http_method = req.method
headers = req.headers headers = req.headers
body = req.get_data() body = req.get_data()
@ -202,8 +202,12 @@ def validate_oauth(authorization_header):
body = None body = None
try: try:
required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(get_url_delimiter()) required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(
valid, oauthlib_request = get_oauth_server().verify_request(uri, http_method, body, headers, required_scopes) get_url_delimiter()
)
valid, oauthlib_request = get_oauth_server().verify_request(
uri, http_method, body, headers, required_scopes
)
if valid: if valid:
frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user")) frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user"))
frappe.local.form_dict = form_dict frappe.local.form_dict = form_dict
@ -216,48 +220,43 @@ def validate_auth_via_api_keys(authorization_header):
Authenticate request using API keys and set session user Authenticate request using API keys and set session user
Args: Args:
authorization_header (list of str): The 'Authorization' header containing the prefix and token authorization_header (list of str): The 'Authorization' header containing the prefix and token
""" """
try: try:
auth_type, auth_token = authorization_header auth_type, auth_token = authorization_header
authorization_source = frappe.get_request_header("Frappe-Authorization-Source") authorization_source = frappe.get_request_header("Frappe-Authorization-Source")
if auth_type.lower() == 'basic': if auth_type.lower() == "basic":
api_key, api_secret = frappe.safe_decode(base64.b64decode(auth_token)).split(":") api_key, api_secret = frappe.safe_decode(base64.b64decode(auth_token)).split(":")
validate_api_key_secret(api_key, api_secret, authorization_source) validate_api_key_secret(api_key, api_secret, authorization_source)
elif auth_type.lower() == 'token': elif auth_type.lower() == "token":
api_key, api_secret = auth_token.split(":") api_key, api_secret = auth_token.split(":")
validate_api_key_secret(api_key, api_secret, authorization_source) validate_api_key_secret(api_key, api_secret, authorization_source)
except binascii.Error: except binascii.Error:
frappe.throw(_("Failed to decode token, please provide a valid base64-encoded token."), frappe.InvalidAuthorizationToken) frappe.throw(
_("Failed to decode token, please provide a valid base64-encoded token."),
frappe.InvalidAuthorizationToken,
)
except (AttributeError, TypeError, ValueError): except (AttributeError, TypeError, ValueError):
pass pass
def validate_api_key_secret(api_key, api_secret, frappe_authorization_source=None): def validate_api_key_secret(api_key, api_secret, frappe_authorization_source=None):
"""frappe_authorization_source to provide api key and secret for a doctype apart from User""" """frappe_authorization_source to provide api key and secret for a doctype apart from User"""
doctype = frappe_authorization_source or 'User' doctype = frappe_authorization_source or "User"
doc = frappe.db.get_value( doc = frappe.db.get_value(doctype=doctype, filters={"api_key": api_key}, fieldname=["name"])
doctype=doctype,
filters={"api_key": api_key},
fieldname=["name"]
)
form_dict = frappe.local.form_dict form_dict = frappe.local.form_dict
doc_secret = frappe.utils.password.get_decrypted_password(doctype, doc, fieldname='api_secret') doc_secret = frappe.utils.password.get_decrypted_password(doctype, doc, fieldname="api_secret")
if api_secret == doc_secret: if api_secret == doc_secret:
if doctype == 'User': if doctype == "User":
user = frappe.db.get_value( user = frappe.db.get_value(doctype="User", filters={"api_key": api_key}, fieldname=["name"])
doctype="User",
filters={"api_key": api_key},
fieldname=["name"]
)
else: else:
user = frappe.db.get_value(doctype, doc, 'user') user = frappe.db.get_value(doctype, doc, "user")
if frappe.local.login_manager.user in ('', 'Guest'): if frappe.local.login_manager.user in ("", "Guest"):
frappe.set_user(user) frappe.set_user(user)
frappe.local.form_dict = form_dict frappe.local.form_dict = form_dict
def validate_auth_via_hooks(): def validate_auth_via_hooks():
for auth_hook in frappe.get_hooks('auth_hooks', []): for auth_hook in frappe.get_hooks("auth_hooks", []):
frappe.get_attr(auth_hook)() frappe.get_attr(auth_hook)()

View file

@ -2,37 +2,37 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import os
import logging import logging
import os
from werkzeug.local import LocalManager
from werkzeug.wrappers import Request, Response
from werkzeug.exceptions import HTTPException, NotFound from werkzeug.exceptions import HTTPException, NotFound
from werkzeug.local import LocalManager
from werkzeug.middleware.profiler import ProfilerMiddleware from werkzeug.middleware.profiler import ProfilerMiddleware
from werkzeug.middleware.shared_data import SharedDataMiddleware from werkzeug.middleware.shared_data import SharedDataMiddleware
from werkzeug.wrappers import Request, Response
import frappe import frappe
import frappe.handler
import frappe.auth
import frappe.api import frappe.api
import frappe.utils.response import frappe.auth
from frappe.utils import get_site_name, sanitize_html import frappe.handler
from frappe.middlewares import StaticDataMiddleware
from frappe.website.serve import get_response
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.monitor
import frappe.rate_limiter import frappe.rate_limiter
import frappe.recorder
import frappe.utils.response
from frappe import _
from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request
from frappe.middlewares import StaticDataMiddleware
from frappe.utils import get_site_name, sanitize_html
from frappe.utils.error import make_error_snapshot
from frappe.website.serve import get_response
local_manager = LocalManager([frappe.local]) local_manager = LocalManager([frappe.local])
_site = None _site = None
_sites_path = os.environ.get("SITES_PATH", ".") _sites_path = os.environ.get("SITES_PATH", ".")
class RequestContext(object):
class RequestContext(object):
def __init__(self, environ): def __init__(self, environ):
self.request = Request(environ) self.request = Request(environ)
@ -42,6 +42,7 @@ class RequestContext(object):
def __exit__(self, type, value, traceback): def __exit__(self, type, value, traceback):
frappe.destroy() frappe.destroy()
@Request.application @Request.application
def application(request): def application(request):
response = None response = None
@ -65,13 +66,13 @@ def application(request):
elif request.path.startswith("/api/"): elif request.path.startswith("/api/"):
response = frappe.api.handle() response = frappe.api.handle()
elif request.path.startswith('/backups'): elif request.path.startswith("/backups"):
response = frappe.utils.response.download_backup(request.path) response = frappe.utils.response.download_backup(request.path)
elif request.path.startswith('/private/files/'): elif request.path.startswith("/private/files/"):
response = frappe.utils.response.download_private_file(request.path) response = frappe.utils.response.download_private_file(request.path)
elif request.method in ('GET', 'HEAD', 'POST'): elif request.method in ("GET", "HEAD", "POST"):
response = get_response() response = get_response()
else: else:
@ -103,41 +104,45 @@ def application(request):
return response return response
def init_request(request): def init_request(request):
frappe.local.request = request frappe.local.request = request
frappe.local.is_ajax = frappe.get_request_header("X-Requested-With")=="XMLHttpRequest" frappe.local.is_ajax = frappe.get_request_header("X-Requested-With") == "XMLHttpRequest"
site = _site or request.headers.get('X-Frappe-Site-Name') or get_site_name(request.host) site = _site or request.headers.get("X-Frappe-Site-Name") or get_site_name(request.host)
frappe.init(site=site, sites_path=_sites_path) frappe.init(site=site, sites_path=_sites_path)
if not (frappe.local.conf and frappe.local.conf.db_name): if not (frappe.local.conf and frappe.local.conf.db_name):
# site does not exist # site does not exist
raise NotFound raise NotFound
if frappe.local.conf.get('maintenance_mode'): if frappe.local.conf.get("maintenance_mode"):
frappe.connect() frappe.connect()
raise frappe.SessionStopped('Session Stopped') raise frappe.SessionStopped("Session Stopped")
else: else:
frappe.connect(set_admin_as_user=False) frappe.connect(set_admin_as_user=False)
request.max_content_length = frappe.local.conf.get('max_file_size') or 10 * 1024 * 1024 request.max_content_length = frappe.local.conf.get("max_file_size") or 10 * 1024 * 1024
make_form_dict(request) make_form_dict(request)
if request.method != "OPTIONS": if request.method != "OPTIONS":
frappe.local.http_request = frappe.auth.HTTPRequest() frappe.local.http_request = frappe.auth.HTTPRequest()
def log_request(request, response): def log_request(request, response):
if hasattr(frappe.local, 'conf') and frappe.local.conf.enable_frappe_logger: if hasattr(frappe.local, "conf") and frappe.local.conf.enable_frappe_logger:
frappe.logger("frappe.web", allow_site=frappe.local.site).info({ frappe.logger("frappe.web", allow_site=frappe.local.site).info(
"site": get_site_name(request.host), {
"remote_addr": getattr(request, "remote_addr", "NOTFOUND"), "site": get_site_name(request.host),
"base_url": getattr(request, "base_url", "NOTFOUND"), "remote_addr": getattr(request, "remote_addr", "NOTFOUND"),
"full_path": getattr(request, "full_path", "NOTFOUND"), "base_url": getattr(request, "base_url", "NOTFOUND"),
"method": getattr(request, "method", "NOTFOUND"), "full_path": getattr(request, "full_path", "NOTFOUND"),
"scheme": getattr(request, "scheme", "NOTFOUND"), "method": getattr(request, "method", "NOTFOUND"),
"http_status_code": getattr(response, "status_code", "NOTFOUND") "scheme": getattr(request, "scheme", "NOTFOUND"),
}) "http_status_code": getattr(response, "status_code", "NOTFOUND"),
}
)
def process_response(response): def process_response(response):
@ -145,19 +150,20 @@ def process_response(response):
return return
# set cookies # set cookies
if hasattr(frappe.local, 'cookie_manager'): if hasattr(frappe.local, "cookie_manager"):
frappe.local.cookie_manager.flush_cookies(response=response) frappe.local.cookie_manager.flush_cookies(response=response)
# rate limiter headers # rate limiter headers
if hasattr(frappe.local, 'rate_limiter'): if hasattr(frappe.local, "rate_limiter"):
response.headers.extend(frappe.local.rate_limiter.headers()) response.headers.extend(frappe.local.rate_limiter.headers())
# CORS headers # CORS headers
if hasattr(frappe.local, 'conf') and frappe.conf.allow_cors: if hasattr(frappe.local, "conf") and frappe.conf.allow_cors:
set_cors_headers(response) set_cors_headers(response)
def set_cors_headers(response): def set_cors_headers(response):
origin = frappe.request.headers.get('Origin') origin = frappe.request.headers.get("Origin")
allow_cors = frappe.conf.allow_cors allow_cors = frappe.conf.allow_cors
if not (origin and allow_cors): if not (origin and allow_cors):
return return
@ -169,20 +175,25 @@ def set_cors_headers(response):
if origin not in allow_cors: if origin not in allow_cors:
return return
response.headers.extend({ response.headers.extend(
'Access-Control-Allow-Origin': origin, {
'Access-Control-Allow-Credentials': 'true', "Access-Control-Allow-Origin": origin,
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', "Access-Control-Allow-Credentials": "true",
'Access-Control-Allow-Headers': ('Authorization,DNT,X-Mx-ReqToken,' "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
'Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,' "Access-Control-Allow-Headers": (
'Cache-Control,Content-Type') "Authorization,DNT,X-Mx-ReqToken,"
}) "Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,"
"Cache-Control,Content-Type"
),
}
)
def make_form_dict(request): def make_form_dict(request):
import json import json
request_data = request.get_data(as_text=True) request_data = request.get_data(as_text=True)
if 'application/json' in (request.content_type or '') and request_data: if "application/json" in (request.content_type or "") and request_data:
args = json.loads(request_data) args = json.loads(request_data)
else: else:
args = {} args = {}
@ -198,20 +209,19 @@ def make_form_dict(request):
# _ is passed by $.ajax so that the request is not cached by the browser. So, remove _ from form_dict # _ is passed by $.ajax so that the request is not cached by the browser. So, remove _ from form_dict
frappe.local.form_dict.pop("_") frappe.local.form_dict.pop("_")
def handle_exception(e): def handle_exception(e):
response = None response = None
http_status_code = getattr(e, "http_status_code", 500) http_status_code = getattr(e, "http_status_code", 500)
return_as_message = False return_as_message = False
accept_header = frappe.get_request_header("Accept") or "" accept_header = frappe.get_request_header("Accept") or ""
respond_as_json = ( respond_as_json = (
frappe.get_request_header('Accept') frappe.get_request_header("Accept")
and (frappe.local.is_ajax or 'application/json' in accept_header) and (frappe.local.is_ajax or "application/json" in accept_header)
or ( or (frappe.local.request.path.startswith("/api/") and not accept_header.startswith("text"))
frappe.local.request.path.startswith("/api/") and not accept_header.startswith("text")
)
) )
if frappe.conf.get('developer_mode'): if frappe.conf.get("developer_mode"):
# don't fail silently # don't fail silently
print(frappe.get_traceback()) print(frappe.get_traceback())
@ -220,27 +230,38 @@ def handle_exception(e):
# if the request is ajax, send back the trace or error message # if the request is ajax, send back the trace or error message
response = frappe.utils.response.report_error(http_status_code) response = frappe.utils.response.report_error(http_status_code)
elif (http_status_code==500 elif (
http_status_code == 500
and (frappe.db and isinstance(e, frappe.db.InternalError)) and (frappe.db and isinstance(e, frappe.db.InternalError))
and (frappe.db and (frappe.db.is_deadlocked(e) or frappe.db.is_timedout(e)))): and (frappe.db and (frappe.db.is_deadlocked(e) or frappe.db.is_timedout(e)))
http_status_code = 508 ):
http_status_code = 508
elif http_status_code==401: elif http_status_code == 401:
frappe.respond_as_web_page(_("Session Expired"), frappe.respond_as_web_page(
_("Session Expired"),
_("Your session has expired, please login again to continue."), _("Your session has expired, please login again to continue."),
http_status_code=http_status_code, indicator_color='red') http_status_code=http_status_code,
indicator_color="red",
)
return_as_message = True return_as_message = True
elif http_status_code==403: elif http_status_code == 403:
frappe.respond_as_web_page(_("Not Permitted"), frappe.respond_as_web_page(
_("Not Permitted"),
_("You do not have enough permissions to complete the action"), _("You do not have enough permissions to complete the action"),
http_status_code=http_status_code, indicator_color='red') http_status_code=http_status_code,
indicator_color="red",
)
return_as_message = True return_as_message = True
elif http_status_code==404: elif http_status_code == 404:
frappe.respond_as_web_page(_("Not Found"), frappe.respond_as_web_page(
_("Not Found"),
_("The resource you are looking for is not available"), _("The resource you are looking for is not available"),
http_status_code=http_status_code, indicator_color='red') http_status_code=http_status_code,
indicator_color="red",
)
return_as_message = True return_as_message = True
elif http_status_code == 429: elif http_status_code == 429:
@ -252,9 +273,9 @@ def handle_exception(e):
if frappe.local.flags.disable_traceback and not frappe.local.dev_server: if frappe.local.flags.disable_traceback and not frappe.local.dev_server:
traceback = "" traceback = ""
frappe.respond_as_web_page("Server Error", frappe.respond_as_web_page(
traceback, http_status_code=http_status_code, "Server Error", traceback, http_status_code=http_status_code, indicator_color="red", width=640
indicator_color='red', width=640) )
return_as_message = True return_as_message = True
if e.__class__ == frappe.AuthenticationError: if e.__class__ == frappe.AuthenticationError:
@ -269,6 +290,7 @@ def handle_exception(e):
return response return response
def after_request(rollback): def after_request(rollback):
if (frappe.local.request.method in ("POST", "PUT") or frappe.local.flags.commit) and frappe.db: if (frappe.local.request.method in ("POST", "PUT") or frappe.local.flags.commit) and frappe.db:
if frappe.db.transaction_writes: if frappe.db.transaction_writes:
@ -286,41 +308,47 @@ def after_request(rollback):
return rollback return rollback
application = local_manager.make_middleware(application) application = local_manager.make_middleware(application)
def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=None, sites_path='.'):
def serve(
port=8000, profile=False, no_reload=False, no_threading=False, site=None, sites_path="."
):
global application, _site, _sites_path global application, _site, _sites_path
_site = site _site = site
_sites_path = sites_path _sites_path = sites_path
from werkzeug.serving import run_simple from werkzeug.serving import run_simple
if profile or os.environ.get('USE_PROFILER'): if profile or os.environ.get("USE_PROFILER"):
application = ProfilerMiddleware(application, sort_by=('cumtime', 'calls')) application = ProfilerMiddleware(application, sort_by=("cumtime", "calls"))
if not os.environ.get('NO_STATICS'): if not os.environ.get("NO_STATICS"):
application = SharedDataMiddleware(application, { application = SharedDataMiddleware(
str('/assets'): str(os.path.join(sites_path, 'assets')) application, {str("/assets"): str(os.path.join(sites_path, "assets"))}
}) )
application = StaticDataMiddleware(application, { application = StaticDataMiddleware(
str('/files'): str(os.path.abspath(sites_path)) application, {str("/files"): str(os.path.abspath(sites_path))}
}) )
application.debug = True application.debug = True
application.config = { application.config = {"SERVER_NAME": "localhost:8000"}
'SERVER_NAME': 'localhost:8000'
}
log = logging.getLogger('werkzeug') log = logging.getLogger("werkzeug")
log.propagate = False log.propagate = False
in_test_env = os.environ.get('CI') in_test_env = os.environ.get("CI")
if in_test_env: if in_test_env:
log.setLevel(logging.ERROR) log.setLevel(logging.ERROR)
run_simple('0.0.0.0', int(port), application, run_simple(
"0.0.0.0",
int(port),
application,
use_reloader=False if in_test_env else not no_reload, use_reloader=False if in_test_env else not no_reload,
use_debugger=not in_test_env, use_debugger=not in_test_env,
use_evalex=not in_test_env, use_evalex=not in_test_env,
threaded=not no_threading) threaded=not no_threading,
)

View file

@ -11,7 +11,12 @@ from frappe.core.doctype.activity_log.activity_log import add_authentication_log
from frappe.modules.patch_handler import check_session_stopped from frappe.modules.patch_handler import check_session_stopped
from frappe.sessions import Session, clear_sessions, delete_session from frappe.sessions import Session, clear_sessions, delete_session
from frappe.translate import get_language from frappe.translate import get_language
from frappe.twofactor import authenticate_for_2factor, confirm_otp_token, get_cached_user_pass, should_run_2fa from frappe.twofactor import (
authenticate_for_2factor,
confirm_otp_token,
get_cached_user_pass,
should_run_2fa,
)
from frappe.utils import cint, date_diff, datetime, get_datetime, today from frappe.utils import cint, date_diff, datetime, get_datetime, today
from frappe.utils.password import check_password from frappe.utils.password import check_password
from frappe.website.utils import get_home_page from frappe.website.utils import get_home_page
@ -47,20 +52,20 @@ class HTTPRequest:
def domain(self): def domain(self):
if not getattr(self, "_domain", None): if not getattr(self, "_domain", None):
self._domain = frappe.request.host self._domain = frappe.request.host
if self._domain and self._domain.startswith('www.'): if self._domain and self._domain.startswith("www."):
self._domain = self._domain[4:] self._domain = self._domain[4:]
return self._domain return self._domain
def set_request_ip(self): def set_request_ip(self):
if frappe.get_request_header('X-Forwarded-For'): if frappe.get_request_header("X-Forwarded-For"):
frappe.local.request_ip = (frappe.get_request_header('X-Forwarded-For').split(",")[0]).strip() frappe.local.request_ip = (frappe.get_request_header("X-Forwarded-For").split(",")[0]).strip()
elif frappe.get_request_header('REMOTE_ADDR'): elif frappe.get_request_header("REMOTE_ADDR"):
frappe.local.request_ip = frappe.get_request_header('REMOTE_ADDR') frappe.local.request_ip = frappe.get_request_header("REMOTE_ADDR")
else: else:
frappe.local.request_ip = '127.0.0.1' frappe.local.request_ip = "127.0.0.1"
def set_cookies(self): def set_cookies(self):
frappe.local.cookie_manager = CookieManager() frappe.local.cookie_manager = CookieManager()
@ -75,7 +80,7 @@ class HTTPRequest:
if ( if (
not frappe.local.session.data.csrf_token not frappe.local.session.data.csrf_token
or frappe.local.session.data.device == "mobile" or frappe.local.session.data.device == "mobile"
or frappe.conf.get('ignore_csrf', None) or frappe.conf.get("ignore_csrf", None)
): ):
# not via boot # not via boot
return return
@ -99,10 +104,10 @@ class HTTPRequest:
def connect(self): def connect(self):
"""connect to db, from ac_name or db_name""" """connect to db, from ac_name or db_name"""
frappe.local.db = frappe.database.get_db( frappe.local.db = frappe.database.get_db(
user=self.get_db_name(), user=self.get_db_name(), password=getattr(conf, "db_password", "")
password=getattr(conf, 'db_password', '')
) )
class LoginManager: class LoginManager:
def __init__(self): def __init__(self):
self.user = None self.user = None
@ -110,13 +115,15 @@ class LoginManager:
self.full_name = None self.full_name = None
self.user_type = None self.user_type = None
if frappe.local.form_dict.get('cmd')=='login' or frappe.local.request.path=="/api/method/login": if (
frappe.local.form_dict.get("cmd") == "login" or frappe.local.request.path == "/api/method/login"
):
if self.login() is False: if self.login() is False:
return return
self.resume = False self.resume = False
# run login triggers # run login triggers
self.run_trigger('on_session_creation') self.run_trigger("on_session_creation")
else: else:
try: try:
self.resume = True self.resume = True
@ -131,12 +138,14 @@ class LoginManager:
def login(self): def login(self):
# clear cache # clear cache
frappe.clear_cache(user = frappe.form_dict.get('usr')) frappe.clear_cache(user=frappe.form_dict.get("usr"))
user, pwd = get_cached_user_pass() user, pwd = get_cached_user_pass()
self.authenticate(user=user, pwd=pwd) self.authenticate(user=user, pwd=pwd)
if self.force_user_to_reset_password(): if self.force_user_to_reset_password():
doc = frappe.get_doc("User", self.user) doc = frappe.get_doc("User", self.user)
frappe.local.response["redirect_to"] = doc.reset_password(send_email=False, password_expired=True) frappe.local.response["redirect_to"] = doc.reset_password(
send_email=False, password_expired=True
)
frappe.local.response["message"] = "Password Reset" frappe.local.response["message"] = "Password Reset"
return False return False
@ -147,7 +156,7 @@ class LoginManager:
self.post_login() self.post_login()
def post_login(self): def post_login(self):
self.run_trigger('on_login') self.run_trigger("on_login")
validate_ip_address(self.user) validate_ip_address(self.user)
self.validate_hour() self.validate_hour()
self.get_user_info() self.get_user_info()
@ -156,8 +165,9 @@ class LoginManager:
self.set_user_info() self.set_user_info()
def get_user_info(self): def get_user_info(self):
self.info = frappe.db.get_value("User", self.user, self.info = frappe.db.get_value(
["user_type", "first_name", "last_name", "user_image"], as_dict=1) "User", self.user, ["user_type", "first_name", "last_name", "user_image"], as_dict=1
)
self.user_type = self.info.user_type self.user_type = self.info.user_type
@ -170,28 +180,27 @@ class LoginManager:
# set sid again # set sid again
frappe.local.cookie_manager.init_cookies() frappe.local.cookie_manager.init_cookies()
self.full_name = " ".join(filter(None, [self.info.first_name, self.full_name = " ".join(filter(None, [self.info.first_name, self.info.last_name]))
self.info.last_name]))
if self.info.user_type=="Website User": if self.info.user_type == "Website User":
frappe.local.cookie_manager.set_cookie("system_user", "no") frappe.local.cookie_manager.set_cookie("system_user", "no")
if not resume: if not resume:
frappe.local.response["message"] = "No App" frappe.local.response["message"] = "No App"
frappe.local.response["home_page"] = '/' + get_home_page() frappe.local.response["home_page"] = "/" + get_home_page()
else: else:
frappe.local.cookie_manager.set_cookie("system_user", "yes") frappe.local.cookie_manager.set_cookie("system_user", "yes")
if not resume: if not resume:
frappe.local.response['message'] = 'Logged In' frappe.local.response["message"] = "Logged In"
frappe.local.response["home_page"] = "/app" frappe.local.response["home_page"] = "/app"
if not resume: if not resume:
frappe.response["full_name"] = self.full_name frappe.response["full_name"] = self.full_name
# redirect information # redirect information
redirect_to = frappe.cache().hget('redirect_after_login', self.user) redirect_to = frappe.cache().hget("redirect_after_login", self.user)
if redirect_to: if redirect_to:
frappe.local.response["redirect_to"] = redirect_to frappe.local.response["redirect_to"] = redirect_to
frappe.cache().hdel('redirect_after_login', self.user) frappe.cache().hdel("redirect_after_login", self.user)
frappe.local.cookie_manager.set_cookie("full_name", self.full_name) frappe.local.cookie_manager.set_cookie("full_name", self.full_name)
frappe.local.cookie_manager.set_cookie("user_id", self.user) frappe.local.cookie_manager.set_cookie("user_id", self.user)
@ -202,8 +211,9 @@ class LoginManager:
def make_session(self, resume=False): def make_session(self, resume=False):
# start session # start session
frappe.local.session_obj = Session(user=self.user, resume=resume, frappe.local.session_obj = Session(
full_name=self.full_name, user_type=self.user_type) user=self.user, resume=resume, full_name=self.full_name, user_type=self.user_type
)
# reset user if changed to Guest # reset user if changed to Guest
self.user = frappe.local.session_obj.user self.user = frappe.local.session_obj.user
@ -212,7 +222,10 @@ class LoginManager:
def clear_active_sessions(self): def clear_active_sessions(self):
"""Clear other sessions of the current user if `deny_multiple_sessions` is not set""" """Clear other sessions of the current user if `deny_multiple_sessions` is not set"""
if not (cint(frappe.conf.get("deny_multiple_sessions")) or cint(frappe.db.get_system_setting('deny_multiple_sessions'))): if not (
cint(frappe.conf.get("deny_multiple_sessions"))
or cint(frappe.db.get_system_setting("deny_multiple_sessions"))
):
return return
if frappe.session.user != "Guest": if frappe.session.user != "Guest":
@ -222,27 +235,27 @@ class LoginManager:
from frappe.core.doctype.user.user import User from frappe.core.doctype.user.user import User
if not (user and pwd): if not (user and pwd):
user, pwd = frappe.form_dict.get('usr'), frappe.form_dict.get('pwd') user, pwd = frappe.form_dict.get("usr"), frappe.form_dict.get("pwd")
if not (user and pwd): if not (user and pwd):
self.fail(_('Incomplete login details'), user=user) self.fail(_("Incomplete login details"), user=user)
user = User.find_by_credentials(user, pwd) user = User.find_by_credentials(user, pwd)
if not user: if not user:
self.fail('Invalid login credentials') self.fail("Invalid login credentials")
# Current login flow uses cached credentials for authentication while checking OTP. # Current login flow uses cached credentials for authentication while checking OTP.
# Incase of OTP check, tracker for auth needs to be disabled(If not, it can remove tracker history as it is going to succeed anyway) # Incase of OTP check, tracker for auth needs to be disabled(If not, it can remove tracker history as it is going to succeed anyway)
# Tracker is activated for 2FA incase of OTP. # Tracker is activated for 2FA incase of OTP.
ignore_tracker = should_run_2fa(user.name) and ('otp' in frappe.form_dict) ignore_tracker = should_run_2fa(user.name) and ("otp" in frappe.form_dict)
tracker = None if ignore_tracker else get_login_attempt_tracker(user.name) tracker = None if ignore_tracker else get_login_attempt_tracker(user.name)
if not user.is_authenticated: if not user.is_authenticated:
tracker and tracker.add_failure_attempt() tracker and tracker.add_failure_attempt()
self.fail('Invalid login credentials', user=user.name) self.fail("Invalid login credentials", user=user.name)
elif not (user.name == 'Administrator' or user.enabled): elif not (user.name == "Administrator" or user.enabled):
tracker and tracker.add_failure_attempt() tracker and tracker.add_failure_attempt()
self.fail('User disabled or missing', user=user.name) self.fail("User disabled or missing", user=user.name)
else: else:
tracker and tracker.add_success_attempt() tracker and tracker.add_success_attempt()
self.user = user.name self.user = user.name
@ -254,12 +267,14 @@ class LoginManager:
if self.user in frappe.STANDARD_USERS: if self.user in frappe.STANDARD_USERS:
return False return False
reset_pwd_after_days = cint(frappe.db.get_single_value("System Settings", reset_pwd_after_days = cint(
"force_user_to_reset_password")) frappe.db.get_single_value("System Settings", "force_user_to_reset_password")
)
if reset_pwd_after_days: if reset_pwd_after_days:
last_password_reset_date = frappe.db.get_value("User", last_password_reset_date = (
self.user, "last_password_reset_date") or today() frappe.db.get_value("User", self.user, "last_password_reset_date") or today()
)
last_pwd_reset_days = date_diff(today(), last_password_reset_date) last_pwd_reset_days = date_diff(today(), last_password_reset_date)
@ -272,30 +287,31 @@ class LoginManager:
# returns user in correct case # returns user in correct case
return check_password(user, pwd) return check_password(user, pwd)
except frappe.AuthenticationError: except frappe.AuthenticationError:
self.fail('Incorrect password', user=user) self.fail("Incorrect password", user=user)
def fail(self, message, user=None): def fail(self, message, user=None):
if not user: if not user:
user = _('Unknown User') user = _("Unknown User")
frappe.local.response['message'] = message frappe.local.response["message"] = message
add_authentication_log(message, user, status="Failed") add_authentication_log(message, user, status="Failed")
frappe.db.commit() frappe.db.commit()
raise frappe.AuthenticationError raise frappe.AuthenticationError
def run_trigger(self, event='on_login'): def run_trigger(self, event="on_login"):
for method in frappe.get_hooks().get(event, []): for method in frappe.get_hooks().get(event, []):
frappe.call(frappe.get_attr(method), login_manager=self) frappe.call(frappe.get_attr(method), login_manager=self)
def validate_hour(self): def validate_hour(self):
"""check if user is logging in during restricted hours""" """check if user is logging in during restricted hours"""
login_before = int(frappe.db.get_value('User', self.user, 'login_before', ignore=True) or 0) login_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_after = int(frappe.db.get_value("User", self.user, "login_after", ignore=True) or 0)
if not (login_before or login_after): if not (login_before or login_after):
return return
from frappe.utils import now_datetime from frappe.utils import now_datetime
current_hour = int(now_datetime().strftime('%H'))
current_hour = int(now_datetime().strftime("%H"))
if login_before and current_hour > login_before: if login_before and current_hour > login_before:
frappe.throw(_("Login not allowed at this time"), frappe.AuthenticationError) frappe.throw(_("Login not allowed at this time"), frappe.AuthenticationError)
@ -311,9 +327,10 @@ class LoginManager:
self.user = user self.user = user
self.post_login() self.post_login()
def logout(self, arg='', user=None): def logout(self, arg="", user=None):
if not user: user = frappe.session.user if not user:
self.run_trigger('on_logout') user = frappe.session.user
self.run_trigger("on_logout")
if user == frappe.session.user: if user == frappe.session.user:
delete_session(frappe.session.sid, user=user, reason="User Manually Logged Out") delete_session(frappe.session.sid, user=user, reason="User Manually Logged Out")
@ -324,13 +341,15 @@ class LoginManager:
def clear_cookies(self): def clear_cookies(self):
clear_cookies() clear_cookies()
class CookieManager: class CookieManager:
def __init__(self): def __init__(self):
self.cookies = {} self.cookies = {}
self.to_delete = [] self.to_delete = []
def init_cookies(self): def init_cookies(self):
if not frappe.local.session.get('sid'): return if not frappe.local.session.get("sid"):
return
# sid expires in 3 days # sid expires in 3 days
expires = datetime.datetime.now() + datetime.timedelta(days=3) expires = datetime.datetime.now() + datetime.timedelta(days=3)
@ -340,7 +359,7 @@ class CookieManager:
self.set_cookie("country", frappe.session.session_country) self.set_cookie("country", frappe.session.session_country)
def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Lax"): def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Lax"):
if not secure and hasattr(frappe.local, 'request'): if not secure and hasattr(frappe.local, "request"):
secure = frappe.local.request.scheme == "https" secure = frappe.local.request.scheme == "https"
# Cordova does not work with Lax # Cordova does not work with Lax
@ -352,7 +371,7 @@ class CookieManager:
"expires": expires, "expires": expires,
"secure": secure, "secure": secure,
"httponly": httponly, "httponly": httponly,
"samesite": samesite "samesite": samesite,
} }
def delete_cookie(self, to_delete): def delete_cookie(self, to_delete):
@ -363,11 +382,14 @@ class CookieManager:
def flush_cookies(self, response): def flush_cookies(self, response):
for key, opts in self.cookies.items(): for key, opts in self.cookies.items():
response.set_cookie(key, quote((opts.get("value") or "").encode('utf-8')), response.set_cookie(
key,
quote((opts.get("value") or "").encode("utf-8")),
expires=opts.get("expires"), expires=opts.get("expires"),
secure=opts.get("secure"), secure=opts.get("secure"),
httponly=opts.get("httponly"), httponly=opts.get("httponly"),
samesite=opts.get("samesite")) samesite=opts.get("samesite"),
)
# expires yesterday! # expires yesterday!
expires = datetime.datetime.now() + datetime.timedelta(days=-1) expires = datetime.datetime.now() + datetime.timedelta(days=-1)
@ -379,19 +401,29 @@ class CookieManager:
def get_logged_user(): def get_logged_user():
return frappe.session.user return frappe.session.user
def clear_cookies(): def clear_cookies():
if hasattr(frappe.local, "session"): if hasattr(frappe.local, "session"):
frappe.session.sid = "" frappe.session.sid = ""
frappe.local.cookie_manager.delete_cookie(["full_name", "user_id", "sid", "user_image", "system_user"]) frappe.local.cookie_manager.delete_cookie(
["full_name", "user_id", "sid", "user_image", "system_user"]
)
def validate_ip_address(user): def validate_ip_address(user):
"""check if IP Address is valid""" """check if IP Address is valid"""
user = frappe.get_cached_doc("User", user) if not frappe.flags.in_test else frappe.get_doc("User", user) user = (
frappe.get_cached_doc("User", user) if not frappe.flags.in_test else frappe.get_doc("User", user)
)
ip_list = user.get_restricted_ip_list() ip_list = user.get_restricted_ip_list()
if not ip_list: if not ip_list:
return return
system_settings = frappe.get_cached_doc("System Settings") if not frappe.flags.in_test else frappe.get_single("System Settings") system_settings = (
frappe.get_cached_doc("System Settings")
if not frappe.flags.in_test
else frappe.get_single("System Settings")
)
# check if bypass restrict ip is enabled for all users # check if bypass restrict ip is enabled for all users
bypass_restrict_ip_check = system_settings.bypass_restrict_ip_check_if_2fa_enabled bypass_restrict_ip_check = system_settings.bypass_restrict_ip_check_if_2fa_enabled
@ -406,6 +438,7 @@ def validate_ip_address(user):
frappe.throw(_("Access not allowed from this IP Address"), frappe.AuthenticationError) frappe.throw(_("Access not allowed from this IP Address"), frappe.AuthenticationError)
def get_login_attempt_tracker(user_name: str, raise_locked_exception: bool = True): def get_login_attempt_tracker(user_name: str, raise_locked_exception: bool = True):
"""Get login attempt tracker instance. """Get login attempt tracker instance.
@ -413,18 +446,22 @@ def get_login_attempt_tracker(user_name: str, raise_locked_exception: bool = Tru
:param raise_locked_exception: If set, raises an exception incase of user not allowed to login :param raise_locked_exception: If set, raises an exception incase of user not allowed to login
""" """
sys_settings = frappe.get_doc("System Settings") sys_settings = frappe.get_doc("System Settings")
track_login_attempts = (sys_settings.allow_consecutive_login_attempts >0) track_login_attempts = sys_settings.allow_consecutive_login_attempts > 0
tracker_kwargs = {} tracker_kwargs = {}
if track_login_attempts: if track_login_attempts:
tracker_kwargs['lock_interval'] = sys_settings.allow_login_after_fail tracker_kwargs["lock_interval"] = sys_settings.allow_login_after_fail
tracker_kwargs['max_consecutive_login_attempts'] = sys_settings.allow_consecutive_login_attempts tracker_kwargs["max_consecutive_login_attempts"] = sys_settings.allow_consecutive_login_attempts
tracker = LoginAttemptTracker(user_name, **tracker_kwargs) tracker = LoginAttemptTracker(user_name, **tracker_kwargs)
if raise_locked_exception and track_login_attempts and not tracker.is_user_allowed(): if raise_locked_exception and track_login_attempts and not tracker.is_user_allowed():
frappe.throw(_("Your account has been locked and will resume after {0} seconds") frappe.throw(
.format(sys_settings.allow_login_after_fail), frappe.SecurityException) _("Your account has been locked and will resume after {0} seconds").format(
sys_settings.allow_login_after_fail
),
frappe.SecurityException,
)
return tracker return tracker
@ -433,8 +470,11 @@ class LoginAttemptTracker(object):
Lock the account for s number of seconds if there have been n consecutive unsuccessful attempts to log in. Lock the account for s number of seconds if there have been n consecutive unsuccessful attempts to log in.
""" """
def __init__(self, user_name: str, max_consecutive_login_attempts: int=3, lock_interval:int = 5*60):
""" Initialize the tracker. def __init__(
self, user_name: str, max_consecutive_login_attempts: int = 3, lock_interval: int = 5 * 60
):
"""Initialize the tracker.
:param user_name: Name of the loggedin user :param user_name: Name of the loggedin user
:param max_consecutive_login_attempts: Maximum allowed consecutive failed login attempts :param max_consecutive_login_attempts: Maximum allowed consecutive failed login attempts
@ -446,15 +486,15 @@ class LoginAttemptTracker(object):
@property @property
def login_failed_count(self): def login_failed_count(self):
return frappe.cache().hget('login_failed_count', self.user_name) return frappe.cache().hget("login_failed_count", self.user_name)
@login_failed_count.setter @login_failed_count.setter
def login_failed_count(self, count): def login_failed_count(self, count):
frappe.cache().hset('login_failed_count', self.user_name, count) frappe.cache().hset("login_failed_count", self.user_name, count)
@login_failed_count.deleter @login_failed_count.deleter
def login_failed_count(self): def login_failed_count(self):
frappe.cache().hdel('login_failed_count', self.user_name) frappe.cache().hdel("login_failed_count", self.user_name)
@property @property
def login_failed_time(self): def login_failed_time(self):
@ -462,23 +502,23 @@ class LoginAttemptTracker(object):
For every user we track only First failed login attempt time within lock interval of time. For every user we track only First failed login attempt time within lock interval of time.
""" """
return frappe.cache().hget('login_failed_time', self.user_name) return frappe.cache().hget("login_failed_time", self.user_name)
@login_failed_time.setter @login_failed_time.setter
def login_failed_time(self, timestamp): def login_failed_time(self, timestamp):
frappe.cache().hset('login_failed_time', self.user_name, timestamp) frappe.cache().hset("login_failed_time", self.user_name, timestamp)
@login_failed_time.deleter @login_failed_time.deleter
def login_failed_time(self): def login_failed_time(self):
frappe.cache().hdel('login_failed_time', self.user_name) frappe.cache().hdel("login_failed_time", self.user_name)
def add_failure_attempt(self): def add_failure_attempt(self):
""" Log user failure attempts into the system. """Log user failure attempts into the system.
Increase the failure count if new failure is with in current lock interval time period, if not reset the login failure count. Increase the failure count if new failure is with in current lock interval time period, if not reset the login failure count.
""" """
login_failed_time = self.login_failed_time login_failed_time = self.login_failed_time
login_failed_count = self.login_failed_count # Consecutive login failure count login_failed_count = self.login_failed_count # Consecutive login failure count
current_time = get_datetime() current_time = get_datetime()
if not (login_failed_time and login_failed_count): if not (login_failed_time and login_failed_count):
@ -493,8 +533,7 @@ class LoginAttemptTracker(object):
self.login_failed_count = login_failed_count self.login_failed_count = login_failed_count
def add_success_attempt(self): def add_success_attempt(self):
"""Reset login failures. """Reset login failures."""
"""
del self.login_failed_count del self.login_failed_count
del self.login_failed_time del self.login_failed_time
@ -507,6 +546,10 @@ class LoginAttemptTracker(object):
login_failed_count = self.login_failed_count or 0 login_failed_count = self.login_failed_count or 0
current_time = get_datetime() current_time = get_datetime()
if login_failed_time and login_failed_time + self.lock_interval > current_time and login_failed_count > self.max_failed_logins: if (
login_failed_time
and login_failed_time + self.lock_interval > current_time
and login_failed_count > self.max_failed_logins
):
return False return False
return True return True

View file

@ -24,9 +24,7 @@ class AssignmentRule(Document):
def validate_document_types(self): def validate_document_types(self):
if self.document_type == "ToDo": if self.document_type == "ToDo":
frappe.throw( frappe.throw(
_('Assignment Rule is not allowed on {0} document type').format( _("Assignment Rule is not allowed on {0} document type").format(frappe.bold("ToDo"))
frappe.bold("ToDo")
)
) )
def validate_assignment_days(self): def validate_assignment_days(self):
@ -38,70 +36,70 @@ class AssignmentRule(Document):
frappe.throw( frappe.throw(
_("Assignment Day{0} {1} has been repeated.").format( _("Assignment Day{0} {1} has been repeated.").format(
plural, plural, frappe.bold(", ".join(repeated_days))
frappe.bold(", ".join(repeated_days))
) )
) )
def apply_unassign(self, doc, assignments): def apply_unassign(self, doc, assignments):
if (self.unassign_condition and if self.unassign_condition and self.name in [d.assignment_rule for d in assignments]:
self.name in [d.assignment_rule for d in assignments]):
return self.clear_assignment(doc) return self.clear_assignment(doc)
return False return False
def apply_assign(self, doc): def apply_assign(self, doc):
if self.safe_eval('assign_condition', doc): if self.safe_eval("assign_condition", doc):
return self.do_assignment(doc) return self.do_assignment(doc)
def do_assignment(self, doc): def do_assignment(self, doc):
# clear existing assignment, to reassign # clear existing assignment, to reassign
assign_to.clear(doc.get('doctype'), doc.get('name')) assign_to.clear(doc.get("doctype"), doc.get("name"))
user = self.get_user(doc) user = self.get_user(doc)
if user: if user:
assign_to.add(dict( assign_to.add(
assign_to = [user], dict(
doctype = doc.get('doctype'), assign_to=[user],
name = doc.get('name'), doctype=doc.get("doctype"),
description = frappe.render_template(self.description, doc), name=doc.get("name"),
assignment_rule = self.name, description=frappe.render_template(self.description, doc),
notify = True, assignment_rule=self.name,
date = doc.get(self.due_date_based_on) if self.due_date_based_on else None notify=True,
)) date=doc.get(self.due_date_based_on) if self.due_date_based_on else None,
)
)
# set for reference in round robin # set for reference in round robin
self.db_set('last_user', user) self.db_set("last_user", user)
return True return True
return False return False
def clear_assignment(self, doc): def clear_assignment(self, doc):
'''Clear assignments''' """Clear assignments"""
if self.safe_eval('unassign_condition', doc): if self.safe_eval("unassign_condition", doc):
return assign_to.clear(doc.get('doctype'), doc.get('name')) return assign_to.clear(doc.get("doctype"), doc.get("name"))
def close_assignments(self, doc): def close_assignments(self, doc):
'''Close assignments''' """Close assignments"""
if self.safe_eval('close_condition', doc): if self.safe_eval("close_condition", doc):
return assign_to.close_all_assignments(doc.get('doctype'), doc.get('name')) return assign_to.close_all_assignments(doc.get("doctype"), doc.get("name"))
def get_user(self, doc): def get_user(self, doc):
''' """
Get the next user for assignment Get the next user for assignment
''' """
if self.rule == 'Round Robin': if self.rule == "Round Robin":
return self.get_user_round_robin() return self.get_user_round_robin()
elif self.rule == 'Load Balancing': elif self.rule == "Load Balancing":
return self.get_user_load_balancing() return self.get_user_load_balancing()
elif self.rule == 'Based on Field': elif self.rule == "Based on Field":
return self.get_user_based_on_field(doc) return self.get_user_based_on_field(doc)
def get_user_round_robin(self): def get_user_round_robin(self):
''' """
Get next user based on round robin Get next user based on round robin
''' """
# first time, or last in list, pick the first # first time, or last in list, pick the first
if not self.last_user or self.last_user == self.users[-1].user: if not self.last_user or self.last_user == self.users[-1].user:
@ -110,32 +108,33 @@ class AssignmentRule(Document):
# find out the next user in the list # find out the next user in the list
for i, d in enumerate(self.users): for i, d in enumerate(self.users):
if self.last_user == d.user: if self.last_user == d.user:
return self.users[i+1].user return self.users[i + 1].user
# bad last user, assign to the first one # bad last user, assign to the first one
return self.users[0].user return self.users[0].user
def get_user_load_balancing(self): def get_user_load_balancing(self):
'''Assign to the user with least number of open assignments''' """Assign to the user with least number of open assignments"""
counts = [] counts = []
for d in self.users: for d in self.users:
counts.append(dict( counts.append(
user = d.user, dict(
count = frappe.db.count('ToDo', dict( user=d.user,
reference_type = self.document_type, count=frappe.db.count(
allocated_to = d.user, "ToDo", dict(reference_type=self.document_type, allocated_to=d.user, status="Open")
status = "Open")) ),
)) )
)
# sort by dict value # sort by dict value
sorted_counts = sorted(counts, key = lambda k: k['count']) sorted_counts = sorted(counts, key=lambda k: k["count"])
# pick the first user # pick the first user
return sorted_counts[0].get('user') return sorted_counts[0].get("user")
def get_user_based_on_field(self, doc): def get_user_based_on_field(self, doc):
val = doc.get(self.field) val = doc.get(self.field)
if frappe.db.exists('User', val): if frappe.db.exists("User", val):
return val return val
def safe_eval(self, fieldname, doc): def safe_eval(self, fieldname, doc):
@ -145,12 +144,12 @@ class AssignmentRule(Document):
except Exception as e: except Exception as e:
# when assignment fails, don't block the document as it may be # when assignment fails, don't block the document as it may be
# a part of the email pulling # a part of the email pulling
frappe.msgprint(frappe._('Auto assignment failed: {0}').format(str(e)), indicator = 'orange') frappe.msgprint(frappe._("Auto assignment failed: {0}").format(str(e)), indicator="orange")
return False return False
def get_assignment_days(self): def get_assignment_days(self):
return [d.day for d in self.get('assignment_days', [])] return [d.day for d in self.get("assignment_days", [])]
def is_rule_not_applicable_today(self): def is_rule_not_applicable_today(self):
today = frappe.flags.assignment_day or frappe.utils.get_weekday() today = frappe.flags.assignment_day or frappe.utils.get_weekday()
@ -159,11 +158,14 @@ class AssignmentRule(Document):
def get_assignments(doc) -> List[Dict]: def get_assignments(doc) -> List[Dict]:
return frappe.get_all('ToDo', fields = ['name', 'assignment_rule'], filters = dict( return frappe.get_all(
reference_type = doc.get('doctype'), "ToDo",
reference_name = doc.get('name'), fields=["name", "assignment_rule"],
status = ('!=', 'Cancelled') filters=dict(
), limit=5) reference_type=doc.get("doctype"), reference_name=doc.get("name"), status=("!=", "Cancelled")
),
limit=5,
)
@frappe.whitelist() @frappe.whitelist()
@ -173,21 +175,30 @@ def bulk_apply(doctype, docnames):
for name in docnames: for name in docnames:
if background: if background:
frappe.enqueue('frappe.automation.doctype.assignment_rule.assignment_rule.apply', doc=None, doctype=doctype, name=name) frappe.enqueue(
"frappe.automation.doctype.assignment_rule.assignment_rule.apply",
doc=None,
doctype=doctype,
name=name,
)
else: else:
apply(doctype=doctype, name=name) apply(doctype=doctype, name=name)
def reopen_closed_assignment(doc): def reopen_closed_assignment(doc):
todo_list = frappe.get_all("ToDo", filters={ todo_list = frappe.get_all(
"reference_type": doc.doctype, "ToDo",
"reference_name": doc.name, filters={
"status": "Closed", "reference_type": doc.doctype,
}, pluck="name") "reference_name": doc.name,
"status": "Closed",
},
pluck="name",
)
for todo in todo_list: for todo in todo_list:
todo_doc = frappe.get_doc('ToDo', todo) todo_doc = frappe.get_doc("ToDo", todo)
todo_doc.status = 'Open' todo_doc.status = "Open"
todo_doc.save(ignore_permissions=True) todo_doc.save(ignore_permissions=True)
return bool(todo_list) return bool(todo_list)
@ -209,13 +220,16 @@ def apply(doc=None, method=None, doctype=None, name=None):
if not doc and doctype and name: if not doc and doctype and name:
doc = frappe.get_doc(doctype, name) doc = frappe.get_doc(doctype, name)
assignment_rules = get_doctype_map("Assignment Rule", doc.doctype, filters={ assignment_rules = get_doctype_map(
"document_type": doc.doctype, "disabled": 0 "Assignment Rule",
}, order_by="priority desc") doc.doctype,
filters={"document_type": doc.doctype, "disabled": 0},
order_by="priority desc",
)
# multiple auto assigns # multiple auto assigns
assignment_rule_docs: List[AssignmentRule] = [ assignment_rule_docs: List[AssignmentRule] = [
frappe.get_cached_doc("Assignment Rule", d.get('name')) for d in assignment_rules frappe.get_cached_doc("Assignment Rule", d.get("name")) for d in assignment_rules
] ]
if not assignment_rule_docs: if not assignment_rule_docs:
@ -224,8 +238,8 @@ def apply(doc=None, method=None, doctype=None, name=None):
doc = doc.as_dict() doc = doc.as_dict()
assignments = get_assignments(doc) assignments = get_assignments(doc)
clear = True # are all assignments cleared clear = True # are all assignments cleared
new_apply = False # are new assignments applied new_apply = False # are new assignments applied
if assignments: if assignments:
# first unassign # first unassign
@ -260,14 +274,18 @@ def apply(doc=None, method=None, doctype=None, name=None):
if not new_apply: if not new_apply:
# only reopen if close condition is not satisfied # only reopen if close condition is not satisfied
to_close_todos = assignment_rule.safe_eval('close_condition', doc) to_close_todos = assignment_rule.safe_eval("close_condition", doc)
if to_close_todos: if to_close_todos:
# close todo status # close todo status
todos_to_close = frappe.get_all("ToDo", filters={ todos_to_close = frappe.get_all(
"reference_type": doc.doctype, "ToDo",
"reference_name": doc.name, filters={
}, pluck="name") "reference_type": doc.doctype,
"reference_name": doc.name,
},
pluck="name",
)
for todo in todos_to_close: for todo in todos_to_close:
_todo = frappe.get_doc("ToDo", todo) _todo = frappe.get_doc("ToDo", todo)
@ -286,8 +304,7 @@ def apply(doc=None, method=None, doctype=None, name=None):
def update_due_date(doc, state=None): def update_due_date(doc, state=None):
"""Run on_update on every Document (via hooks.py) """Run on_update on every Document (via hooks.py)"""
"""
skip_document_update = ( skip_document_update = (
frappe.flags.in_migrate frappe.flags.in_migrate
or frappe.flags.in_patch or frappe.flags.in_patch
@ -306,7 +323,7 @@ def update_due_date(doc, state=None):
"due_date_based_on": ["is", "set"], "due_date_based_on": ["is", "set"],
"document_type": doc.doctype, "document_type": doc.doctype,
"disabled": 0, "disabled": 0,
} },
) )
for rule in assignment_rules: for rule in assignment_rules:
@ -319,20 +336,24 @@ def update_due_date(doc, state=None):
) )
if field_updated: if field_updated:
assignment_todos = frappe.get_all("ToDo", filters={ assignment_todos = frappe.get_all(
"assignment_rule": rule.get("name"), "ToDo",
"reference_type": doc.doctype, filters={
"reference_name": doc.name, "assignment_rule": rule.get("name"),
"status": "Open", "reference_type": doc.doctype,
}, pluck="name") "reference_name": doc.name,
"status": "Open",
},
pluck="name",
)
for todo in assignment_todos: for todo in assignment_todos:
todo_doc = frappe.get_doc('ToDo', todo) todo_doc = frappe.get_doc("ToDo", todo)
todo_doc.date = doc.get(due_date_field) todo_doc.date = doc.get(due_date_field)
todo_doc.flags.updater_reference = { todo_doc.flags.updater_reference = {
'doctype': 'Assignment Rule', "doctype": "Assignment Rule",
'docname': rule.get('name'), "docname": rule.get("name"),
'label': _('via Assignment Rule') "label": _("via Assignment Rule"),
} }
todo_doc.save(ignore_permissions=True) todo_doc.save(ignore_permissions=True)

View file

@ -20,13 +20,13 @@ class TestAutoAssign(unittest.TestCase):
def setUp(self): def setUp(self):
make_test_records("User") make_test_records("User")
days = [ days = [
dict(day = 'Sunday'), dict(day="Sunday"),
dict(day = 'Monday'), dict(day="Monday"),
dict(day = 'Tuesday'), dict(day="Tuesday"),
dict(day = 'Wednesday'), dict(day="Wednesday"),
dict(day = 'Thursday'), dict(day="Thursday"),
dict(day = 'Friday'), dict(day="Friday"),
dict(day = 'Saturday'), dict(day="Saturday"),
] ]
self.days = days self.days = days
self.assignment_rule = get_assignment_rule([days, days]) self.assignment_rule = get_assignment_rule([days, days])
@ -36,20 +36,22 @@ class TestAutoAssign(unittest.TestCase):
note = make_note(dict(public=1)) note = make_note(dict(public=1))
# check if auto assigned to first user # check if auto assigned to first user
self.assertEqual(frappe.db.get_value('ToDo', dict( self.assertEqual(
reference_type = 'Note', frappe.db.get_value(
reference_name = note.name, "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
status = 'Open' ),
), 'allocated_to'), 'test@example.com') "test@example.com",
)
note = make_note(dict(public=1)) note = make_note(dict(public=1))
# check if auto assigned to second user # check if auto assigned to second user
self.assertEqual(frappe.db.get_value('ToDo', dict( self.assertEqual(
reference_type = 'Note', frappe.db.get_value(
reference_name = note.name, "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
status = 'Open' ),
), 'allocated_to'), 'test1@example.com') "test1@example.com",
)
clear_assignments() clear_assignments()
@ -57,35 +59,41 @@ class TestAutoAssign(unittest.TestCase):
# check if auto assigned to third user, even if # check if auto assigned to third user, even if
# previous assignments where closed # previous assignments where closed
self.assertEqual(frappe.db.get_value('ToDo', dict( self.assertEqual(
reference_type = 'Note', frappe.db.get_value(
reference_name = note.name, "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
status = 'Open' ),
), 'allocated_to'), 'test2@example.com') "test2@example.com",
)
# check loop back to first user # check loop back to first user
note = make_note(dict(public=1)) note = make_note(dict(public=1))
self.assertEqual(frappe.db.get_value('ToDo', dict( self.assertEqual(
reference_type = 'Note', frappe.db.get_value(
reference_name = note.name, "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
status = 'Open' ),
), 'allocated_to'), 'test@example.com') "test@example.com",
)
def test_load_balancing(self): def test_load_balancing(self):
self.assignment_rule.rule = 'Load Balancing' self.assignment_rule.rule = "Load Balancing"
self.assignment_rule.save() self.assignment_rule.save()
for _ in range(30): for _ in range(30):
note = make_note(dict(public=1)) note = make_note(dict(public=1))
# check if each user has 10 assignments (?) # check if each user has 10 assignments (?)
for user in ('test@example.com', 'test1@example.com', 'test2@example.com'): 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) self.assertEqual(
len(frappe.get_all("ToDo", dict(allocated_to=user, reference_type="Note"))), 10
)
# clear 5 assignments for first user # clear 5 assignments for first user
# can't do a limit in "delete" since postgres does not support it # can't do a limit in "delete" since postgres does not support it
for d in frappe.get_all('ToDo', dict(reference_type = 'Note', allocated_to = 'test@example.com'), limit=5): for d in frappe.get_all(
"ToDo", dict(reference_type="Note", allocated_to="test@example.com"), limit=5
):
frappe.db.delete("ToDo", {"name": d.name}) frappe.db.delete("ToDo", {"name": d.name})
# add 5 more assignments # add 5 more assignments
@ -93,56 +101,59 @@ class TestAutoAssign(unittest.TestCase):
make_note(dict(public=1)) make_note(dict(public=1))
# check if each user still has 10 assignments # check if each user still has 10 assignments
for user in ('test@example.com', 'test1@example.com', 'test2@example.com'): 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) self.assertEqual(
len(frappe.get_all("ToDo", dict(allocated_to=user, reference_type="Note"))), 10
)
def test_based_on_field(self): def test_based_on_field(self):
self.assignment_rule.rule = 'Based on Field' self.assignment_rule.rule = "Based on Field"
self.assignment_rule.field = 'owner' self.assignment_rule.field = "owner"
self.assignment_rule.save() self.assignment_rule.save()
frappe.set_user('test1@example.com') frappe.set_user("test1@example.com")
note = make_note(dict(public=1)) note = make_note(dict(public=1))
# check if auto assigned to doc owner, test1@example.com # check if auto assigned to doc owner, test1@example.com
self.assertEqual(frappe.db.get_value('ToDo', dict( self.assertEqual(
reference_type = 'Note', frappe.db.get_value(
reference_name = note.name, "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "owner"
status = 'Open' ),
), 'owner'), 'test1@example.com') "test1@example.com",
)
frappe.set_user('test2@example.com') frappe.set_user("test2@example.com")
note = make_note(dict(public=1)) note = make_note(dict(public=1))
# check if auto assigned to doc owner, test2@example.com # check if auto assigned to doc owner, test2@example.com
self.assertEqual(frappe.db.get_value('ToDo', dict( self.assertEqual(
reference_type = 'Note', frappe.db.get_value(
reference_name = note.name, "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "owner"
status = 'Open' ),
), 'owner'), 'test2@example.com') "test2@example.com",
)
frappe.set_user('Administrator') frappe.set_user("Administrator")
def test_assign_condition(self): def test_assign_condition(self):
# check condition # check condition
note = make_note(dict(public=0)) note = make_note(dict(public=0))
self.assertEqual(frappe.db.get_value('ToDo', dict( self.assertEqual(
reference_type = 'Note', frappe.db.get_value(
reference_name = note.name, "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
status = 'Open' ),
), 'allocated_to'), None) None,
)
def test_clear_assignment(self): def test_clear_assignment(self):
note = make_note(dict(public=1)) note = make_note(dict(public=1))
# check if auto assigned to first user # check if auto assigned to first user
todo = frappe.get_list('ToDo', dict( todo = frappe.get_list(
reference_type = 'Note', "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), limit=1
reference_name = note.name, )[0]
status = 'Open'
), limit=1)[0]
todo = frappe.get_doc('ToDo', todo['name']) todo = frappe.get_doc("ToDo", todo["name"])
self.assertEqual(todo.allocated_to, 'test@example.com') self.assertEqual(todo.allocated_to, "test@example.com")
# test auto unassign # test auto unassign
note.public = 0 note.public = 0
@ -151,99 +162,101 @@ class TestAutoAssign(unittest.TestCase):
todo.load_from_db() todo.load_from_db()
# check if todo is cancelled # check if todo is cancelled
self.assertEqual(todo.status, 'Cancelled') self.assertEqual(todo.status, "Cancelled")
def test_close_assignment(self): def test_close_assignment(self):
note = make_note(dict(public=1, content="valid")) note = make_note(dict(public=1, content="valid"))
# check if auto assigned # check if auto assigned
todo = frappe.get_list('ToDo', dict( todo = frappe.get_list(
reference_type = 'Note', "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), limit=1
reference_name = note.name, )[0]
status = 'Open'
), limit=1)[0]
todo = frappe.get_doc('ToDo', todo['name']) todo = frappe.get_doc("ToDo", todo["name"])
self.assertEqual(todo.allocated_to, 'test@example.com') self.assertEqual(todo.allocated_to, "test@example.com")
note.content="Closed" note.content = "Closed"
note.save() note.save()
todo.load_from_db() todo.load_from_db()
# check if todo is closed # check if todo is closed
self.assertEqual(todo.status, 'Closed') self.assertEqual(todo.status, "Closed")
# check if closed todo retained assignment # check if closed todo retained assignment
self.assertEqual(todo.allocated_to, 'test@example.com') self.assertEqual(todo.allocated_to, "test@example.com")
def check_multiple_rules(self): def check_multiple_rules(self):
note = make_note(dict(public=1, notify_on_login=1)) note = make_note(dict(public=1, notify_on_login=1))
# check if auto assigned to test3 (2nd rule is applied, as it has higher priority) # check if auto assigned to test3 (2nd rule is applied, as it has higher priority)
self.assertEqual(frappe.db.get_value('ToDo', dict( self.assertEqual(
reference_type = 'Note', frappe.db.get_value(
reference_name = note.name, "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
status = 'Open' ),
), 'allocated_to'), 'test@example.com') "test@example.com",
)
def check_assignment_rule_scheduling(self): def check_assignment_rule_scheduling(self):
frappe.db.delete("Assignment Rule") frappe.db.delete("Assignment Rule")
days_1 = [dict(day = 'Sunday'), dict(day = 'Monday'), dict(day = 'Tuesday')] days_1 = [dict(day="Sunday"), dict(day="Monday"), dict(day="Tuesday")]
days_2 = [dict(day = 'Wednesday'), dict(day = 'Thursday'), dict(day = 'Friday'), dict(day = 'Saturday')] days_2 = [dict(day="Wednesday"), dict(day="Thursday"), dict(day="Friday"), dict(day="Saturday")]
get_assignment_rule([days_1, days_2], ['public == 1', 'public == 1']) get_assignment_rule([days_1, days_2], ["public == 1", "public == 1"])
frappe.flags.assignment_day = "Monday" frappe.flags.assignment_day = "Monday"
note = make_note(dict(public=1)) note = make_note(dict(public=1))
self.assertIn(frappe.db.get_value('ToDo', dict( self.assertIn(
reference_type = 'Note', frappe.db.get_value(
reference_name = note.name, "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
status = 'Open' ),
), 'allocated_to'), ['test@example.com', 'test1@example.com', 'test2@example.com']) ["test@example.com", "test1@example.com", "test2@example.com"],
)
frappe.flags.assignment_day = "Friday" frappe.flags.assignment_day = "Friday"
note = make_note(dict(public=1)) note = make_note(dict(public=1))
self.assertIn(frappe.db.get_value('ToDo', dict( self.assertIn(
reference_type = 'Note', frappe.db.get_value(
reference_name = note.name, "ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
status = 'Open' ),
), 'allocated_to'), ['test3@example.com']) ["test3@example.com"],
)
def test_assignment_rule_condition(self): def test_assignment_rule_condition(self):
frappe.db.delete("Assignment Rule") frappe.db.delete("Assignment Rule")
# Add expiry_date custom field # Add expiry_date custom field
from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.custom.doctype.custom_field.custom_field import create_custom_field
df = dict(fieldname='expiry_date', label='Expiry Date', fieldtype='Date')
create_custom_field('Note', df)
assignment_rule = frappe.get_doc(dict( df = dict(fieldname="expiry_date", label="Expiry Date", fieldtype="Date")
name = 'Assignment with Due Date', create_custom_field("Note", df)
doctype = 'Assignment Rule',
document_type = 'Note', assignment_rule = frappe.get_doc(
assign_condition = 'public == 0', dict(
due_date_based_on = 'expiry_date', name="Assignment with Due Date",
assignment_days = self.days, doctype="Assignment Rule",
users = [ document_type="Note",
dict(user = 'test@example.com'), assign_condition="public == 0",
] due_date_based_on="expiry_date",
)).insert() assignment_days=self.days,
users=[
dict(user="test@example.com"),
],
)
).insert()
expiry_date = frappe.utils.add_days(frappe.utils.nowdate(), 2) expiry_date = frappe.utils.add_days(frappe.utils.nowdate(), 2)
note1 = make_note({'expiry_date': expiry_date}) note1 = make_note({"expiry_date": expiry_date})
note2 = make_note({'expiry_date': expiry_date}) note2 = make_note({"expiry_date": expiry_date})
note1_todo = frappe.get_all('ToDo', filters=dict( note1_todo = frappe.get_all(
reference_type = 'Note', "ToDo", filters=dict(reference_type="Note", reference_name=note1.name, status="Open")
reference_name = note1.name, )[0]
status = 'Open'
))[0]
note1_todo_doc = frappe.get_doc('ToDo', note1_todo.name) note1_todo_doc = frappe.get_doc("ToDo", note1_todo.name)
self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), expiry_date) self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), expiry_date)
# due date should be updated if the reference doc's date is updated. # due date should be updated if the reference doc's date is updated.
@ -253,66 +266,67 @@ class TestAutoAssign(unittest.TestCase):
self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), note1.expiry_date) self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), note1.expiry_date)
# saving one note's expiry should not update other note todo's due date # saving one note's expiry should not update other note todo's due date
note2_todo = frappe.get_all('ToDo', filters=dict( note2_todo = frappe.get_all(
reference_type = 'Note', "ToDo",
reference_name = note2.name, filters=dict(reference_type="Note", reference_name=note2.name, status="Open"),
status = 'Open' fields=["name", "date"],
), fields=['name', 'date'])[0] )[0]
self.assertNotEqual(frappe.utils.get_date_str(note2_todo.date), note1.expiry_date) self.assertNotEqual(frappe.utils.get_date_str(note2_todo.date), note1.expiry_date)
self.assertEqual(frappe.utils.get_date_str(note2_todo.date), expiry_date) self.assertEqual(frappe.utils.get_date_str(note2_todo.date), expiry_date)
assignment_rule.delete() assignment_rule.delete()
def clear_assignments(): def clear_assignments():
frappe.db.delete("ToDo", {"reference_type": "Note"}) frappe.db.delete("ToDo", {"reference_type": "Note"})
def get_assignment_rule(days, assign=None): def get_assignment_rule(days, assign=None):
frappe.delete_doc_if_exists('Assignment Rule', 'For Note 1') frappe.delete_doc_if_exists("Assignment Rule", "For Note 1")
if not assign: if not assign:
assign = ['public == 1', 'notify_on_login == 1'] assign = ["public == 1", "notify_on_login == 1"]
assignment_rule = frappe.get_doc(dict( assignment_rule = frappe.get_doc(
name = 'For Note 1', dict(
doctype = 'Assignment Rule', name="For Note 1",
priority = 0, doctype="Assignment Rule",
document_type = 'Note', priority=0,
assign_condition = assign[0], document_type="Note",
unassign_condition = 'public == 0 or notify_on_login == 1', assign_condition=assign[0],
close_condition = '"Closed" in content', unassign_condition="public == 0 or notify_on_login == 1",
rule = 'Round Robin', close_condition='"Closed" in content',
assignment_days = days[0], rule="Round Robin",
users = [ assignment_days=days[0],
dict(user = 'test@example.com'), users=[
dict(user = 'test1@example.com'), dict(user="test@example.com"),
dict(user = 'test2@example.com'), dict(user="test1@example.com"),
] dict(user="test2@example.com"),
)).insert() ],
)
).insert()
frappe.delete_doc_if_exists('Assignment Rule', 'For Note 2') frappe.delete_doc_if_exists("Assignment Rule", "For Note 2")
# 2nd rule # 2nd rule
frappe.get_doc(dict( frappe.get_doc(
name = 'For Note 2', dict(
doctype = 'Assignment Rule', name="For Note 2",
priority = 1, doctype="Assignment Rule",
document_type = 'Note', priority=1,
assign_condition = assign[1], document_type="Note",
unassign_condition = 'notify_on_login == 0', assign_condition=assign[1],
rule = 'Round Robin', unassign_condition="notify_on_login == 0",
assignment_days = days[1], rule="Round Robin",
users = [ assignment_days=days[1],
dict(user = 'test3@example.com') users=[dict(user="test3@example.com")],
] )
)).insert() ).insert()
return assignment_rule return assignment_rule
def make_note(values=None): def make_note(values=None):
note = frappe.get_doc(dict( note = frappe.get_doc(dict(doctype="Note", title=random_string(10), content=random_string(20)))
doctype = 'Note',
title = random_string(10),
content = random_string(20)
))
if values: if values:
note.update(values) note.update(values)

View file

@ -5,5 +5,6 @@
# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document
class AssignmentRuleDay(Document): class AssignmentRuleDay(Document):
pass pass

View file

@ -5,5 +5,6 @@
# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document
class AssignmentRuleUser(Document): class AssignmentRuleUser(Document):
pass pass

View file

@ -2,23 +2,45 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
from datetime import timedelta
from dateutil.relativedelta import relativedelta
import frappe import frappe
from frappe import _ from frappe import _
from datetime import timedelta
from frappe.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.background_jobs import get_jobs
from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeated 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 (
from frappe.contacts.doctype.contact.contact import get_contacts_linking_to 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.model.document import Document
from frappe.utils import (
add_days,
cstr,
get_first_day,
get_last_day,
getdate,
month_diff,
split_emails,
today,
)
from frappe.utils.background_jobs import get_jobs
from frappe.utils.jinja import validate_template
from frappe.utils.user import get_system_managers
month_map = {"Monthly": 1, "Quarterly": 3, "Half-yearly": 6, "Yearly": 12}
week_map = {
"Monday": 0,
"Tuesday": 1,
"Wednesday": 2,
"Thursday": 3,
"Friday": 4,
"Saturday": 5,
"Sunday": 6,
}
month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}
week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6}
class AutoRepeat(Document): class AutoRepeat(Document):
def validate(self): def validate(self):
@ -46,7 +68,7 @@ class AutoRepeat(Document):
frappe.get_doc(self.reference_doctype, self.reference_document).notify_update() frappe.get_doc(self.reference_doctype, self.reference_document).notify_update()
def on_trash(self): def on_trash(self):
frappe.db.set_value(self.reference_doctype, self.reference_document, 'auto_repeat', '') frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", "")
frappe.get_doc(self.reference_doctype, self.reference_document).notify_update() frappe.get_doc(self.reference_doctype, self.reference_document).notify_update()
def set_dates(self): def set_dates(self):
@ -56,29 +78,36 @@ class AutoRepeat(Document):
self.next_schedule_date = self.get_next_schedule_date(schedule_date=self.start_date) self.next_schedule_date = self.get_next_schedule_date(schedule_date=self.start_date)
def unlink_if_applicable(self): def unlink_if_applicable(self):
if self.status == 'Completed' or self.disabled: if self.status == "Completed" or self.disabled:
frappe.db.set_value(self.reference_doctype, self.reference_document, 'auto_repeat', '') frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", "")
def validate_reference_doctype(self): def validate_reference_doctype(self):
if frappe.flags.in_test or frappe.flags.in_patch: if frappe.flags.in_test or frappe.flags.in_patch:
return return
if not frappe.get_meta(self.reference_doctype).allow_auto_repeat: if not frappe.get_meta(self.reference_doctype).allow_auto_repeat:
frappe.throw(_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(self.reference_doctype)) frappe.throw(
_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(
self.reference_doctype
)
)
def validate_submit_on_creation(self): def validate_submit_on_creation(self):
if self.submit_on_creation and not frappe.get_meta(self.reference_doctype).is_submittable: if self.submit_on_creation and not frappe.get_meta(self.reference_doctype).is_submittable:
frappe.throw(_('Cannot enable {0} for a non-submittable doctype').format( frappe.throw(
frappe.bold('Submit on Creation'))) _("Cannot enable {0} for a non-submittable doctype").format(frappe.bold("Submit on Creation"))
)
def validate_dates(self): def validate_dates(self):
if frappe.flags.in_patch: if frappe.flags.in_patch:
return return
if self.end_date: if self.end_date:
self.validate_from_to_dates('start_date', 'end_date') self.validate_from_to_dates("start_date", "end_date")
if self.end_date == self.start_date: if self.end_date == self.start_date:
frappe.throw(_('{0} should not be same as {1}').format(frappe.bold('End Date'), frappe.bold('Start Date'))) frappe.throw(
_("{0} should not be same as {1}").format(frappe.bold("End Date"), frappe.bold("Start Date"))
)
def validate_email_id(self): def validate_email_id(self):
if self.notify_by_email: if self.notify_by_email:
@ -100,17 +129,17 @@ class AutoRepeat(Document):
frappe.throw( frappe.throw(
_("Auto Repeat Day{0} {1} has been repeated.").format( _("Auto Repeat Day{0} {1} has been repeated.").format(
plural, plural, frappe.bold(", ".join(repeated_days))
frappe.bold(", ".join(repeated_days))
) )
) )
def update_auto_repeat_id(self): def update_auto_repeat_id(self):
#check if document is already on auto repeat # check if document is already on auto repeat
auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat") auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat")
if auto_repeat and auto_repeat != self.name and not frappe.flags.in_patch: if auto_repeat and auto_repeat != self.name and not frappe.flags.in_patch:
frappe.throw(_("The {0} is already on auto repeat {1}").format(self.reference_document, auto_repeat)) frappe.throw(
_("The {0} is already on auto repeat {1}").format(self.reference_document, auto_repeat)
)
else: else:
frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", self.name) frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", self.name)
@ -136,18 +165,18 @@ class AutoRepeat(Document):
row = { row = {
"reference_document": self.reference_document, "reference_document": self.reference_document,
"frequency": self.frequency, "frequency": self.frequency,
"next_scheduled_date": next_date "next_scheduled_date": next_date,
} }
schedule_details.append(row) schedule_details.append(row)
if self.end_date: if self.end_date:
next_date = self.get_next_schedule_date(schedule_date=start_date, for_full_schedule=True) next_date = self.get_next_schedule_date(schedule_date=start_date, for_full_schedule=True)
while (getdate(next_date) < getdate(end_date)): while getdate(next_date) < getdate(end_date):
row = { row = {
"reference_document" : self.reference_document, "reference_document": self.reference_document,
"frequency" : self.frequency, "frequency": self.frequency,
"next_scheduled_date" : next_date "next_scheduled_date": next_date,
} }
schedule_details.append(row) schedule_details.append(row)
next_date = self.get_next_schedule_date(schedule_date=next_date, for_full_schedule=True) next_date = self.get_next_schedule_date(schedule_date=next_date, for_full_schedule=True)
@ -169,9 +198,9 @@ class AutoRepeat(Document):
def make_new_document(self): def make_new_document(self):
reference_doc = frappe.get_doc(self.reference_doctype, self.reference_document) reference_doc = frappe.get_doc(self.reference_doctype, self.reference_document)
new_doc = frappe.copy_doc(reference_doc, ignore_no_copy = False) new_doc = frappe.copy_doc(reference_doc, ignore_no_copy=False)
self.update_doc(new_doc, reference_doc) self.update_doc(new_doc, reference_doc)
new_doc.insert(ignore_permissions = True) new_doc.insert(ignore_permissions=True)
if self.submit_on_creation: if self.submit_on_creation:
new_doc.submit() new_doc.submit()
@ -180,61 +209,72 @@ class AutoRepeat(Document):
def update_doc(self, new_doc, reference_doc): def update_doc(self, new_doc, reference_doc):
new_doc.docstatus = 0 new_doc.docstatus = 0
if new_doc.meta.get_field('set_posting_time'): if new_doc.meta.get_field("set_posting_time"):
new_doc.set('set_posting_time', 1) new_doc.set("set_posting_time", 1)
if new_doc.meta.get_field('auto_repeat'): if new_doc.meta.get_field("auto_repeat"):
new_doc.set('auto_repeat', self.name) new_doc.set("auto_repeat", self.name)
for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'user_remark', 'remarks', 'owner']: for fieldname in [
"naming_series",
"ignore_pricing_rule",
"posting_time",
"select_print_heading",
"user_remark",
"remarks",
"owner",
]:
if new_doc.meta.get_field(fieldname): if new_doc.meta.get_field(fieldname):
new_doc.set(fieldname, reference_doc.get(fieldname)) new_doc.set(fieldname, reference_doc.get(fieldname))
for data in new_doc.meta.fields: for data in new_doc.meta.fields:
if data.fieldtype == 'Date' and data.reqd: if data.fieldtype == "Date" and data.reqd:
new_doc.set(data.fieldname, self.next_schedule_date) new_doc.set(data.fieldname, self.next_schedule_date)
self.set_auto_repeat_period(new_doc) self.set_auto_repeat_period(new_doc)
auto_repeat_doc = frappe.get_doc('Auto Repeat', self.name) auto_repeat_doc = frappe.get_doc("Auto Repeat", self.name)
#for any action that needs to take place after the recurring document creation # for any action that needs to take place after the recurring document creation
#on recurring method of that doctype is triggered # on recurring method of that doctype is triggered
new_doc.run_method('on_recurring', reference_doc = reference_doc, auto_repeat_doc = auto_repeat_doc) new_doc.run_method("on_recurring", reference_doc=reference_doc, auto_repeat_doc=auto_repeat_doc)
def set_auto_repeat_period(self, new_doc): def set_auto_repeat_period(self, new_doc):
mcount = month_map.get(self.frequency) mcount = month_map.get(self.frequency)
if mcount and new_doc.meta.get_field('from_date') and new_doc.meta.get_field('to_date'): 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, last_ref_doc = frappe.db.get_all(
fields = ['name', 'from_date', 'to_date'], doctype=self.reference_doctype,
filters = [ fields=["name", "from_date", "to_date"],
['auto_repeat', '=', self.name], filters=[
['docstatus', '<', 2], ["auto_repeat", "=", self.name],
["docstatus", "<", 2],
], ],
order_by = 'creation desc', order_by="creation desc",
limit = 1) limit=1,
)
if not last_ref_doc: if not last_ref_doc:
return return
from_date = get_next_date(last_ref_doc[0].from_date, mcount) from_date = get_next_date(last_ref_doc[0].from_date, mcount)
if (cstr(get_first_day(last_ref_doc[0].from_date)) == cstr(last_ref_doc[0].from_date)) and \ 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)): cstr(get_last_day(last_ref_doc[0].to_date)) == cstr(last_ref_doc[0].to_date)
):
to_date = get_last_day(get_next_date(last_ref_doc[0].to_date, mcount)) to_date = get_last_day(get_next_date(last_ref_doc[0].to_date, mcount))
else: else:
to_date = get_next_date(last_ref_doc[0].to_date, mcount) to_date = get_next_date(last_ref_doc[0].to_date, mcount)
new_doc.set('from_date', from_date) new_doc.set("from_date", from_date)
new_doc.set('to_date', to_date) new_doc.set("to_date", to_date)
def get_next_schedule_date(self, schedule_date, for_full_schedule=False): def get_next_schedule_date(self, schedule_date, for_full_schedule=False):
""" """
Returns the next schedule date for auto repeat after a recurring document has been created. 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. 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 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 for_full_schedule: If True, returns the immediate next schedule date, else the full schedule.
""" """
if month_map.get(self.frequency): if month_map.get(self.frequency):
month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1 month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1
@ -295,60 +335,75 @@ class AutoRepeat(Document):
return 7 return 7
def get_auto_repeat_days(self): def get_auto_repeat_days(self):
return [d.day for d in self.get('repeat_on_days', [])] return [d.day for d in self.get("repeat_on_days", [])]
def send_notification(self, new_doc): def send_notification(self, new_doc):
"""Notify concerned people about recurring document generation""" """Notify concerned people about recurring document generation"""
subject = self.subject or '' subject = self.subject or ""
message = self.message or '' message = self.message or ""
if not self.subject: if not self.subject:
subject = _("New {0}: {1}").format(new_doc.doctype, new_doc.name) subject = _("New {0}: {1}").format(new_doc.doctype, new_doc.name)
elif "{" in self.subject: elif "{" in self.subject:
subject = frappe.render_template(self.subject, {'doc': new_doc}) subject = frappe.render_template(self.subject, {"doc": new_doc})
print_format = self.print_format or 'Standard' print_format = self.print_format or "Standard"
error_string = None error_string = None
try: try:
attachments = [frappe.attach_print(new_doc.doctype, new_doc.name, attachments = [
file_name=new_doc.name, print_format=print_format)] frappe.attach_print(
new_doc.doctype, new_doc.name, file_name=new_doc.name, print_format=print_format
)
]
except frappe.PermissionError: except frappe.PermissionError:
error_string = _("A recurring {0} {1} has been created for you via Auto Repeat {2}.").format(new_doc.doctype, new_doc.name, self.name) error_string = _("A recurring {0} {1} has been created for you via Auto Repeat {2}.").format(
new_doc.doctype, new_doc.name, self.name
)
error_string += "<br><br>" error_string += "<br><br>"
error_string += _("{0}: Failed to attach new recurring document. To enable attaching document in the auto repeat notification email, enable {1} in Print Settings").format( error_string += _(
frappe.bold(_('Note')), "{0}: Failed to attach new recurring document. To enable attaching document in the auto repeat notification email, enable {1} in Print Settings"
frappe.bold(_('Allow Print for Draft')) ).format(frappe.bold(_("Note")), frappe.bold(_("Allow Print for Draft")))
) attachments = "[]"
attachments = '[]'
if error_string: if error_string:
message = error_string message = error_string
elif not self.message: elif not self.message:
message = _("Please find attached {0}: {1}").format(new_doc.doctype, new_doc.name) message = _("Please find attached {0}: {1}").format(new_doc.doctype, new_doc.name)
elif "{" in self.message: elif "{" in self.message:
message = frappe.render_template(self.message, {'doc': new_doc}) message = frappe.render_template(self.message, {"doc": new_doc})
recipients = self.recipients.split('\n') recipients = self.recipients.split("\n")
make(doctype=new_doc.doctype, name=new_doc.name, recipients=recipients, make(
subject=subject, content=message, attachments=attachments, send_email=1) doctype=new_doc.doctype,
name=new_doc.name,
recipients=recipients,
subject=subject,
content=message,
attachments=attachments,
send_email=1,
)
@frappe.whitelist() @frappe.whitelist()
def fetch_linked_contacts(self): def fetch_linked_contacts(self):
if self.reference_doctype and self.reference_document: if self.reference_doctype and self.reference_document:
res = get_contacts_linking_to(self.reference_doctype, self.reference_document, fields=['email_id']) res = get_contacts_linking_to(
res += get_contacts_linked_from(self.reference_doctype, self.reference_document, fields=['email_id']) self.reference_doctype, self.reference_document, fields=["email_id"]
)
res += get_contacts_linked_from(
self.reference_doctype, self.reference_document, fields=["email_id"]
)
email_ids = {d.email_id for d in res} email_ids = {d.email_id for d in res}
if not email_ids: if not email_ids:
frappe.msgprint(_('No contacts linked to document'), alert=True) frappe.msgprint(_("No contacts linked to document"), alert=True)
else: else:
self.recipients = ', '.join(email_ids) self.recipients = ", ".join(email_ids)
def disable_auto_repeat(self): def disable_auto_repeat(self):
frappe.db.set_value('Auto Repeat', self.name, 'disabled', 1) frappe.db.set_value("Auto Repeat", self.name, "disabled", 1)
def notify_error_to_user(self, error_log): def notify_error_to_user(self, error_log):
recipients = list(get_system_managers(only_name=True)) recipients = list(get_system_managers(only_name=True))
@ -356,20 +411,17 @@ class AutoRepeat(Document):
subject = _("Auto Repeat Document Creation Failed") subject = _("Auto Repeat Document Creation Failed")
form_link = frappe.utils.get_link_to_form(self.reference_doctype, self.reference_document) form_link = frappe.utils.get_link_to_form(self.reference_doctype, self.reference_document)
auto_repeat_failed_for = _('Auto Repeat failed for {0}').format(form_link) auto_repeat_failed_for = _("Auto Repeat failed for {0}").format(form_link)
error_log_link = frappe.utils.get_link_to_form('Error Log', error_log.name) error_log_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_message = _("Check the Error Log for more information: {0}").format(error_log_link)
frappe.sendmail( frappe.sendmail(
recipients=recipients, recipients=recipients,
subject=subject, subject=subject,
template="auto_repeat_fail", template="auto_repeat_fail",
args={ args={"auto_repeat_failed_for": auto_repeat_failed_for, "error_log_message": error_log_message},
'auto_repeat_failed_for': auto_repeat_failed_for, header=[subject, "red"],
'error_log_message': error_log_message
},
header=[subject, 'red']
) )
@ -382,18 +434,18 @@ def get_next_date(dt, mcount, day=None):
def get_next_weekday(current_schedule_day, weekdays): def get_next_weekday(current_schedule_day, weekdays):
days = list(week_map.keys()) days = list(week_map.keys())
if current_schedule_day > 0: if current_schedule_day > 0:
days = days[(current_schedule_day + 1):] + days[:current_schedule_day] days = days[(current_schedule_day + 1) :] + days[:current_schedule_day]
else: else:
days = days[(current_schedule_day + 1):] days = days[(current_schedule_day + 1) :]
for entry in days: for entry in days:
if entry in weekdays: if entry in weekdays:
return entry return entry
#called through hooks # called through hooks
def make_auto_repeat_entry(): def make_auto_repeat_entry():
enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries' enqueued_method = "frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries"
jobs = get_jobs() jobs = get_jobs()
if not jobs or enqueued_method not in jobs[frappe.local.site]: if not jobs or enqueued_method not in jobs[frappe.local.site]:
@ -404,7 +456,7 @@ def make_auto_repeat_entry():
def create_repeated_entries(data): def create_repeated_entries(data):
for d in data: for d in data:
doc = frappe.get_doc('Auto Repeat', d.name) doc = frappe.get_doc("Auto Repeat", d.name)
current_date = getdate(today()) current_date = getdate(today())
schedule_date = getdate(doc.next_schedule_date) schedule_date = getdate(doc.next_schedule_date)
@ -413,33 +465,32 @@ def create_repeated_entries(data):
doc.create_documents() doc.create_documents()
schedule_date = doc.get_next_schedule_date(schedule_date=schedule_date) schedule_date = doc.get_next_schedule_date(schedule_date=schedule_date)
if schedule_date and not doc.disabled: if schedule_date and not doc.disabled:
frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date) frappe.db.set_value("Auto Repeat", doc.name, "next_schedule_date", schedule_date)
def get_auto_repeat_entries(date=None): def get_auto_repeat_entries(date=None):
if not date: if not date:
date = getdate(today()) date = getdate(today())
return frappe.db.get_all('Auto Repeat', filters=[ return frappe.db.get_all(
['next_schedule_date', '<=', date], "Auto Repeat", filters=[["next_schedule_date", "<=", date], ["status", "=", "Active"]]
['status', '=', 'Active'] )
])
#called through hooks # called through hooks
def set_auto_repeat_as_completed(): def set_auto_repeat_as_completed():
auto_repeat = frappe.get_all("Auto Repeat", filters = {'status': ['!=', 'Disabled']}) auto_repeat = frappe.get_all("Auto Repeat", filters={"status": ["!=", "Disabled"]})
for entry in auto_repeat: for entry in auto_repeat:
doc = frappe.get_doc("Auto Repeat", entry.name) doc = frappe.get_doc("Auto Repeat", entry.name)
if doc.is_completed(): if doc.is_completed():
doc.status = 'Completed' doc.status = "Completed"
doc.save() doc.save()
@frappe.whitelist() @frappe.whitelist()
def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, end_date = None): def make_auto_repeat(doctype, docname, frequency="Daily", start_date=None, end_date=None):
if not start_date: if not start_date:
start_date = getdate(today()) start_date = getdate(today())
doc = frappe.new_doc('Auto Repeat') doc = frappe.new_doc("Auto Repeat")
doc.reference_doctype = doctype doc.reference_doctype = doctype
doc.reference_document = docname doc.reference_document = docname
doc.frequency = frequency doc.frequency = frequency
@ -449,24 +500,34 @@ def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, e
doc.save() doc.save()
return doc return doc
# method for reference_doctype filter # method for reference_doctype filter
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_auto_repeat_doctypes(doctype, txt, searchfield, start, page_len, filters): def get_auto_repeat_doctypes(doctype, txt, searchfield, start, page_len, filters):
res = frappe.db.get_all('Property Setter', { res = frappe.db.get_all(
'property': 'allow_auto_repeat', "Property Setter",
'value': '1', {
}, ['doc_type']) "property": "allow_auto_repeat",
"value": "1",
},
["doc_type"],
)
docs = [r.doc_type for r in res] docs = [r.doc_type for r in res]
res = frappe.db.get_all('DocType', { res = frappe.db.get_all(
'allow_auto_repeat': 1, "DocType",
}, ['name']) {
"allow_auto_repeat": 1,
},
["name"],
)
docs += [r.name for r in res] docs += [r.name for r in res]
docs = set(list(docs)) docs = set(list(docs))
return [[d] for d in docs] return [[d] for d in docs]
@frappe.whitelist() @frappe.whitelist()
def update_reference(docname, reference): def update_reference(docname, reference):
result = "" result = ""
@ -478,13 +539,14 @@ def update_reference(docname, reference):
raise e raise e
return result return result
@frappe.whitelist() @frappe.whitelist()
def generate_message_preview(reference_dt, reference_doc, message=None, subject=None): def generate_message_preview(reference_dt, reference_doc, message=None, subject=None):
frappe.has_permission("Auto Repeat", "write", throw=True) frappe.has_permission("Auto Repeat", "write", throw=True)
doc = frappe.get_doc(reference_dt, reference_doc) doc = frappe.get_doc(reference_dt, reference_doc)
subject_preview = _("Please add a subject to your email") subject_preview = _("Please add a subject to your email")
msg_preview = frappe.render_template(message, {'doc': doc}) msg_preview = frappe.render_template(message, {"doc": doc})
if subject: if subject:
subject_preview = frappe.render_template(subject, {'doc': doc}) subject_preview = frappe.render_template(subject, {"doc": doc})
return {'message': msg_preview, 'subject': subject_preview} return {"message": msg_preview, "subject": subject_preview}

View file

@ -4,24 +4,40 @@
import unittest import unittest
import frappe import frappe
from frappe.automation.doctype.auto_repeat.auto_repeat import (
create_repeated_entries,
get_auto_repeat_entries,
week_map,
)
from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries, week_map from frappe.utils import add_days, add_months, getdate, today
from frappe.utils import today, add_days, getdate, add_months
def add_custom_fields(): def add_custom_fields():
df = dict( df = dict(
fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', insert_after='sender', fieldname="auto_repeat",
options='Auto Repeat', hidden=1, print_hide=1, read_only=1) label="Auto Repeat",
create_custom_field('ToDo', df) fieldtype="Link",
insert_after="sender",
options="Auto Repeat",
hidden=1,
print_hide=1,
read_only=1,
)
create_custom_field("ToDo", df)
class TestAutoRepeat(unittest.TestCase): class TestAutoRepeat(unittest.TestCase):
def setUp(self): def setUp(self):
if not frappe.db.sql("SELECT `fieldname` FROM `tabCustom Field` WHERE `fieldname`='auto_repeat' and `dt`=%s", "Todo"): if not frappe.db.sql(
"SELECT `fieldname` FROM `tabCustom Field` WHERE `fieldname`='auto_repeat' and `dt`=%s", "Todo"
):
add_custom_fields() add_custom_fields()
def test_daily_auto_repeat(self): def test_daily_auto_repeat(self):
todo = frappe.get_doc( todo = frappe.get_doc(
dict(doctype='ToDo', description='test recurring todo', assigned_by='Administrator')).insert() dict(doctype="ToDo", description="test recurring todo", assigned_by="Administrator")
).insert()
doc = make_auto_repeat(reference_document=todo.name) doc = make_auto_repeat(reference_document=todo.name)
self.assertEqual(doc.next_schedule_date, today()) self.assertEqual(doc.next_schedule_date, today())
@ -32,19 +48,25 @@ class TestAutoRepeat(unittest.TestCase):
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document) todo = frappe.get_doc(doc.reference_doctype, doc.reference_document)
self.assertEqual(todo.auto_repeat, doc.name) self.assertEqual(todo.auto_repeat, doc.name)
new_todo = frappe.db.get_value('ToDo', new_todo = frappe.db.get_value(
{'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name') "ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name"
)
new_todo = frappe.get_doc('ToDo', new_todo) new_todo = frappe.get_doc("ToDo", new_todo)
self.assertEqual(todo.get('description'), new_todo.get('description')) self.assertEqual(todo.get("description"), new_todo.get("description"))
def test_weekly_auto_repeat(self): def test_weekly_auto_repeat(self):
todo = frappe.get_doc( todo = frappe.get_doc(
dict(doctype='ToDo', description='test weekly todo', assigned_by='Administrator')).insert() dict(doctype="ToDo", description="test weekly todo", assigned_by="Administrator")
).insert()
doc = make_auto_repeat(reference_doctype='ToDo', doc = make_auto_repeat(
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7)) reference_doctype="ToDo",
frequency="Weekly",
reference_document=todo.name,
start_date=add_days(today(), -7),
)
self.assertEqual(doc.next_schedule_date, today()) self.assertEqual(doc.next_schedule_date, today())
data = get_auto_repeat_entries(getdate(today())) data = get_auto_repeat_entries(getdate(today()))
@ -54,25 +76,29 @@ class TestAutoRepeat(unittest.TestCase):
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document) todo = frappe.get_doc(doc.reference_doctype, doc.reference_document)
self.assertEqual(todo.auto_repeat, doc.name) self.assertEqual(todo.auto_repeat, doc.name)
new_todo = frappe.db.get_value('ToDo', new_todo = frappe.db.get_value(
{'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name') "ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name"
)
new_todo = frappe.get_doc('ToDo', new_todo) new_todo = frappe.get_doc("ToDo", new_todo)
self.assertEqual(todo.get('description'), new_todo.get('description')) self.assertEqual(todo.get("description"), new_todo.get("description"))
def test_weekly_auto_repeat_with_weekdays(self): def test_weekly_auto_repeat_with_weekdays(self):
todo = frappe.get_doc( todo = frappe.get_doc(
dict(doctype='ToDo', description='test auto repeat with weekdays', assigned_by='Administrator')).insert() dict(doctype="ToDo", description="test auto repeat with weekdays", assigned_by="Administrator")
).insert()
weekdays = list(week_map.keys()) weekdays = list(week_map.keys())
current_weekday = getdate().weekday() current_weekday = getdate().weekday()
days = [ days = [{"day": weekdays[current_weekday]}, {"day": weekdays[(current_weekday + 2) % 7]}]
{'day': weekdays[current_weekday]}, doc = make_auto_repeat(
{'day': weekdays[(current_weekday + 2) % 7]} reference_doctype="ToDo",
] frequency="Weekly",
doc = make_auto_repeat(reference_doctype='ToDo', reference_document=todo.name,
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7), days=days) start_date=add_days(today(), -7),
days=days,
)
self.assertEqual(doc.next_schedule_date, today()) self.assertEqual(doc.next_schedule_date, today())
data = get_auto_repeat_entries(getdate(today())) data = get_auto_repeat_entries(getdate(today()))
@ -90,136 +116,173 @@ class TestAutoRepeat(unittest.TestCase):
end_date = add_months(start_date, 12) end_date = add_months(start_date, 12)
todo = frappe.get_doc( todo = frappe.get_doc(
dict(doctype='ToDo', description='test recurring todo', assigned_by='Administrator')).insert() dict(doctype="ToDo", description="test recurring todo", assigned_by="Administrator")
).insert()
self.monthly_auto_repeat('ToDo', todo.name, start_date, end_date) self.monthly_auto_repeat("ToDo", todo.name, start_date, end_date)
#test without end_date # test without end_date
todo = frappe.get_doc(dict(doctype='ToDo', description='test recurring todo without end_date', assigned_by='Administrator')).insert() todo = frappe.get_doc(
self.monthly_auto_repeat('ToDo', todo.name, start_date) dict(
doctype="ToDo", description="test recurring todo without end_date", assigned_by="Administrator"
)
).insert()
self.monthly_auto_repeat("ToDo", todo.name, start_date)
def monthly_auto_repeat(self, doctype, docname, start_date, end_date = None): def monthly_auto_repeat(self, doctype, docname, start_date, end_date=None):
def get_months(start, end): def get_months(start, end):
diff = (12 * end.year + end.month) - (12 * start.year + start.month) diff = (12 * end.year + end.month) - (12 * start.year + start.month)
return diff + 1 return diff + 1
doc = make_auto_repeat( doc = make_auto_repeat(
reference_doctype=doctype, frequency='Monthly', reference_document=docname, start_date=start_date, reference_doctype=doctype,
end_date=end_date) frequency="Monthly",
reference_document=docname,
start_date=start_date,
end_date=end_date,
)
doc.disable_auto_repeat() doc.disable_auto_repeat()
data = get_auto_repeat_entries(getdate(today())) data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data) create_repeated_entries(data)
docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name}) docnames = frappe.get_all(doc.reference_doctype, {"auto_repeat": doc.name})
self.assertEqual(len(docnames), 1) self.assertEqual(len(docnames), 1)
doc = frappe.get_doc('Auto Repeat', doc.name) doc = frappe.get_doc("Auto Repeat", doc.name)
doc.db_set('disabled', 0) doc.db_set("disabled", 0)
months = get_months(getdate(start_date), getdate(today())) months = get_months(getdate(start_date), getdate(today()))
data = get_auto_repeat_entries(getdate(today())) data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data) create_repeated_entries(data)
docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name}) docnames = frappe.get_all(doc.reference_doctype, {"auto_repeat": doc.name})
self.assertEqual(len(docnames), months) self.assertEqual(len(docnames), months)
def test_notification_is_attached(self): def test_notification_is_attached(self):
todo = frappe.get_doc( todo = frappe.get_doc(
dict(doctype='ToDo', description='Test recurring notification attachment', assigned_by='Administrator')).insert() dict(
doctype="ToDo",
description="Test recurring notification attachment",
assigned_by="Administrator",
)
).insert()
doc = make_auto_repeat(reference_document=todo.name, notify=1, recipients="test@domain.com", subject="New ToDo", doc = make_auto_repeat(
message="A new ToDo has just been created for you") reference_document=todo.name,
notify=1,
recipients="test@domain.com",
subject="New ToDo",
message="A new ToDo has just been created for you",
)
data = get_auto_repeat_entries(getdate(today())) data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data) create_repeated_entries(data)
frappe.db.commit() frappe.db.commit()
new_todo = frappe.db.get_value('ToDo', new_todo = frappe.db.get_value(
{'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name') "ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name"
)
linked_comm = frappe.db.exists("Communication", dict(reference_doctype="ToDo", reference_name=new_todo)) linked_comm = frappe.db.exists(
"Communication", dict(reference_doctype="ToDo", reference_name=new_todo)
)
self.assertTrue(linked_comm) self.assertTrue(linked_comm)
def test_next_schedule_date(self): def test_next_schedule_date(self):
current_date = getdate(today()) current_date = getdate(today())
todo = frappe.get_doc( todo = frappe.get_doc(
dict(doctype='ToDo', description='test next schedule date for monthly', assigned_by='Administrator')).insert() dict(
doc = make_auto_repeat(frequency='Monthly', reference_document=todo.name, start_date=add_months(today(), -2)) doctype="ToDo", description="test next schedule date for monthly", assigned_by="Administrator"
)
).insert()
doc = make_auto_repeat(
frequency="Monthly", reference_document=todo.name, start_date=add_months(today(), -2)
)
# next_schedule_date is set as on or after current date # next_schedule_date is set as on or after current date
# it should not be a previous month's date # it should not be a previous month's date
self.assertTrue((doc.next_schedule_date >= current_date)) self.assertTrue((doc.next_schedule_date >= current_date))
todo = frappe.get_doc( todo = frappe.get_doc(
dict(doctype='ToDo', description='test next schedule date for daily', assigned_by='Administrator')).insert() dict(
doc = make_auto_repeat(frequency='Daily', reference_document=todo.name, start_date=add_days(today(), -2)) doctype="ToDo", description="test next schedule date for daily", assigned_by="Administrator"
)
).insert()
doc = make_auto_repeat(
frequency="Daily", reference_document=todo.name, start_date=add_days(today(), -2)
)
self.assertEqual(getdate(doc.next_schedule_date), current_date) self.assertEqual(getdate(doc.next_schedule_date), current_date)
def test_submit_on_creation(self): def test_submit_on_creation(self):
doctype = 'Test Submittable DocType' doctype = "Test Submittable DocType"
create_submittable_doctype(doctype) create_submittable_doctype(doctype)
current_date = getdate() current_date = getdate()
submittable_doc = frappe.get_doc(dict(doctype=doctype, test='test submit on creation')).insert() submittable_doc = frappe.get_doc(dict(doctype=doctype, test="test submit on creation")).insert()
submittable_doc.submit() submittable_doc.submit()
doc = make_auto_repeat(frequency='Daily', reference_doctype=doctype, reference_document=submittable_doc.name, doc = make_auto_repeat(
start_date=add_days(current_date, -1), submit_on_creation=1) frequency="Daily",
reference_doctype=doctype,
reference_document=submittable_doc.name,
start_date=add_days(current_date, -1),
submit_on_creation=1,
)
data = get_auto_repeat_entries(current_date) data = get_auto_repeat_entries(current_date)
create_repeated_entries(data) create_repeated_entries(data)
docnames = frappe.db.get_all(doc.reference_doctype, docnames = frappe.db.get_all(
filters={'auto_repeat': doc.name}, doc.reference_doctype, filters={"auto_repeat": doc.name}, fields=["docstatus"], limit=1
fields=['docstatus'],
limit=1
) )
self.assertEqual(docnames[0].docstatus, 1) self.assertEqual(docnames[0].docstatus, 1)
def make_auto_repeat(**args): def make_auto_repeat(**args):
args = frappe._dict(args) args = frappe._dict(args)
doc = frappe.get_doc({ doc = frappe.get_doc(
'doctype': 'Auto Repeat', {
'reference_doctype': args.reference_doctype or 'ToDo', "doctype": "Auto Repeat",
'reference_document': args.reference_document or frappe.db.get_value('ToDo', 'name'), "reference_doctype": args.reference_doctype or "ToDo",
'submit_on_creation': args.submit_on_creation or 0, "reference_document": args.reference_document or frappe.db.get_value("ToDo", "name"),
'frequency': args.frequency or 'Daily', "submit_on_creation": args.submit_on_creation or 0,
'start_date': args.start_date or add_days(today(), -1), "frequency": args.frequency or "Daily",
'end_date': args.end_date or "", "start_date": args.start_date or add_days(today(), -1),
'notify_by_email': args.notify or 0, "end_date": args.end_date or "",
'recipients': args.recipients or "", "notify_by_email": args.notify or 0,
'subject': args.subject or "", "recipients": args.recipients or "",
'message': args.message or "", "subject": args.subject or "",
'repeat_on_days': args.days or [] "message": args.message or "",
}).insert(ignore_permissions=True) "repeat_on_days": args.days or [],
}
).insert(ignore_permissions=True)
return doc return doc
def create_submittable_doctype(doctype, submit_perms=1): def create_submittable_doctype(doctype, submit_perms=1):
if frappe.db.exists('DocType', doctype): if frappe.db.exists("DocType", doctype):
return return
else: else:
doc = frappe.get_doc({ doc = frappe.get_doc(
'doctype': 'DocType', {
'__newname': doctype, "doctype": "DocType",
'module': 'Custom', "__newname": doctype,
'custom': 1, "module": "Custom",
'is_submittable': 1, "custom": 1,
'fields': [{ "is_submittable": 1,
'fieldname': 'test', "fields": [{"fieldname": "test", "label": "Test", "fieldtype": "Data"}],
'label': 'Test', "permissions": [
'fieldtype': 'Data' {
}], "role": "System Manager",
'permissions': [{ "read": 1,
'role': 'System Manager', "write": 1,
'read': 1, "create": 1,
'write': 1, "delete": 1,
'create': 1, "submit": submit_perms,
'delete': 1, "cancel": submit_perms,
'submit': submit_perms, "amend": submit_perms,
'cancel': submit_perms, }
'amend': submit_perms ],
}] }
}).insert() ).insert()
doc.allow_auto_repeat = 1 doc.allow_auto_repeat = 1
doc.save() doc.save()

View file

@ -5,5 +5,6 @@
# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document
class AutoRepeatDay(Document): class AutoRepeatDay(Document):
pass pass

View file

@ -5,8 +5,10 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
class Milestone(Document): class Milestone(Document):
pass pass
def on_doctype_update(): def on_doctype_update():
frappe.db.add_index("Milestone", ["reference_type", "reference_name"]) frappe.db.add_index("Milestone", ["reference_type", "reference_name"])

View file

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

View file

@ -3,43 +3,50 @@
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe import frappe
from frappe.model.document import Document
import frappe.cache_manager import frappe.cache_manager
from frappe.model import log_types from frappe.model import log_types
from frappe.model.document import Document
class MilestoneTracker(Document): class MilestoneTracker(Document):
def on_update(self): def on_update(self):
frappe.cache_manager.clear_doctype_map('Milestone Tracker', self.document_type) frappe.cache_manager.clear_doctype_map("Milestone Tracker", self.document_type)
def on_trash(self): def on_trash(self):
frappe.cache_manager.clear_doctype_map('Milestone Tracker', self.document_type) frappe.cache_manager.clear_doctype_map("Milestone Tracker", self.document_type)
def apply(self, doc): def apply(self, doc):
before_save = doc.get_doc_before_save() before_save = doc.get_doc_before_save()
from_value = before_save and before_save.get(self.track_field) or None from_value = before_save and before_save.get(self.track_field) or None
if from_value != doc.get(self.track_field): if from_value != doc.get(self.track_field):
frappe.get_doc(dict( frappe.get_doc(
doctype = 'Milestone', dict(
reference_type = doc.doctype, doctype="Milestone",
reference_name = doc.name, reference_type=doc.doctype,
track_field = self.track_field, reference_name=doc.name,
from_value = from_value, track_field=self.track_field,
value = doc.get(self.track_field), from_value=from_value,
milestone_tracker = self.name, value=doc.get(self.track_field),
)).insert(ignore_permissions=True) milestone_tracker=self.name,
)
).insert(ignore_permissions=True)
def evaluate_milestone(doc, event): def evaluate_milestone(doc, event):
if (frappe.flags.in_install if (
frappe.flags.in_install
or frappe.flags.in_migrate or frappe.flags.in_migrate
or frappe.flags.in_setup_wizard or frappe.flags.in_setup_wizard
or doc.doctype in log_types): or doc.doctype in log_types
):
return return
# track milestones related to this doctype # track milestones related to this doctype
for d in get_milestone_trackers(doc.doctype): for d in get_milestone_trackers(doc.doctype):
frappe.get_doc('Milestone Tracker', d.get('name')).apply(doc) frappe.get_doc("Milestone Tracker", d.get("name")).apply(doc)
def get_milestone_trackers(doctype): def get_milestone_trackers(doctype):
return frappe.cache_manager.get_doctype_map('Milestone Tracker', doctype, return frappe.cache_manager.get_doctype_map(
dict(document_type = doctype, disabled=0)) "Milestone Tracker", doctype, dict(document_type=doctype, disabled=0)
)

View file

@ -1,48 +1,48 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors # Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import unittest
import frappe import frappe
import frappe.cache_manager import frappe.cache_manager
import unittest
class TestMilestoneTracker(unittest.TestCase): class TestMilestoneTracker(unittest.TestCase):
def test_milestone(self): def test_milestone(self):
frappe.db.delete("Milestone Tracker") frappe.db.delete("Milestone Tracker")
frappe.cache().delete_key('milestone_tracker_map') frappe.cache().delete_key("milestone_tracker_map")
milestone_tracker = frappe.get_doc(dict( milestone_tracker = frappe.get_doc(
doctype = 'Milestone Tracker', dict(doctype="Milestone Tracker", document_type="ToDo", track_field="status")
document_type = 'ToDo', ).insert()
track_field = 'status'
)).insert()
todo = frappe.get_doc(dict( todo = frappe.get_doc(dict(doctype="ToDo", description="test milestone", status="Open")).insert()
doctype = 'ToDo',
description = 'test milestone',
status = 'Open'
)).insert()
milestones = frappe.get_all('Milestone', milestones = frappe.get_all(
fields = ['track_field', 'value', 'milestone_tracker'], "Milestone",
filters = dict(reference_type = todo.doctype, reference_name=todo.name)) fields=["track_field", "value", "milestone_tracker"],
filters=dict(reference_type=todo.doctype, reference_name=todo.name),
)
self.assertEqual(len(milestones), 1) self.assertEqual(len(milestones), 1)
self.assertEqual(milestones[0].track_field, 'status') self.assertEqual(milestones[0].track_field, "status")
self.assertEqual(milestones[0].value, 'Open') self.assertEqual(milestones[0].value, "Open")
todo.status = 'Closed' todo.status = "Closed"
todo.save() todo.save()
milestones = frappe.get_all('Milestone', milestones = frappe.get_all(
fields = ['track_field', 'value', 'milestone_tracker'], "Milestone",
filters = dict(reference_type = todo.doctype, reference_name=todo.name), fields=["track_field", "value", "milestone_tracker"],
order_by = 'modified desc') filters=dict(reference_type=todo.doctype, reference_name=todo.name),
order_by="modified desc",
)
self.assertEqual(len(milestones), 2) self.assertEqual(len(milestones), 2)
self.assertEqual(milestones[0].track_field, 'status') self.assertEqual(milestones[0].track_field, "status")
self.assertEqual(milestones[0].value, 'Closed') self.assertEqual(milestones[0].value, "Closed")
# cleanup # cleanup
frappe.db.delete("Milestone") frappe.db.delete("Milestone")
milestone_tracker.delete() milestone_tracker.delete()

View file

@ -7,20 +7,22 @@ bootstrap client session
import frappe import frappe
import frappe.defaults import frappe.defaults
import frappe.desk.desk_page import frappe.desk.desk_page
from frappe.core.doctype.navbar_settings.navbar_settings import get_app_logo, get_navbar_settings
from frappe.desk.doctype.route_history.route_history import frequently_visited_links from frappe.desk.doctype.route_history.route_history import frequently_visited_links
from frappe.desk.form.load import get_meta_bundle from frappe.desk.form.load import get_meta_bundle
from frappe.utils.change_log import get_versions
from frappe.translate import get_lang_dict
from frappe.email.inbox import get_email_accounts from frappe.email.inbox import get_email_accounts
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
from frappe.model.base_document import get_controller from frappe.model.base_document import get_controller
from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings, get_app_logo
from frappe.utils import get_time_zone, add_user_info
from frappe.query_builder import DocType from frappe.query_builder import DocType
from frappe.query_builder.functions import Count from frappe.query_builder.functions import Count
from frappe.query_builder.terms import subqry from frappe.query_builder.terms import subqry
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
from frappe.social.doctype.energy_point_settings.energy_point_settings import (
is_energy_point_enabled,
)
from frappe.translate import get_lang_dict
from frappe.utils import add_user_info, get_time_zone
from frappe.utils.change_log import get_versions
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled
def get_bootinfo(): def get_bootinfo():
@ -38,9 +40,9 @@ def get_bootinfo():
bootinfo.sysdefaults = frappe.defaults.get_defaults() bootinfo.sysdefaults = frappe.defaults.get_defaults()
bootinfo.server_date = frappe.utils.nowdate() bootinfo.server_date = frappe.utils.nowdate()
if frappe.session['user'] != 'Guest': if frappe.session["user"] != "Guest":
bootinfo.user_info = get_user_info() bootinfo.user_info = get_user_info()
bootinfo.sid = frappe.session['sid'] bootinfo.sid = frappe.session["sid"]
bootinfo.modules = {} bootinfo.modules = {}
bootinfo.module_list = [] bootinfo.module_list = []
@ -51,8 +53,10 @@ def get_bootinfo():
add_layouts(bootinfo) add_layouts(bootinfo)
bootinfo.module_app = frappe.local.module_app bootinfo.module_app = frappe.local.module_app
bootinfo.single_types = [d.name for d in frappe.get_all('DocType', {'issingle': 1})] bootinfo.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.nested_set_doctypes = [
d.parent for d in frappe.get_all("DocField", {"fieldname": "lft"}, ["parent"])
]
add_home_page(bootinfo, doclist) add_home_page(bootinfo, doclist)
bootinfo.page_info = get_allowed_pages() bootinfo.page_info = get_allowed_pages()
load_translations(bootinfo) load_translations(bootinfo)
@ -66,8 +70,8 @@ def get_bootinfo():
set_time_zone(bootinfo) set_time_zone(bootinfo)
# ipinfo # ipinfo
if frappe.session.data.get('ipinfo'): if frappe.session.data.get("ipinfo"):
bootinfo.ipinfo = frappe.session['data']['ipinfo'] bootinfo.ipinfo = frappe.session["data"]["ipinfo"]
# add docs # add docs
bootinfo.docs = doclist bootinfo.docs = doclist
@ -77,7 +81,7 @@ def get_bootinfo():
if bootinfo.lang: if bootinfo.lang:
bootinfo.lang = str(bootinfo.lang) bootinfo.lang = str(bootinfo.lang)
bootinfo.versions = {k: v['version'] for k, v in get_versions().items()} bootinfo.versions = {k: v["version"] for k, v in get_versions().items()}
bootinfo.error_report_email = frappe.conf.error_report_email bootinfo.error_report_email = frappe.conf.error_report_email
bootinfo.calendars = sorted(frappe.get_hooks("calendars")) bootinfo.calendars = sorted(frappe.get_hooks("calendars"))
@ -97,37 +101,47 @@ def get_bootinfo():
return bootinfo return bootinfo
def get_letter_heads(): def get_letter_heads():
letter_heads = {} letter_heads = {}
for letter_head in frappe.get_all("Letter Head", fields = ["name", "content", "footer"]): for letter_head in frappe.get_all("Letter Head", fields=["name", "content", "footer"]):
letter_heads.setdefault(letter_head.name, letter_heads.setdefault(
{'header': letter_head.content, 'footer': letter_head.footer}) letter_head.name, {"header": letter_head.content, "footer": letter_head.footer}
)
return letter_heads return letter_heads
def load_conf_settings(bootinfo): def load_conf_settings(bootinfo):
from frappe import conf from frappe import conf
bootinfo.max_file_size = conf.get('max_file_size') or 10485760
for key in ('developer_mode', 'socketio_port', 'file_watcher_port'): bootinfo.max_file_size = conf.get("max_file_size") or 10485760
if key in conf: bootinfo[key] = conf.get(key) for key in ("developer_mode", "socketio_port", "file_watcher_port"):
if key in conf:
bootinfo[key] = conf.get(key)
def load_desktop_data(bootinfo): def load_desktop_data(bootinfo):
from frappe.desk.desktop import get_workspace_sidebar_items from frappe.desk.desktop import get_workspace_sidebar_items
bootinfo.allowed_workspaces = get_workspace_sidebar_items().get('pages')
bootinfo.allowed_workspaces = get_workspace_sidebar_items().get("pages")
bootinfo.module_page_map = get_controller("Workspace").get_module_page_map() bootinfo.module_page_map = get_controller("Workspace").get_module_page_map()
bootinfo.dashboards = frappe.get_all("Dashboard") bootinfo.dashboards = frappe.get_all("Dashboard")
def get_allowed_pages(cache=False): def get_allowed_pages(cache=False):
return get_user_pages_or_reports('Page', cache=cache) return get_user_pages_or_reports("Page", cache=cache)
def get_allowed_reports(cache=False): def get_allowed_reports(cache=False):
return get_user_pages_or_reports('Report', cache=cache) return get_user_pages_or_reports("Report", cache=cache)
def get_user_pages_or_reports(parent, cache=False): def get_user_pages_or_reports(parent, cache=False):
_cache = frappe.cache() _cache = frappe.cache()
if cache: if cache:
has_role = _cache.get_value('has_role:' + parent, user=frappe.session.user) has_role = _cache.get_value("has_role:" + parent, user=frappe.session.user)
if has_role: if has_role:
return has_role return has_role
@ -140,8 +154,7 @@ def get_user_pages_or_reports(parent, cache=False):
if parent == "Report": if parent == "Report":
columns = (report.name.as_("title"), report.ref_doctype, report.report_type) columns = (report.name.as_("title"), report.ref_doctype, report.report_type)
else: else:
columns = (page.title.as_("title"), ) columns = (page.title.as_("title"),)
customRole = DocType("Custom Role") customRole = DocType("Custom Role")
hasRole = DocType("Has Role") hasRole = DocType("Has Role")
@ -149,31 +162,39 @@ def get_user_pages_or_reports(parent, cache=False):
# get pages or reports set on custom role # get pages or reports set on custom role
pages_with_custom_roles = ( pages_with_custom_roles = (
frappe.qb.from_(customRole).from_(hasRole).from_(parentTable) frappe.qb.from_(customRole)
.select(customRole[parent.lower()].as_("name"), customRole.modified, customRole.ref_doctype, *columns) .from_(hasRole)
.from_(parentTable)
.select(
customRole[parent.lower()].as_("name"), customRole.modified, customRole.ref_doctype, *columns
)
.where( .where(
(hasRole.parent == customRole.name) (hasRole.parent == customRole.name)
& (parentTable.name == customRole[parent.lower()]) & (parentTable.name == customRole[parent.lower()])
& (customRole[parent.lower()].isnotnull()) & (customRole[parent.lower()].isnotnull())
& (hasRole.role.isin(roles))) & (hasRole.role.isin(roles))
)
).run(as_dict=True) ).run(as_dict=True)
for p in pages_with_custom_roles: for p in pages_with_custom_roles:
has_role[p.name] = {"modified":p.modified, "title": p.title, "ref_doctype": p.ref_doctype} has_role[p.name] = {"modified": p.modified, "title": p.title, "ref_doctype": p.ref_doctype}
subq = ( subq = (
frappe.qb.from_(customRole).select(customRole[parent.lower()]) frappe.qb.from_(customRole)
.select(customRole[parent.lower()])
.where(customRole[parent.lower()].isnotnull()) .where(customRole[parent.lower()].isnotnull())
) )
pages_with_standard_roles = ( pages_with_standard_roles = (
frappe.qb.from_(hasRole).from_(parentTable) frappe.qb.from_(hasRole)
.from_(parentTable)
.select(parentTable.name.as_("name"), parentTable.modified, *columns) .select(parentTable.name.as_("name"), parentTable.modified, *columns)
.where( .where(
(hasRole.role.isin(roles)) (hasRole.role.isin(roles))
& (hasRole.parent == parentTable.name) & (hasRole.parent == parentTable.name)
& (parentTable.name.notin(subq)) & (parentTable.name.notin(subq))
).distinct() )
.distinct()
) )
if parent == "Report": if parent == "Report":
@ -183,18 +204,20 @@ def get_user_pages_or_reports(parent, cache=False):
for p in pages_with_standard_roles: for p in pages_with_standard_roles:
if p.name not in has_role: if p.name not in has_role:
has_role[p.name] = {"modified":p.modified, "title": p.title} has_role[p.name] = {"modified": p.modified, "title": p.title}
if parent == "Report": if parent == "Report":
has_role[p.name].update({'ref_doctype': p.ref_doctype}) has_role[p.name].update({"ref_doctype": p.ref_doctype})
no_of_roles = (frappe.qb.from_(hasRole).select(Count("*")) no_of_roles = (
.where(hasRole.parent == parentTable.name) frappe.qb.from_(hasRole).select(Count("*")).where(hasRole.parent == parentTable.name)
) )
# pages with no role are allowed # pages with no role are allowed
if parent =="Page": if parent == "Page":
pages_with_no_roles = (frappe.qb.from_(parentTable).select(parentTable.name, parentTable.modified, *columns) pages_with_no_roles = (
frappe.qb.from_(parentTable)
.select(parentTable.name, parentTable.modified, *columns)
.where(subqry(no_of_roles) == 0) .where(subqry(no_of_roles) == 0)
).run(as_dict=True) ).run(as_dict=True)
@ -203,18 +226,20 @@ def get_user_pages_or_reports(parent, cache=False):
has_role[p.name] = {"modified": p.modified, "title": p.title} has_role[p.name] = {"modified": p.modified, "title": p.title}
elif parent == "Report": elif parent == "Report":
reports = frappe.get_all("Report", reports = frappe.get_all(
"Report",
fields=["name", "report_type"], fields=["name", "report_type"],
filters={"name": ("in", has_role.keys())}, filters={"name": ("in", has_role.keys())},
ignore_ifnull=True ignore_ifnull=True,
) )
for report in reports: for report in reports:
has_role[report.name]["report_type"] = report.report_type has_role[report.name]["report_type"] = report.report_type
# Expire every six hours # Expire every six hours
_cache.set_value('has_role:' + parent, has_role, frappe.session.user, 21600) _cache.set_value("has_role:" + parent, has_role, frappe.session.user, 21600)
return has_role return has_role
def load_translations(bootinfo): def load_translations(bootinfo):
messages = frappe.get_lang_dict("boot") messages = frappe.get_lang_dict("boot")
@ -225,27 +250,30 @@ def load_translations(bootinfo):
messages[name] = frappe._(name) messages[name] = frappe._(name)
# only untranslated # only untranslated
messages = {k: v for k, v in messages.items() if k!=v} messages = {k: v for k, v in messages.items() if k != v}
bootinfo["__messages"] = messages bootinfo["__messages"] = messages
def get_user_info(): def get_user_info():
# get info for current user # get info for current user
user_info = frappe._dict() user_info = frappe._dict()
add_user_info(frappe.session.user, user_info) add_user_info(frappe.session.user, user_info)
if frappe.session.user == 'Administrator' and user_info.Administrator.email: if frappe.session.user == "Administrator" and user_info.Administrator.email:
user_info[user_info.Administrator.email] = user_info.Administrator user_info[user_info.Administrator.email] = user_info.Administrator
return user_info return user_info
def get_user(bootinfo): def get_user(bootinfo):
"""get user info""" """get user info"""
bootinfo.user = frappe.get_user().load_user() bootinfo.user = frappe.get_user().load_user()
def add_home_page(bootinfo, docs): def add_home_page(bootinfo, docs):
"""load home page""" """load home page"""
if frappe.session.user=="Guest": if frappe.session.user == "Guest":
return return
home_page = frappe.db.get_default("desktop:home_page") home_page = frappe.db.get_default("desktop:home_page")
@ -255,50 +283,65 @@ def add_home_page(bootinfo, docs):
try: try:
page = frappe.desk.desk_page.get(home_page) page = frappe.desk.desk_page.get(home_page)
docs.append(page) docs.append(page)
bootinfo['home_page'] = page.name bootinfo["home_page"] = page.name
except (frappe.DoesNotExistError, frappe.PermissionError): except (frappe.DoesNotExistError, frappe.PermissionError):
if frappe.message_log: if frappe.message_log:
frappe.message_log.pop() frappe.message_log.pop()
bootinfo['home_page'] = 'Workspaces' bootinfo["home_page"] = "Workspaces"
def add_timezone_info(bootinfo): def add_timezone_info(bootinfo):
system = bootinfo.sysdefaults.get("time_zone") system = bootinfo.sysdefaults.get("time_zone")
import frappe.utils.momentjs import frappe.utils.momentjs
bootinfo.timezone_info = {"zones":{}, "rules":{}, "links":{}}
bootinfo.timezone_info = {"zones": {}, "rules": {}, "links": {}}
frappe.utils.momentjs.update(system, bootinfo.timezone_info) frappe.utils.momentjs.update(system, bootinfo.timezone_info)
def load_print(bootinfo, doclist): def load_print(bootinfo, doclist):
print_settings = frappe.db.get_singles_dict("Print Settings") print_settings = frappe.db.get_singles_dict("Print Settings")
print_settings.doctype = ":Print Settings" print_settings.doctype = ":Print Settings"
doclist.append(print_settings) doclist.append(print_settings)
load_print_css(bootinfo, print_settings) load_print_css(bootinfo, print_settings)
def load_print_css(bootinfo, print_settings): def load_print_css(bootinfo, print_settings):
import frappe.www.printview import frappe.www.printview
bootinfo.print_css = frappe.www.printview.get_print_style(print_settings.print_style or "Redesign", for_legacy=True)
bootinfo.print_css = frappe.www.printview.get_print_style(
print_settings.print_style or "Redesign", for_legacy=True
)
def get_unseen_notes(): def get_unseen_notes():
note = DocType("Note") note = DocType("Note")
nsb = DocType("Note Seen By").as_("nsb") nsb = DocType("Note Seen By").as_("nsb")
return ( return (
frappe.qb.from_(note).select(note.name, note.title, note.content, note.notify_on_every_login) frappe.qb.from_(note)
.select(note.name, note.title, note.content, note.notify_on_every_login)
.where( .where(
(note.notify_on_every_login == 1) (note.notify_on_every_login == 1)
& (note.expire_notification_on > frappe.utils.now()) & (note.expire_notification_on > frappe.utils.now())
& (subqry(frappe.qb.from_(nsb).select(nsb.user).where(nsb.parent == note.name)).notin([frappe.session.user]))) & (
).run(as_dict=1) subqry(frappe.qb.from_(nsb).select(nsb.user).where(nsb.parent == note.name)).notin(
[frappe.session.user]
)
)
)
).run(as_dict=1)
def get_success_action(): def get_success_action():
return frappe.get_all("Success Action", fields=["*"]) return frappe.get_all("Success Action", fields=["*"])
def get_link_preview_doctypes(): def get_link_preview_doctypes():
from frappe.utils import cint from frappe.utils import cint
link_preview_doctypes = [d.name for d in frappe.db.get_all('DocType', {'show_preview_popup': 1})] link_preview_doctypes = [d.name for d in frappe.db.get_all("DocType", {"show_preview_popup": 1})]
customizations = frappe.get_all("Property Setter", customizations = frappe.get_all(
fields=['doc_type', 'value'], "Property Setter", fields=["doc_type", "value"], filters={"property": "show_preview_popup"}
filters={'property': 'show_preview_popup'}
) )
for custom in customizations: for custom in customizations:
@ -309,22 +352,23 @@ def get_link_preview_doctypes():
return link_preview_doctypes return link_preview_doctypes
def get_additional_filters_from_hooks(): def get_additional_filters_from_hooks():
filter_config = frappe._dict() filter_config = frappe._dict()
filter_hooks = frappe.get_hooks('filters_config') filter_hooks = frappe.get_hooks("filters_config")
for hook in filter_hooks: for hook in filter_hooks:
filter_config.update(frappe.get_attr(hook)()) filter_config.update(frappe.get_attr(hook)())
return filter_config return filter_config
def add_layouts(bootinfo): def add_layouts(bootinfo):
# add routes for readable doctypes # add routes for readable doctypes
bootinfo.doctype_layouts = frappe.get_all('DocType Layout', ['name', 'route', 'document_type']) bootinfo.doctype_layouts = frappe.get_all("DocType Layout", ["name", "route", "document_type"])
def get_desk_settings(): def get_desk_settings():
role_list = frappe.get_all('Role', fields=['*'], filters=dict( role_list = frappe.get_all("Role", fields=["*"], filters=dict(name=["in", frappe.get_roles()]))
name=['in', frappe.get_roles()]
))
desk_settings = {} desk_settings = {}
from frappe.core.doctype.role.role import desk_properties from frappe.core.doctype.role.role import desk_properties
@ -335,8 +379,10 @@ def get_desk_settings():
return desk_settings return desk_settings
def get_notification_settings(): def get_notification_settings():
return frappe.get_cached_doc('Notification Settings', frappe.session.user) return frappe.get_cached_doc("Notification Settings", frappe.session.user)
@frappe.whitelist() @frappe.whitelist()
def get_link_title_doctypes(): def get_link_title_doctypes():
@ -348,8 +394,10 @@ def get_link_title_doctypes():
) )
return [d.name for d in dts + custom_dts if d] return [d.name for d in dts + custom_dts if d]
def set_time_zone(bootinfo): def set_time_zone(bootinfo):
bootinfo.time_zone = { bootinfo.time_zone = {
"system": get_time_zone(), "system": get_time_zone(),
"user": bootinfo.get("user_info", {}).get(frappe.session.user, {}).get("time_zone", None) or get_time_zone() "user": bootinfo.get("user_info", {}).get(frappe.session.user, {}).get("time_zone", None)
or get_time_zone(),
} }

View file

@ -1,8 +1,8 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import os import os
import shutil
import re import re
import shutil
import subprocess import subprocess
from distutils.spawn import find_executable from distutils.spawn import find_executable
from subprocess import getoutput from subprocess import getoutput
@ -25,6 +25,7 @@ sites_path = os.path.abspath(os.getcwd())
class AssetsNotDownloadedError(Exception): class AssetsNotDownloadedError(Exception):
pass pass
class AssetsDontExistError(HTTPError): class AssetsDontExistError(HTTPError):
pass pass
@ -43,7 +44,7 @@ def download_file(url, prefix):
def build_missing_files(): def build_missing_files():
'''Check which files dont exist yet from the assets.json and run build for those files''' """Check which files dont exist yet from the assets.json and run build for those files"""
missing_assets = [] missing_assets = []
current_asset_files = [] current_asset_files = []
@ -60,7 +61,7 @@ def build_missing_files():
assets_json = frappe.parse_json(assets_json) assets_json = frappe.parse_json(assets_json)
for bundle_file, output_file in assets_json.items(): for bundle_file, output_file in assets_json.items():
if not output_file.startswith('/assets/frappe'): if not output_file.startswith("/assets/frappe"):
continue continue
if os.path.basename(output_file) not in current_asset_files: if os.path.basename(output_file) not in current_asset_files:
@ -78,8 +79,7 @@ def build_missing_files():
def get_assets_link(frappe_head) -> str: def get_assets_link(frappe_head) -> str:
tag = getoutput( tag = getoutput(
r"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*" r"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*"
r" refs/tags/,,' -e 's/\^{}//'" r" refs/tags/,,' -e 's/\^{}//'" % frappe_head
% frappe_head
) )
if tag: if tag:
@ -111,6 +111,7 @@ def fetch_assets(url, frappe_head):
def setup_assets(assets_archive): def setup_assets(assets_archive):
import tarfile import tarfile
directories_created = set() directories_created = set()
click.secho("\nExtracting assets...\n", fg="yellow") click.secho("\nExtracting assets...\n", fg="yellow")
@ -221,7 +222,16 @@ def setup():
assets_path = os.path.join(frappe.local.sites_path, "assets") assets_path = os.path.join(frappe.local.sites_path, "assets")
def bundle(mode, apps=None, hard_link=False, make_copy=False, restore=False, verbose=False, skip_frappe=False, files=None): def bundle(
mode,
apps=None,
hard_link=False,
make_copy=False,
restore=False,
verbose=False,
skip_frappe=False,
files=None,
):
"""concat / minify js files""" """concat / minify js files"""
setup() setup()
make_asset_dirs(hard_link=hard_link) make_asset_dirs(hard_link=hard_link)
@ -236,7 +246,7 @@ def bundle(mode, apps=None, hard_link=False, make_copy=False, restore=False, ver
command += " --skip_frappe" command += " --skip_frappe"
if files: if files:
command += " --files {files}".format(files=','.join(files)) command += " --files {files}".format(files=",".join(files))
command += " --run-build-command" command += " --run-build-command"
@ -253,9 +263,7 @@ def watch(apps=None):
if apps: if apps:
command += " --apps {apps}".format(apps=apps) command += " --apps {apps}".format(apps=apps)
live_reload = frappe.utils.cint( live_reload = frappe.utils.cint(os.environ.get("LIVE_RELOAD", frappe.conf.live_reload))
os.environ.get("LIVE_RELOAD", frappe.conf.live_reload)
)
if live_reload: if live_reload:
command += " --live-reload" command += " --live-reload"
@ -266,8 +274,8 @@ def watch(apps=None):
def check_node_executable(): def check_node_executable():
node_version = Version(subprocess.getoutput('node -v')[1:]) node_version = Version(subprocess.getoutput("node -v")[1:])
warn = '⚠️ ' warn = "⚠️ "
if node_version.major < 14: if node_version.major < 14:
click.echo(f"{warn} Please update your node version to 14") click.echo(f"{warn} Please update your node version to 14")
if not find_executable("yarn"): if not find_executable("yarn"):
@ -276,9 +284,7 @@ def check_node_executable():
def get_node_env(): def get_node_env():
node_env = { node_env = {"NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}"}
"NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}"
}
return node_env return node_env
@ -345,8 +351,7 @@ def clear_broken_symlinks():
def unstrip(message: str) -> str: def unstrip(message: str) -> str:
"""Pads input string on the right side until the last available column in the terminal """Pads input string on the right side until the last available column in the terminal"""
"""
_len = len(message) _len = len(message)
try: try:
max_str = os.get_terminal_size().columns max_str = os.get_terminal_size().columns
@ -367,7 +372,9 @@ def make_asset_dirs(hard_link=False):
symlinks = generate_assets_map() symlinks = generate_assets_map()
for source, target in symlinks.items(): for source, target in symlinks.items():
start_message = unstrip(f"{'Copying assets from' if hard_link else 'Linking'} {source} to {target}") start_message = unstrip(
f"{'Copying assets from' if hard_link else 'Linking'} {source} to {target}"
)
fail_message = unstrip(f"Cannot {'copy' if hard_link else 'link'} {source} to {target}") fail_message = unstrip(f"Cannot {'copy' if hard_link else 'link'} {source} to {target}")
# Used '\r' instead of '\x1b[1K\r' to print entire lines in smaller terminal sizes # Used '\r' instead of '\x1b[1K\r' to print entire lines in smaller terminal sizes
@ -404,10 +411,11 @@ def scrub_html_template(content):
# strip comments # strip comments
content = re.sub(r"(<!--.*?-->)", "", content) content = re.sub(r"(<!--.*?-->)", "", content)
return content.replace("'", "\'") return content.replace("'", "'")
def html_to_js_template(path, content): def html_to_js_template(path, content):
"""returns HTML template content as Javascript code, adding it to `frappe.templates`""" """returns HTML template content as Javascript code, adding it to `frappe.templates`"""
return """frappe.templates["{key}"] = '{content}';\n""".format( return """frappe.templates["{key}"] = '{content}';\n""".format(
key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content)) key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content)
)

View file

@ -1,33 +1,75 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe, json import json
import frappe
from frappe.desk.notifications import clear_notifications, delete_notification_count_for
from frappe.model.document import Document from frappe.model.document import Document
from frappe.desk.notifications import (delete_notification_count_for,
clear_notifications)
common_default_keys = ["__default", "__global"] common_default_keys = ["__default", "__global"]
doctype_map_keys = ('energy_point_rule_map', 'assignment_rule_map', doctype_map_keys = (
'milestone_tracker_map', 'event_consumer_document_type_map') "energy_point_rule_map",
"assignment_rule_map",
"milestone_tracker_map",
"event_consumer_document_type_map",
)
bench_cache_keys = ('assets_json',) bench_cache_keys = ("assets_json",)
global_cache_keys = ("app_hooks", "installed_apps", 'all_apps', global_cache_keys = (
"app_modules", "module_app", "system_settings", "app_hooks",
'scheduler_events', 'time_zone', 'webhooks', 'active_domains', "installed_apps",
'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version', "all_apps",
'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts', "app_modules",
'sitemap_routes', 'db_tables', 'server_script_autocompletion_items') + doctype_map_keys "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", user_cache_keys = (
"defaults", "user_permissions", "home_page", "linked_with", "bootinfo",
"desktop_icons", 'portal_menu_items', 'user_perm_can_read', "user_recent",
"has_role:Page", "has_role:Report", "desk_sidebar_items") "roles",
"user_doc",
"lang",
"defaults",
"user_permissions",
"home_page",
"linked_with",
"desktop_icons",
"portal_menu_items",
"user_perm_can_read",
"has_role:Page",
"has_role:Report",
"desk_sidebar_items",
)
doctype_cache_keys = (
"meta",
"form_meta",
"table_columns",
"last_modified",
"linked_doctypes",
"notifications",
"workflow",
"data_import_column_header_map",
) + doctype_map_keys
doctype_cache_keys = ("meta", "form_meta", "table_columns", "last_modified",
"linked_doctypes", 'notifications', 'workflow' ,
'data_import_column_header_map') + doctype_map_keys
def clear_user_cache(user=None): def clear_user_cache(user=None):
cache = frappe.cache() cache = frappe.cache()
@ -47,11 +89,13 @@ def clear_user_cache(user=None):
clear_defaults_cache() clear_defaults_cache()
clear_global_cache() clear_global_cache()
def clear_domain_cache(user=None): def clear_domain_cache(user=None):
cache = frappe.cache() cache = frappe.cache()
domain_cache_keys = ('domain_restricted_doctypes', 'domain_restricted_pages') domain_cache_keys = ("domain_restricted_doctypes", "domain_restricted_pages")
cache.delete_value(domain_cache_keys) cache.delete_value(domain_cache_keys)
def clear_global_cache(): def clear_global_cache():
from frappe.website.utils import clear_website_cache from frappe.website.utils import clear_website_cache
@ -61,21 +105,23 @@ def clear_global_cache():
frappe.cache().delete_value(bench_cache_keys) frappe.cache().delete_value(bench_cache_keys)
frappe.setup_module_map() frappe.setup_module_map()
def clear_defaults_cache(user=None): def clear_defaults_cache(user=None):
if user: if user:
for p in ([user] + common_default_keys): for p in [user] + common_default_keys:
frappe.cache().hdel("defaults", p) frappe.cache().hdel("defaults", p)
elif frappe.flags.in_install!="frappe": elif frappe.flags.in_install != "frappe":
frappe.cache().delete_key("defaults") frappe.cache().delete_key("defaults")
def clear_doctype_cache(doctype=None): def clear_doctype_cache(doctype=None):
clear_controller_cache(doctype) clear_controller_cache(doctype)
cache = frappe.cache() cache = frappe.cache()
if getattr(frappe.local, 'meta_cache') and (doctype in frappe.local.meta_cache): if getattr(frappe.local, "meta_cache") and (doctype in frappe.local.meta_cache):
del frappe.local.meta_cache[doctype] del frappe.local.meta_cache[doctype]
for key in ('is_table', 'doctype_modules', 'document_cache'): for key in ("is_table", "doctype_modules", "document_cache"):
cache.delete_value(key) cache.delete_value(key)
frappe.local.document_cache = {} frappe.local.document_cache = {}
@ -89,8 +135,9 @@ def clear_doctype_cache(doctype=None):
# clear all parent doctypes # clear all parent doctypes
for dt in frappe.db.get_all('DocField', 'parent', for dt in frappe.db.get_all(
dict(fieldtype=['in', frappe.model.table_fields], options=doctype)): "DocField", "parent", dict(fieldtype=["in", frappe.model.table_fields], options=doctype)
):
clear_single(dt.parent) clear_single(dt.parent)
# clear all notifications # clear all notifications
@ -101,6 +148,7 @@ def clear_doctype_cache(doctype=None):
for name in doctype_cache_keys: for name in doctype_cache_keys:
cache.delete_value(name) cache.delete_value(name)
def clear_controller_cache(doctype=None): def clear_controller_cache(doctype=None):
if not doctype: if not doctype:
del frappe.controllers del frappe.controllers
@ -110,9 +158,10 @@ def clear_controller_cache(doctype=None):
for site_controllers in frappe.controllers.values(): for site_controllers in frappe.controllers.values():
site_controllers.pop(doctype, None) site_controllers.pop(doctype, None)
def get_doctype_map(doctype, name, filters=None, order_by=None): def get_doctype_map(doctype, name, filters=None, order_by=None):
cache = frappe.cache() cache = frappe.cache()
cache_key = frappe.scrub(doctype) + '_map' cache_key = frappe.scrub(doctype) + "_map"
doctype_map = cache.hget(cache_key, name) doctype_map = cache.hget(cache_key, name)
if doctype_map is not None: if doctype_map is not None:
@ -121,7 +170,7 @@ def get_doctype_map(doctype, name, filters=None, order_by=None):
else: else:
# non cached, build cache # non cached, build cache
try: try:
items = frappe.get_all(doctype, filters=filters, order_by = order_by) items = frappe.get_all(doctype, filters=filters, order_by=order_by)
cache.hset(cache_key, name, json.dumps(items)) cache.hset(cache_key, name, json.dumps(items))
except frappe.db.TableMissingError: except frappe.db.TableMissingError:
# executed from inside patch, ignore # executed from inside patch, ignore
@ -129,15 +178,19 @@ def get_doctype_map(doctype, name, filters=None, order_by=None):
return items return items
def clear_doctype_map(doctype, name): def clear_doctype_map(doctype, name):
frappe.cache().hdel(frappe.scrub(doctype) + '_map', name) frappe.cache().hdel(frappe.scrub(doctype) + "_map", name)
def build_table_count_cache(): def build_table_count_cache():
if (frappe.flags.in_patch if (
frappe.flags.in_patch
or frappe.flags.in_install or frappe.flags.in_install
or frappe.flags.in_migrate or frappe.flags.in_migrate
or frappe.flags.in_import or frappe.flags.in_import
or frappe.flags.in_setup_wizard): or frappe.flags.in_setup_wizard
):
return return
_cache = frappe.cache() _cache = frappe.cache()
@ -145,39 +198,45 @@ def build_table_count_cache():
table_rows = frappe.qb.Field("table_rows").as_("count") table_rows = frappe.qb.Field("table_rows").as_("count")
information_schema = frappe.qb.Schema("information_schema") information_schema = frappe.qb.Schema("information_schema")
data = ( data = (frappe.qb.from_(information_schema.tables).select(table_name, table_rows)).run(
frappe.qb.from_(information_schema.tables).select(table_name, table_rows) as_dict=True
).run(as_dict=True) )
counts = {d.get('name').replace('tab', '', 1): d.get('count', None) for d in data} counts = {d.get("name").replace("tab", "", 1): d.get("count", None) for d in data}
_cache.set_value("information_schema:counts", counts) _cache.set_value("information_schema:counts", counts)
return counts return counts
def build_domain_restriced_doctype_cache(*args, **kwargs): def build_domain_restriced_doctype_cache(*args, **kwargs):
if (frappe.flags.in_patch if (
frappe.flags.in_patch
or frappe.flags.in_install or frappe.flags.in_install
or frappe.flags.in_migrate or frappe.flags.in_migrate
or frappe.flags.in_import or frappe.flags.in_import
or frappe.flags.in_setup_wizard): or frappe.flags.in_setup_wizard
):
return return
_cache = frappe.cache() _cache = frappe.cache()
active_domains = frappe.get_active_domains() active_domains = frappe.get_active_domains()
doctypes = frappe.get_all("DocType", filters={'restrict_to_domain': ('IN', active_domains)}) doctypes = frappe.get_all("DocType", filters={"restrict_to_domain": ("IN", active_domains)})
doctypes = [doc.name for doc in doctypes] doctypes = [doc.name for doc in doctypes]
_cache.set_value("domain_restricted_doctypes", doctypes) _cache.set_value("domain_restricted_doctypes", doctypes)
return doctypes return doctypes
def build_domain_restriced_page_cache(*args, **kwargs): def build_domain_restriced_page_cache(*args, **kwargs):
if (frappe.flags.in_patch if (
frappe.flags.in_patch
or frappe.flags.in_install or frappe.flags.in_install
or frappe.flags.in_migrate or frappe.flags.in_migrate
or frappe.flags.in_import or frappe.flags.in_import
or frappe.flags.in_setup_wizard): or frappe.flags.in_setup_wizard
):
return return
_cache = frappe.cache() _cache = frappe.cache()
active_domains = frappe.get_active_domains() active_domains = frappe.get_active_domains()
pages = frappe.get_all("Page", filters={'restrict_to_domain': ('IN', active_domains)}) pages = frappe.get_all("Page", filters={"restrict_to_domain": ("IN", active_domains)})
pages = [page.name for page in pages] pages = [page.name for page in pages]
_cache.set_value("domain_restricted_pages", pages) _cache.set_value("domain_restricted_pages", pages)

View file

@ -1,32 +1,44 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import json
import os
import frappe import frappe
from frappe import _
import frappe.model import frappe.model
import frappe.utils import frappe.utils
import json, os from frappe import _
from frappe.utils import get_safe_filters
from frappe.desk.reportview import validate_args from frappe.desk.reportview import validate_args
from frappe.model.db_query import check_parent_permission from frappe.model.db_query import check_parent_permission
from frappe.utils import get_safe_filters
"""
'''
Handle RESTful requests that are mapped to the `/api/resource` route. Handle RESTful requests that are mapped to the `/api/resource` route.
Requests via FrappeClient are also handled here. Requests via FrappeClient are also handled here.
''' """
@frappe.whitelist() @frappe.whitelist()
def get_list(doctype, fields=None, filters=None, order_by=None, def get_list(
limit_start=None, limit_page_length=20, parent=None, debug=False, as_dict=True, or_filters=None): doctype,
'''Returns a list of records by filters, fields, ordering and limit fields=None,
filters=None,
order_by=None,
limit_start=None,
limit_page_length=20,
parent=None,
debug=False,
as_dict=True,
or_filters=None,
):
"""Returns a list of records by filters, fields, ordering and limit
:param doctype: DocType of the data to be queried :param doctype: DocType of the data to be queried
:param fields: fields to be returned. Default is `name` :param fields: fields to be returned. Default is `name`
:param filters: filter list by this dict :param filters: filter list by this dict
:param order_by: Order by this fieldname :param order_by: Order by this fieldname
:param limit_start: Start at this index :param limit_start: Start at this index
:param limit_page_length: Number of records to be returned (default 20)''' :param limit_page_length: Number of records to be returned (default 20)"""
if frappe.is_table(doctype): if frappe.is_table(doctype):
check_parent_permission(parent, doctype) check_parent_permission(parent, doctype)
@ -40,23 +52,25 @@ def get_list(doctype, fields=None, filters=None, order_by=None,
limit_start=limit_start, limit_start=limit_start,
limit_page_length=limit_page_length, limit_page_length=limit_page_length,
debug=debug, debug=debug,
as_list=not as_dict as_list=not as_dict,
) )
validate_args(args) validate_args(args)
return frappe.get_list(**args) return frappe.get_list(**args)
@frappe.whitelist() @frappe.whitelist()
def get_count(doctype, filters=None, debug=False, cache=False): def get_count(doctype, filters=None, debug=False, cache=False):
return frappe.db.count(doctype, get_safe_filters(filters), debug, cache) return frappe.db.count(doctype, get_safe_filters(filters), debug, cache)
@frappe.whitelist() @frappe.whitelist()
def get(doctype, name=None, filters=None, parent=None): def get(doctype, name=None, filters=None, parent=None):
'''Returns a document by name or filters """Returns a document by name or filters
:param doctype: DocType of the document to be returned :param doctype: DocType of the document to be returned
:param name: return document of this `name` :param name: return document of this `name`
:param filters: If name is not set, filter by these values and return the first match''' :param filters: If name is not set, filter by these values and return the first match"""
if frappe.is_table(doctype): if frappe.is_table(doctype):
check_parent_permission(parent, doctype) check_parent_permission(parent, doctype)
@ -71,13 +85,14 @@ def get(doctype, name=None, filters=None, parent=None):
return frappe.get_doc(doctype, name).as_dict() return frappe.get_doc(doctype, name).as_dict()
@frappe.whitelist() @frappe.whitelist()
def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, parent=None): def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, parent=None):
'''Returns a value form a document """Returns a value form a document
:param doctype: DocType to be queried :param doctype: DocType to be queried
:param fieldname: Field to be returned (default `name`) :param fieldname: Field to be returned (default `name`)
:param filters: dict or string for identifying the record''' :param filters: dict or string for identifying the record"""
if frappe.is_table(doctype): if frappe.is_table(doctype):
check_parent_permission(parent, doctype) check_parent_permission(parent, doctype)
@ -102,7 +117,15 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, paren
if frappe.get_meta(doctype).issingle: if frappe.get_meta(doctype).issingle:
value = frappe.db.get_values_from_single(fields, filters, doctype, as_dict=as_dict, debug=debug) value = frappe.db.get_values_from_single(fields, filters, doctype, as_dict=as_dict, debug=debug)
else: else:
value = get_list(doctype, filters=filters, fields=fields, debug=debug, limit_page_length=1, parent=parent, as_dict=as_dict) value = get_list(
doctype,
filters=filters,
fields=fields,
debug=debug,
limit_page_length=1,
parent=parent,
as_dict=as_dict,
)
if as_dict: if as_dict:
return value[0] if value else {} return value[0] if value else {}
@ -112,6 +135,7 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, paren
return value[0] if len(fields) > 1 else value[0][0] return value[0] if len(fields) > 1 else value[0][0]
@frappe.whitelist() @frappe.whitelist()
def get_single_value(doctype, field): def get_single_value(doctype, field):
if not frappe.has_permission(doctype): if not frappe.has_permission(doctype):
@ -119,14 +143,15 @@ def get_single_value(doctype, field):
value = frappe.db.get_single_value(doctype, field) value = frappe.db.get_single_value(doctype, field)
return value return value
@frappe.whitelist(methods=['POST', 'PUT'])
@frappe.whitelist(methods=["POST", "PUT"])
def set_value(doctype, name, fieldname, value=None): def set_value(doctype, name, fieldname, value=None):
'''Set a value using get_doc, group of values """Set a value using get_doc, group of values
:param doctype: DocType of the document :param doctype: DocType of the document
:param name: name of the document :param name: name of the document
:param fieldname: fieldname string or JSON / dict with key value pair :param fieldname: fieldname string or JSON / dict with key value pair
:param value: value if fieldname is JSON / dict''' :param value: value if fieldname is JSON / dict"""
if fieldname in (frappe.model.default_fields + frappe.model.child_table_fields): if fieldname in (frappe.model.default_fields + frappe.model.child_table_fields):
frappe.throw(_("Cannot edit standard fields")) frappe.throw(_("Cannot edit standard fields"))
@ -137,7 +162,7 @@ def set_value(doctype, name, fieldname, value=None):
try: try:
values = json.loads(fieldname) values = json.loads(fieldname)
except ValueError: except ValueError:
values = {fieldname: ''} values = {fieldname: ""}
else: else:
values = {fieldname: value} values = {fieldname: value}
@ -155,11 +180,12 @@ def set_value(doctype, name, fieldname, value=None):
return doc.as_dict() return doc.as_dict()
@frappe.whitelist(methods=['POST', 'PUT'])
def insert(doc=None):
'''Insert a document
:param doc: JSON or dict object to be inserted''' @frappe.whitelist(methods=["POST", "PUT"])
def insert(doc=None):
"""Insert a document
:param doc: JSON or dict object to be inserted"""
if isinstance(doc, str): if isinstance(doc, str):
doc = json.loads(doc) doc = json.loads(doc)
@ -173,18 +199,19 @@ def insert(doc=None):
doc = frappe.get_doc(doc).insert() doc = frappe.get_doc(doc).insert()
return doc.as_dict() return doc.as_dict()
@frappe.whitelist(methods=['POST', 'PUT'])
def insert_many(docs=None):
'''Insert multiple documents
:param docs: JSON or list of dict objects to be inserted in one request''' @frappe.whitelist(methods=["POST", "PUT"])
def insert_many(docs=None):
"""Insert multiple documents
:param docs: JSON or list of dict objects to be inserted in one request"""
if isinstance(docs, str): if isinstance(docs, str):
docs = json.loads(docs) docs = json.loads(docs)
out = [] out = []
if len(docs) > 200: if len(docs) > 200:
frappe.throw(_('Only 200 inserts allowed in one request')) frappe.throw(_("Only 200 inserts allowed in one request"))
for doc in docs: for doc in docs:
if doc.get("parenttype"): if doc.get("parenttype"):
@ -199,11 +226,12 @@ def insert_many(docs=None):
return out return out
@frappe.whitelist(methods=['POST', 'PUT'])
def save(doc):
'''Update (save) an existing document
:param doc: JSON or dict object with the properties of the document to be updated''' @frappe.whitelist(methods=["POST", "PUT"])
def save(doc):
"""Update (save) an existing document
:param doc: JSON or dict object with the properties of the document to be updated"""
if isinstance(doc, str): if isinstance(doc, str):
doc = json.loads(doc) doc = json.loads(doc)
@ -212,21 +240,23 @@ def save(doc):
return doc.as_dict() return doc.as_dict()
@frappe.whitelist(methods=['POST', 'PUT'])
@frappe.whitelist(methods=["POST", "PUT"])
def rename_doc(doctype, old_name, new_name, merge=False): def rename_doc(doctype, old_name, new_name, merge=False):
'''Rename document """Rename document
:param doctype: DocType of the document to be renamed :param doctype: DocType of the document to be renamed
:param old_name: Current `name` of the document to be renamed :param old_name: Current `name` of the document to be renamed
:param new_name: New `name` to be set''' :param new_name: New `name` to be set"""
new_name = frappe.rename_doc(doctype, old_name, new_name, merge=merge) new_name = frappe.rename_doc(doctype, old_name, new_name, merge=merge)
return new_name return new_name
@frappe.whitelist(methods=['POST', 'PUT'])
def submit(doc):
'''Submit a document
:param doc: JSON or dict object to be submitted remotely''' @frappe.whitelist(methods=["POST", "PUT"])
def submit(doc):
"""Submit a document
:param doc: JSON or dict object to be submitted remotely"""
if isinstance(doc, str): if isinstance(doc, str):
doc = json.loads(doc) doc = json.loads(doc)
@ -235,52 +265,57 @@ def submit(doc):
return doc.as_dict() return doc.as_dict()
@frappe.whitelist(methods=['POST', 'PUT'])
@frappe.whitelist(methods=["POST", "PUT"])
def cancel(doctype, name): def cancel(doctype, name):
'''Cancel a document """Cancel a document
:param doctype: DocType of the document to be cancelled :param doctype: DocType of the document to be cancelled
:param name: name of the document to be cancelled''' :param name: name of the document to be cancelled"""
wrapper = frappe.get_doc(doctype, name) wrapper = frappe.get_doc(doctype, name)
wrapper.cancel() wrapper.cancel()
return wrapper.as_dict() return wrapper.as_dict()
@frappe.whitelist(methods=['DELETE', 'POST'])
@frappe.whitelist(methods=["DELETE", "POST"])
def delete(doctype, name): def delete(doctype, name):
'''Delete a remote document """Delete a remote document
:param doctype: DocType of the document to be deleted :param doctype: DocType of the document to be deleted
:param name: name of the document to be deleted''' :param name: name of the document to be deleted"""
frappe.delete_doc(doctype, name, ignore_missing=False) frappe.delete_doc(doctype, name, ignore_missing=False)
@frappe.whitelist(methods=['POST', 'PUT'])
@frappe.whitelist(methods=["POST", "PUT"])
def set_default(key, value, parent=None): def set_default(key, value, parent=None):
"""set a user default value""" """set a user default value"""
frappe.db.set_default(key, value, parent or frappe.session.user) frappe.db.set_default(key, value, parent or frappe.session.user)
frappe.clear_cache(user=frappe.session.user) frappe.clear_cache(user=frappe.session.user)
@frappe.whitelist() @frappe.whitelist()
def get_default(key, parent=None): def get_default(key, parent=None):
"""set a user default value""" """set a user default value"""
return frappe.db.get_default(key, parent) return frappe.db.get_default(key, parent)
@frappe.whitelist(methods=['POST', 'PUT']) @frappe.whitelist(methods=["POST", "PUT"])
def make_width_property_setter(doc): def make_width_property_setter(doc):
'''Set width Property Setter """Set width Property Setter
:param doc: Property Setter document with `width` property''' :param doc: Property Setter document with `width` property"""
if isinstance(doc, str): if isinstance(doc, str):
doc = json.loads(doc) doc = json.loads(doc)
if doc["doctype"]=="Property Setter" and doc["property"]=="width": if doc["doctype"] == "Property Setter" and doc["property"] == "width":
frappe.get_doc(doc).insert(ignore_permissions = True) frappe.get_doc(doc).insert(ignore_permissions=True)
@frappe.whitelist(methods=['POST', 'PUT'])
@frappe.whitelist(methods=["POST", "PUT"])
def bulk_update(docs): def bulk_update(docs):
'''Bulk update documents """Bulk update documents
:param docs: JSON list of documents to be updated remotely. Each document must have `docname` property''' :param docs: JSON list of documents to be updated remotely. Each document must have `docname` property"""
docs = json.loads(docs) docs = json.loads(docs)
failed_docs = [] failed_docs = []
for doc in docs: for doc in docs:
@ -290,41 +325,40 @@ def bulk_update(docs):
existing_doc.update(doc) existing_doc.update(doc)
existing_doc.save() existing_doc.save()
except Exception: except Exception:
failed_docs.append({ failed_docs.append({"doc": doc, "exc": frappe.utils.get_traceback()})
'doc': doc,
'exc': frappe.utils.get_traceback() return {"failed_docs": failed_docs}
})
return {'failed_docs': failed_docs}
@frappe.whitelist() @frappe.whitelist()
def has_permission(doctype, docname, perm_type="read"): def has_permission(doctype, docname, perm_type="read"):
'''Returns a JSON with data whether the document has the requested permission """Returns a JSON with data whether the document has the requested permission
:param doctype: DocType of the document to be checked :param doctype: DocType of the document to be checked
:param docname: `name` of the document to be checked :param docname: `name` of the document to be checked
:param perm_type: one of `read`, `write`, `create`, `submit`, `cancel`, `report`. Default is `read`''' :param perm_type: one of `read`, `write`, `create`, `submit`, `cancel`, `report`. Default is `read`"""
# perm_type can be one of read, write, create, submit, cancel, report # perm_type can be one of read, write, create, submit, cancel, report
return {"has_permission": frappe.has_permission(doctype, perm_type.lower(), docname)} return {"has_permission": frappe.has_permission(doctype, perm_type.lower(), docname)}
@frappe.whitelist() @frappe.whitelist()
def get_password(doctype, name, fieldname): def get_password(doctype, name, fieldname):
'''Return a password type property. Only applicable for System Managers """Return a password type property. Only applicable for System Managers
:param doctype: DocType of the document that holds the password :param doctype: DocType of the document that holds the password
:param name: `name` of the document that holds the password :param name: `name` of the document that holds the password
:param fieldname: `fieldname` of the password property :param fieldname: `fieldname` of the password property
''' """
frappe.only_for("System Manager") frappe.only_for("System Manager")
return frappe.get_doc(doctype, name).get_password(fieldname) return frappe.get_doc(doctype, name).get_password(fieldname)
@frappe.whitelist() @frappe.whitelist()
def get_js(items): def get_js(items):
'''Load JS code files. Will also append translations """Load JS code files. Will also append translations
and extend `frappe._messages` and extend `frappe._messages`
:param items: JSON list of paths of the js files to be loaded.''' :param items: JSON list of paths of the js files to be loaded."""
items = json.loads(items) items = json.loads(items)
out = [] out = []
for src in items: for src in items:
@ -346,14 +380,25 @@ def get_js(items):
return out return out
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def get_time_zone(): def get_time_zone():
'''Returns default time zone''' """Returns default time zone"""
return {"time_zone": frappe.defaults.get_defaults().get("time_zone")} return {"time_zone": frappe.defaults.get_defaults().get("time_zone")}
@frappe.whitelist(methods=['POST', 'PUT'])
def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder=None, decode_base64=False, is_private=None, docfield=None): @frappe.whitelist(methods=["POST", "PUT"])
'''Attach a file to Document (POST) def attach_file(
filename=None,
filedata=None,
doctype=None,
docname=None,
folder=None,
decode_base64=False,
is_private=None,
docfield=None,
):
"""Attach a file to Document (POST)
:param filename: filename e.g. test-file.txt :param filename: filename e.g. test-file.txt
:param filedata: base64 encode filedata which must be urlencoded :param filedata: base64 encode filedata which must be urlencoded
@ -362,7 +407,7 @@ def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder
:param folder: Folder to add File into :param folder: Folder to add File into
:param decode_base64: decode filedata from base64 encode, default is False :param decode_base64: decode filedata from base64 encode, default is False
:param is_private: Attach file as private file (1 or 0) :param is_private: Attach file as private file (1 or 0)
:param docfield: file to attach to (optional)''' :param docfield: file to attach to (optional)"""
request_method = frappe.local.request.environ.get("REQUEST_METHOD") request_method = frappe.local.request.environ.get("REQUEST_METHOD")
@ -374,16 +419,19 @@ def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder
if not doc.has_permission(): if not doc.has_permission():
frappe.throw(_("Not permitted"), frappe.PermissionError) frappe.throw(_("Not permitted"), frappe.PermissionError)
_file = frappe.get_doc({ _file = frappe.get_doc(
"doctype": "File", {
"file_name": filename, "doctype": "File",
"attached_to_doctype": doctype, "file_name": filename,
"attached_to_name": docname, "attached_to_doctype": doctype,
"attached_to_field": docfield, "attached_to_name": docname,
"folder": folder, "attached_to_field": docfield,
"is_private": is_private, "folder": folder,
"content": filedata, "is_private": is_private,
"decode": decode_base64}) "content": filedata,
"decode": decode_base64,
}
)
_file.save() _file.save()
if docfield and doctype: if docfield and doctype:
@ -392,22 +440,23 @@ def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder
return _file.as_dict() return _file.as_dict()
@frappe.whitelist() @frappe.whitelist()
def get_hooks(hook, app_name=None): def get_hooks(hook, app_name=None):
return frappe.get_hooks(hook, app_name) return frappe.get_hooks(hook, app_name)
@frappe.whitelist() @frappe.whitelist()
def is_document_amended(doctype, docname): def is_document_amended(doctype, docname):
if frappe.permissions.has_permission(doctype): if frappe.permissions.has_permission(doctype):
try: try:
return frappe.db.exists(doctype, { return frappe.db.exists(doctype, {"amended_from": docname})
'amended_from': docname
})
except frappe.db.InternalError: except frappe.db.InternalError:
pass pass
return False return False
@frappe.whitelist() @frappe.whitelist()
def validate_link(doctype: str, docname: str, fields=None): def validate_link(doctype: str, docname: str, fields=None):
if not isinstance(doctype, str): if not isinstance(doctype, str):
@ -417,13 +466,11 @@ def validate_link(doctype: str, docname: str, fields=None):
frappe.throw(_("Document Name must be a string")) frappe.throw(_("Document Name must be a string"))
if doctype != "DocType" and not ( if doctype != "DocType" and not (
frappe.has_permission(doctype, "select") frappe.has_permission(doctype, "select") or frappe.has_permission(doctype, "read")
or frappe.has_permission(doctype, "read")
): ):
frappe.throw( frappe.throw(
_("You do not have Read or Select Permissions for {}") _("You do not have Read or Select Permissions for {}").format(frappe.bold(doctype)),
.format(frappe.bold(doctype)), frappe.PermissionError,
frappe.PermissionError
) )
values = frappe._dict() values = frappe._dict()
@ -438,14 +485,11 @@ def validate_link(doctype: str, docname: str, fields=None):
except frappe.PermissionError: except frappe.PermissionError:
frappe.clear_last_message() frappe.clear_last_message()
frappe.msgprint( frappe.msgprint(
_("You need {0} permission to fetch values from {1} {2}") _("You need {0} permission to fetch values from {1} {2}").format(
.format( frappe.bold(_("Read")), frappe.bold(doctype), frappe.bold(docname)
frappe.bold(_("Read")),
frappe.bold(doctype),
frappe.bold(docname)
), ),
title=_("Cannot Fetch Values"), title=_("Cannot Fetch Values"),
indicator="orange" indicator="orange",
) )
return values return values

View file

@ -1,23 +1,26 @@
# Copyright (c) 2015, Web Notes Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Web Notes Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import sys
import click
import cProfile import cProfile
import pstats import pstats
import frappe import subprocess # nosec
import frappe.utils import sys
import subprocess # nosec
from functools import wraps from functools import wraps
from io import StringIO from io import StringIO
from os import environ from os import environ
import click
import frappe
import frappe.utils
click.disable_unicode_literals_warning = True click.disable_unicode_literals_warning = True
def pass_context(f): def pass_context(f):
@wraps(f) @wraps(f)
def _func(ctx, *args, **kwargs): def _func(ctx, *args, **kwargs):
profile = ctx.obj['profile'] profile = ctx.obj["profile"]
if profile: if profile:
pr = cProfile.Profile() pr = cProfile.Profile()
pr.enable() pr.enable()
@ -25,18 +28,17 @@ def pass_context(f):
try: try:
ret = f(frappe._dict(ctx.obj), *args, **kwargs) ret = f(frappe._dict(ctx.obj), *args, **kwargs)
except frappe.exceptions.SiteNotSpecifiedError as e: except frappe.exceptions.SiteNotSpecifiedError as e:
click.secho(str(e), fg='yellow') click.secho(str(e), fg="yellow")
sys.exit(1) sys.exit(1)
except frappe.exceptions.IncorrectSitePath: except frappe.exceptions.IncorrectSitePath:
site = ctx.obj.get("sites", "")[0] site = ctx.obj.get("sites", "")[0]
click.secho(f'Site {site} does not exist!', fg='yellow') click.secho(f"Site {site} does not exist!", fg="yellow")
sys.exit(1) sys.exit(1)
if profile: if profile:
pr.disable() pr.disable()
s = StringIO() s = StringIO()
ps = pstats.Stats(pr, stream=s)\ ps = pstats.Stats(pr, stream=s).sort_stats("cumtime", "tottime", "ncalls")
.sort_stats('cumtime', 'tottime', 'ncalls')
ps.print_stats() ps.print_stats()
# print the top-100 # print the top-100
@ -47,6 +49,7 @@ def pass_context(f):
return click.pass_context(_func) return click.pass_context(_func)
def get_site(context, raise_err=True): def get_site(context, raise_err=True):
try: try:
site = context.sites[0] site = context.sites[0]
@ -56,17 +59,19 @@ def get_site(context, raise_err=True):
raise frappe.SiteNotSpecifiedError raise frappe.SiteNotSpecifiedError
return None return None
def popen(command, *args, **kwargs): def popen(command, *args, **kwargs):
output = kwargs.get('output', True) output = kwargs.get("output", True)
cwd = kwargs.get('cwd') cwd = kwargs.get("cwd")
shell = kwargs.get('shell', True) shell = kwargs.get("shell", True)
raise_err = kwargs.get('raise_err') raise_err = kwargs.get("raise_err")
env = kwargs.get('env') env = kwargs.get("env")
if env: if env:
env = dict(environ, **env) env = dict(environ, **env)
def set_low_prio(): def set_low_prio():
import psutil import psutil
if psutil.LINUX: if psutil.LINUX:
psutil.Process().nice(19) psutil.Process().nice(19)
psutil.Process().ionice(psutil.IOPRIO_CLASS_IDLE) psutil.Process().ionice(psutil.IOPRIO_CLASS_IDLE)
@ -77,13 +82,14 @@ def popen(command, *args, **kwargs):
psutil.Process().nice(19) psutil.Process().nice(19)
# ionice not supported # ionice not supported
proc = subprocess.Popen(command, proc = subprocess.Popen(
command,
stdout=None if output else subprocess.PIPE, stdout=None if output else subprocess.PIPE,
stderr=None if output else subprocess.PIPE, stderr=None if output else subprocess.PIPE,
shell=shell, shell=shell,
cwd=cwd, cwd=cwd,
preexec_fn=set_low_prio, preexec_fn=set_low_prio,
env=env env=env,
) )
return_ = proc.wait() return_ = proc.wait()
@ -93,26 +99,22 @@ def popen(command, *args, **kwargs):
return return_ return return_
def call_command(cmd, context): def call_command(cmd, context):
return click.Context(cmd, obj=context).forward(cmd) return click.Context(cmd, obj=context).forward(cmd)
def get_commands(): def get_commands():
# prevent circular imports # prevent circular imports
from .redis_utils import commands as redis_commands
from .scheduler import commands as scheduler_commands from .scheduler import commands as scheduler_commands
from .site import commands as site_commands from .site import commands as site_commands
from .translate import commands as translate_commands from .translate import commands as translate_commands
from .utils import commands as utils_commands from .utils import commands as utils_commands
from .redis_utils import commands as redis_commands
clickable_link = ( clickable_link = "\x1b]8;;https://frappeframework.com/docs\afrappeframework.com\x1b]8;;\a"
"\x1b]8;;https://frappeframework.com/docs\afrappeframework.com\x1b]8;;\a"
)
all_commands = ( all_commands = (
scheduler_commands scheduler_commands + site_commands + translate_commands + utils_commands + redis_commands
+ site_commands
+ translate_commands
+ utils_commands
+ redis_commands
) )
for command in all_commands: for command in all_commands:

View file

@ -3,51 +3,71 @@ import os
import click import click
import frappe import frappe
from frappe.utils.redis_queue import RedisQueue
from frappe.installer import update_site_config from frappe.installer import update_site_config
from frappe.utils.redis_queue import RedisQueue
@click.command('create-rq-users')
@click.option('--set-admin-password', is_flag=True, default=False, help='Set new Redis admin(default user) password') @click.command("create-rq-users")
@click.option('--use-rq-auth', is_flag=True, default=False, help='Enable Redis authentication for sites') @click.option(
"--set-admin-password",
is_flag=True,
default=False,
help="Set new Redis admin(default user) password",
)
@click.option(
"--use-rq-auth", is_flag=True, default=False, help="Enable Redis authentication for sites"
)
def create_rq_users(set_admin_password=False, use_rq_auth=False): def create_rq_users(set_admin_password=False, use_rq_auth=False):
"""Create Redis Queue users and add to acl and app configs. """Create Redis Queue users and add to acl and app configs.
acl config file will be used by redis server while starting the server acl config file will be used by redis server while starting the server
and app config is used by app while connecting to redis server. and app config is used by app while connecting to redis server.
""" """
acl_file_path = os.path.abspath('../config/redis_queue.acl') acl_file_path = os.path.abspath("../config/redis_queue.acl")
with frappe.init_site(): with frappe.init_site():
acl_list, user_credentials = RedisQueue.gen_acl_list( acl_list, user_credentials = RedisQueue.gen_acl_list(set_admin_password=set_admin_password)
set_admin_password=set_admin_password)
with open(acl_file_path, 'w') as f: with open(acl_file_path, "w") as f:
f.writelines([acl+'\n' for acl in acl_list]) f.writelines([acl + "\n" for acl in acl_list])
sites_path = os.getcwd() sites_path = os.getcwd()
common_site_config_path = os.path.join(sites_path, 'common_site_config.json') common_site_config_path = os.path.join(sites_path, "common_site_config.json")
update_site_config("rq_username", user_credentials['bench'][0], validate=False, update_site_config(
site_config_path=common_site_config_path) "rq_username",
update_site_config("rq_password", user_credentials['bench'][1], validate=False, user_credentials["bench"][0],
site_config_path=common_site_config_path) validate=False,
update_site_config("use_rq_auth", use_rq_auth, validate=False, site_config_path=common_site_config_path,
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. ' click.secho(
'Please restart Redis Queue server to enable namespaces.', "* ACL and site configs are updated with new user credentials. "
fg='green') "Please restart Redis Queue server to enable namespaces.",
fg="green",
)
if set_admin_password: if set_admin_password:
env_key = 'RQ_ADMIN_PASWORD' env_key = "RQ_ADMIN_PASWORD"
click.secho('* Redis admin password is successfully set up. ' click.secho(
'Include below line in .bashrc file for system to use', "* Redis admin password is successfully set up. "
fg='green') "Include below line in .bashrc file for system to use",
fg="green",
)
click.secho(f"`export {env_key}={user_credentials['default'][1]}`") click.secho(f"`export {env_key}={user_credentials['default'][1]}`")
click.secho('NOTE: Please save the admin password as you ' click.secho(
'can not access redis server without the password', "NOTE: Please save the admin password as you "
fg='yellow') "can not access redis server without the password",
fg="yellow",
)
commands = [ commands = [create_rq_users]
create_rq_users
]

View file

@ -1,15 +1,20 @@
import click
import sys import sys
import click
import frappe import frappe
from frappe.utils import cint from frappe.commands import get_site, pass_context
from frappe.commands import pass_context, get_site
from frappe.exceptions import SiteNotSpecifiedError from frappe.exceptions import SiteNotSpecifiedError
from frappe.utils import cint
def _is_scheduler_enabled(): def _is_scheduler_enabled():
enable_scheduler = False enable_scheduler = False
try: try:
frappe.connect() frappe.connect()
enable_scheduler = cint(frappe.db.get_single_value("System Settings", "enable_scheduler")) and True or False enable_scheduler = (
cint(frappe.db.get_single_value("System Settings", "enable_scheduler")) and True or False
)
except: except:
pass pass
finally: finally:
@ -44,11 +49,12 @@ def trigger_scheduler_event(context, event):
sys.exit(exit_code) sys.exit(exit_code)
@click.command('enable-scheduler') @click.command("enable-scheduler")
@pass_context @pass_context
def enable_scheduler(context): def enable_scheduler(context):
"Enable scheduler" "Enable scheduler"
import frappe.utils.scheduler import frappe.utils.scheduler
for site in context.sites: for site in context.sites:
try: try:
frappe.init(site=site) frappe.init(site=site)
@ -61,11 +67,13 @@ def enable_scheduler(context):
if not context.sites: if not context.sites:
raise SiteNotSpecifiedError raise SiteNotSpecifiedError
@click.command('disable-scheduler')
@click.command("disable-scheduler")
@pass_context @pass_context
def disable_scheduler(context): def disable_scheduler(context):
"Disable scheduler" "Disable scheduler"
import frappe.utils.scheduler import frappe.utils.scheduler
for site in context.sites: for site in context.sites:
try: try:
frappe.init(site=site) frappe.init(site=site)
@ -79,13 +87,13 @@ def disable_scheduler(context):
raise SiteNotSpecifiedError raise SiteNotSpecifiedError
@click.command('scheduler') @click.command("scheduler")
@click.option('--site', help='site name') @click.option("--site", help="site name")
@click.argument('state', type=click.Choice(['pause', 'resume', 'disable', 'enable'])) @click.argument("state", type=click.Choice(["pause", "resume", "disable", "enable"]))
@pass_context @pass_context
def scheduler(context, state, site=None): def scheduler(context, state, site=None):
from frappe.installer import update_site_config
import frappe.utils.scheduler import frappe.utils.scheduler
from frappe.installer import update_site_config
if not site: if not site:
site = get_site(context) site = get_site(context)
@ -93,58 +101,64 @@ def scheduler(context, state, site=None):
try: try:
frappe.init(site=site) frappe.init(site=site)
if state == 'pause': if state == "pause":
update_site_config('pause_scheduler', 1) update_site_config("pause_scheduler", 1)
elif state == 'resume': elif state == "resume":
update_site_config('pause_scheduler', 0) update_site_config("pause_scheduler", 0)
elif state == 'disable': elif state == "disable":
frappe.connect() frappe.connect()
frappe.utils.scheduler.disable_scheduler() frappe.utils.scheduler.disable_scheduler()
frappe.db.commit() frappe.db.commit()
elif state == 'enable': elif state == "enable":
frappe.connect() frappe.connect()
frappe.utils.scheduler.enable_scheduler() frappe.utils.scheduler.enable_scheduler()
frappe.db.commit() frappe.db.commit()
print('Scheduler {0}d for site {1}'.format(state, site)) print("Scheduler {0}d for site {1}".format(state, site))
finally: finally:
frappe.destroy() frappe.destroy()
@click.command('set-maintenance-mode') @click.command("set-maintenance-mode")
@click.option('--site', help='site name') @click.option("--site", help="site name")
@click.argument('state', type=click.Choice(['on', 'off'])) @click.argument("state", type=click.Choice(["on", "off"]))
@pass_context @pass_context
def set_maintenance_mode(context, state, site=None): def set_maintenance_mode(context, state, site=None):
from frappe.installer import update_site_config from frappe.installer import update_site_config
if not site: if not site:
site = get_site(context) site = get_site(context)
try: try:
frappe.init(site=site) frappe.init(site=site)
update_site_config('maintenance_mode', 1 if (state == 'on') else 0) update_site_config("maintenance_mode", 1 if (state == "on") else 0)
finally: finally:
frappe.destroy() frappe.destroy()
@click.command('doctor') #Passing context always gets a site and if there is no use site it breaks @click.command(
@click.option('--site', help='site name') "doctor"
) # Passing context always gets a site and if there is no use site it breaks
@click.option("--site", help="site name")
@pass_context @pass_context
def doctor(context, site=None): def doctor(context, site=None):
"Get diagnostic info about background workers" "Get diagnostic info about background workers"
from frappe.utils.doctor import doctor as _doctor from frappe.utils.doctor import doctor as _doctor
if not site: if not site:
site = get_site(context, raise_err=False) site = get_site(context, raise_err=False)
return _doctor(site=site) return _doctor(site=site)
@click.command('show-pending-jobs')
@click.option('--site', help='site name') @click.command("show-pending-jobs")
@click.option("--site", help="site name")
@pass_context @pass_context
def show_pending_jobs(context, site=None): def show_pending_jobs(context, site=None):
"Get diagnostic info about background jobs" "Get diagnostic info about background jobs"
from frappe.utils.doctor import pending_jobs as _pending_jobs from frappe.utils.doctor import pending_jobs as _pending_jobs
if not site: if not site:
site = get_site(context) site = get_site(context)
@ -153,35 +167,45 @@ def show_pending_jobs(context, site=None):
return pending_jobs return pending_jobs
@click.command('purge-jobs')
@click.option('--site', help='site name') @click.command("purge-jobs")
@click.option('--queue', default=None, help='one of "low", "default", "high') @click.option("--site", help="site name")
@click.option('--event', default=None, help='one of "all", "weekly", "monthly", "hourly", "daily", "weekly_long", "daily_long"') @click.option("--queue", default=None, help='one of "low", "default", "high')
@click.option(
"--event",
default=None,
help='one of "all", "weekly", "monthly", "hourly", "daily", "weekly_long", "daily_long"',
)
def purge_jobs(site=None, queue=None, event=None): def purge_jobs(site=None, queue=None, event=None):
"Purge any pending periodic tasks, if event option is not given, it will purge everything for the site" "Purge any pending periodic tasks, if event option is not given, it will purge everything for the site"
from frappe.utils.doctor import purge_pending_jobs from frappe.utils.doctor import purge_pending_jobs
frappe.init(site or '')
frappe.init(site or "")
count = purge_pending_jobs(event=event, site=site, queue=queue) count = purge_pending_jobs(event=event, site=site, queue=queue)
print("Purged {} jobs".format(count)) print("Purged {} jobs".format(count))
@click.command('schedule')
@click.command("schedule")
def start_scheduler(): def start_scheduler():
from frappe.utils.scheduler import start_scheduler from frappe.utils.scheduler import start_scheduler
start_scheduler() start_scheduler()
@click.command('worker')
@click.option('--queue', type=str)
@click.option('--quiet', is_flag = True, default = False, help = 'Hide Log Outputs')
@click.option('-u', '--rq-username', default=None, help='Redis ACL user')
@click.option('-p', '--rq-password', default=None, help='Redis ACL user password')
def start_worker(queue, quiet = False, rq_username=None, rq_password=None):
"""Site is used to find redis credentals.
"""
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.command("worker")
@click.option('--site', help='site name') @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")
@pass_context @pass_context
def ready_for_migration(context, site=None): def ready_for_migration(context, site=None):
from frappe.utils.doctor import get_pending_jobs from frappe.utils.doctor import get_pending_jobs
@ -194,16 +218,17 @@ def ready_for_migration(context, site=None):
pending_jobs = get_pending_jobs(site=site) pending_jobs = get_pending_jobs(site=site)
if pending_jobs: if pending_jobs:
print('NOT READY for migration: site {0} has pending background jobs'.format(site)) print("NOT READY for migration: site {0} has pending background jobs".format(site))
sys.exit(1) sys.exit(1)
else: else:
print('READY for migration: site {0} does not have any background jobs'.format(site)) print("READY for migration: site {0} does not have any background jobs".format(site))
return 0 return 0
finally: finally:
frappe.destroy() frappe.destroy()
commands = [ commands = [
disable_scheduler, disable_scheduler,
doctor, doctor,

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,16 @@
import click import click
from frappe.commands import pass_context, get_site
from frappe.commands import get_site, pass_context
from frappe.exceptions import SiteNotSpecifiedError from frappe.exceptions import SiteNotSpecifiedError
# translation # translation
@click.command('build-message-files') @click.command("build-message-files")
@pass_context @pass_context
def build_message_files(context): def build_message_files(context):
"Build message files for translation" "Build message files for translation"
import frappe.translate import frappe.translate
for site in context.sites: for site in context.sites:
try: try:
frappe.init(site=site) frappe.init(site=site)
@ -18,32 +21,41 @@ def build_message_files(context):
if not context.sites: if not context.sites:
raise SiteNotSpecifiedError raise SiteNotSpecifiedError
@click.command('new-language') #, help="Create lang-code.csv for given app")
@click.command("new-language") # , help="Create lang-code.csv for given app")
@pass_context @pass_context
@click.argument('lang_code') #, help="Language code eg. en") @click.argument("lang_code") # , help="Language code eg. en")
@click.argument('app') #, help="App name eg. frappe") @click.argument("app") # , help="App name eg. frappe")
def new_language(context, lang_code, app): def new_language(context, lang_code, app):
"""Create lang-code.csv for given app""" """Create lang-code.csv for given app"""
import frappe.translate import frappe.translate
if not context['sites']: if not context["sites"]:
raise Exception('--site is required') raise Exception("--site is required")
# init site # init site
frappe.connect(site=context['sites'][0]) frappe.connect(site=context["sites"][0])
frappe.translate.write_translations_file(app, lang_code) frappe.translate.write_translations_file(app, lang_code)
print("File created at ./apps/{app}/{app}/translations/{lang_code}.csv".format(app=app, lang_code=lang_code)) print(
print("You will need to add the language in frappe/geo/languages.json, if you haven't done it already.") "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.command("get-untranslated")
@click.argument('untranslated_file') @click.argument("lang")
@click.option('--all', default=False, is_flag=True, help='Get all message strings') @click.argument("untranslated_file")
@click.option("--all", default=False, is_flag=True, help="Get all message strings")
@pass_context @pass_context
def get_untranslated(context, lang, untranslated_file, all=None): def get_untranslated(context, lang, untranslated_file, all=None):
"Get untranslated strings for language" "Get untranslated strings for language"
import frappe.translate import frappe.translate
site = get_site(context) site = get_site(context)
try: try:
frappe.init(site=site) frappe.init(site=site)
@ -52,14 +64,16 @@ def get_untranslated(context, lang, untranslated_file, all=None):
finally: finally:
frappe.destroy() frappe.destroy()
@click.command('update-translations')
@click.argument('lang') @click.command("update-translations")
@click.argument('untranslated_file') @click.argument("lang")
@click.argument('translated-file') @click.argument("untranslated_file")
@click.argument("translated-file")
@pass_context @pass_context
def update_translations(context, lang, untranslated_file, translated_file): def update_translations(context, lang, untranslated_file, translated_file):
"Update translated strings" "Update translated strings"
import frappe.translate import frappe.translate
site = get_site(context) site = get_site(context)
try: try:
frappe.init(site=site) frappe.init(site=site)
@ -68,13 +82,15 @@ def update_translations(context, lang, untranslated_file, translated_file):
finally: finally:
frappe.destroy() frappe.destroy()
@click.command('import-translations')
@click.argument('lang') @click.command("import-translations")
@click.argument('path') @click.argument("lang")
@click.argument("path")
@pass_context @pass_context
def import_translations(context, lang, path): def import_translations(context, lang, path):
"Update translated strings" "Update translated strings"
import frappe.translate import frappe.translate
site = get_site(context) site = get_site(context)
try: try:
frappe.init(site=site) frappe.init(site=site)
@ -83,6 +99,7 @@ def import_translations(context, lang, path):
finally: finally:
frappe.destroy() frappe.destroy()
commands = [ commands = [
build_message_files, build_message_files,
get_untranslated, get_untranslated,

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,20 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.desk.moduleview import (get_data, get_onboard_items, config_exists, get_module_link_items_from_list) from frappe.desk.moduleview import (
config_exists,
get_data,
get_module_link_items_from_list,
get_onboard_items,
)
def get_modules_from_all_apps_for_user(user=None): def get_modules_from_all_apps_for_user(user=None):
if not user: if not user:
user = frappe.session.user user = frappe.session.user
all_modules = get_modules_from_all_apps() all_modules = get_modules_from_all_apps()
global_blocked_modules = frappe.get_doc('User', 'Administrator').get_blocked_modules() global_blocked_modules = frappe.get_doc("User", "Administrator").get_blocked_modules()
user_blocked_modules = frappe.get_doc('User', user).get_blocked_modules() user_blocked_modules = frappe.get_doc("User", user).get_blocked_modules()
blocked_modules = global_blocked_modules + user_blocked_modules blocked_modules = global_blocked_modules + user_blocked_modules
allowed_modules_list = [m for m in all_modules if m.get("module_name") not in blocked_modules] allowed_modules_list = [m for m in all_modules if m.get("module_name") not in blocked_modules]
@ -22,31 +28,31 @@ def get_modules_from_all_apps_for_user(user=None):
module["onboard_present"] = 1 module["onboard_present"] = 1
# Set defaults links # Set defaults links
module["links"] = get_onboard_items(module["app"], frappe.scrub(module_name))[:5] module["links"] = get_onboard_items(module["app"], frappe.scrub(module_name))[:5]
return allowed_modules_list return allowed_modules_list
def get_modules_from_all_apps(): def get_modules_from_all_apps():
modules_list = [] modules_list = []
for app in frappe.get_installed_apps(): for app in frappe.get_installed_apps():
modules_list += get_modules_from_app(app) modules_list += get_modules_from_app(app)
return modules_list return modules_list
def get_modules_from_app(app): def get_modules_from_app(app):
return frappe.get_all('Module Def', return frappe.get_all(
filters={'app_name': app}, "Module Def", filters={"app_name": app}, fields=["module_name", "app_name as app"]
fields=['module_name', 'app_name as app']
) )
def get_all_empty_tables_by_module(): def get_all_empty_tables_by_module():
table_rows = frappe.qb.Field("table_rows") table_rows = frappe.qb.Field("table_rows")
table_name = frappe.qb.Field("table_name") table_name = frappe.qb.Field("table_name")
information_schema = frappe.qb.Schema("information_schema") information_schema = frappe.qb.Schema("information_schema")
empty_tables = ( empty_tables = (
frappe.qb.from_(information_schema.tables) frappe.qb.from_(information_schema.tables).select(table_name).where(table_rows == 0)
.select(table_name)
.where(table_rows == 0)
).run() ).run()
empty_tables = {r[0] for r in empty_tables} empty_tables = {r[0] for r in empty_tables}
@ -62,8 +68,10 @@ def get_all_empty_tables_by_module():
empty_tables_by_module[module] = [doctype] empty_tables_by_module[module] = [doctype]
return empty_tables_by_module return empty_tables_by_module
def is_domain(module): def is_domain(module):
return module.get("category") == "Domains" return module.get("category") == "Domains"
def is_module(module): def is_module(module):
return module.get("type") == "module" return module.get("type") == "module"

View file

@ -1,12 +1,13 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe
from frappe import _
import functools import functools
import re import re
import frappe
from frappe import _
def load_address_and_contact(doc, key=None): def load_address_and_contact(doc, key=None):
"""Loads address list and contact list in `__onload`""" """Loads address list and contact list in `__onload`"""
from frappe.contacts.doctype.address.address import get_address_display, get_condensed_address from frappe.contacts.doctype.address.address import get_address_display, get_condensed_address
@ -18,15 +19,18 @@ def load_address_and_contact(doc, key=None):
] ]
address_list = frappe.get_list("Address", filters=filters, fields=["*"]) address_list = frappe.get_list("Address", filters=filters, fields=["*"])
address_list = [a.update({"display": get_address_display(a)}) address_list = [a.update({"display": get_address_display(a)}) for a in address_list]
for a in address_list]
address_list = sorted(address_list, address_list = sorted(
key = functools.cmp_to_key(lambda a, b: address_list,
(int(a.is_primary_address - b.is_primary_address)) or key=functools.cmp_to_key(
(1 if a.modified - b.modified else 0)), reverse=True) lambda a, b: (int(a.is_primary_address - b.is_primary_address))
or (1 if a.modified - b.modified else 0)
),
reverse=True,
)
doc.set_onload('addr_list', address_list) doc.set_onload("addr_list", address_list)
contact_list = [] contact_list = []
filters = [ filters = [
@ -37,29 +41,38 @@ def load_address_and_contact(doc, key=None):
contact_list = frappe.get_list("Contact", filters=filters, fields=["*"]) contact_list = frappe.get_list("Contact", filters=filters, fields=["*"])
for contact in contact_list: for contact in contact_list:
contact["email_ids"] = frappe.get_all("Contact Email", filters={ contact["email_ids"] = frappe.get_all(
"parenttype": "Contact", "Contact Email",
"parent": contact.name, filters={"parenttype": "Contact", "parent": contact.name, "is_primary": 0},
"is_primary": 0 fields=["email_id"],
}, fields=["email_id"]) )
contact["phone_nos"] = frappe.get_all("Contact Phone", filters={ contact["phone_nos"] = frappe.get_all(
"Contact Phone",
filters={
"parenttype": "Contact", "parenttype": "Contact",
"parent": contact.name, "parent": contact.name,
"is_primary_phone": 0, "is_primary_phone": 0,
"is_primary_mobile_no": 0 "is_primary_mobile_no": 0,
}, fields=["phone"]) },
fields=["phone"],
)
if contact.address: if contact.address:
address = frappe.get_doc("Address", contact.address) address = frappe.get_doc("Address", contact.address)
contact["address"] = get_condensed_address(address) contact["address"] = get_condensed_address(address)
contact_list = sorted(contact_list, contact_list = sorted(
key = functools.cmp_to_key(lambda a, b: contact_list,
(int(a.is_primary_contact - b.is_primary_contact)) or key=functools.cmp_to_key(
(1 if a.modified - b.modified else 0)), reverse=True) lambda a, b: (int(a.is_primary_contact - b.is_primary_contact))
or (1 if a.modified - b.modified else 0)
),
reverse=True,
)
doc.set_onload("contact_list", contact_list)
doc.set_onload('contact_list', contact_list)
def has_permission(doc, ptype, user): def has_permission(doc, ptype, user):
links = get_permitted_and_not_permitted_links(doc.doctype) links = get_permitted_and_not_permitted_links(doc.doctype)
@ -69,7 +82,7 @@ def has_permission(doc, ptype, user):
# True if any one is True or all are empty # True if any one is True or all are empty
names = [] names = []
for df in (links.get("permitted_links") + links.get("not_permitted_links")): for df in links.get("permitted_links") + links.get("not_permitted_links"):
doctype = df.options doctype = df.options
name = doc.get(df.fieldname) name = doc.get(df.fieldname)
names.append(name) names.append(name)
@ -81,12 +94,15 @@ def has_permission(doc, ptype, user):
return True return True
return False return False
def get_permission_query_conditions_for_contact(user): def get_permission_query_conditions_for_contact(user):
return get_permission_query_conditions("Contact") return get_permission_query_conditions("Contact")
def get_permission_query_conditions_for_address(user): def get_permission_query_conditions_for_address(user):
return get_permission_query_conditions("Address") return get_permission_query_conditions("Address")
def get_permission_query_conditions(doctype): def get_permission_query_conditions(doctype):
links = get_permitted_and_not_permitted_links(doctype) links = get_permitted_and_not_permitted_links(doctype)
@ -100,7 +116,9 @@ def get_permission_query_conditions(doctype):
# when everything is not permitted # when everything is not permitted
for df in links.get("not_permitted_links"): for df in links.get("not_permitted_links"):
# like ifnull(customer, '')='' and ifnull(supplier, '')='' # like ifnull(customer, '')='' and ifnull(supplier, '')=''
conditions.append("ifnull(`tab{doctype}`.`{fieldname}`, '')=''".format(doctype=doctype, fieldname=df.fieldname)) conditions.append(
"ifnull(`tab{doctype}`.`{fieldname}`, '')=''".format(doctype=doctype, fieldname=df.fieldname)
)
return "( " + " and ".join(conditions) + " )" return "( " + " and ".join(conditions) + " )"
@ -109,10 +127,13 @@ def get_permission_query_conditions(doctype):
for df in links.get("permitted_links"): for df in links.get("permitted_links"):
# like ifnull(customer, '')!='' or ifnull(supplier, '')!='' # like ifnull(customer, '')!='' or ifnull(supplier, '')!=''
conditions.append("ifnull(`tab{doctype}`.`{fieldname}`, '')!=''".format(doctype=doctype, fieldname=df.fieldname)) conditions.append(
"ifnull(`tab{doctype}`.`{fieldname}`, '')!=''".format(doctype=doctype, fieldname=df.fieldname)
)
return "( " + " or ".join(conditions) + " )" return "( " + " or ".join(conditions) + " )"
def get_permitted_and_not_permitted_links(doctype): def get_permitted_and_not_permitted_links(doctype):
permitted_links = [] permitted_links = []
not_permitted_links = [] not_permitted_links = []
@ -129,40 +150,40 @@ def get_permitted_and_not_permitted_links(doctype):
else: else:
not_permitted_links.append(df) not_permitted_links.append(df)
return { return {"permitted_links": permitted_links, "not_permitted_links": not_permitted_links}
"permitted_links": permitted_links,
"not_permitted_links": not_permitted_links
}
def delete_contact_and_address(doctype, docname): def delete_contact_and_address(doctype, docname):
for parenttype in ('Contact', 'Address'): for parenttype in ("Contact", "Address"):
items = frappe.db.sql_list("""select parent from `tabDynamic Link` items = frappe.db.sql_list(
"""select parent from `tabDynamic Link`
where parenttype=%s and link_doctype=%s and link_name=%s""", where parenttype=%s and link_doctype=%s and link_name=%s""",
(parenttype, doctype, docname)) (parenttype, doctype, docname),
)
for name in items: for name in items:
doc = frappe.get_doc(parenttype, name) doc = frappe.get_doc(parenttype, name)
if len(doc.links)==1: if len(doc.links) == 1:
doc.delete() doc.delete()
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, filters): def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, filters):
if not txt: txt = "" if not txt:
txt = ""
doctypes = frappe.db.get_all("DocField", filters=filters, fields=["parent"], doctypes = frappe.db.get_all(
distinct=True, as_list=True) "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({ filters.update({"dt": ("not in", [d[0] for d in doctypes])})
"dt": ("not in", [d[0] for d in doctypes])
})
_doctypes = frappe.db.get_all("Custom Field", filters=filters, fields=["dt"], _doctypes = frappe.db.get_all("Custom Field", filters=filters, fields=["dt"], as_list=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)])
all_doctypes = [d[0] for d in doctypes + _doctypes] all_doctypes = [d[0] for d in doctypes + _doctypes]
allowed_doctypes = frappe.permissions.get_doctypes_with_read() allowed_doctypes = frappe.permissions.get_doctypes_with_read()
@ -172,6 +193,7 @@ def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, fil
return valid_doctypes return valid_doctypes
def set_link_title(doc): def set_link_title(doc):
if not doc.links: if not doc.links:
return return

View file

@ -2,16 +2,15 @@
# Copyright (c) 2015, Frappe Technologies and contributors # Copyright (c) 2015, Frappe Technologies and contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe
from frappe import throw, _
from frappe.utils import cstr
from frappe.model.document import Document
from jinja2 import TemplateSyntaxError from jinja2 import TemplateSyntaxError
from frappe.model.naming import make_autoname
from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links import frappe
from frappe import _, throw
from frappe.contacts.address_and_contact import set_link_title 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 make_autoname
from frappe.utils import cstr
class Address(Document): class Address(Document):
@ -24,10 +23,11 @@ class Address(Document):
self.address_title = self.links[0].link_name self.address_title = self.links[0].link_name
if self.address_title: if self.address_title:
self.name = (cstr(self.address_title).strip() + "-" + cstr(_(self.address_type)).strip()) self.name = cstr(self.address_title).strip() + "-" + cstr(_(self.address_type)).strip()
if frappe.db.exists("Address", self.name): if frappe.db.exists("Address", self.name):
self.name = make_autoname(cstr(self.address_title).strip() + "-" + self.name = make_autoname(
cstr(self.address_type).strip() + "-.#") cstr(self.address_title).strip() + "-" + cstr(self.address_type).strip() + "-.#"
)
else: else:
throw(_("Address Title is mandatory.")) throw(_("Address Title is mandatory."))
@ -42,15 +42,15 @@ class Address(Document):
if not self.links: if not self.links:
contact_name = frappe.db.get_value("Contact", {"email_id": self.owner}) contact_name = frappe.db.get_value("Contact", {"email_id": self.owner})
if contact_name: if contact_name:
contact = frappe.get_cached_doc('Contact', contact_name) contact = frappe.get_cached_doc("Contact", contact_name)
for link in contact.links: for link in contact.links:
self.append('links', dict(link_doctype=link.link_doctype, link_name=link.link_name)) self.append("links", dict(link_doctype=link.link_doctype, link_name=link.link_name))
return True return True
return False return False
def validate_preferred_address(self): def validate_preferred_address(self):
preferred_fields = ['is_primary_address', 'is_shipping_address'] preferred_fields = ["is_primary_address", "is_shipping_address"]
for field in preferred_fields: for field in preferred_fields:
if self.get(field): if self.get(field):
@ -76,9 +76,11 @@ class Address(Document):
return False return False
def get_preferred_address(doctype, name, preferred_key='is_primary_address'):
if preferred_key in ['is_shipping_address', 'is_primary_address']: def get_preferred_address(doctype, name, preferred_key="is_primary_address"):
address = frappe.db.sql(""" SELECT if preferred_key in ["is_shipping_address", "is_primary_address"]:
address = frappe.db.sql(
""" SELECT
addr.name addr.name
FROM FROM
`tabAddress` addr, `tabDynamic Link` dl `tabAddress` addr, `tabDynamic Link` dl
@ -86,27 +88,37 @@ def get_preferred_address(doctype, name, preferred_key='is_primary_address'):
dl.parent = addr.name and dl.link_doctype = %s and dl.parent = addr.name and dl.link_doctype = %s and
dl.link_name = %s and ifnull(addr.disabled, 0) = 0 and dl.link_name = %s and ifnull(addr.disabled, 0) = 0 and
%s = %s %s = %s
""" % ('%s', '%s', preferred_key, '%s'), (doctype, name, 1), as_dict=1) """
% ("%s", "%s", preferred_key, "%s"),
(doctype, name, 1),
as_dict=1,
)
if address: if address:
return address[0].name return address[0].name
return return
@frappe.whitelist() @frappe.whitelist()
def get_default_address(doctype, name, sort_key='is_primary_address'): def get_default_address(doctype, name, sort_key="is_primary_address"):
'''Returns default Address name for the given doctype, name''' """Returns default Address name for the given doctype, name"""
if sort_key not in ['is_shipping_address', 'is_primary_address']: if sort_key not in ["is_shipping_address", "is_primary_address"]:
return None return None
out = frappe.db.sql(""" SELECT out = frappe.db.sql(
""" SELECT
addr.name, addr.%s addr.name, addr.%s
FROM FROM
`tabAddress` addr, `tabDynamic Link` dl `tabAddress` addr, `tabDynamic Link` dl
WHERE WHERE
dl.parent = addr.name and dl.link_doctype = %s and dl.parent = addr.name and dl.link_doctype = %s and
dl.link_name = %s and ifnull(addr.disabled, 0) = 0 dl.link_name = %s and ifnull(addr.disabled, 0) = 0
""" %(sort_key, '%s', '%s'), (doctype, name), as_dict=True) """
% (sort_key, "%s", "%s"),
(doctype, name),
as_dict=True,
)
if out: if out:
for contact in out: for contact in out:
@ -150,84 +162,96 @@ def get_territory_from_address(address):
return territory return territory
def get_list_context(context=None): def get_list_context(context=None):
return { return {
"title": _("Addresses"), "title": _("Addresses"),
"get_list": get_address_list, "get_list": get_address_list,
"row_template": "templates/includes/address_row.html", "row_template": "templates/includes/address_row.html",
'no_breadcrumbs': True, "no_breadcrumbs": True,
} }
def get_address_list(doctype, txt, filters, limit_start, limit_page_length = 20, order_by = None):
def get_address_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by=None):
from frappe.www.list import get_list from frappe.www.list import get_list
user = frappe.session.user user = frappe.session.user
ignore_permissions = True ignore_permissions = True
if not filters: filters = [] if not filters:
filters = []
filters.append(("Address", "owner", "=", user)) filters.append(("Address", "owner", "=", user))
return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions) return get_list(
doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions
)
def has_website_permission(doc, ptype, user, verbose=False): def has_website_permission(doc, ptype, user, verbose=False):
"""Returns true if there is a related lead or contact related to this document""" """Returns true if there is a related lead or contact related to this document"""
contact_name = frappe.db.get_value("Contact", {"email_id": frappe.session.user}) contact_name = frappe.db.get_value("Contact", {"email_id": frappe.session.user})
if contact_name: if contact_name:
contact = frappe.get_doc('Contact', contact_name) contact = frappe.get_doc("Contact", contact_name)
return contact.has_common_link(doc) return contact.has_common_link(doc)
return False return False
def get_address_templates(address): def get_address_templates(address):
result = frappe.db.get_value("Address Template", \ result = frappe.db.get_value(
{"country": address.get("country")}, ["name", "template"]) "Address Template", {"country": address.get("country")}, ["name", "template"]
)
if not result: if not result:
result = frappe.db.get_value("Address Template", \ result = frappe.db.get_value("Address Template", {"is_default": 1}, ["name", "template"])
{"is_default": 1}, ["name", "template"])
if not result: if not result:
frappe.throw(_("No default Address Template found. Please create a new one from Setup > Printing and Branding > Address Template.")) frappe.throw(
_(
"No default Address Template found. Please create a new one from Setup > Printing and Branding > Address Template."
)
)
else: else:
return result return result
def get_company_address(company): def get_company_address(company):
ret = frappe._dict() ret = frappe._dict()
ret.company_address = get_default_address('Company', company) ret.company_address = get_default_address("Company", company)
ret.company_address_display = get_address_display(ret.company_address) ret.company_address_display = get_address_display(ret.company_address)
return ret return ret
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def address_query(doctype, txt, searchfield, start, page_len, filters): def address_query(doctype, txt, searchfield, start, page_len, filters):
from frappe.desk.reportview import get_match_cond from frappe.desk.reportview import get_match_cond
link_doctype = filters.pop('link_doctype') link_doctype = filters.pop("link_doctype")
link_name = filters.pop('link_name') link_name = filters.pop("link_name")
condition = "" condition = ""
meta = frappe.get_meta("Address") meta = frappe.get_meta("Address")
for fieldname, value in filters.items(): for fieldname, value in filters.items():
if meta.get_field(fieldname) or fieldname in frappe.db.DEFAULT_COLUMNS: if meta.get_field(fieldname) or fieldname in frappe.db.DEFAULT_COLUMNS:
condition += " and {field}={value}".format( condition += " and {field}={value}".format(field=fieldname, value=frappe.db.escape(value))
field=fieldname,
value=frappe.db.escape(value))
searchfields = meta.get_search_fields() searchfields = meta.get_search_fields()
if searchfield and (meta.get_field(searchfield)\ if searchfield and (meta.get_field(searchfield) or searchfield in frappe.db.DEFAULT_COLUMNS):
or searchfield in frappe.db.DEFAULT_COLUMNS):
searchfields.append(searchfield) searchfields.append(searchfield)
search_condition = '' search_condition = ""
for field in searchfields: for field in searchfields:
if search_condition == '': if search_condition == "":
search_condition += '`tabAddress`.`{field}` like %(txt)s'.format(field=field) search_condition += "`tabAddress`.`{field}` like %(txt)s".format(field=field)
else: else:
search_condition += ' or `tabAddress`.`{field}` like %(txt)s'.format(field=field) search_condition += " or `tabAddress`.`{field}` like %(txt)s".format(field=field)
return frappe.db.sql("""select return frappe.db.sql(
"""select
`tabAddress`.name, `tabAddress`.city, `tabAddress`.country `tabAddress`.name, `tabAddress`.city, `tabAddress`.country
from from
`tabAddress`, `tabDynamic Link` `tabAddress`, `tabDynamic Link`
@ -245,19 +269,24 @@ def address_query(doctype, txt, searchfield, start, page_len, filters):
limit %(start)s, %(page_len)s """.format( limit %(start)s, %(page_len)s """.format(
mcond=get_match_cond(doctype), mcond=get_match_cond(doctype),
key=searchfield, key=searchfield,
search_condition = search_condition, search_condition=search_condition,
condition=condition or ""), { condition=condition or "",
'txt': '%' + txt + '%', ),
'_txt': txt.replace("%", ""), {
'start': start, "txt": "%" + txt + "%",
'page_len': page_len, "_txt": txt.replace("%", ""),
'link_name': link_name, "start": start,
'link_doctype': link_doctype "page_len": page_len,
}) "link_name": link_name,
"link_doctype": link_doctype,
},
)
def get_condensed_address(doc): def get_condensed_address(doc):
fields = ["address_title", "address_line1", "address_line2", "city", "county", "state", "country"] fields = ["address_title", "address_line1", "address_line2", "city", "county", "state", "country"]
return ", ".join(doc.get(d) for d in fields if doc.get(d)) return ", ".join(doc.get(d) for d in fields if doc.get(d))
def update_preferred_address(address, field): def update_preferred_address(address, field):
frappe.db.set_value('Address', address, field, 0) frappe.db.set_value("Address", address, field, 0)

View file

@ -1,31 +1,32 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors # Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe, unittest import unittest
import frappe
from frappe.contacts.doctype.address.address import get_address_display from frappe.contacts.doctype.address.address import get_address_display
class TestAddress(unittest.TestCase): class TestAddress(unittest.TestCase):
def test_template_works(self): def test_template_works(self):
if not frappe.db.exists('Address Template', 'India'): if not frappe.db.exists("Address Template", "India"):
frappe.get_doc({ frappe.get_doc({"doctype": "Address Template", "country": "India", "is_default": 1}).insert()
"doctype": "Address Template",
"country": 'India',
"is_default": 1
}).insert()
if not frappe.db.exists('Address', '_Test Address-Office'): if not frappe.db.exists("Address", "_Test Address-Office"):
frappe.get_doc({ frappe.get_doc(
"address_line1": "_Test Address Line 1", {
"address_title": "_Test Address", "address_line1": "_Test Address Line 1",
"address_type": "Office", "address_title": "_Test Address",
"city": "_Test City", "address_type": "Office",
"state": "Test State", "city": "_Test City",
"country": "India", "state": "Test State",
"doctype": "Address", "country": "India",
"is_primary_address": 1, "doctype": "Address",
"phone": "+91 0000000000" "is_primary_address": 1,
}).insert() "phone": "+91 0000000000",
}
).insert()
address = frappe.get_list("Address")[0].name address = frappe.get_list("Address")[0].name
display = get_address_display(frappe.get_doc("Address", address).as_dict()) display = get_address_display(frappe.get_doc("Address", address).as_dict())
self.assertTrue(display) self.assertTrue(display)

View file

@ -3,21 +3,24 @@
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe import frappe
from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint from frappe.utils import cint
from frappe.utils.jinja import validate_template from frappe.utils.jinja import validate_template
from frappe import _
class AddressTemplate(Document): class AddressTemplate(Document):
def validate(self): def validate(self):
if not self.template: if not self.template:
self.template = get_default_address_template() self.template = get_default_address_template()
self.defaults = frappe.db.get_values("Address Template", {"is_default":1, "name":("!=", self.name)}) self.defaults = frappe.db.get_values(
"Address Template", {"is_default": 1, "name": ("!=", self.name)}
)
if not self.is_default: if not self.is_default:
if not self.defaults: if not self.defaults:
self.is_default = 1 self.is_default = 1
if cint(frappe.db.get_single_value('System Settings', 'setup_complete')): if cint(frappe.db.get_single_value("System Settings", "setup_complete")):
frappe.msgprint(_("Setting this Address Template as default as there is no other default")) frappe.msgprint(_("Setting this Address Template as default as there is no other default"))
validate_template(self.template) validate_template(self.template)
@ -31,14 +34,23 @@ class AddressTemplate(Document):
if self.is_default: if self.is_default:
frappe.throw(_("Default Address Template cannot be deleted")) frappe.throw(_("Default Address Template cannot be deleted"))
@frappe.whitelist() @frappe.whitelist()
def get_default_address_template(): def get_default_address_template():
'''Get default address template (translated)''' """Get default address template (translated)"""
return '''{{ address_line1 }}<br>{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}\ return (
"""{{ address_line1 }}<br>{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}\
{{ city }}<br> {{ city }}<br>
{% if state %}{{ state }}<br>{% endif -%} {% if state %}{{ state }}<br>{% endif -%}
{% if pincode %}{{ pincode }}<br>{% endif -%} {% if pincode %}{{ pincode }}<br>{% endif -%}
{{ country }}<br> {{ country }}<br>
{% if phone %}'''+_('Phone')+''': {{ phone }}<br>{% endif -%} {% if phone %}"""
{% if fax %}'''+_('Fax')+''': {{ fax }}<br>{% endif -%} + _("Phone")
{% if email_id %}'''+_('Email')+''': {{ email_id }}<br>{% endif -%}''' + """: {{ phone }}<br>{% endif -%}
{% if fax %}"""
+ _("Fax")
+ """: {{ fax }}<br>{% endif -%}
{% if email_id %}"""
+ _("Email")
+ """: {{ email_id }}<br>{% endif -%}"""
)

View file

@ -1,7 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors # Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe, unittest import unittest
import frappe
class TestAddressTemplate(unittest.TestCase): class TestAddressTemplate(unittest.TestCase):
def setUp(self): def setUp(self):
@ -27,17 +30,12 @@ class TestAddressTemplate(unittest.TestCase):
def make_default_address_template(self): def make_default_address_template(self):
template = """{{ address_line1 }}<br>{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}{{ city }}<br>{% if state %}{{ state }}<br>{% endif -%}{% if pincode %}{{ pincode }}<br>{% endif -%}{{ country }}<br>{% if phone %}Phone: {{ phone }}<br>{% endif -%}{% if fax %}Fax: {{ fax }}<br>{% endif -%}{% if email_id %}Email: {{ email_id }}<br>{% endif -%}""" template = """{{ address_line1 }}<br>{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}{{ city }}<br>{% if state %}{{ state }}<br>{% endif -%}{% if pincode %}{{ pincode }}<br>{% endif -%}{{ country }}<br>{% if phone %}Phone: {{ phone }}<br>{% endif -%}{% if fax %}Fax: {{ fax }}<br>{% endif -%}{% if email_id %}Email: {{ email_id }}<br>{% endif -%}"""
if not frappe.db.exists('Address Template', 'India'): if not frappe.db.exists("Address Template", "India"):
frappe.get_doc({ frappe.get_doc(
"doctype": "Address Template", {"doctype": "Address Template", "country": "India", "is_default": 1, "template": template}
"country": 'India', ).insert()
"is_default": 1,
"template": template
}).insert()
if not frappe.db.exists('Address Template', 'Brazil'): if not frappe.db.exists("Address Template", "Brazil"):
frappe.get_doc({ frappe.get_doc(
"doctype": "Address Template", {"doctype": "Address Template", "country": "Brazil", "template": template}
"country": 'Brazil', ).insert()
"template": template
}).insert()

View file

@ -1,26 +1,27 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe import frappe
from frappe.utils import cstr, has_gravatar
from frappe import _ from frappe import _
from frappe.model.document import Document
from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links
from frappe.model.naming import append_number_if_name_exists
from frappe.contacts.address_and_contact import set_link_title 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.utils import cstr, has_gravatar
class Contact(Document): class Contact(Document):
def autoname(self): def autoname(self):
# concat first and last name # concat first and last name
self.name = " ".join(filter(None, self.name = " ".join(
[cstr(self.get(f)).strip() for f in ["first_name", "last_name"]])) filter(None, [cstr(self.get(f)).strip() for f in ["first_name", "last_name"]])
)
if frappe.db.exists("Contact", self.name): if frappe.db.exists("Contact", self.name):
self.name = append_number_if_name_exists('Contact', self.name) self.name = append_number_if_name_exists("Contact", self.name)
# concat party name if reqd # concat party name if reqd
for link in self.links: for link in self.links:
self.name = self.name + '-' + link.link_name.strip() self.name = self.name + "-" + link.link_name.strip()
break break
def validate(self): def validate(self):
@ -45,7 +46,7 @@ class Contact(Document):
self.user = frappe.db.get_value("User", {"email": self.email_id}) self.user = frappe.db.get_value("User", {"email": self.email_id})
def get_link_for(self, link_doctype): def get_link_for(self, link_doctype):
'''Return the link name, if exists for the given link DocType''' """Return the link name, if exists for the given link DocType"""
for link in self.links: for link in self.links:
if link.link_doctype == link_doctype: if link.link_doctype == link_doctype:
return link.link_name return link.link_name
@ -65,21 +66,21 @@ class Contact(Document):
def add_email(self, email_id, is_primary=0, autosave=False): def add_email(self, email_id, is_primary=0, autosave=False):
if not frappe.db.exists("Contact Email", {"email_id": email_id, "parent": self.name}): if not frappe.db.exists("Contact Email", {"email_id": email_id, "parent": self.name}):
self.append("email_ids", { self.append("email_ids", {"email_id": email_id, "is_primary": is_primary})
"email_id": email_id,
"is_primary": is_primary
})
if autosave: if autosave:
self.save(ignore_permissions=True) self.save(ignore_permissions=True)
def add_phone(self, phone, is_primary_phone=0, is_primary_mobile_no=0, autosave=False): def add_phone(self, phone, is_primary_phone=0, is_primary_mobile_no=0, autosave=False):
if not frappe.db.exists("Contact Phone", {"phone": phone, "parent": self.name}): if not frappe.db.exists("Contact Phone", {"phone": phone, "parent": self.name}):
self.append("phone_nos", { self.append(
"phone": phone, "phone_nos",
"is_primary_phone": is_primary_phone, {
"is_primary_mobile_no": is_primary_mobile_no "phone": phone,
}) "is_primary_phone": is_primary_phone,
"is_primary_mobile_no": is_primary_mobile_no,
},
)
if autosave: if autosave:
self.save(ignore_permissions=True) self.save(ignore_permissions=True)
@ -113,7 +114,9 @@ class Contact(Document):
is_primary = [phone.phone for phone in self.phone_nos if phone.get(field_name)] is_primary = [phone.phone for phone in self.phone_nos if phone.get(field_name)]
if len(is_primary) > 1: if len(is_primary) > 1:
frappe.throw(_("Only one {0} can be set as primary.").format(frappe.bold(frappe.unscrub(fieldname)))) frappe.throw(
_("Only one {0} can be set as primary.").format(frappe.bold(frappe.unscrub(fieldname)))
)
primary_number_exists = False primary_number_exists = False
for d in self.phone_nos: for d in self.phone_nos:
@ -125,9 +128,11 @@ class Contact(Document):
if not primary_number_exists: if not primary_number_exists:
setattr(self, fieldname, "") setattr(self, fieldname, "")
def get_default_contact(doctype, name): def get_default_contact(doctype, name):
'''Returns default contact for the given doctype, name''' """Returns default contact for the given doctype, name"""
out = frappe.db.sql('''select parent, out = frappe.db.sql(
'''select parent,
IFNULL((select is_primary_contact from tabContact c where c.name = dl.parent), 0) IFNULL((select is_primary_contact from tabContact c where c.name = dl.parent), 0)
as is_primary_contact as is_primary_contact
from from
@ -135,7 +140,10 @@ def get_default_contact(doctype, name):
where where
dl.link_doctype=%s and dl.link_doctype=%s and
dl.link_name=%s and dl.link_name=%s and
dl.parenttype = "Contact"''', (doctype, name), as_dict=True) dl.parenttype = "Contact"''',
(doctype, name),
as_dict=True,
)
if out: if out:
for contact in out: for contact in out:
@ -145,6 +153,7 @@ def get_default_contact(doctype, name):
else: else:
return None return None
@frappe.whitelist() @frappe.whitelist()
def invite_user(contact): def invite_user(contact):
contact = frappe.get_doc("Contact", contact) contact = frappe.get_doc("Contact", contact)
@ -153,34 +162,39 @@ def invite_user(contact):
frappe.throw(_("Please set Email Address")) frappe.throw(_("Please set Email Address"))
if contact.has_permission("write"): if contact.has_permission("write"):
user = frappe.get_doc({ user = frappe.get_doc(
"doctype": "User", {
"first_name": contact.first_name, "doctype": "User",
"last_name": contact.last_name, "first_name": contact.first_name,
"email": contact.email_id, "last_name": contact.last_name,
"user_type": "Website User", "email": contact.email_id,
"send_welcome_email": 1 "user_type": "Website User",
}).insert(ignore_permissions = True) "send_welcome_email": 1,
}
).insert(ignore_permissions=True)
return user.name return user.name
@frappe.whitelist() @frappe.whitelist()
def get_contact_details(contact): def get_contact_details(contact):
contact = frappe.get_doc("Contact", contact) contact = frappe.get_doc("Contact", contact)
out = { out = {
"contact_person": contact.get("name"), "contact_person": contact.get("name"),
"contact_display": " ".join(filter(None, "contact_display": " ".join(
[contact.get("salutation"), contact.get("first_name"), contact.get("last_name")])), filter(None, [contact.get("salutation"), contact.get("first_name"), contact.get("last_name")])
),
"contact_email": contact.get("email_id"), "contact_email": contact.get("email_id"),
"contact_mobile": contact.get("mobile_no"), "contact_mobile": contact.get("mobile_no"),
"contact_phone": contact.get("phone"), "contact_phone": contact.get("phone"),
"contact_designation": contact.get("designation"), "contact_designation": contact.get("designation"),
"contact_department": contact.get("department") "contact_department": contact.get("department"),
} }
return out return out
def update_contact(doc, method): def update_contact(doc, method):
'''Update contact when user is updated, if contact is found. Called via hooks''' """Update contact when user is updated, if contact is found. Called via hooks"""
contact_name = frappe.db.get_value("Contact", {"email_id": doc.name}) contact_name = frappe.db.get_value("Contact", {"email_id": doc.name})
if contact_name: if contact_name:
contact = frappe.get_doc("Contact", contact_name) contact = frappe.get_doc("Contact", contact_name)
@ -190,19 +204,23 @@ def update_contact(doc, method):
contact.flags.ignore_mandatory = True contact.flags.ignore_mandatory = True
contact.save(ignore_permissions=True) contact.save(ignore_permissions=True)
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def contact_query(doctype, txt, searchfield, start, page_len, filters): def contact_query(doctype, txt, searchfield, start, page_len, filters):
from frappe.desk.reportview import get_match_cond from frappe.desk.reportview import get_match_cond
if not frappe.get_meta("Contact").get_field(searchfield)\ if (
and searchfield not in frappe.db.DEFAULT_COLUMNS: not frappe.get_meta("Contact").get_field(searchfield)
and searchfield not in frappe.db.DEFAULT_COLUMNS
):
return [] return []
link_doctype = filters.pop('link_doctype') link_doctype = filters.pop("link_doctype")
link_name = filters.pop('link_name') link_name = filters.pop("link_name")
return frappe.db.sql("""select return frappe.db.sql(
"""select
`tabContact`.name, `tabContact`.first_name, `tabContact`.last_name `tabContact`.name, `tabContact`.first_name, `tabContact`.last_name
from from
`tabContact`, `tabDynamic Link` `tabContact`, `tabDynamic Link`
@ -216,68 +234,90 @@ def contact_query(doctype, txt, searchfield, start, page_len, filters):
order by order by
if(locate(%(_txt)s, `tabContact`.name), locate(%(_txt)s, `tabContact`.name), 99999), if(locate(%(_txt)s, `tabContact`.name), locate(%(_txt)s, `tabContact`.name), 99999),
`tabContact`.idx desc, `tabContact`.name `tabContact`.idx desc, `tabContact`.name
limit %(start)s, %(page_len)s """.format(mcond=get_match_cond(doctype), key=searchfield), { limit %(start)s, %(page_len)s """.format(
'txt': '%' + txt + '%', mcond=get_match_cond(doctype), key=searchfield
'_txt': txt.replace("%", ""), ),
'start': start, {
'page_len': page_len, "txt": "%" + txt + "%",
'link_name': link_name, "_txt": txt.replace("%", ""),
'link_doctype': link_doctype "start": start,
}) "page_len": page_len,
"link_name": link_name,
"link_doctype": link_doctype,
},
)
@frappe.whitelist() @frappe.whitelist()
def address_query(links): def address_query(links):
import json import json
links = [{"link_doctype": d.get("link_doctype"), "link_name": d.get("link_name")} for d in json.loads(links)] links = [
{"link_doctype": d.get("link_doctype"), "link_name": d.get("link_name")}
for d in json.loads(links)
]
result = [] result = []
for link in links: for link in links:
if not frappe.has_permission(doctype=link.get("link_doctype"), ptype="read", doc=link.get("link_name")): if not frappe.has_permission(
doctype=link.get("link_doctype"), ptype="read", doc=link.get("link_name")
):
continue continue
res = frappe.db.sql(""" res = frappe.db.sql(
"""
SELECT `tabAddress`.name SELECT `tabAddress`.name
FROM `tabAddress`, `tabDynamic Link` FROM `tabAddress`, `tabDynamic Link`
WHERE `tabDynamic Link`.parenttype='Address' WHERE `tabDynamic Link`.parenttype='Address'
AND `tabDynamic Link`.parent=`tabAddress`.name AND `tabDynamic Link`.parent=`tabAddress`.name
AND `tabDynamic Link`.link_doctype = %(link_doctype)s AND `tabDynamic Link`.link_doctype = %(link_doctype)s
AND `tabDynamic Link`.link_name = %(link_name)s AND `tabDynamic Link`.link_name = %(link_name)s
""", { """,
"link_doctype": link.get("link_doctype"), {
"link_name": link.get("link_name"), "link_doctype": link.get("link_doctype"),
}, as_dict=True) "link_name": link.get("link_name"),
},
as_dict=True,
)
result.extend([l.name for l in res]) result.extend([l.name for l in res])
return result return result
def get_contact_with_phone_number(number):
if not number: return
contacts = frappe.get_all('Contact Phone', filters=[ def get_contact_with_phone_number(number):
['phone', 'like', '%{0}'.format(number)] if not number:
], fields=["parent"], limit=1) return
contacts = frappe.get_all(
"Contact Phone", filters=[["phone", "like", "%{0}".format(number)]], fields=["parent"], limit=1
)
return contacts[0].parent if contacts else None return contacts[0].parent if contacts else None
def get_contact_name(email_id): def get_contact_name(email_id):
contact = frappe.get_all("Contact Email", filters={"email_id": email_id}, fields=["parent"], limit=1) contact = frappe.get_all(
"Contact Email", filters={"email_id": email_id}, fields=["parent"], limit=1
)
return contact[0].parent if contact else None return contact[0].parent if contact else None
def get_contacts_linking_to(doctype, docname, fields=None): def get_contacts_linking_to(doctype, docname, fields=None):
"""Return a list of contacts containing a link to the given document.""" """Return a list of contacts containing a link to the given document."""
return frappe.get_list('Contact', fields=fields, filters=[ return frappe.get_list(
['Dynamic Link', 'link_doctype', '=', doctype], "Contact",
['Dynamic Link', 'link_name', '=', docname] fields=fields,
]) filters=[
["Dynamic Link", "link_doctype", "=", doctype],
["Dynamic Link", "link_name", "=", docname],
],
)
def get_contacts_linked_from(doctype, docname, fields=None): def get_contacts_linked_from(doctype, docname, fields=None):
"""Return a list of contacts that are contained in (linked from) the given document.""" """Return a list of contacts that are contained in (linked from) the given document."""
link_fields = frappe.get_meta(doctype).get('fields', { link_fields = frappe.get_meta(doctype).get("fields", {"fieldtype": "Link", "options": "Contact"})
'fieldtype': 'Link',
'options': 'Contact'
})
if not link_fields: if not link_fields:
return [] return []
@ -285,6 +325,4 @@ def get_contacts_linked_from(doctype, docname, fields=None):
if not contact_names: if not contact_names:
return [] return []
return frappe.get_list('Contact', fields=fields, filters={ return frappe.get_list("Contact", fields=fields, filters={"name": ("in", contact_names)})
'name': ('in', contact_names)
})

View file

@ -1,13 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors # Copyright (c) 2017, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe
import unittest import unittest
test_dependencies = ['Contact', 'Salutation'] import frappe
test_dependencies = ["Contact", "Salutation"]
class TestContact(unittest.TestCase): class TestContact(unittest.TestCase):
def test_check_default_email(self): def test_check_default_email(self):
emails = [ emails = [
{"email": "test1@example.com", "is_primary": 0}, {"email": "test1@example.com", "is_primary": 0},
@ -32,13 +33,11 @@ class TestContact(unittest.TestCase):
self.assertEqual(contact.phone, "+91 0000000002") self.assertEqual(contact.phone, "+91 0000000002")
self.assertEqual(contact.mobile_no, "+91 0000000003") self.assertEqual(contact.mobile_no, "+91 0000000003")
def create_contact(name, salutation, emails=None, phones=None, save=True): def create_contact(name, salutation, emails=None, phones=None, save=True):
doc = frappe.get_doc({ doc = frappe.get_doc(
"doctype": "Contact", {"doctype": "Contact", "first_name": name, "status": "Open", "salutation": salutation}
"first_name": name, )
"status": "Open",
"salutation": salutation
})
if emails: if emails:
for d in emails: for d in emails:

View file

@ -5,5 +5,6 @@
# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document
class ContactEmail(Document): class ContactEmail(Document):
pass pass

View file

@ -5,5 +5,6 @@
# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document
class ContactPhone(Document): class ContactPhone(Document):
pass pass

View file

@ -4,5 +4,6 @@
from frappe.model.document import Document from frappe.model.document import Document
class Gender(Document): class Gender(Document):
pass pass

View file

@ -3,5 +3,6 @@
# License: MIT. See LICENSE # License: MIT. See LICENSE
import unittest import unittest
class TestGender(unittest.TestCase): class TestGender(unittest.TestCase):
pass pass

View file

@ -4,5 +4,6 @@
from frappe.model.document import Document from frappe.model.document import Document
class Salutation(Document): class Salutation(Document):
pass pass

View file

@ -3,5 +3,6 @@
# License: MIT. See LICENSE # License: MIT. See LICENSE
import unittest import unittest
class TestSalutation(unittest.TestCase): class TestSalutation(unittest.TestCase):
pass pass

View file

@ -4,17 +4,37 @@ import frappe
from frappe import _ from frappe import _
field_map = { field_map = {
"Contact": ["first_name", "last_name", "address", "phone", "mobile_no", "email_id", "is_primary_contact"], "Contact": [
"Address": ["address_line1", "address_line2", "city", "state", "pincode", "country", "is_primary_address"] "first_name",
"last_name",
"address",
"phone",
"mobile_no",
"email_id",
"is_primary_contact",
],
"Address": [
"address_line1",
"address_line2",
"city",
"state",
"pincode",
"country",
"is_primary_address",
],
} }
def execute(filters=None): def execute(filters=None):
columns, data = get_columns(filters), get_data(filters) columns, data = get_columns(filters), get_data(filters)
return columns, data return columns, data
def get_columns(filters): def get_columns(filters):
return [ return [
"{reference_doctype}:Link/{reference_doctype}".format(reference_doctype=filters.get("reference_doctype")), "{reference_doctype}:Link/{reference_doctype}".format(
reference_doctype=filters.get("reference_doctype")
),
"Address Line 1", "Address Line 1",
"Address Line 2", "Address Line 2",
"City", "City",
@ -27,9 +47,10 @@ def get_columns(filters):
"Address", "Address",
"Phone", "Phone",
"Email Id", "Email Id",
"Is Primary Contact:Check" "Is Primary Contact:Check",
] ]
def get_data(filters): def get_data(filters):
data = [] data = []
reference_doctype = filters.get("reference_doctype") reference_doctype = filters.get("reference_doctype")
@ -37,6 +58,7 @@ def get_data(filters):
return get_reference_addresses_and_contact(reference_doctype, reference_name) return get_reference_addresses_and_contact(reference_doctype, reference_name)
def get_reference_addresses_and_contact(reference_doctype, reference_name): def get_reference_addresses_and_contact(reference_doctype, reference_name):
data = [] data = []
filters = None filters = None
@ -48,16 +70,22 @@ def get_reference_addresses_and_contact(reference_doctype, reference_name):
if reference_name: if reference_name:
filters = {"name": reference_name} filters = {"name": reference_name}
reference_list = [d[0] for d in frappe.get_list(reference_doctype, filters=filters, fields=["name"], as_list=True)] reference_list = [
d[0] for d in frappe.get_list(reference_doctype, filters=filters, fields=["name"], as_list=True)
]
for d in reference_list: for d in reference_list:
reference_details.setdefault(d, frappe._dict()) reference_details.setdefault(d, frappe._dict())
reference_details = get_reference_details(reference_doctype, "Address", reference_list, reference_details) reference_details = get_reference_details(
reference_details = get_reference_details(reference_doctype, "Contact", reference_list, reference_details) reference_doctype, "Address", reference_list, reference_details
)
reference_details = get_reference_details(
reference_doctype, "Contact", reference_list, reference_details
)
for reference_name, details in reference_details.items(): for reference_name, details in reference_details.items():
addresses = details.get("address", []) addresses = details.get("address", [])
contacts = details.get("contact", []) contacts = details.get("contact", [])
if not any([addresses, contacts]): if not any([addresses, contacts]):
result = [reference_name] result = [reference_name]
result.extend(add_blank_columns_for("Address")) result.extend(add_blank_columns_for("Address"))
@ -78,10 +106,11 @@ def get_reference_addresses_and_contact(reference_doctype, reference_name):
return data return data
def get_reference_details(reference_doctype, doctype, reference_list, reference_details): def get_reference_details(reference_doctype, doctype, reference_list, reference_details):
filters = [ filters = [
["Dynamic Link", "link_doctype", "=", reference_doctype], ["Dynamic Link", "link_doctype", "=", reference_doctype],
["Dynamic Link", "link_name", "in", reference_list] ["Dynamic Link", "link_name", "in", reference_list],
] ]
fields = ["`tabDynamic Link`.link_name"] + field_map.get(doctype, []) fields = ["`tabDynamic Link`.link_name"] + field_map.get(doctype, [])
@ -97,5 +126,6 @@ def get_reference_details(reference_doctype, doctype, reference_list, reference_
reference_details[reference_list[0]][frappe.scrub(doctype)] = temp_records reference_details[reference_list[0]][frappe.scrub(doctype)] = temp_records
return reference_details return reference_details
def add_blank_columns_for(doctype): def add_blank_columns_for(doctype):
return ["" for field in field_map.get(doctype, [])] return ["" for field in field_map.get(doctype, [])]

View file

@ -1,95 +1,87 @@
import unittest
import frappe import frappe
import frappe.defaults import frappe.defaults
import unittest
from frappe.contacts.report.addresses_and_contacts.addresses_and_contacts import get_data from frappe.contacts.report.addresses_and_contacts.addresses_and_contacts import get_data
def get_custom_linked_doctype(): def get_custom_linked_doctype():
if bool(frappe.get_all("DocType", filters={'name':'Test Custom Doctype'})): if bool(frappe.get_all("DocType", filters={"name": "Test Custom Doctype"})):
return return
doc = frappe.get_doc({ doc = frappe.get_doc(
"doctype": "DocType",
"module": "Core",
"custom": 1,
"fields": [{
"label": "Test Field",
"fieldname": "test_field",
"fieldtype": "Data"
},
{ {
"label": "Contact HTML", "doctype": "DocType",
"fieldname": "contact_html", "module": "Core",
"fieldtype": "HTML" "custom": 1,
}, "fields": [
{ {"label": "Test Field", "fieldname": "test_field", "fieldtype": "Data"},
"label": "Address HTML", {"label": "Contact HTML", "fieldname": "contact_html", "fieldtype": "HTML"},
"fieldname": "address_html", {"label": "Address HTML", "fieldname": "address_html", "fieldtype": "HTML"},
"fieldtype": "HTML" ],
}], "permissions": [{"role": "System Manager", "read": 1}],
"permissions": [{ "name": "Test Custom Doctype",
"role": "System Manager", }
"read": 1 )
}],
"name": "Test Custom Doctype",
})
doc.insert() doc.insert()
def get_custom_doc_for_address_and_contacts(): def get_custom_doc_for_address_and_contacts():
get_custom_linked_doctype() get_custom_linked_doctype()
linked_doc = frappe.get_doc({ linked_doc = frappe.get_doc(
"doctype": "Test Custom Doctype", {
"test_field": "Hello", "doctype": "Test Custom Doctype",
}).insert() "test_field": "Hello",
}
).insert()
return linked_doc return linked_doc
def create_linked_address(link_list): def create_linked_address(link_list):
if frappe.flags.test_address_created: if frappe.flags.test_address_created:
return return
address = frappe.get_doc({ address = frappe.get_doc(
"doctype": "Address", {
"address_title": "_Test Address", "doctype": "Address",
"address_type": "Billing", "address_title": "_Test Address",
"address_line1": "test address line 1", "address_type": "Billing",
"address_line2": "test address line 2", "address_line1": "test address line 1",
"city": "Milan", "address_line2": "test address line 2",
"country": "Italy" "city": "Milan",
}) "country": "Italy",
}
)
for name in link_list: for name in link_list:
address.append("links",{ address.append("links", {"link_doctype": "Test Custom Doctype", "link_name": name})
'link_doctype': 'Test Custom Doctype',
'link_name': name
})
address.insert() address.insert()
frappe.flags.test_address_created = True frappe.flags.test_address_created = True
return address.name return address.name
def create_linked_contact(link_list, address): def create_linked_contact(link_list, address):
if frappe.flags.test_contact_created: if frappe.flags.test_contact_created:
return return
contact = frappe.get_doc({ contact = frappe.get_doc(
"doctype": "Contact", {
"salutation": "Mr", "doctype": "Contact",
"first_name": "_Test First Name", "salutation": "Mr",
"last_name": "_Test Last Name", "first_name": "_Test First Name",
"is_primary_contact": 1, "last_name": "_Test Last Name",
"address": address, "is_primary_contact": 1,
"status": "Open" "address": address,
}) "status": "Open",
}
)
contact.add_email("test_contact@example.com", is_primary=True) contact.add_email("test_contact@example.com", is_primary=True)
contact.add_phone("+91 0000000000", is_primary_phone=True) contact.add_phone("+91 0000000000", is_primary_phone=True)
for name in link_list: for name in link_list:
contact.append("links",{ contact.append("links", {"link_doctype": "Test Custom Doctype", "link_name": name})
'link_doctype': 'Test Custom Doctype',
'link_name': name
})
contact.insert(ignore_permissions=True) contact.insert(ignore_permissions=True)
frappe.flags.test_contact_created = True frappe.flags.test_contact_created = True
@ -103,7 +95,23 @@ class TestAddressesAndContacts(unittest.TestCase):
create_linked_contact(links_list, d) create_linked_contact(links_list, d)
report_data = get_data({"reference_doctype": "Test Custom Doctype"}) report_data = get_data({"reference_doctype": "Test Custom Doctype"})
for idx, link in enumerate(links_list): for idx, link in enumerate(links_list):
test_item = [link, 'test address line 1', 'test address line 2', 'Milan', None, None, 'Italy', 0, '_Test First Name', '_Test Last Name', '_Test Address-Billing', '+91 0000000000', '', 'test_contact@example.com', 1] test_item = [
link,
"test address line 1",
"test address line 2",
"Milan",
None,
None,
"Italy",
0,
"_Test First Name",
"_Test Last Name",
"_Test Address-Billing",
"+91 0000000000",
"",
"test_contact@example.com",
1,
]
self.assertListEqual(test_item, report_data[idx]) self.assertListEqual(test_item, report_data[idx])
def tearDown(self): def tearDown(self):

View file

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

View file

@ -1,9 +1,10 @@
# Copyright (c) 2021, Frappe Technologies and contributors # Copyright (c) 2021, Frappe Technologies and contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe
from frappe.utils import cstr
from tenacity import retry, retry_if_exception_type, stop_after_attempt from tenacity import retry, retry_if_exception_type, stop_after_attempt
import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cstr
class AccessLog(Document): class AccessLog(Document):
@ -22,14 +23,19 @@ def make_access_log(
columns=None, columns=None,
): ):
_make_access_log( _make_access_log(
doctype, document, method, file_type, report_name, filters, page, columns, doctype,
document,
method,
file_type,
report_name,
filters,
page,
columns,
) )
@frappe.write_only() @frappe.write_only()
@retry( @retry(stop=stop_after_attempt(3), retry=retry_if_exception_type(frappe.DuplicateEntryError))
stop=stop_after_attempt(3), retry=retry_if_exception_type(frappe.DuplicateEntryError)
)
def _make_access_log( def _make_access_log(
doctype=None, doctype=None,
document=None, document=None,
@ -43,18 +49,20 @@ def _make_access_log(
user = frappe.session.user user = frappe.session.user
in_request = frappe.request and frappe.request.method == "GET" in_request = frappe.request and frappe.request.method == "GET"
frappe.get_doc({ frappe.get_doc(
"doctype": "Access Log", {
"user": user, "doctype": "Access Log",
"export_from": doctype, "user": user,
"reference_document": document, "export_from": doctype,
"file_type": file_type, "reference_document": document,
"report_name": report_name, "file_type": file_type,
"page": page, "report_name": report_name,
"method": method, "page": page,
"filters": cstr(filters) or None, "method": method,
"columns": columns, "filters": cstr(filters) or None,
}).db_insert() "columns": columns,
}
).db_insert()
# `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview` # `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview`
# dont commit in test mode. It must be tempting to put this block along with the in_request in the # dont commit in test mode. It must be tempting to put this block along with the in_request in the

View file

@ -2,20 +2,21 @@
# Copyright (c) 2019, Frappe Technologies and Contributors # Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
# imports - standard imports
import unittest
import base64 import base64
import os import os
# imports - standard imports
import unittest
# imports - third party imports
import requests
# imports - module imports # imports - module imports
import frappe import frappe
from frappe.core.doctype.access_log.access_log import make_access_log from frappe.core.doctype.access_log.access_log import make_access_log
from frappe.utils import cstr, get_site_url
from frappe.core.doctype.data_import.data_import import export_csv from frappe.core.doctype.data_import.data_import import export_csv
from frappe.core.doctype.user.user import generate_keys from frappe.core.doctype.user.user import generate_keys
from frappe.utils import cstr, get_site_url
# imports - third party imports
import requests
class TestAccessLog(unittest.TestCase): class TestAccessLog(unittest.TestCase):
@ -23,8 +24,9 @@ class TestAccessLog(unittest.TestCase):
# generate keys for current user to send requests for the following tests # generate keys for current user to send requests for the following tests
generate_keys(frappe.session.user) generate_keys(frappe.session.user)
frappe.db.commit() frappe.db.commit()
generated_secret = frappe.utils.password.get_decrypted_password("User", generated_secret = frappe.utils.password.get_decrypted_password(
frappe.session.user, fieldname='api_secret') "User", frappe.session.user, fieldname="api_secret"
)
api_key = frappe.db.get_value("User", "Administrator", "api_key") api_key = frappe.db.get_value("User", "Administrator", "api_key")
self.header = {"Authorization": "token {}:{}".format(api_key, generated_secret)} self.header = {"Authorization": "token {}:{}".format(api_key, generated_secret)}
@ -101,54 +103,55 @@ class TestAccessLog(unittest.TestCase):
"party": [], "party": [],
"group_by": "Group by Voucher (Consolidated)", "group_by": "Group by Voucher (Consolidated)",
"cost_center": [], "cost_center": [],
"project": [] "project": [],
} }
self.test_doctype = 'File' self.test_doctype = "File"
self.test_document = 'Test Document' self.test_document = "Test Document"
self.test_report_name = 'General Ledger' self.test_report_name = "General Ledger"
self.test_file_type = 'CSV' self.test_file_type = "CSV"
self.test_method = 'Test Method' self.test_method = "Test Method"
self.file_name = frappe.utils.random_string(10) + '.txt' self.file_name = frappe.utils.random_string(10) + ".txt"
self.test_content = frappe.utils.random_string(1024) self.test_content = frappe.utils.random_string(1024)
def test_make_full_access_log(self): def test_make_full_access_log(self):
self.maxDiff = None self.maxDiff = None
# test if all fields maintain data: html page and filters are converted? # test if all fields maintain data: html page and filters are converted?
make_access_log(doctype=self.test_doctype, make_access_log(
doctype=self.test_doctype,
document=self.test_document, document=self.test_document,
report_name=self.test_report_name, report_name=self.test_report_name,
page=self.test_html_template, page=self.test_html_template,
file_type=self.test_file_type, file_type=self.test_file_type,
method=self.test_method, method=self.test_method,
filters=self.test_filters) filters=self.test_filters,
)
last_doc = frappe.get_last_doc('Access Log') last_doc = frappe.get_last_doc("Access Log")
self.assertEqual(last_doc.filters, cstr(self.test_filters)) self.assertEqual(last_doc.filters, cstr(self.test_filters))
self.assertEqual(self.test_doctype, last_doc.export_from) self.assertEqual(self.test_doctype, last_doc.export_from)
self.assertEqual(self.test_document, last_doc.reference_document) self.assertEqual(self.test_document, last_doc.reference_document)
def test_make_export_log(self): def test_make_export_log(self):
# export data and delete temp file generated on disk # export data and delete temp file generated on disk
export_csv(self.test_doctype, self.file_name) export_csv(self.test_doctype, self.file_name)
os.remove(self.file_name) os.remove(self.file_name)
# test if the exported data is logged # test if the exported data is logged
last_doc = frappe.get_last_doc('Access Log') last_doc = frappe.get_last_doc("Access Log")
self.assertEqual(self.test_doctype, last_doc.export_from) self.assertEqual(self.test_doctype, last_doc.export_from)
def test_private_file_download(self): def test_private_file_download(self):
# create new private file # create new private file
new_private_file = frappe.get_doc({ new_private_file = frappe.get_doc(
'doctype': self.test_doctype, {
'file_name': self.file_name, "doctype": self.test_doctype,
'content': base64.b64encode(self.test_content.encode('utf-8')), "file_name": self.file_name,
'is_private': 1, "content": base64.b64encode(self.test_content.encode("utf-8")),
}) "is_private": 1,
}
)
new_private_file.insert() new_private_file.insert()
# access the created file # access the created file
@ -156,7 +159,7 @@ class TestAccessLog(unittest.TestCase):
try: try:
request = requests.post(private_file_link, headers=self.header) request = requests.post(private_file_link, headers=self.header)
last_doc = frappe.get_last_doc('Access Log') last_doc = frappe.get_last_doc("Access Log")
if request.ok: if request.ok:
# check for the access log of downloaded file # check for the access log of downloaded file
@ -169,6 +172,5 @@ class TestAccessLog(unittest.TestCase):
# cleanup # cleanup
new_private_file.delete() new_private_file.delete()
def tearDown(self): def tearDown(self):
pass pass

View file

@ -26,20 +26,25 @@ class ActivityLog(Document):
if self.reference_doctype and self.reference_name: if self.reference_doctype and self.reference_name:
self.status = "Linked" self.status = "Linked"
def on_doctype_update(): def on_doctype_update():
"""Add indexes in `tabActivity Log`""" """Add indexes in `tabActivity Log`"""
frappe.db.add_index("Activity Log", ["reference_doctype", "reference_name"]) frappe.db.add_index("Activity Log", ["reference_doctype", "reference_name"])
frappe.db.add_index("Activity Log", ["timeline_doctype", "timeline_name"]) frappe.db.add_index("Activity Log", ["timeline_doctype", "timeline_name"])
frappe.db.add_index("Activity Log", ["link_doctype", "link_name"]) frappe.db.add_index("Activity Log", ["link_doctype", "link_name"])
def add_authentication_log(subject, user, operation="Login", status="Success"): def add_authentication_log(subject, user, operation="Login", status="Success"):
frappe.get_doc({ frappe.get_doc(
"doctype": "Activity Log", {
"user": user, "doctype": "Activity Log",
"status": status, "user": user,
"subject": subject, "status": status,
"operation": operation, "subject": subject,
}).insert(ignore_permissions=True, ignore_links=True) "operation": operation,
}
).insert(ignore_permissions=True, ignore_links=True)
def clear_activity_logs(days=None): def clear_activity_logs(days=None):
"""clear 90 day old authentication logs or configured in log settings""" """clear 90 day old authentication logs or configured in log settings"""
@ -47,6 +52,4 @@ def clear_activity_logs(days=None):
if not days: if not days:
days = 90 days = 90
doctype = DocType("Activity Log") doctype = DocType("Activity Log")
frappe.db.delete(doctype, filters=( frappe.db.delete(doctype, filters=(doctype.creation < (Now() - Interval(days=days))))
doctype.creation < (Now() - Interval(days=days))
))

View file

@ -3,15 +3,16 @@
import frappe import frappe
import frappe.permissions import frappe.permissions
from frappe.utils import get_fullname
from frappe import _ from frappe import _
from frappe.core.doctype.activity_log.activity_log import add_authentication_log from frappe.core.doctype.activity_log.activity_log import add_authentication_log
from frappe.utils import get_fullname
def update_feed(doc, method=None): def update_feed(doc, method=None):
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import: if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import:
return return
if doc._action!="save" or doc.flags.ignore_feed: if doc._action != "save" or doc.flags.ignore_feed:
return return
if doc.doctype == "Activity Log" or doc.meta.issingle: if doc.doctype == "Activity Log" or doc.meta.issingle:
@ -29,65 +30,75 @@ def update_feed(doc, method=None):
name = feed.name or doc.name name = feed.name or doc.name
# delete earlier feed # delete earlier feed
frappe.db.delete("Activity Log", { frappe.db.delete(
"reference_doctype": doctype, "Activity Log",
"reference_name": name, {"reference_doctype": doctype, "reference_name": name, "link_doctype": feed.link_doctype},
"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.get_doc({
"doctype": "Activity Log",
"reference_doctype": doctype,
"reference_name": name,
"subject": feed.subject,
"full_name": get_fullname(doc.owner),
"reference_owner": frappe.db.get_value(doctype, name, "owner"),
"link_doctype": feed.link_doctype,
"link_name": feed.link_name
}).insert(ignore_permissions=True)
def login_feed(login_manager): def login_feed(login_manager):
if login_manager.user != "Guest": if login_manager.user != "Guest":
subject = _("{0} logged in").format(get_fullname(login_manager.user)) subject = _("{0} logged in").format(get_fullname(login_manager.user))
add_authentication_log(subject, login_manager.user) add_authentication_log(subject, login_manager.user)
def logout_feed(user, reason): def logout_feed(user, reason):
if user and user != "Guest": if user and user != "Guest":
subject = _("{0} logged out: {1}").format(get_fullname(user), frappe.bold(reason)) subject = _("{0} logged out: {1}").format(get_fullname(user), frappe.bold(reason))
add_authentication_log(subject, user, operation="Logout") add_authentication_log(subject, user, operation="Logout")
def get_feed_match_conditions(user=None, doctype='Comment'):
if not user: user = frappe.session.user
conditions = ['`tab{doctype}`.owner={user} or `tab{doctype}`.reference_owner={user}'.format( def get_feed_match_conditions(user=None, doctype="Comment"):
user = frappe.db.escape(user), if not user:
doctype = doctype user = frappe.session.user
)]
conditions = [
"`tab{doctype}`.owner={user} or `tab{doctype}`.reference_owner={user}".format(
user=frappe.db.escape(user), doctype=doctype
)
]
user_permissions = frappe.permissions.get_user_permissions(user) user_permissions = frappe.permissions.get_user_permissions(user)
can_read = frappe.get_user().get_can_read() can_read = frappe.get_user().get_can_read()
can_read_doctypes = ["'{}'".format(dt) for dt in can_read_doctypes = [
list(set(can_read) - set(list(user_permissions)))] "'{}'".format(dt) for dt in list(set(can_read) - set(list(user_permissions)))
]
if can_read_doctypes: if can_read_doctypes:
conditions += ["""(`tab{doctype}`.reference_doctype is null conditions += [
"""(`tab{doctype}`.reference_doctype is null
or `tab{doctype}`.reference_doctype = '' or `tab{doctype}`.reference_doctype = ''
or `tab{doctype}`.reference_doctype or `tab{doctype}`.reference_doctype
in ({values}))""".format( in ({values}))""".format(
doctype = doctype, doctype=doctype, values=", ".join(can_read_doctypes)
values =", ".join(can_read_doctypes) )
)] ]
if user_permissions: if user_permissions:
can_read_docs = [] can_read_docs = []
for dt, obj in user_permissions.items(): for dt, obj in user_permissions.items():
for n in obj: for n in obj:
can_read_docs.append('{}|{}'.format(frappe.db.escape(dt), frappe.db.escape(n.get('doc', '')))) can_read_docs.append("{}|{}".format(frappe.db.escape(dt), frappe.db.escape(n.get("doc", ""))))
if can_read_docs: if can_read_docs:
conditions.append("concat_ws('|', `tab{doctype}`.reference_doctype, `tab{doctype}`.reference_name) in ({values})".format( conditions.append(
doctype = doctype, "concat_ws('|', `tab{doctype}`.reference_doctype, `tab{doctype}`.reference_name) in ({values})".format(
values = ", ".join(can_read_docs))) doctype=doctype, values=", ".join(can_read_docs)
)
)
return "(" + " or ".join(conditions) + ")" return "(" + " or ".join(conditions) + ")"

View file

@ -1,77 +1,74 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors # Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe
import unittest
import time import time
from frappe.auth import LoginManager, CookieManager import unittest
import frappe
from frappe.auth import CookieManager, LoginManager
class TestActivityLog(unittest.TestCase): class TestActivityLog(unittest.TestCase):
def test_activity_log(self): def test_activity_log(self):
# test user login log # test user login log
frappe.local.form_dict = frappe._dict({ frappe.local.form_dict = frappe._dict(
'cmd': 'login', {"cmd": "login", "sid": "Guest", "pwd": "admin", "usr": "Administrator"}
'sid': 'Guest', )
'pwd': 'admin',
'usr': 'Administrator'
})
frappe.local.cookie_manager = CookieManager() frappe.local.cookie_manager = CookieManager()
frappe.local.login_manager = LoginManager() frappe.local.login_manager = LoginManager()
auth_log = self.get_auth_log() auth_log = self.get_auth_log()
self.assertEqual(auth_log.status, 'Success') self.assertEqual(auth_log.status, "Success")
# test user logout log # test user logout log
frappe.local.login_manager.logout() frappe.local.login_manager.logout()
auth_log = self.get_auth_log(operation='Logout') auth_log = self.get_auth_log(operation="Logout")
self.assertEqual(auth_log.status, 'Success') self.assertEqual(auth_log.status, "Success")
# test invalid login # test invalid login
frappe.form_dict.update({ 'pwd': 'password' }) frappe.form_dict.update({"pwd": "password"})
self.assertRaises(frappe.AuthenticationError, LoginManager) self.assertRaises(frappe.AuthenticationError, LoginManager)
auth_log = self.get_auth_log() auth_log = self.get_auth_log()
self.assertEqual(auth_log.status, 'Failed') self.assertEqual(auth_log.status, "Failed")
frappe.local.form_dict = frappe._dict() frappe.local.form_dict = frappe._dict()
def get_auth_log(self, operation='Login'): def get_auth_log(self, operation="Login"):
names = frappe.db.get_all('Activity Log', filters={ names = frappe.db.get_all(
'user': 'Administrator', "Activity Log",
'operation': operation, filters={
}, order_by='`creation` DESC') "user": "Administrator",
"operation": operation,
},
order_by="`creation` DESC",
)
name = names[0] name = names[0]
auth_log = frappe.get_doc('Activity Log', name) auth_log = frappe.get_doc("Activity Log", name)
return auth_log return auth_log
def test_brute_security(self): def test_brute_security(self):
update_system_settings({ update_system_settings({"allow_consecutive_login_attempts": 3, "allow_login_after_fail": 5})
'allow_consecutive_login_attempts': 3,
'allow_login_after_fail': 5
})
frappe.local.form_dict = frappe._dict({ frappe.local.form_dict = frappe._dict(
'cmd': 'login', {"cmd": "login", "sid": "Guest", "pwd": "admin", "usr": "Administrator"}
'sid': 'Guest', )
'pwd': 'admin',
'usr': 'Administrator'
})
frappe.local.cookie_manager = CookieManager() frappe.local.cookie_manager = CookieManager()
frappe.local.login_manager = LoginManager() frappe.local.login_manager = LoginManager()
auth_log = self.get_auth_log() auth_log = self.get_auth_log()
self.assertEqual(auth_log.status, 'Success') self.assertEqual(auth_log.status, "Success")
# test user logout log # test user logout log
frappe.local.login_manager.logout() frappe.local.login_manager.logout()
auth_log = self.get_auth_log(operation='Logout') auth_log = self.get_auth_log(operation="Logout")
self.assertEqual(auth_log.status, 'Success') self.assertEqual(auth_log.status, "Success")
# test invalid login # test invalid login
frappe.form_dict.update({ 'pwd': 'password' }) frappe.form_dict.update({"pwd": "password"})
self.assertRaises(frappe.AuthenticationError, LoginManager) self.assertRaises(frappe.AuthenticationError, LoginManager)
self.assertRaises(frappe.AuthenticationError, LoginManager) self.assertRaises(frappe.AuthenticationError, LoginManager)
self.assertRaises(frappe.AuthenticationError, LoginManager) self.assertRaises(frappe.AuthenticationError, LoginManager)
@ -85,8 +82,9 @@ class TestActivityLog(unittest.TestCase):
frappe.local.form_dict = frappe._dict() frappe.local.form_dict = frappe._dict()
def update_system_settings(args): def update_system_settings(args):
doc = frappe.get_doc('System Settings') doc = frappe.get_doc("System Settings")
doc.update(args) doc.update(args)
doc.flags.ignore_mandatory = 1 doc.flags.ignore_mandatory = 1
doc.save() doc.save()

View file

@ -5,5 +5,6 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
class BlockModule(Document): class BlockModule(Document):
pass pass

View file

@ -1,22 +1,27 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors # Copyright (c) 2019, Frappe Technologies and contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import json
import frappe import frappe
from frappe import _ from frappe import _
import json
from frappe.model.document import Document
from frappe.core.doctype.user.user import extract_mentions from frappe.core.doctype.user.user import extract_mentions
from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification,\ from frappe.database.schema import add_column
get_title, get_title_html 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.utils import get_fullname
from frappe.website.utils import clear_cache from frappe.website.utils import clear_cache
from frappe.database.schema import add_column
from frappe.exceptions import ImplicitCommitError
class Comment(Document): class Comment(Document):
def after_insert(self): def after_insert(self):
self.notify_mentions() self.notify_mentions()
self.notify_change('add') self.notify_change("add")
def validate(self): def validate(self):
if not self.comment_email: if not self.comment_email:
@ -26,34 +31,35 @@ class Comment(Document):
def on_update(self): def on_update(self):
update_comment_in_doc(self) update_comment_in_doc(self)
if self.is_new(): if self.is_new():
self.notify_change('update') self.notify_change("update")
def on_trash(self): def on_trash(self):
self.remove_comment_from_cache() self.remove_comment_from_cache()
self.notify_change('delete') self.notify_change("delete")
def notify_change(self, action): def notify_change(self, action):
key_map = { key_map = {
'Like': 'like_logs', "Like": "like_logs",
'Assigned': 'assignment_logs', "Assigned": "assignment_logs",
'Assignment Completed': 'assignment_logs', "Assignment Completed": "assignment_logs",
'Comment': 'comments', "Comment": "comments",
'Attachment': 'attachment_logs', "Attachment": "attachment_logs",
'Attachment Removed': 'attachment_logs', "Attachment Removed": "attachment_logs",
} }
key = key_map.get(self.comment_type) key = key_map.get(self.comment_type)
if not key: return if not key:
return
frappe.publish_realtime('update_docinfo_for_{}_{}'.format(self.reference_doctype, self.reference_name), { frappe.publish_realtime(
'doc': self.as_dict(), "update_docinfo_for_{}_{}".format(self.reference_doctype, self.reference_name),
'key': key, {"doc": self.as_dict(), "key": key, "action": action},
'action': action after_commit=True,
}, after_commit=True) )
def remove_comment_from_cache(self): def remove_comment_from_cache(self):
_comments = get_comments_from_parent(self) _comments = get_comments_from_parent(self)
for c in _comments: for c in _comments:
if c.get("name")==self.name: if c.get("name") == self.name:
_comments.remove(c) _comments.remove(c)
update_comments_in_parent(self.reference_doctype, self.reference_name, _comments) update_comments_in_parent(self.reference_doctype, self.reference_name, _comments)
@ -68,19 +74,26 @@ class Comment(Document):
sender_fullname = get_fullname(frappe.session.user) sender_fullname = get_fullname(frappe.session.user)
title = get_title(self.reference_doctype, self.reference_name) title = get_title(self.reference_doctype, self.reference_name)
recipients = [frappe.db.get_value("User", {"enabled": 1, "name": name, "user_type": "System User", "allowed_in_mentions": 1}, "email") recipients = [
for name in mentions] 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}''')\ notification_message = _("""{0} mentioned you in a comment in {1} {2}""").format(
.format(frappe.bold(sender_fullname), frappe.bold(self.reference_doctype), get_title_html(title)) frappe.bold(sender_fullname), frappe.bold(self.reference_doctype), get_title_html(title)
)
notification_doc = { notification_doc = {
'type': 'Mention', "type": "Mention",
'document_type': self.reference_doctype, "document_type": self.reference_doctype,
'document_name': self.reference_name, "document_name": self.reference_name,
'subject': notification_message, "subject": notification_message,
'from_user': frappe.session.user, "from_user": frappe.session.user,
'email_content': self.content "email_content": self.content,
} }
enqueue_create_notification(recipients, notification_doc) enqueue_create_notification(recipients, notification_doc)
@ -99,45 +112,46 @@ def update_comment_in_doc(doc):
`_comments` format `_comments` format
{ {
"comment": [String], "comment": [String],
"by": [user], "by": [user],
"name": [Comment Document name] "name": [Comment Document name]
}""" }"""
# only comments get updates, not likes, assignments etc. # only comments get updates, not likes, assignments etc.
if doc.doctype == 'Comment' and doc.comment_type != 'Comment': if doc.doctype == "Comment" and doc.comment_type != "Comment":
return return
def get_truncated(content): def get_truncated(content):
return (content[:97] + '...') if len(content) > 100 else content return (content[:97] + "...") if len(content) > 100 else content
if doc.reference_doctype and doc.reference_name and doc.content: if doc.reference_doctype and doc.reference_name and doc.content:
_comments = get_comments_from_parent(doc) _comments = get_comments_from_parent(doc)
updated = False updated = False
for c in _comments: for c in _comments:
if c.get("name")==doc.name: if c.get("name") == doc.name:
c["comment"] = get_truncated(doc.content) c["comment"] = get_truncated(doc.content)
updated = True updated = True
if not updated: if not updated:
_comments.append({ _comments.append(
"comment": get_truncated(doc.content), {
"comment": get_truncated(doc.content),
# "comment_email" for Comment and "sender" for Communication # "comment_email" for Comment and "sender" for Communication
"by": getattr(doc, 'comment_email', None) or getattr(doc, 'sender', None) or doc.owner, "by": getattr(doc, "comment_email", None) or getattr(doc, "sender", None) or doc.owner,
"name": doc.name "name": doc.name,
}) }
)
update_comments_in_parent(doc.reference_doctype, doc.reference_name, _comments) update_comments_in_parent(doc.reference_doctype, doc.reference_name, _comments)
def get_comments_from_parent(doc): def get_comments_from_parent(doc):
''' """
get the list of comments cached in the document record in the column get the list of comments cached in the document record in the column
`_comments` `_comments`
''' """
try: try:
_comments = frappe.db.get_value(doc.reference_doctype, doc.reference_name, "_comments") or "[]" _comments = frappe.db.get_value(doc.reference_doctype, doc.reference_name, "_comments") or "[]"
@ -153,23 +167,32 @@ def get_comments_from_parent(doc):
except ValueError: except ValueError:
return [] return []
def update_comments_in_parent(reference_doctype, reference_name, _comments): def update_comments_in_parent(reference_doctype, reference_name, _comments):
"""Updates `_comments` property in parent Document with given dict. """Updates `_comments` property in parent Document with given dict.
:param _comments: Dict of comments.""" :param _comments: Dict of comments."""
if not reference_doctype or not reference_name or frappe.db.get_value("DocType", reference_doctype, "issingle") or frappe.db.get_value("DocType", reference_doctype, "is_virtual"): if (
not reference_doctype
or not reference_name
or frappe.db.get_value("DocType", reference_doctype, "issingle")
or frappe.db.get_value("DocType", reference_doctype, "is_virtual")
):
return return
try: try:
# use sql, so that we do not mess with the timestamp # use sql, so that we do not mess with the timestamp
frappe.db.sql("""update `tab{0}` set `_comments`=%s where name=%s""".format(reference_doctype), # nosec frappe.db.sql(
(json.dumps(_comments[-100:]), reference_name)) """update `tab{0}` set `_comments`=%s where name=%s""".format(reference_doctype), # nosec
(json.dumps(_comments[-100:]), reference_name),
)
except Exception as e: except Exception as e:
if frappe.db.is_column_missing(e) and getattr(frappe.local, 'request', None): if frappe.db.is_column_missing(e) and getattr(frappe.local, "request", None):
# missing column and in request, add column and update after commit # missing column and in request, add column and update after commit
frappe.local._comments = (getattr(frappe.local, "_comments", []) frappe.local._comments = getattr(frappe.local, "_comments", []) + [
+ [(reference_doctype, reference_name, _comments)]) (reference_doctype, reference_name, _comments)
]
elif frappe.db.is_data_too_long(e): elif frappe.db.is_data_too_long(e):
raise frappe.DataTooLongException raise frappe.DataTooLongException
@ -183,6 +206,7 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments):
if getattr(reference_doc, "route", None): if getattr(reference_doc, "route", None):
clear_cache(reference_doc.route) clear_cache(reference_doc.route)
def update_comments_in_parent_after_request(): def update_comments_in_parent_after_request():
"""update _comments in parent if _comments column is missing""" """update _comments in parent if _comments column is missing"""
if hasattr(frappe.local, "_comments"): if hasattr(frappe.local, "_comments"):

View file

@ -1,9 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors # Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe, json import json
import unittest import unittest
import frappe
class TestComment(unittest.TestCase): class TestComment(unittest.TestCase):
def tearDown(self): def tearDown(self):
frappe.form_dict.comment = None frappe.form_dict.comment = None
@ -15,75 +18,88 @@ class TestComment(unittest.TestCase):
frappe.local.request_ip = None frappe.local.request_ip = None
def test_comment_creation(self): def test_comment_creation(self):
test_doc = frappe.get_doc(dict(doctype = 'ToDo', description = 'test')) test_doc = frappe.get_doc(dict(doctype="ToDo", description="test"))
test_doc.insert() test_doc.insert()
comment = test_doc.add_comment('Comment', 'test comment') comment = test_doc.add_comment("Comment", "test comment")
test_doc.reload() test_doc.reload()
# check if updated in _comments cache # check if updated in _comments cache
comments = json.loads(test_doc.get('_comments')) comments = json.loads(test_doc.get("_comments"))
self.assertEqual(comments[0].get('name'), comment.name) self.assertEqual(comments[0].get("name"), comment.name)
self.assertEqual(comments[0].get('comment'), comment.content) self.assertEqual(comments[0].get("comment"), comment.content)
# check document creation # check document creation
comment_1 = frappe.get_all('Comment', fields = ['*'], filters = dict( comment_1 = frappe.get_all(
reference_doctype = test_doc.doctype, "Comment",
reference_name = test_doc.name fields=["*"],
))[0] filters=dict(reference_doctype=test_doc.doctype, reference_name=test_doc.name),
)[0]
self.assertEqual(comment_1.content, 'test comment') self.assertEqual(comment_1.content, "test comment")
# test via blog # test via blog
def test_public_comment(self): def test_public_comment(self):
from frappe.website.doctype.blog_post.test_blog_post import make_test_blog from frappe.website.doctype.blog_post.test_blog_post import make_test_blog
test_blog = make_test_blog() test_blog = make_test_blog()
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"}) frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})
from frappe.templates.includes.comments.comments import add_comment from frappe.templates.includes.comments.comments import add_comment
frappe.form_dict.comment = 'Good comment with 10 chars' frappe.form_dict.comment = "Good comment with 10 chars"
frappe.form_dict.comment_email = 'test@test.com' frappe.form_dict.comment_email = "test@test.com"
frappe.form_dict.comment_by = 'Good Tester' frappe.form_dict.comment_by = "Good Tester"
frappe.form_dict.reference_doctype = 'Blog Post' frappe.form_dict.reference_doctype = "Blog Post"
frappe.form_dict.reference_name = test_blog.name frappe.form_dict.reference_name = test_blog.name
frappe.form_dict.route = test_blog.route frappe.form_dict.route = test_blog.route
frappe.local.request_ip = '127.0.0.1' frappe.local.request_ip = "127.0.0.1"
add_comment() add_comment()
self.assertEqual(frappe.get_all('Comment', fields = ['*'], filters = dict( self.assertEqual(
reference_doctype = test_blog.doctype, frappe.get_all(
reference_name = test_blog.name "Comment",
))[0].published, 1) fields=["*"],
filters=dict(reference_doctype=test_blog.doctype, reference_name=test_blog.name),
)[0].published,
1,
)
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"}) frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})
frappe.form_dict.comment = 'pleez vizits my site http://mysite.com' frappe.form_dict.comment = "pleez vizits my site http://mysite.com"
frappe.form_dict.comment_by = 'bad commentor' frappe.form_dict.comment_by = "bad commentor"
add_comment() add_comment()
self.assertEqual(len(frappe.get_all('Comment', fields = ['*'], filters = dict( self.assertEqual(
reference_doctype = test_blog.doctype, len(
reference_name = test_blog.name frappe.get_all(
))), 0) "Comment",
fields=["*"],
filters=dict(reference_doctype=test_blog.doctype, reference_name=test_blog.name),
)
),
0,
)
# test for filtering html and css injection elements # test for filtering html and css injection elements
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"}) frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})
frappe.form_dict.comment = '<script>alert(1)</script>Comment' frappe.form_dict.comment = "<script>alert(1)</script>Comment"
frappe.form_dict.comment_by = 'hacker' frappe.form_dict.comment_by = "hacker"
add_comment() add_comment()
self.assertEqual(frappe.get_all('Comment', fields = ['content'], filters = dict( self.assertEqual(
reference_doctype = test_blog.doctype, frappe.get_all(
reference_name = test_blog.name "Comment",
))[0]['content'], 'Comment') fields=["content"],
filters=dict(reference_doctype=test_blog.doctype, reference_name=test_blog.name),
)[0]["content"],
"Comment",
)
test_blog.delete() test_blog.delete()

View file

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

View file

@ -2,49 +2,67 @@
# License: MIT. See LICENSE # License: MIT. See LICENSE
from collections import Counter from collections import Counter
from email.utils import getaddresses
from typing import List from typing import List
from urllib.parse import unquote
from parse import compile
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.automation.doctype.assignment_rule.assignment_rule import (
from frappe.utils import validate_email_address, strip_html, cstr, time_diff_in_seconds apply as apply_assignment_rule,
)
from frappe.contacts.doctype.contact.contact import get_contact_name
from frappe.core.doctype.comment.comment import update_comment_in_doc
from frappe.core.doctype.communication.email import validate_email from frappe.core.doctype.communication.email import validate_email
from frappe.core.doctype.communication.mixins import CommunicationEmailMixin from frappe.core.doctype.communication.mixins import CommunicationEmailMixin
from frappe.core.utils import get_parent_doc from frappe.core.utils import get_parent_doc
from frappe.utils import parse_addr, split_emails from frappe.model.document import Document
from frappe.core.doctype.comment.comment import update_comment_in_doc from frappe.utils import (
from email.utils import getaddresses cstr,
from urllib.parse import unquote parse_addr,
split_emails,
strip_html,
time_diff_in_seconds,
validate_email_address,
)
from frappe.utils.user import is_system_user from frappe.utils.user import is_system_user
from frappe.contacts.doctype.contact.contact import get_contact_name
from frappe.automation.doctype.assignment_rule.assignment_rule import apply as apply_assignment_rule
from parse import compile
exclude_from_linked_with = True exclude_from_linked_with = True
class Communication(Document, CommunicationEmailMixin): class Communication(Document, CommunicationEmailMixin):
"""Communication represents an external communication like Email. """Communication represents an external communication like Email."""
"""
no_feed_on_delete = True no_feed_on_delete = True
DOCTYPE = 'Communication' DOCTYPE = "Communication"
def onload(self): def onload(self):
"""create email flag queue""" """create email flag queue"""
if self.communication_type == "Communication" and self.communication_medium == "Email" \ if (
and self.sent_or_received == "Received" and self.uid and self.uid != -1: 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", { email_flag_queue = frappe.db.get_value(
"communication": self.name, "Email Flag Queue", {"communication": self.name, "is_completed": 0}
"is_completed": 0}) )
if email_flag_queue: if email_flag_queue:
return return
frappe.get_doc({ frappe.get_doc(
"doctype": "Email Flag Queue", {
"action": "Read", "doctype": "Email Flag Queue",
"communication": self.name, "action": "Read",
"uid": self.uid, "communication": self.name,
"email_account": self.email_account "uid": self.uid,
}).insert(ignore_permissions=True) "email_account": self.email_account,
}
).insert(ignore_permissions=True)
frappe.db.commit() frappe.db.commit()
def validate(self): def validate(self):
@ -74,25 +92,33 @@ class Communication(Document, CommunicationEmailMixin):
def validate_reference(self): def validate_reference(self):
if self.reference_doctype and self.reference_name: if self.reference_doctype and self.reference_name:
if not self.reference_owner: if not self.reference_owner:
self.reference_owner = frappe.db.get_value(self.reference_doctype, self.reference_name, "owner") self.reference_owner = frappe.db.get_value(
self.reference_doctype, self.reference_name, "owner"
)
# prevent communication against a child table # prevent communication against a child table
if frappe.get_meta(self.reference_doctype).istable: if frappe.get_meta(self.reference_doctype).istable:
frappe.throw(_("Cannot create a {0} against a child document: {1}") frappe.throw(
.format(_(self.communication_type), _(self.reference_doctype))) _("Cannot create a {0} against a child document: {1}").format(
_(self.communication_type), _(self.reference_doctype)
)
)
# Prevent circular linking of Communication DocTypes # Prevent circular linking of Communication DocTypes
if self.reference_doctype == "Communication": if self.reference_doctype == "Communication":
circular_linking = False circular_linking = False
doc = get_parent_doc(self) doc = get_parent_doc(self)
while doc.reference_doctype == "Communication": while doc.reference_doctype == "Communication":
if get_parent_doc(doc).name==self.name: if get_parent_doc(doc).name == self.name:
circular_linking = True circular_linking = True
break break
doc = get_parent_doc(doc) doc = get_parent_doc(doc)
if circular_linking: if circular_linking:
frappe.throw(_("Please make sure the Reference Communication Docs are not circularly linked."), frappe.CircularLinkingError) frappe.throw(
_("Please make sure the Reference Communication Docs are not circularly linked."),
frappe.CircularLinkingError,
)
def after_insert(self): def after_insert(self):
if not (self.reference_doctype and self.reference_name): if not (self.reference_doctype and self.reference_name):
@ -102,21 +128,21 @@ class Communication(Document, CommunicationEmailMixin):
frappe.db.set_value("Communication", self.reference_name, "status", "Replied") frappe.db.set_value("Communication", self.reference_name, "status", "Replied")
if self.communication_type == "Communication": if self.communication_type == "Communication":
self.notify_change('add') self.notify_change("add")
elif self.communication_type in ("Chat", "Notification"): elif self.communication_type in ("Chat", "Notification"):
if self.reference_name == frappe.session.user: if self.reference_name == frappe.session.user:
message = self.as_dict() message = self.as_dict()
message['broadcast'] = True message["broadcast"] = True
frappe.publish_realtime('new_message', message, after_commit=True) frappe.publish_realtime("new_message", message, after_commit=True)
else: else:
# reference_name contains the user who is addressed in the messages' page comment # reference_name contains the user who is addressed in the messages' page comment
frappe.publish_realtime('new_message', self.as_dict(), frappe.publish_realtime(
user=self.reference_name, after_commit=True) "new_message", self.as_dict(), user=self.reference_name, after_commit=True
)
def set_signature_in_email_content(self): def set_signature_in_email_content(self):
"""Set sender's User.email_signature or default outgoing's EmailAccount.signature to the email """Set sender's User.email_signature or default outgoing's EmailAccount.signature to the email"""
"""
if not self.content: if not self.content:
return return
@ -128,11 +154,15 @@ class Communication(Document, CommunicationEmailMixin):
email_body = email_body[0] email_body = email_body[0]
user_email_signature = frappe.db.get_value( user_email_signature = (
"User", frappe.db.get_value(
self.sender, "User",
"email_signature", self.sender,
) if self.sender else None "email_signature",
)
if self.sender
else None
)
signature = user_email_signature or frappe.db.get_value( signature = user_email_signature or frappe.db.get_value(
"Email Account", "Email Account",
@ -157,19 +187,19 @@ class Communication(Document, CommunicationEmailMixin):
# comments count for the list view # comments count for the list view
update_comment_in_doc(self) update_comment_in_doc(self)
if self.comment_type != 'Updated': if self.comment_type != "Updated":
update_parent_document_on_communication(self) update_parent_document_on_communication(self)
def on_trash(self): def on_trash(self):
if self.communication_type == "Communication": if self.communication_type == "Communication":
self.notify_change('delete') self.notify_change("delete")
@property @property
def sender_mailid(self): def sender_mailid(self):
return parse_addr(self.sender)[1] if self.sender else "" return parse_addr(self.sender)[1] if self.sender else ""
@staticmethod @staticmethod
def _get_emails_list(emails=None, exclude_displayname = False): def _get_emails_list(emails=None, exclude_displayname=False):
"""Returns list of emails from given email string. """Returns list of emails from given email string.
* Removes duplicate mailids * Removes duplicate mailids
@ -180,35 +210,32 @@ class Communication(Document, CommunicationEmailMixin):
return [email.lower() for email in set([parse_addr(email)[1] for email in emails]) if email] return [email.lower() for email in set([parse_addr(email)[1] for email in emails]) if email]
return [email.lower() for email in set(emails) if email] return [email.lower() for email in set(emails) if email]
def to_list(self, exclude_displayname = True): def to_list(self, exclude_displayname=True):
"""Returns to list. """Returns to list."""
"""
return self._get_emails_list(self.recipients, exclude_displayname=exclude_displayname) return self._get_emails_list(self.recipients, exclude_displayname=exclude_displayname)
def cc_list(self, exclude_displayname = True): def cc_list(self, exclude_displayname=True):
"""Returns cc list. """Returns cc list."""
"""
return self._get_emails_list(self.cc, exclude_displayname=exclude_displayname) return self._get_emails_list(self.cc, exclude_displayname=exclude_displayname)
def bcc_list(self, exclude_displayname = True): def bcc_list(self, exclude_displayname=True):
"""Returns bcc list. """Returns bcc list."""
"""
return self._get_emails_list(self.bcc, exclude_displayname=exclude_displayname) return self._get_emails_list(self.bcc, exclude_displayname=exclude_displayname)
def get_attachments(self): def get_attachments(self):
attachments = frappe.get_all( attachments = frappe.get_all(
"File", "File",
fields=["name", "file_name", "file_url", "is_private"], fields=["name", "file_name", "file_url", "is_private"],
filters = {"attached_to_name": self.name, "attached_to_doctype": self.DOCTYPE} filters={"attached_to_name": self.name, "attached_to_doctype": self.DOCTYPE},
) )
return attachments return attachments
def notify_change(self, action): def notify_change(self, action):
frappe.publish_realtime('update_docinfo_for_{}_{}'.format(self.reference_doctype, self.reference_name), { frappe.publish_realtime(
'doc': self.as_dict(), "update_docinfo_for_{}_{}".format(self.reference_doctype, self.reference_name),
'key': 'communications', {"doc": self.as_dict(), "key": "communications", "action": action},
'action': action after_commit=True,
}, after_commit=True) )
def set_status(self): def set_status(self):
if not self.is_new(): if not self.is_new():
@ -216,15 +243,19 @@ class Communication(Document, CommunicationEmailMixin):
if self.reference_doctype and self.reference_name: if self.reference_doctype and self.reference_name:
self.status = "Linked" self.status = "Linked"
elif self.communication_type=="Communication": elif self.communication_type == "Communication":
self.status = "Open" self.status = "Open"
else: else:
self.status = "Closed" self.status = "Closed"
# set email status to spam # set email status to spam
email_rule = frappe.db.get_value("Email Rule", { "email_id": self.sender, "is_spam":1 }) 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" \ if (
and self.sent_or_received == "Sent" and email_rule: self.communication_type == "Communication"
and self.communication_medium == "Email"
and self.sent_or_received == "Sent"
and email_rule
):
self.email_status = "Spam" self.email_status = "Spam"
@ -254,7 +285,7 @@ class Communication(Document, CommunicationEmailMixin):
self.sender_full_name = self.sender self.sender_full_name = self.sender
self.sender = None self.sender = None
else: else:
if self.sent_or_received=='Sent': if self.sent_or_received == "Sent":
validate_email_address(self.sender, throw=True) validate_email_address(self.sender, throw=True)
sender_name, sender_email = parse_addr(self.sender) sender_name, sender_email = parse_addr(self.sender)
if sender_name == sender_email: if sender_name == sender_email:
@ -264,40 +295,41 @@ class Communication(Document, CommunicationEmailMixin):
self.sender_full_name = sender_name self.sender_full_name = sender_name
if not self.sender_full_name: if not self.sender_full_name:
self.sender_full_name = frappe.db.get_value('User', self.sender, 'full_name') self.sender_full_name = frappe.db.get_value("User", self.sender, "full_name")
if not self.sender_full_name: if not self.sender_full_name:
first_name, last_name = frappe.db.get_value('Contact', first_name, last_name = frappe.db.get_value(
filters={'email_id': sender_email}, "Contact", filters={"email_id": sender_email}, fieldname=["first_name", "last_name"]
fieldname=['first_name', 'last_name']
) or [None, None] ) or [None, None]
self.sender_full_name = (first_name or '') + (last_name or '') self.sender_full_name = (first_name or "") + (last_name or "")
if not self.sender_full_name: if not self.sender_full_name:
self.sender_full_name = sender_email self.sender_full_name = sender_email
def set_delivery_status(self, commit=False): def set_delivery_status(self, commit=False):
'''Look into the status of Email Queue linked to this Communication and set the Delivery Status of this Communication''' """Look into the status of Email Queue linked to this Communication and set the Delivery Status of this Communication"""
delivery_status = None delivery_status = None
status_counts = Counter(frappe.get_all("Email Queue", pluck="status", filters={"communication": self.name})) status_counts = Counter(
frappe.get_all("Email Queue", pluck="status", filters={"communication": self.name})
)
if self.sent_or_received == "Received": if self.sent_or_received == "Received":
return return
if status_counts.get('Not Sent') or status_counts.get('Sending'): if status_counts.get("Not Sent") or status_counts.get("Sending"):
delivery_status = 'Sending' delivery_status = "Sending"
elif status_counts.get('Error'): elif status_counts.get("Error"):
delivery_status = 'Error' delivery_status = "Error"
elif status_counts.get('Expired'): elif status_counts.get("Expired"):
delivery_status = 'Expired' delivery_status = "Expired"
elif status_counts.get('Sent'): elif status_counts.get("Sent"):
delivery_status = 'Sent' delivery_status = "Sent"
if delivery_status: if delivery_status:
self.db_set('delivery_status', delivery_status) self.db_set("delivery_status", delivery_status)
self.notify_change('update') self.notify_change("update")
# for list views and forms # for list views and forms
self.notify_update() self.notify_update()
@ -311,13 +343,17 @@ class Communication(Document, CommunicationEmailMixin):
# Timeline Links # Timeline Links
def set_timeline_links(self): def set_timeline_links(self):
contacts = [] contacts = []
create_contact_enabled = self.email_account and frappe.db.get_value("Email Account", self.email_account, "create_contact") create_contact_enabled = self.email_account and frappe.db.get_value(
contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc], auto_create_contact=create_contact_enabled) "Email Account", self.email_account, "create_contact"
)
contacts = get_contacts(
[self.sender, self.recipients, self.cc, self.bcc], auto_create_contact=create_contact_enabled
)
for contact_name in contacts: for contact_name in contacts:
self.add_link('Contact', contact_name) self.add_link("Contact", contact_name)
#link contact's dynamic links to communication # link contact's dynamic links to communication
add_contact_links_to_communication(self, contact_name) add_contact_links_to_communication(self, contact_name)
def deduplicate_timeline_links(self): def deduplicate_timeline_links(self):
@ -332,17 +368,12 @@ class Communication(Document, CommunicationEmailMixin):
duplicate = True duplicate = True
if duplicate: if duplicate:
del self.timeline_links[:] # make it python 2 compatible as list.clear() is python 3 only del self.timeline_links[:] # make it python 2 compatible as list.clear() is python 3 only
for l in links: for l in links:
self.add_link(link_doctype=l[0], link_name=l[1]) self.add_link(link_doctype=l[0], link_name=l[1])
def add_link(self, link_doctype, link_name, autosave=False): def add_link(self, link_doctype, link_name, autosave=False):
self.append("timeline_links", self.append("timeline_links", {"link_doctype": link_doctype, "link_name": link_name})
{
"link_doctype": link_doctype,
"link_name": link_name
}
)
if autosave: if autosave:
self.save(ignore_permissions=True) self.save(ignore_permissions=True)
@ -358,13 +389,15 @@ class Communication(Document, CommunicationEmailMixin):
if autosave: if autosave:
self.save(ignore_permissions=ignore_permissions) self.save(ignore_permissions=ignore_permissions)
def on_doctype_update(): def on_doctype_update():
"""Add indexes in `tabCommunication`""" """Add indexes in `tabCommunication`"""
frappe.db.add_index("Communication", ["reference_doctype", "reference_name"]) frappe.db.add_index("Communication", ["reference_doctype", "reference_name"])
frappe.db.add_index("Communication", ["status", "communication_type"]) frappe.db.add_index("Communication", ["status", "communication_type"])
def has_permission(doc, ptype, user): def has_permission(doc, ptype, user):
if ptype=="read": if ptype == "read":
if doc.reference_doctype == "Communication" and doc.reference_name == doc.name: if doc.reference_doctype == "Communication" and doc.reference_name == doc.name:
return return
@ -372,24 +405,28 @@ def has_permission(doc, ptype, user):
if frappe.has_permission(doc.reference_doctype, ptype="read", doc=doc.reference_name): if frappe.has_permission(doc.reference_doctype, ptype="read", doc=doc.reference_name):
return True return True
def get_permission_query_conditions_for_communication(user): def get_permission_query_conditions_for_communication(user):
if not user: user = frappe.session.user if not user:
user = frappe.session.user
roles = frappe.get_roles(user) roles = frappe.get_roles(user)
if "Super Email User" in roles or "System Manager" in roles: if "Super Email User" in roles or "System Manager" in roles:
return None return None
else: else:
accounts = frappe.get_all("User Email", filters={ "parent": user }, accounts = frappe.get_all(
fields=["email_account"], "User Email", filters={"parent": user}, fields=["email_account"], distinct=True, order_by="idx"
distinct=True, order_by="idx") )
if not accounts: if not accounts:
return """`tabCommunication`.communication_medium!='Email'""" return """`tabCommunication`.communication_medium!='Email'"""
email_accounts = [ '"%s"'%account.get("email_account") for account in accounts ] email_accounts = ['"%s"' % account.get("email_account") for account in accounts]
return """`tabCommunication`.email_account in ({email_accounts})"""\ return """`tabCommunication`.email_account in ({email_accounts})""".format(
.format(email_accounts=','.join(email_accounts)) email_accounts=",".join(email_accounts)
)
def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[str]: def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[str]:
email_addrs = get_emails(email_strings) email_addrs = get_emails(email_strings)
@ -403,12 +440,12 @@ def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[st
first_name = frappe.unscrub(email_parts[0]) first_name = frappe.unscrub(email_parts[0])
try: try:
contact_name = '{0}-{1}'.format(first_name, email_parts[1]) if first_name == 'Contact' else first_name contact_name = (
contact = frappe.get_doc({ "{0}-{1}".format(first_name, email_parts[1]) if first_name == "Contact" else first_name
"doctype": "Contact", )
"first_name": contact_name, contact = frappe.get_doc(
"name": contact_name {"doctype": "Contact", "first_name": contact_name, "name": contact_name}
}) )
contact.add_email(email_id=email, is_primary=True) contact.add_email(email_id=email, is_primary=True)
contact.insert(ignore_permissions=True) contact.insert(ignore_permissions=True)
contact_name = contact.name contact_name = contact.name
@ -421,6 +458,7 @@ def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[st
return contacts return contacts
def get_emails(email_strings: List[str]) -> List[str]: def get_emails(email_strings: List[str]) -> List[str]:
email_addrs = [] email_addrs = []
@ -432,22 +470,25 @@ def get_emails(email_strings: List[str]) -> List[str]:
return email_addrs return email_addrs
def add_contact_links_to_communication(communication, contact_name): def add_contact_links_to_communication(communication, contact_name):
contact_links = frappe.get_all("Dynamic Link", filters={ contact_links = frappe.get_all(
"parenttype": "Contact", "Dynamic Link",
"parent": contact_name filters={"parenttype": "Contact", "parent": contact_name},
}, fields=["link_doctype", "link_name"]) fields=["link_doctype", "link_name"],
)
if contact_links: if contact_links:
for contact_link in contact_links: for contact_link in contact_links:
communication.add_link(contact_link.link_doctype, contact_link.link_name) communication.add_link(contact_link.link_doctype, contact_link.link_name)
def parse_email(communication, email_strings): def parse_email(communication, email_strings):
""" """
Parse email to add timeline links. Parse email to add timeline links.
When automatic email linking is enabled, an email from email_strings can contain 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`, 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. the email is parsed and doctype and docname is extracted and timeline link is added.
""" """
if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}): if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}):
return return
@ -469,10 +510,11 @@ def parse_email(communication, email_strings):
if doctype and docname and frappe.db.exists(doctype, docname): if doctype and docname and frappe.db.exists(doctype, docname):
communication.add_link(doctype, docname) communication.add_link(doctype, docname)
def get_email_without_link(email): def get_email_without_link(email):
""" """
returns email address without doctype links returns email address without doctype links
returns admin@example.com for email admin+doctype+docname@example.com returns admin@example.com for email admin+doctype+docname@example.com
""" """
if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}): if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}):
return email return email
@ -486,6 +528,7 @@ def get_email_without_link(email):
return "{0}@{1}".format(email_id, email_host) return "{0}@{1}".format(email_id, email_host)
def update_parent_document_on_communication(doc): def update_parent_document_on_communication(doc):
"""Update mins_to_first_communication of parent document based on who is replying.""" """Update mins_to_first_communication of parent document based on who is replying."""
@ -516,6 +559,7 @@ def update_parent_document_on_communication(doc):
parent.run_method("notify_communication", doc) parent.run_method("notify_communication", doc)
parent.notify_update() parent.notify_update()
def update_first_response_time(parent, communication): def update_first_response_time(parent, communication):
if parent.meta.has_field("first_response_time") and not parent.get("first_response_time"): if parent.meta.has_field("first_response_time") and not parent.get("first_response_time"):
if is_system_user(communication.sender): if is_system_user(communication.sender):
@ -526,25 +570,29 @@ def update_first_response_time(parent, communication):
first_response_time = round(time_diff_in_seconds(first_responded_on, parent.creation), 2) first_response_time = round(time_diff_in_seconds(first_responded_on, parent.creation), 2)
parent.db_set("first_response_time", first_response_time) parent.db_set("first_response_time", first_response_time)
def set_avg_response_time(parent, communication): def set_avg_response_time(parent, communication):
if parent.meta.has_field("avg_response_time") and communication.sent_or_received == "Sent": if parent.meta.has_field("avg_response_time") and communication.sent_or_received == "Sent":
# avg response time for all the responses # avg response time for all the responses
communications = frappe.get_list("Communication", filters={ communications = frappe.get_list(
"reference_doctype": parent.doctype, "Communication",
"reference_name": parent.name filters={"reference_doctype": parent.doctype, "reference_name": parent.name},
},
fields=["sent_or_received", "name", "creation"], fields=["sent_or_received", "name", "creation"],
order_by="creation" order_by="creation",
) )
if len(communications): if len(communications):
response_times = [] response_times = []
for i in range(len(communications)): for i in range(len(communications)):
if communications[i].sent_or_received == "Sent" and communications[i-1].sent_or_received == "Received": if (
response_time = round(time_diff_in_seconds(communications[i].creation, communications[i-1].creation), 2) communications[i].sent_or_received == "Sent"
and communications[i - 1].sent_or_received == "Received"
):
response_time = round(
time_diff_in_seconds(communications[i].creation, communications[i - 1].creation), 2
)
if response_time > 0: if response_time > 0:
response_times.append(response_time) response_times.append(response_time)
if response_times: if response_times:
avg_response_time = sum(response_times) / len(response_times) avg_response_time = sum(response_times) / len(response_times)
parent.db_set("avg_response_time", avg_response_time) parent.db_set("avg_response_time", avg_response_time)

View file

@ -8,17 +8,25 @@ import frappe
import frappe.email.smtp import frappe.email.smtp
from frappe import _ from frappe import _
from frappe.email.email_body import get_message_id from frappe.email.email_body import get_message_id
from frappe.utils import (cint, get_datetime, get_formatted_email, from frappe.utils import (
list_to_str, split_emails, validate_email_address) cint,
get_datetime,
get_formatted_email,
list_to_str,
split_emails,
validate_email_address,
)
if TYPE_CHECKING: if TYPE_CHECKING:
from frappe.core.doctype.communication.communication import Communication from frappe.core.doctype.communication.communication import Communication
OUTGOING_EMAIL_ACCOUNT_MISSING = _(""" OUTGOING_EMAIL_ACCOUNT_MISSING = _(
"""
Unable to send mail because of a missing email account. Unable to send mail because of a missing email account.
Please setup default Email Account from Setup > Email > Email Account Please setup default Email Account from Setup > Email > Email Account
""") """
)
@frappe.whitelist() @frappe.whitelist()
@ -64,16 +72,15 @@ def make(
""" """
if kwargs: if kwargs:
from frappe.utils.commands import warn from frappe.utils.commands import warn
warn( warn(
f"Options {kwargs} used in frappe.core.doctype.communication.email.make " f"Options {kwargs} used in frappe.core.doctype.communication.email.make "
"are deprecated or unsupported", "are deprecated or unsupported",
category=DeprecationWarning category=DeprecationWarning,
) )
if doctype and name and not frappe.has_permission(doctype=doctype, ptype="email", doc=name): if doctype and name and not frappe.has_permission(doctype=doctype, ptype="email", doc=name):
raise frappe.PermissionError( raise frappe.PermissionError(f"You are not allowed to send emails related to: {doctype} {name}")
f"You are not allowed to send emails related to: {doctype} {name}"
)
return _make( return _make(
doctype=doctype, doctype=doctype,
@ -123,33 +130,34 @@ def _make(
communication_type=None, communication_type=None,
add_signature=True, add_signature=True,
) -> Dict[str, str]: ) -> Dict[str, str]:
"""Internal method to make a new communication that ignores Permission checks. """Internal method to make a new communication that ignores Permission checks."""
"""
sender = sender or get_formatted_email(frappe.session.user) sender = sender or get_formatted_email(frappe.session.user)
recipients = list_to_str(recipients) if isinstance(recipients, list) else recipients recipients = list_to_str(recipients) if isinstance(recipients, list) else recipients
cc = list_to_str(cc) if isinstance(cc, list) else cc cc = list_to_str(cc) if isinstance(cc, list) else cc
bcc = list_to_str(bcc) if isinstance(bcc, list) else bcc bcc = list_to_str(bcc) if isinstance(bcc, list) else bcc
comm: "Communication" = frappe.get_doc({ comm: "Communication" = frappe.get_doc(
"doctype":"Communication", {
"subject": subject, "doctype": "Communication",
"content": content, "subject": subject,
"sender": sender, "content": content,
"sender_full_name":sender_full_name, "sender": sender,
"recipients": recipients, "sender_full_name": sender_full_name,
"cc": cc or None, "recipients": recipients,
"bcc": bcc or None, "cc": cc or None,
"communication_medium": communication_medium, "bcc": bcc or None,
"sent_or_received": sent_or_received, "communication_medium": communication_medium,
"reference_doctype": doctype, "sent_or_received": sent_or_received,
"reference_name": name, "reference_doctype": doctype,
"email_template": email_template, "reference_name": name,
"message_id":get_message_id().strip(" <>"), "email_template": email_template,
"read_receipt":read_receipt, "message_id": get_message_id().strip(" <>"),
"has_attachment": 1 if attachments else 0, "read_receipt": read_receipt,
"communication_type": communication_type, "has_attachment": 1 if attachments else 0,
}) "communication_type": communication_type,
}
)
comm.flags.skip_add_signature = not add_signature comm.flags.skip_add_signature = not add_signature
comm.insert(ignore_permissions=True) comm.insert(ignore_permissions=True)
@ -161,9 +169,7 @@ def _make(
if cint(send_email): if cint(send_email):
if not comm.get_outgoing_email_account(): if not comm.get_outgoing_email_account():
frappe.throw( frappe.throw(msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError)
msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError
)
comm.send_email( comm.send_email(
print_html=print_html, print_html=print_html,
@ -179,7 +185,10 @@ def _make(
def validate_email(doc: "Communication") -> None: def validate_email(doc: "Communication") -> None:
"""Validate Email Addresses of Recipients and CC""" """Validate Email Addresses of Recipients and CC"""
if not (doc.communication_type=="Communication" and doc.communication_medium == "Email") or doc.flags.in_receive: if (
not (doc.communication_type == "Communication" and doc.communication_medium == "Email")
or doc.flags.in_receive
):
return return
# validate recipients # validate recipients
@ -193,36 +202,45 @@ def validate_email(doc: "Communication") -> None:
for email in split_emails(doc.bcc): for email in split_emails(doc.bcc):
validate_email_address(email, throw=True) validate_email_address(email, throw=True)
def set_incoming_outgoing_accounts(doc): def set_incoming_outgoing_accounts(doc):
from frappe.email.doctype.email_account.email_account import EmailAccount from frappe.email.doctype.email_account.email_account import EmailAccount
incoming_email_account = EmailAccount.find_incoming( incoming_email_account = EmailAccount.find_incoming(
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype) match_by_email=doc.sender, match_by_doctype=doc.reference_doctype
)
doc.incoming_email_account = incoming_email_account.email_id if incoming_email_account else None doc.incoming_email_account = incoming_email_account.email_id if incoming_email_account else None
doc.outgoing_email_account = EmailAccount.find_outgoing( doc.outgoing_email_account = EmailAccount.find_outgoing(
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype) match_by_email=doc.sender, match_by_doctype=doc.reference_doctype
)
if doc.sent_or_received == "Sent": if doc.sent_or_received == "Sent":
doc.db_set("email_account", doc.outgoing_email_account.name) doc.db_set("email_account", doc.outgoing_email_account.name)
def add_attachments(name, attachments): def add_attachments(name, attachments):
'''Add attachments to the given Communication''' """Add attachments to the given Communication"""
# loop through attachments # loop through attachments
for a in attachments: for a in attachments:
if isinstance(a, str): if isinstance(a, str):
attach = frappe.db.get_value("File", {"name":a}, attach = frappe.db.get_value(
["file_name", "file_url", "is_private"], as_dict=1) "File", {"name": a}, ["file_name", "file_url", "is_private"], as_dict=1
)
# save attachments to new doc # save attachments to new doc
_file = frappe.get_doc({ _file = frappe.get_doc(
"doctype": "File", {
"file_url": attach.file_url, "doctype": "File",
"attached_to_doctype": "Communication", "file_url": attach.file_url,
"attached_to_name": name, "attached_to_doctype": "Communication",
"folder": "Home/Attachments", "attached_to_name": name,
"is_private": attach.is_private "folder": "Home/Attachments",
}) "is_private": attach.is_private,
}
)
_file.save(ignore_permissions=True) _file.save(ignore_permissions=True)
@frappe.whitelist(allow_guest=True, methods=("GET",)) @frappe.whitelist(allow_guest=True, methods=("GET",))
def mark_email_as_seen(name: str = None): def mark_email_as_seen(name: str = None):
try: try:
@ -233,33 +251,31 @@ def mark_email_as_seen(name: str = None):
frappe.log_error(frappe.get_traceback()) frappe.log_error(frappe.get_traceback())
finally: finally:
frappe.response.update({ frappe.response.update(
"type": "binary", {
"filename": "imaginary_pixel.png", "type": "binary",
"filecontent": ( "filename": "imaginary_pixel.png",
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00" "filecontent": (
b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\r" b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00"
b"IDATx\x9cc\xf8\xff\xff?\x03\x00\x08\xfc\x02\xfe\xa7\x9a\xa0" b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\r"
b"\xa0\x00\x00\x00\x00IEND\xaeB`\x82" b"IDATx\x9cc\xf8\xff\xff?\x03\x00\x08\xfc\x02\xfe\xa7\x9a\xa0"
) b"\xa0\x00\x00\x00\x00IEND\xaeB`\x82"
}) ),
}
)
def update_communication_as_read(name): def update_communication_as_read(name):
if not name or not isinstance(name, str): if not name or not isinstance(name, str):
return return
communication = frappe.db.get_value( communication = frappe.db.get_value("Communication", name, "read_by_recipient", as_dict=True)
"Communication",
name,
"read_by_recipient",
as_dict=True
)
if not communication or communication.read_by_recipient: if not communication or communication.read_by_recipient:
return return
frappe.db.set_value("Communication", name, { frappe.db.set_value(
"read_by_recipient": 1, "Communication",
"delivery_status": "Read", name,
"read_by_recipient_on": get_datetime() {"read_by_recipient": 1, "delivery_status": "Read", "read_by_recipient_on": get_datetime()},
}) )

View file

@ -1,33 +1,34 @@
from typing import List from typing import List
import frappe import frappe
from frappe import _ from frappe import _
from frappe.core.utils import get_parent_doc from frappe.core.utils import get_parent_doc
from frappe.utils import parse_addr, get_formatted_email, get_url
from frappe.email.doctype.email_account.email_account import EmailAccount
from frappe.desk.doctype.todo.todo import ToDo from frappe.desk.doctype.todo.todo import ToDo
from frappe.email.doctype.email_account.email_account import EmailAccount
from frappe.utils import get_formatted_email, get_url, parse_addr
class CommunicationEmailMixin: class CommunicationEmailMixin:
"""Mixin class to handle communication mails. """Mixin class to handle communication mails."""
"""
def is_email_communication(self): def is_email_communication(self):
return self.communication_type=="Communication" and self.communication_medium == "Email" return self.communication_type == "Communication" and self.communication_medium == "Email"
def get_owner(self): def get_owner(self):
"""Get owner of the communication docs parent. """Get owner of the communication docs parent."""
"""
parent_doc = get_parent_doc(self) parent_doc = get_parent_doc(self)
return parent_doc.owner if parent_doc else None return parent_doc.owner if parent_doc else None
def get_all_email_addresses(self, exclude_displayname=False): def get_all_email_addresses(self, exclude_displayname=False):
"""Get all Email addresses mentioned in the doc along with display name. """Get all Email addresses mentioned in the doc along with display name."""
""" return (
return self.to_list(exclude_displayname=exclude_displayname) + \ self.to_list(exclude_displayname=exclude_displayname)
self.cc_list(exclude_displayname=exclude_displayname) + \ + self.cc_list(exclude_displayname=exclude_displayname)
self.bcc_list(exclude_displayname=exclude_displayname) + self.bcc_list(exclude_displayname=exclude_displayname)
)
def get_email_with_displayname(self, email_address): def get_email_with_displayname(self, email_address):
"""Returns email address after adding displayname. """Returns email address after adding displayname."""
"""
display_name, email = parse_addr(email_address) display_name, email = parse_addr(email_address)
if display_name and display_name != email: if display_name and display_name != email:
return email_address return email_address
@ -37,26 +38,24 @@ class CommunicationEmailMixin:
return email_map.get(email, email) return email_map.get(email, email)
def mail_recipients(self, is_inbound_mail_communcation=False): def mail_recipients(self, is_inbound_mail_communcation=False):
"""Build to(recipient) list to send an email. """Build to(recipient) list to send an email."""
"""
# Incase of inbound mail, recipients already received the mail, no need to send again. # Incase of inbound mail, recipients already received the mail, no need to send again.
if is_inbound_mail_communcation: if is_inbound_mail_communcation:
return [] return []
if hasattr(self, '_final_recipients'): if hasattr(self, "_final_recipients"):
return self._final_recipients return self._final_recipients
to = self.to_list() to = self.to_list()
self._final_recipients = list(filter(lambda id: id != 'Administrator', to)) self._final_recipients = list(filter(lambda id: id != "Administrator", to))
return self._final_recipients return self._final_recipients
def get_mail_recipients_with_displayname(self, is_inbound_mail_communcation=False): def get_mail_recipients_with_displayname(self, is_inbound_mail_communcation=False):
"""Build to(recipient) list to send an email including displayname in email. """Build to(recipient) list to send an email including displayname in email."""
"""
to_list = self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation) to_list = self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation)
return [self.get_email_with_displayname(email) for email in to_list] return [self.get_email_with_displayname(email) for email in to_list]
def mail_cc(self, is_inbound_mail_communcation=False, include_sender = False): def mail_cc(self, is_inbound_mail_communcation=False, include_sender=False):
"""Build cc list to send an email. """Build cc list to send an email.
* if email copy is requested by sender, then add sender to CC. * if email copy is requested by sender, then add sender to CC.
@ -67,7 +66,7 @@ class CommunicationEmailMixin:
* FixMe: Removed adding TODO owners to cc list. Check if that is needed. * FixMe: Removed adding TODO owners to cc list. Check if that is needed.
""" """
if hasattr(self, '_final_cc'): if hasattr(self, "_final_cc"):
return self._final_cc return self._final_cc
cc = self.cc_list() cc = self.cc_list()
@ -88,11 +87,13 @@ class CommunicationEmailMixin:
if is_inbound_mail_communcation: if is_inbound_mail_communcation:
cc = cc - set(self.cc_list() + self.to_list()) cc = cc - set(self.cc_list() + self.to_list())
self._final_cc = list(filter(lambda id: id != 'Administrator', cc)) self._final_cc = list(filter(lambda id: id != "Administrator", cc))
return self._final_cc return self._final_cc
def get_mail_cc_with_displayname(self, is_inbound_mail_communcation=False, include_sender = False): 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) cc_list = self.mail_cc(
is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=include_sender
)
return [self.get_email_with_displayname(email) for email in cc_list] return [self.get_email_with_displayname(email) for email in cc_list]
def mail_bcc(self, is_inbound_mail_communcation=False): def mail_bcc(self, is_inbound_mail_communcation=False):
@ -102,7 +103,7 @@ class CommunicationEmailMixin:
* User must be enabled in the system * User must be enabled in the system
* remove_administrator_from_email_list * remove_administrator_from_email_list
""" """
if hasattr(self, '_final_bcc'): if hasattr(self, "_final_bcc"):
return self._final_bcc return self._final_bcc
bcc = set(self.bcc_list()) bcc = set(self.bcc_list())
@ -116,7 +117,7 @@ class CommunicationEmailMixin:
if is_inbound_mail_communcation: if is_inbound_mail_communcation:
bcc = bcc - set(self.bcc_list() + self.to_list()) bcc = bcc - set(self.bcc_list() + self.to_list())
self._final_bcc = list(filter(lambda id: id != 'Administrator', bcc)) self._final_bcc = list(filter(lambda id: id != "Administrator", bcc))
return self._final_bcc return self._final_bcc
def get_mail_bcc_with_displayname(self, is_inbound_mail_communcation=False): def get_mail_bcc_with_displayname(self, is_inbound_mail_communcation=False):
@ -145,22 +146,23 @@ class CommunicationEmailMixin:
def get_attach_link(self, print_format): def get_attach_link(self, print_format):
"""Returns public link for the attachment via `templates/emails/print_link.html`.""" """Returns public link for the attachment via `templates/emails/print_link.html`."""
return frappe.get_template("templates/emails/print_link.html").render({ return frappe.get_template("templates/emails/print_link.html").render(
"url": get_url(), {
"doctype": self.reference_doctype, "url": get_url(),
"name": self.reference_name, "doctype": self.reference_doctype,
"print_format": print_format, "name": self.reference_name,
"key": get_parent_doc(self).get_signature() "print_format": print_format,
}) "key": get_parent_doc(self).get_signature(),
}
)
def get_outgoing_email_account(self): def get_outgoing_email_account(self):
if not hasattr(self, '_outgoing_email_account'): if not hasattr(self, "_outgoing_email_account"):
if self.email_account: if self.email_account:
self._outgoing_email_account = EmailAccount.find(self.email_account) self._outgoing_email_account = EmailAccount.find(self.email_account)
else: else:
self._outgoing_email_account = EmailAccount.find_outgoing( self._outgoing_email_account = EmailAccount.find_outgoing(
match_by_email=self.sender_mailid, match_by_email=self.sender_mailid, match_by_doctype=self.reference_doctype
match_by_doctype=self.reference_doctype
) )
if self.sent_or_received == "Sent" and self._outgoing_email_account: if self.sent_or_received == "Sent" and self._outgoing_email_account:
@ -169,10 +171,9 @@ class CommunicationEmailMixin:
return self._outgoing_email_account return self._outgoing_email_account
def get_incoming_email_account(self): def get_incoming_email_account(self):
if not hasattr(self, '_incoming_email_account'): if not hasattr(self, "_incoming_email_account"):
self._incoming_email_account = EmailAccount.find_incoming( self._incoming_email_account = EmailAccount.find_incoming(
match_by_email=self.sender_mailid, match_by_email=self.sender_mailid, match_by_doctype=self.reference_doctype
match_by_doctype=self.reference_doctype
) )
return self._incoming_email_account return self._incoming_email_account
@ -180,12 +181,17 @@ class CommunicationEmailMixin:
final_attachments = [] final_attachments = []
if print_format or print_html: if print_format or print_html:
d = {'print_format': print_format, 'html': print_html, 'print_format_attachment': 1, d = {
'doctype': self.reference_doctype, 'name': self.reference_name} "print_format": print_format,
"html": print_html,
"print_format_attachment": 1,
"doctype": self.reference_doctype,
"name": self.reference_name,
}
final_attachments.append(d) final_attachments.append(d)
for a in self.get_attachments() or []: for a in self.get_attachments() or []:
final_attachments.append({"fid": a['name']}) final_attachments.append({"fid": a["name"]})
return final_attachments return final_attachments
@ -193,48 +199,57 @@ class CommunicationEmailMixin:
email_account = self.get_outgoing_email_account() email_account = self.get_outgoing_email_account()
if email_account and email_account.send_unsubscribe_message: if email_account and email_account.send_unsubscribe_message:
return _("Leave this conversation") return _("Leave this conversation")
return '' return ""
def exclude_emails_list(self, is_inbound_mail_communcation=False, include_sender=False) -> List: def exclude_emails_list(self, is_inbound_mail_communcation=False, include_sender=False) -> List:
"""List of mail id's excluded while sending mail. """List of mail id's excluded while sending mail."""
"""
all_ids = self.get_all_email_addresses(exclude_displayname=True) all_ids = self.get_all_email_addresses(exclude_displayname=True)
final_ids = ( final_ids = (
self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation) self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation)
+ self.mail_bcc(is_inbound_mail_communcation=is_inbound_mail_communcation) + self.mail_bcc(is_inbound_mail_communcation=is_inbound_mail_communcation)
+ self.mail_cc(is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=include_sender) + self.mail_cc(
is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=include_sender
)
) )
return list(set(all_ids) - set(final_ids)) return list(set(all_ids) - set(final_ids))
def get_assignees(self): def get_assignees(self):
"""Get owners of the reference document. """Get owners of the reference document."""
""" filters = {
filters = {'status': 'Open', 'reference_name': self.reference_name, "status": "Open",
'reference_type': self.reference_doctype} "reference_name": self.reference_name,
"reference_type": self.reference_doctype,
}
return ToDo.get_owners(filters) return ToDo.get_owners(filters)
@staticmethod @staticmethod
def filter_thread_notification_disbled_users(emails): def filter_thread_notification_disbled_users(emails):
"""Filter users based on notifications for email threads setting is disabled. """Filter users based on notifications for email threads setting is disabled."""
"""
if not emails: if not emails:
return [] return []
return frappe.get_all("User", pluck="email", filters={"email": ["in", emails], "thread_notify": 0}) return frappe.get_all(
"User", pluck="email", filters={"email": ["in", emails], "thread_notify": 0}
)
@staticmethod @staticmethod
def filter_disabled_users(emails): def filter_disabled_users(emails):
""" """ """
"""
if not emails: if not emails:
return [] return []
return frappe.get_all("User", pluck="email", filters={"email": ["in", emails], "enabled": 0}) return frappe.get_all("User", pluck="email", filters={"email": ["in", emails], "enabled": 0})
def sendmail_input_dict(self, print_html=None, print_format=None, def sendmail_input_dict(
send_me_a_copy=None, print_letterhead=None, is_inbound_mail_communcation=None): self,
print_html=None,
print_format=None,
send_me_a_copy=None,
print_letterhead=None,
is_inbound_mail_communcation=None,
):
outgoing_email_account = self.get_outgoing_email_account() outgoing_email_account = self.get_outgoing_email_account()
if not outgoing_email_account: if not outgoing_email_account:
@ -244,8 +259,7 @@ class CommunicationEmailMixin:
is_inbound_mail_communcation=is_inbound_mail_communcation is_inbound_mail_communcation=is_inbound_mail_communcation
) )
cc = self.get_mail_cc_with_displayname( cc = self.get_mail_cc_with_displayname(
is_inbound_mail_communcation=is_inbound_mail_communcation, is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=send_me_a_copy
include_sender = send_me_a_copy
) )
bcc = self.get_mail_bcc_with_displayname( bcc = self.get_mail_bcc_with_displayname(
is_inbound_mail_communcation=is_inbound_mail_communcation is_inbound_mail_communcation=is_inbound_mail_communcation
@ -273,18 +287,24 @@ class CommunicationEmailMixin:
"delayed": True, "delayed": True,
"communication": self.name, "communication": self.name,
"read_receipt": self.read_receipt, "read_receipt": self.read_receipt,
"is_notification": (self.sent_or_received =="Received" and True) or False, "is_notification": (self.sent_or_received == "Received" and True) or False,
"print_letterhead": print_letterhead "print_letterhead": print_letterhead,
} }
def send_email(self, print_html=None, print_format=None, def send_email(
send_me_a_copy=None, print_letterhead=None, is_inbound_mail_communcation=None): self,
print_html=None,
print_format=None,
send_me_a_copy=None,
print_letterhead=None,
is_inbound_mail_communcation=None,
):
input_dict = self.sendmail_input_dict( input_dict = self.sendmail_input_dict(
print_html=print_html, print_html=print_html,
print_format=print_format, print_format=print_format,
send_me_a_copy=send_me_a_copy, send_me_a_copy=send_me_a_copy,
print_letterhead=print_letterhead, print_letterhead=print_letterhead,
is_inbound_mail_communcation=is_inbound_mail_communcation is_inbound_mail_communcation=is_inbound_mail_communcation,
) )
if input_dict: if input_dict:

View file

@ -7,20 +7,30 @@ import frappe
from frappe.core.doctype.communication.communication import get_emails from frappe.core.doctype.communication.communication import get_emails
from frappe.email.doctype.email_queue.email_queue import EmailQueue from frappe.email.doctype.email_queue.email_queue import EmailQueue
test_records = frappe.get_test_records('Communication') test_records = frappe.get_test_records("Communication")
class TestCommunication(unittest.TestCase): class TestCommunication(unittest.TestCase):
def test_email(self): def test_email(self):
valid_email_list = ["Full Name <full@example.com>", valid_email_list = [
'"Full Name with quotes and <weird@chars.com>" <weird@example.com>', "Full Name <full@example.com>",
"Surname, Name <name.surname@domain.com>", '"Full Name with quotes and <weird@chars.com>" <weird@example.com>',
"Purchase@ABC <purchase@abc.com>", "xyz@abc2.com <xyz@abc.com>", "Surname, Name <name.surname@domain.com>",
"Name [something else] <name@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", invalid_email_list = [
"tes2", "e", "rrrrrrrr", "manas","[[[sample]]]", "[invalid!email]",
"[invalid!email].com"] "invalid-email",
"tes2",
"e",
"rrrrrrrr",
"manas",
"[[[sample]]]",
"[invalid!email].com",
]
for x in valid_email_list: for x in valid_email_list:
self.assertTrue(frappe.utils.parse_addr(x)[1]) self.assertTrue(frappe.utils.parse_addr(x)[1])
@ -29,15 +39,25 @@ class TestCommunication(unittest.TestCase):
self.assertFalse(frappe.utils.parse_addr(x)[0]) self.assertFalse(frappe.utils.parse_addr(x)[0])
def test_name(self): def test_name(self):
valid_email_list = ["Full Name <full@example.com>", valid_email_list = [
'"Full Name with quotes and <weird@chars.com>" <weird@example.com>', "Full Name <full@example.com>",
"Surname, Name <name.surname@domain.com>", '"Full Name with quotes and <weird@chars.com>" <weird@example.com>',
"Purchase@ABC <purchase@abc.com>", "xyz@abc2.com <xyz@abc.com>", "Surname, Name <name.surname@domain.com>",
"Name [something else] <name@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", invalid_email_list = [
"tes2", "e", "rrrrrrrr", "manas","[[[sample]]]", "[invalid!email]",
"[invalid!email].com"] "invalid-email",
"tes2",
"e",
"rrrrrrrr",
"manas",
"[[[sample]]]",
"[invalid!email].com",
]
for x in valid_email_list: for x in valid_email_list:
self.assertTrue(frappe.utils.parse_addr(x)[0]) self.assertTrue(frappe.utils.parse_addr(x)[0])
@ -46,27 +66,33 @@ class TestCommunication(unittest.TestCase):
self.assertFalse(frappe.utils.parse_addr(x)[0]) self.assertFalse(frappe.utils.parse_addr(x)[0])
def test_circular_linking(self): def test_circular_linking(self):
a = frappe.get_doc({ a = frappe.get_doc(
"doctype": "Communication", {
"communication_type": "Communication", "doctype": "Communication",
"content": "This was created to test circular linking: Communication A", "communication_type": "Communication",
}).insert(ignore_permissions=True) "content": "This was created to test circular linking: Communication A",
}
).insert(ignore_permissions=True)
b = frappe.get_doc({ b = frappe.get_doc(
"doctype": "Communication", {
"communication_type": "Communication", "doctype": "Communication",
"content": "This was created to test circular linking: Communication B", "communication_type": "Communication",
"reference_doctype": "Communication", "content": "This was created to test circular linking: Communication B",
"reference_name": a.name "reference_doctype": "Communication",
}).insert(ignore_permissions=True) "reference_name": a.name,
}
).insert(ignore_permissions=True)
c = frappe.get_doc({ c = frappe.get_doc(
"doctype": "Communication", {
"communication_type": "Communication", "doctype": "Communication",
"content": "This was created to test circular linking: Communication C", "communication_type": "Communication",
"reference_doctype": "Communication", "content": "This was created to test circular linking: Communication C",
"reference_name": b.name "reference_doctype": "Communication",
}).insert(ignore_permissions=True) "reference_name": b.name,
}
).insert(ignore_permissions=True)
a = frappe.get_doc("Communication", a.name) a = frappe.get_doc("Communication", a.name)
a.reference_doctype = "Communication" a.reference_doctype = "Communication"
@ -77,20 +103,24 @@ class TestCommunication(unittest.TestCase):
def test_deduplication_timeline_links(self): def test_deduplication_timeline_links(self):
frappe.delete_doc_if_exists("Note", "deduplication timeline links") frappe.delete_doc_if_exists("Note", "deduplication timeline links")
note = frappe.get_doc({ note = frappe.get_doc(
"doctype": "Note", {
"title": "deduplication timeline links", "doctype": "Note",
"content": "deduplication timeline links" "title": "deduplication timeline links",
}).insert(ignore_permissions=True) "content": "deduplication timeline links",
}
).insert(ignore_permissions=True)
comm = frappe.get_doc({ comm = frappe.get_doc(
"doctype": "Communication", {
"communication_type": "Communication", "doctype": "Communication",
"content": "Deduplication of Links", "communication_type": "Communication",
"communication_medium": "Email" "content": "Deduplication of Links",
}).insert(ignore_permissions=True) "communication_medium": "Email",
}
).insert(ignore_permissions=True)
#adding same link twice # adding same link twice
comm.add_link(link_doctype="Note", link_name=note.name, autosave=True) comm.add_link(link_doctype="Note", link_name=note.name, autosave=True)
comm.add_link(link_doctype="Note", link_name=note.name, autosave=True) comm.add_link(link_doctype="Note", link_name=note.name, autosave=True)
@ -99,35 +129,43 @@ class TestCommunication(unittest.TestCase):
self.assertNotEqual(2, len(comm.timeline_links)) self.assertNotEqual(2, len(comm.timeline_links))
def test_contacts_attached(self): def test_contacts_attached(self):
contact_sender = frappe.get_doc({ contact_sender = frappe.get_doc(
"doctype": "Contact", {
"first_name": "contact_sender", "doctype": "Contact",
}) "first_name": "contact_sender",
}
)
contact_sender.add_email("comm_sender@example.com") contact_sender.add_email("comm_sender@example.com")
contact_sender.insert(ignore_permissions=True) contact_sender.insert(ignore_permissions=True)
contact_recipient = frappe.get_doc({ contact_recipient = frappe.get_doc(
"doctype": "Contact", {
"first_name": "contact_recipient", "doctype": "Contact",
}) "first_name": "contact_recipient",
}
)
contact_recipient.add_email("comm_recipient@example.com") contact_recipient.add_email("comm_recipient@example.com")
contact_recipient.insert(ignore_permissions=True) contact_recipient.insert(ignore_permissions=True)
contact_cc = frappe.get_doc({ contact_cc = frappe.get_doc(
"doctype": "Contact", {
"first_name": "contact_cc", "doctype": "Contact",
}) "first_name": "contact_cc",
}
)
contact_cc.add_email("comm_cc@example.com") contact_cc.add_email("comm_cc@example.com")
contact_cc.insert(ignore_permissions=True) contact_cc.insert(ignore_permissions=True)
comm = frappe.get_doc({ comm = frappe.get_doc(
"doctype": "Communication", {
"communication_medium": "Email", "doctype": "Communication",
"subject": "Contacts Attached Test", "communication_medium": "Email",
"sender": "comm_sender@example.com", "subject": "Contacts Attached Test",
"recipients": "comm_recipient@example.com", "sender": "comm_sender@example.com",
"cc": "comm_cc@example.com" "recipients": "comm_recipient@example.com",
}).insert(ignore_permissions=True) "cc": "comm_cc@example.com",
}
).insert(ignore_permissions=True)
comm = frappe.get_doc("Communication", comm.name) comm = frappe.get_doc("Communication", comm.name)
@ -144,27 +182,29 @@ class TestCommunication(unittest.TestCase):
frappe.delete_doc_if_exists("Note", "get communication data") frappe.delete_doc_if_exists("Note", "get communication data")
note = frappe.get_doc({ note = frappe.get_doc(
"doctype": "Note", {"doctype": "Note", "title": "get communication data", "content": "get communication data"}
"title": "get communication data", ).insert(ignore_permissions=True)
"content": "get communication data"
}).insert(ignore_permissions=True)
comm_note_1 = frappe.get_doc({ comm_note_1 = frappe.get_doc(
"doctype": "Communication", {
"communication_type": "Communication", "doctype": "Communication",
"content": "Test Get Communication Data 1", "communication_type": "Communication",
"communication_medium": "Email" "content": "Test Get Communication Data 1",
}).insert(ignore_permissions=True) "communication_medium": "Email",
}
).insert(ignore_permissions=True)
comm_note_1.add_link(link_doctype="Note", link_name=note.name, autosave=True) comm_note_1.add_link(link_doctype="Note", link_name=note.name, autosave=True)
comm_note_2 = frappe.get_doc({ comm_note_2 = frappe.get_doc(
"doctype": "Communication", {
"communication_type": "Communication", "doctype": "Communication",
"content": "Test Get Communication Data 2", "communication_type": "Communication",
"communication_medium": "Email" "content": "Test Get Communication Data 2",
}).insert(ignore_permissions=True) "communication_medium": "Email",
}
).insert(ignore_permissions=True)
comm_note_2.add_link(link_doctype="Note", link_name=note.name, autosave=True) comm_note_2.add_link(link_doctype="Note", link_name=note.name, autosave=True)
@ -182,19 +222,23 @@ class TestCommunication(unittest.TestCase):
create_email_account() create_email_account()
note = frappe.get_doc({ note = frappe.get_doc(
"doctype": "Note", {
"title": "test document link in email", "doctype": "Note",
"content": "test document link in email" "title": "test document link in email",
}).insert(ignore_permissions=True) "content": "test document link in email",
}
).insert(ignore_permissions=True)
comm = frappe.get_doc({ comm = frappe.get_doc(
"doctype": "Communication", {
"communication_medium": "Email", "doctype": "Communication",
"subject": "Document Link in Email", "communication_medium": "Email",
"sender": "comm_sender@example.com", "subject": "Document Link in Email",
"recipients": "comm_recipient+{0}+{1}@example.com".format(quote("Note"), quote(note.name)), "sender": "comm_sender@example.com",
}).insert(ignore_permissions=True) "recipients": "comm_recipient+{0}+{1}@example.com".format(quote("Note"), quote(note.name)),
}
).insert(ignore_permissions=True)
doc_links = [] doc_links = []
for timeline_link in comm.timeline_links: for timeline_link in comm.timeline_links:
@ -205,9 +249,9 @@ class TestCommunication(unittest.TestCase):
def test_parse_emails(self): def test_parse_emails(self):
emails = get_emails( emails = get_emails(
[ [
'comm_recipient+DocType+DocName@example.com', "comm_recipient+DocType+DocName@example.com",
'"First, LastName" <first.lastname@email.com>', '"First, LastName" <first.lastname@email.com>',
'test@user.com' "test@user.com",
] ]
) )
@ -215,99 +259,108 @@ class TestCommunication(unittest.TestCase):
self.assertEqual(emails[1], "first.lastname@email.com") self.assertEqual(emails[1], "first.lastname@email.com")
self.assertEqual(emails[2], "test@user.com") self.assertEqual(emails[2], "test@user.com")
class TestCommunicationEmailMixin(unittest.TestCase): class TestCommunicationEmailMixin(unittest.TestCase):
def new_communication(self, recipients=None, cc=None, bcc=None): def new_communication(self, recipients=None, cc=None, bcc=None):
recipients = ', '.join(recipients or []) recipients = ", ".join(recipients or [])
cc = ', '.join(cc or []) cc = ", ".join(cc or [])
bcc = ', '.join(bcc or []) bcc = ", ".join(bcc or [])
comm = frappe.get_doc({ comm = frappe.get_doc(
"doctype": "Communication", {
"communication_type": "Communication", "doctype": "Communication",
"communication_medium": "Email", "communication_type": "Communication",
"content": "Test content", "communication_medium": "Email",
"recipients": recipients, "content": "Test content",
"cc": cc, "recipients": recipients,
"bcc": bcc "cc": cc,
}).insert(ignore_permissions=True) "bcc": bcc,
}
).insert(ignore_permissions=True)
return comm return comm
def new_user(self, email, **user_data): def new_user(self, email, **user_data):
user_data.setdefault('first_name', 'first_name') user_data.setdefault("first_name", "first_name")
user = frappe.new_doc('User') user = frappe.new_doc("User")
user.email = email user.email = email
user.update(user_data) user.update(user_data)
user.insert(ignore_permissions=True, ignore_if_duplicate=True) user.insert(ignore_permissions=True, ignore_if_duplicate=True)
return user return user
def test_recipients(self): def test_recipients(self):
to_list = ['to@test.com', 'receiver <to+1@test.com>', 'to@test.com'] to_list = ["to@test.com", "receiver <to+1@test.com>", "to@test.com"]
comm = self.new_communication(recipients = to_list) comm = self.new_communication(recipients=to_list)
res = comm.get_mail_recipients_with_displayname() res = comm.get_mail_recipients_with_displayname()
self.assertCountEqual(res, ['to@test.com', 'receiver <to+1@test.com>']) self.assertCountEqual(res, ["to@test.com", "receiver <to+1@test.com>"])
comm.delete() comm.delete()
def test_cc(self): def test_cc(self):
to_list = ['to@test.com'] to_list = ["to@test.com"]
cc_list = ['cc+1@test.com', 'cc <cc+2@test.com>', '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) user = self.new_user(email="cc+1@test.com", thread_notify=0)
comm = self.new_communication(recipients=to_list, cc=cc_list) comm = self.new_communication(recipients=to_list, cc=cc_list)
res = comm.get_mail_cc_with_displayname() res = comm.get_mail_cc_with_displayname()
self.assertCountEqual(res, ['cc <cc+2@test.com>']) self.assertCountEqual(res, ["cc <cc+2@test.com>"])
user.delete() user.delete()
comm.delete() comm.delete()
def test_bcc(self): def test_bcc(self):
bcc_list = ['bcc+1@test.com', 'cc <bcc+2@test.com>', ] bcc_list = [
user = self.new_user(email='bcc+2@test.com', enabled=0) "bcc+1@test.com",
"cc <bcc+2@test.com>",
]
user = self.new_user(email="bcc+2@test.com", enabled=0)
comm = self.new_communication(bcc=bcc_list) comm = self.new_communication(bcc=bcc_list)
res = comm.get_mail_bcc_with_displayname() res = comm.get_mail_bcc_with_displayname()
self.assertCountEqual(res, ['bcc+1@test.com']) self.assertCountEqual(res, ["bcc+1@test.com"])
user.delete() user.delete()
comm.delete() comm.delete()
def test_sendmail(self): def test_sendmail(self):
to_list = ['to <to@test.com>'] to_list = ["to <to@test.com>"]
cc_list = ['cc <cc+1@test.com>', 'cc <cc+2@test.com>'] cc_list = ["cc <cc+1@test.com>", "cc <cc+2@test.com>"]
comm = self.new_communication(recipients=to_list, cc=cc_list) comm = self.new_communication(recipients=to_list, cc=cc_list)
comm.send_email() comm.send_email()
doc = EmailQueue.find_one_by_filters(communication=comm.name) doc = EmailQueue.find_one_by_filters(communication=comm.name)
mail_receivers = [each.recipient for each in doc.recipients] mail_receivers = [each.recipient for each in doc.recipients]
self.assertIsNotNone(doc) self.assertIsNotNone(doc)
self.assertCountEqual(to_list+cc_list, mail_receivers) self.assertCountEqual(to_list + cc_list, mail_receivers)
doc.delete() doc.delete()
comm.delete() comm.delete()
def create_email_account(): def create_email_account():
frappe.delete_doc_if_exists("Email Account", "_Test Comm Account 1") frappe.delete_doc_if_exists("Email Account", "_Test Comm Account 1")
frappe.flags.mute_emails = False frappe.flags.mute_emails = False
frappe.flags.sent_mail = None frappe.flags.sent_mail = None
email_account = frappe.get_doc({ email_account = frappe.get_doc(
"is_default": 1, {
"is_global": 1, "is_default": 1,
"doctype": "Email Account", "is_global": 1,
"domain":"example.com", "doctype": "Email Account",
"append_to": "ToDo", "domain": "example.com",
"email_account_name": "_Test Comm Account 1", "append_to": "ToDo",
"enable_outgoing": 1, "email_account_name": "_Test Comm Account 1",
"smtp_server": "test.example.com", "enable_outgoing": 1,
"email_id": "test_comm@example.com", "smtp_server": "test.example.com",
"password": "password", "email_id": "test_comm@example.com",
"add_signature": 1, "password": "password",
"signature": "\nBest Wishes\nTest Signature", "add_signature": 1,
"enable_auto_reply": 1, "signature": "\nBest Wishes\nTest Signature",
"auto_reply_message": "", "enable_auto_reply": 1,
"enable_incoming": 1, "auto_reply_message": "",
"notify_if_unreplied": 1, "enable_incoming": 1,
"unreplied_for_mins": 20, "notify_if_unreplied": 1,
"send_notification_to": "test_comm@example.com", "unreplied_for_mins": 20,
"pop3_server": "pop.test.example.com", "send_notification_to": "test_comm@example.com",
"imap_folder": [{"folder_name": "INBOX", "append_to": "ToDo"}], "pop3_server": "pop.test.example.com",
"no_remaining":"0", "imap_folder": [{"folder_name": "INBOX", "append_to": "ToDo"}],
"enable_automatic_linking": 1 "no_remaining": "0",
}).insert(ignore_permissions=True) "enable_automatic_linking": 1,
}
).insert(ignore_permissions=True)
return email_account return email_account

View file

@ -5,8 +5,10 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
class CommunicationLink(Document): class CommunicationLink(Document):
pass pass
def on_doctype_update(): def on_doctype_update():
frappe.db.add_index("Communication Link", ["link_doctype", "link_name"]) frappe.db.add_index("Communication Link", ["link_doctype", "link_name"])

View file

@ -5,6 +5,7 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
class CustomDocPerm(Document): class CustomDocPerm(Document):
def on_update(self): def on_update(self):
frappe.clear_cache(doctype = self.parent) frappe.clear_cache(doctype=self.parent)

View file

@ -1,10 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors # Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe
import unittest import unittest
import frappe
# test_records = frappe.get_test_records('Custom DocPerm') # test_records = frappe.get_test_records('Custom DocPerm')
class TestCustomDocPerm(unittest.TestCase): class TestCustomDocPerm(unittest.TestCase):
pass pass

View file

@ -5,16 +5,18 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
class CustomRole(Document): class CustomRole(Document):
def validate(self): def validate(self):
if self.report and not self.ref_doctype: if self.report and not self.ref_doctype:
self.ref_doctype = frappe.db.get_value('Report', self.report, 'ref_doctype') self.ref_doctype = frappe.db.get_value("Report", self.report, "ref_doctype")
def get_custom_allowed_roles(field, name): def get_custom_allowed_roles(field, name):
allowed_roles = [] allowed_roles = []
custom_role = frappe.db.get_value('Custom Role', {field: name}, 'name') custom_role = frappe.db.get_value("Custom Role", {field: name}, "name")
if custom_role: if custom_role:
custom_role_doc = frappe.get_doc('Custom Role', custom_role) custom_role_doc = frappe.get_doc("Custom Role", custom_role)
allowed_roles = [d.role for d in custom_role_doc.roles] allowed_roles = [d.role for d in custom_role_doc.roles]
return allowed_roles return allowed_roles

View file

@ -1,10 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors # Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe
import unittest import unittest
import frappe
# test_records = frappe.get_test_records('Custom Role') # test_records = frappe.get_test_records('Custom Role')
class TestCustomRole(unittest.TestCase): class TestCustomRole(unittest.TestCase):
pass pass

View file

@ -4,5 +4,6 @@
from frappe.model.document import Document from frappe.model.document import Document
class DataExport(Document): class DataExport(Document):
pass pass

View file

@ -1,47 +1,78 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe import csv
from frappe import _ import os
import frappe.permissions import re
import re, csv, os
from frappe.utils.csvutils import UnicodeWriter import frappe
from frappe.utils import cstr, formatdate, format_datetime, parse_json, cint, format_duration import frappe.permissions
from frappe.core.doctype.access_log.access_log import make_access_log 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(): def get_data_keys():
return frappe._dict({ return frappe._dict(
"data_separator": _('Start entering data below this line'), {
"main_table": _("Table") + ":", "data_separator": _("Start entering data below this line"),
"parent_table": _("Parent Table") + ":", "main_table": _("Table") + ":",
"columns": _("Column Name") + ":", "parent_table": _("Parent Table") + ":",
"doctype": _("DocType") + ":" "columns": _("Column Name") + ":",
}) "doctype": _("DocType") + ":",
}
)
@frappe.whitelist() @frappe.whitelist()
def export_data(doctype=None, parent_doctype=None, all_doctypes=True, with_data=False, def export_data(
select_columns=None, file_type='CSV', template=False, filters=None): doctype=None,
parent_doctype=None,
all_doctypes=True,
with_data=False,
select_columns=None,
file_type="CSV",
template=False,
filters=None,
):
_doctype = doctype _doctype = doctype
if isinstance(_doctype, list): if isinstance(_doctype, list):
_doctype = _doctype[0] _doctype = _doctype[0]
make_access_log(doctype=_doctype, file_type=file_type, columns=select_columns, filters=filters, method=parent_doctype) make_access_log(
exporter = DataExporter(doctype=doctype, parent_doctype=parent_doctype, all_doctypes=all_doctypes, with_data=with_data, doctype=_doctype,
select_columns=select_columns, file_type=file_type, template=template, filters=filters) file_type=file_type,
columns=select_columns,
filters=filters,
method=parent_doctype,
)
exporter = DataExporter(
doctype=doctype,
parent_doctype=parent_doctype,
all_doctypes=all_doctypes,
with_data=with_data,
select_columns=select_columns,
file_type=file_type,
template=template,
filters=filters,
)
exporter.build_response() exporter.build_response()
class DataExporter: class DataExporter:
def __init__(self, doctype=None, parent_doctype=None, all_doctypes=True, with_data=False, def __init__(
select_columns=None, file_type='CSV', template=False, filters=None): self,
doctype=None,
parent_doctype=None,
all_doctypes=True,
with_data=False,
select_columns=None,
file_type="CSV",
template=False,
filters=None,
):
self.doctype = doctype self.doctype = doctype
self.parent_doctype = parent_doctype self.parent_doctype = parent_doctype
self.all_doctypes = all_doctypes self.all_doctypes = all_doctypes
@ -81,18 +112,18 @@ class DataExporter:
def build_response(self): def build_response(self):
self.writer = UnicodeWriter() self.writer = UnicodeWriter()
self.name_field = 'parent' if self.parent_doctype != self.doctype else 'name' self.name_field = "parent" if self.parent_doctype != self.doctype else "name"
if self.template: if self.template:
self.add_main_header() self.add_main_header()
self.writer.writerow(['']) self.writer.writerow([""])
self.tablerow = [self.data_keys.doctype] self.tablerow = [self.data_keys.doctype]
self.labelrow = [_("Column Labels:")] self.labelrow = [_("Column Labels:")]
self.fieldrow = [self.data_keys.columns] self.fieldrow = [self.data_keys.columns]
self.mandatoryrow = [_("Mandatory:")] self.mandatoryrow = [_("Mandatory:")]
self.typerow = [_('Type:')] self.typerow = [_("Type:")]
self.inforow = [_('Info:')] self.inforow = [_("Info:")]
self.columns = [] self.columns = []
self.build_field_columns(self.doctype) self.build_field_columns(self.doctype)
@ -100,74 +131,99 @@ class DataExporter:
if self.all_doctypes: if self.all_doctypes:
for d in self.child_doctypes: for d in self.child_doctypes:
self.append_empty_field_column() self.append_empty_field_column()
if (self.select_columns and self.select_columns.get(d['doctype'], None)) or not self.select_columns: if (
self.select_columns and self.select_columns.get(d["doctype"], None)
) or not self.select_columns:
# if atleast one column is selected for this doctype # if atleast one column is selected for this doctype
self.build_field_columns(d['doctype'], d['parentfield']) self.build_field_columns(d["doctype"], d["parentfield"])
self.add_field_headings() self.add_field_headings()
self.add_data() self.add_data()
if self.with_data and not self.data: if self.with_data and not self.data:
frappe.respond_as_web_page(_('No Data'), _('There is no data to be exported'), indicator_color='orange') frappe.respond_as_web_page(
_("No Data"), _("There is no data to be exported"), indicator_color="orange"
)
if self.file_type == 'Excel': if self.file_type == "Excel":
self.build_response_as_excel() self.build_response_as_excel()
else: else:
# write out response as a type csv # write out response as a type csv
frappe.response['result'] = cstr(self.writer.getvalue()) frappe.response["result"] = cstr(self.writer.getvalue())
frappe.response['type'] = 'csv' frappe.response["type"] = "csv"
frappe.response['doctype'] = self.doctype frappe.response["doctype"] = self.doctype
def add_main_header(self): def add_main_header(self):
self.writer.writerow([_('Data Import Template')]) self.writer.writerow([_("Data Import Template")])
self.writer.writerow([self.data_keys.main_table, self.doctype]) self.writer.writerow([self.data_keys.main_table, self.doctype])
if self.parent_doctype != self.doctype: if self.parent_doctype != self.doctype:
self.writer.writerow([self.data_keys.parent_table, self.parent_doctype]) self.writer.writerow([self.data_keys.parent_table, self.parent_doctype])
else: else:
self.writer.writerow(['']) self.writer.writerow([""])
self.writer.writerow(['']) self.writer.writerow([""])
self.writer.writerow([_('Notes:')]) self.writer.writerow([_("Notes:")])
self.writer.writerow([_('Please do not change the template headings.')]) self.writer.writerow([_("Please do not change the template headings.")])
self.writer.writerow([_('First data column must be blank.')]) 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(
self.writer.writerow([_('If you are uploading new records, "Naming Series" becomes mandatory, if present.')]) [_('If you are uploading new records, leave the "name" (ID) column blank.')]
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(
self.writer.writerow([_('You can only upload upto 5000 records in one go. (may be less in some cases)')]) [_('If you are uploading new records, "Naming Series" becomes mandatory, if present.')]
)
self.writer.writerow(
[
_(
"Only mandatory fields are necessary for new records. You can delete non-mandatory columns if you wish."
)
]
)
self.writer.writerow([_("For updating, you can update only selective columns.")])
self.writer.writerow(
[_("You can only upload upto 5000 records in one go. (may be less in some cases)")]
)
if self.name_field == "parent": if self.name_field == "parent":
self.writer.writerow([_('"Parent" signifies the parent table in which this row must be added')]) self.writer.writerow([_('"Parent" signifies the parent table in which this row must be added')])
self.writer.writerow([_('If you are updating, please select "Overwrite" else existing rows will not be deleted.')]) self.writer.writerow(
[_('If you are updating, please select "Overwrite" else existing rows will not be deleted.')]
)
def build_field_columns(self, dt, parentfield=None): def build_field_columns(self, dt, parentfield=None):
meta = frappe.get_meta(dt) meta = frappe.get_meta(dt)
# build list of valid docfields # build list of valid docfields
tablecolumns = [] tablecolumns = []
table_name = 'tab' + dt table_name = "tab" + dt
for f in frappe.db.get_table_columns_description(table_name): for f in frappe.db.get_table_columns_description(table_name):
field = meta.get_field(f.name) field = meta.get_field(f.name)
if field and ((self.select_columns and f.name in self.select_columns[dt]) or not self.select_columns): if field and (
(self.select_columns and f.name in self.select_columns[dt]) or not self.select_columns
):
tablecolumns.append(field) tablecolumns.append(field)
tablecolumns.sort(key = lambda a: int(a.idx)) tablecolumns.sort(key=lambda a: int(a.idx))
_column_start_end = frappe._dict(start=0) _column_start_end = frappe._dict(start=0)
if dt==self.doctype: if dt == self.doctype:
if (meta.get('autoname') and meta.get('autoname').lower()=='prompt') or (self.with_data): if (meta.get("autoname") and meta.get("autoname").lower() == "prompt") or (self.with_data):
self._append_name_column() self._append_name_column()
# if importing only child table for new record, add parent field # if importing only child table for new record, add parent field
if meta.get('istable') and not self.with_data: if meta.get("istable") and not self.with_data:
self.append_field_column(frappe._dict({ self.append_field_column(
"fieldname": "parent", frappe._dict(
"parent": "", {
"label": "Parent", "fieldname": "parent",
"fieldtype": "Data", "parent": "",
"reqd": 1, "label": "Parent",
"info": _("Parent is the name of the document to which the data will get added to.") "fieldtype": "Data",
}), True) "reqd": 1,
"info": _("Parent is the name of the document to which the data will get added to."),
}
),
True,
)
_column_start_end = frappe._dict(start=0) _column_start_end = frappe._dict(start=0)
else: else:
@ -184,7 +240,7 @@ class DataExporter:
self.append_field_column(docfield, False) self.append_field_column(docfield, False)
# if there is one column, add a blank column (?) # if there is one column, add a blank column (?)
if len(self.columns)-_column_start_end.start == 1: if len(self.columns) - _column_start_end.start == 1:
self.append_empty_field_column() self.append_empty_field_column()
# append DocType name # append DocType name
@ -204,18 +260,21 @@ class DataExporter:
return return
if not for_mandatory and docfield.reqd: if not for_mandatory and docfield.reqd:
return return
if docfield.fieldname in ('parenttype', 'trash_reason'): if docfield.fieldname in ("parenttype", "trash_reason"):
return return
if docfield.hidden: if docfield.hidden:
return return
if self.select_columns and docfield.fieldname not in self.select_columns.get(docfield.parent, []) \ if (
and docfield.fieldname!="name": self.select_columns
and docfield.fieldname not in self.select_columns.get(docfield.parent, [])
and docfield.fieldname != "name"
):
return return
self.tablerow.append("") self.tablerow.append("")
self.fieldrow.append(docfield.fieldname) self.fieldrow.append(docfield.fieldname)
self.labelrow.append(_(docfield.label)) self.labelrow.append(_(docfield.label))
self.mandatoryrow.append(docfield.reqd and 'Yes' or 'No') self.mandatoryrow.append(docfield.reqd and "Yes" or "No")
self.typerow.append(docfield.fieldtype) self.typerow.append(docfield.fieldtype)
self.inforow.append(self.getinforow(docfield)) self.inforow.append(self.getinforow(docfield))
self.columns.append(docfield.fieldname) self.columns.append(docfield.fieldname)
@ -232,15 +291,15 @@ class DataExporter:
@staticmethod @staticmethod
def getinforow(docfield): def getinforow(docfield):
"""make info comment for options, links etc.""" """make info comment for options, links etc."""
if docfield.fieldtype == 'Select': if docfield.fieldtype == "Select":
if not docfield.options: if not docfield.options:
return '' return ""
else: else:
return _("One of") + ': %s' % ', '.join(filter(None, docfield.options.split('\n'))) return _("One of") + ": %s" % ", ".join(filter(None, docfield.options.split("\n")))
elif docfield.fieldtype == 'Link': elif docfield.fieldtype == "Link":
return 'Valid %s' % docfield.options return "Valid %s" % docfield.options
elif docfield.fieldtype == 'Int': elif docfield.fieldtype == "Int":
return 'Integer' return "Integer"
elif docfield.fieldtype == "Check": elif docfield.fieldtype == "Check":
return "0 or 1" return "0 or 1"
elif docfield.fieldtype in ["Date", "Datetime"]: elif docfield.fieldtype in ["Date", "Datetime"]:
@ -248,7 +307,7 @@ class DataExporter:
elif hasattr(docfield, "info"): elif hasattr(docfield, "info"):
return docfield.info return docfield.info
else: else:
return '' return ""
def add_field_headings(self): def add_field_headings(self):
self.writer.writerow(self.tablerow) self.writer.writerow(self.tablerow)
@ -262,6 +321,7 @@ class DataExporter:
def add_data(self): def add_data(self):
from frappe.query_builder import DocType from frappe.query_builder import DocType
if self.template and not self.with_data: if self.template and not self.with_data:
return return
@ -270,26 +330,28 @@ class DataExporter:
# sort nested set doctypes by `lft asc` # sort nested set doctypes by `lft asc`
order_by = None order_by = None
table_columns = frappe.db.get_table_columns(self.parent_doctype) table_columns = frappe.db.get_table_columns(self.parent_doctype)
if 'lft' in table_columns and 'rgt' in table_columns: if "lft" in table_columns and "rgt" in table_columns:
order_by = '`tab{doctype}`.`lft` asc'.format(doctype=self.parent_doctype) order_by = "`tab{doctype}`.`lft` asc".format(doctype=self.parent_doctype)
# get permitted data only # get permitted data only
self.data = frappe.get_list(self.doctype, fields=["*"], filters=self.filters, limit_page_length=None, order_by=order_by) self.data = frappe.get_list(
self.doctype, fields=["*"], filters=self.filters, limit_page_length=None, order_by=order_by
)
for doc in self.data: for doc in self.data:
op = self.docs_to_export.get("op") op = self.docs_to_export.get("op")
names = self.docs_to_export.get("name") names = self.docs_to_export.get("name")
if names and op: if names and op:
if op == '=' and doc.name not in names: if op == "=" and doc.name not in names:
continue continue
elif op == '!=' and doc.name in names: elif op == "!=" and doc.name in names:
continue continue
elif names: elif names:
try: try:
sflags = self.docs_to_export.get("flags", "I,U").upper() sflags = self.docs_to_export.get("flags", "I,U").upper()
flags = 0 flags = 0
for a in re.split(r'\W+', sflags): for a in re.split(r"\W+", sflags):
flags = flags | reflags.get(a,0) flags = flags | reflags.get(a, 0)
c = re.compile(names, flags) c = re.compile(names, flags)
m = c.match(doc.name) m = c.match(doc.name)
@ -315,7 +377,7 @@ class DataExporter:
.orderby(child_doctype_table.idx) .orderby(child_doctype_table.idx)
) )
for ci, child in enumerate(data_row.run(as_dict=True)): for ci, child in enumerate(data_row.run(as_dict=True)):
self.add_data_row(rows, c['doctype'], c['parentfield'], child, ci) self.add_data_row(rows, c["doctype"], c["parentfield"], child, ci)
for row in rows: for row in rows:
self.writer.writerow(row) self.writer.writerow(row)
@ -333,7 +395,7 @@ class DataExporter:
_column_start_end = self.column_start_end.get((dt, parentfield)) _column_start_end = self.column_start_end.get((dt, parentfield))
if _column_start_end: if _column_start_end:
for i, c in enumerate(self.columns[_column_start_end.start:_column_start_end.end]): for i, c in enumerate(self.columns[_column_start_end.start : _column_start_end.end]):
df = meta.get_field(c) df = meta.get_field(c)
fieldtype = df.fieldtype if df else "Data" fieldtype = df.fieldtype if df else "Data"
value = d.get(c, "") value = d.get(c, "")
@ -349,27 +411,33 @@ class DataExporter:
def build_response_as_excel(self): def build_response_as_excel(self):
filename = frappe.generate_hash("", 10) filename = frappe.generate_hash("", 10)
with open(filename, 'wb') as f: with open(filename, "wb") as f:
f.write(cstr(self.writer.getvalue()).encode('utf-8')) f.write(cstr(self.writer.getvalue()).encode("utf-8"))
f = open(filename) f = open(filename)
reader = csv.reader(f) reader = csv.reader(f)
from frappe.utils.xlsxutils import make_xlsx from frappe.utils.xlsxutils import make_xlsx
xlsx_file = make_xlsx(reader, "Data Import Template" if self.template else 'Data Export')
xlsx_file = make_xlsx(reader, "Data Import Template" if self.template else "Data Export")
f.close() f.close()
os.remove(filename) os.remove(filename)
# write out response as a xlsx type # write out response as a xlsx type
frappe.response['filename'] = self.doctype + '.xlsx' frappe.response["filename"] = self.doctype + ".xlsx"
frappe.response['filecontent'] = xlsx_file.getvalue() frappe.response["filecontent"] = xlsx_file.getvalue()
frappe.response['type'] = 'binary' frappe.response["type"] = "binary"
def _append_name_column(self, dt=None): def _append_name_column(self, dt=None):
self.append_field_column(frappe._dict({ self.append_field_column(
"fieldname": "name" if dt else self.name_field, frappe._dict(
"parent": dt or "", {
"label": "ID", "fieldname": "name" if dt else self.name_field,
"fieldtype": "Data", "parent": dt or "",
"reqd": 1, "label": "ID",
}), True) "fieldtype": "Data",
"reqd": 1,
}
),
True,
)

View file

@ -2,13 +2,15 @@
# Copyright (c) 2019, Frappe Technologies and Contributors # Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import unittest import unittest
import frappe import frappe
from frappe.core.doctype.data_export.exporter import DataExporter from frappe.core.doctype.data_export.exporter import DataExporter
class TestDataExporter(unittest.TestCase): class TestDataExporter(unittest.TestCase):
def setUp(self): def setUp(self):
self.doctype_name = 'Test DocType for Export Tool' self.doctype_name = "Test DocType for Export Tool"
self.doc_name = 'Test Data for Export Tool' self.doc_name = "Test Data for Export Tool"
self.create_doctype_if_not_exists(doctype_name=self.doctype_name) self.create_doctype_if_not_exists(doctype_name=self.doctype_name)
self.create_test_data() self.create_test_data()
@ -17,42 +19,49 @@ class TestDataExporter(unittest.TestCase):
Helper Function for setting up doctypes Helper Function for setting up doctypes
""" """
if force: if force:
frappe.delete_doc_if_exists('DocType', doctype_name) frappe.delete_doc_if_exists("DocType", doctype_name)
frappe.delete_doc_if_exists('DocType', 'Child 1 of ' + doctype_name) frappe.delete_doc_if_exists("DocType", "Child 1 of " + doctype_name)
if frappe.db.exists('DocType', doctype_name): if frappe.db.exists("DocType", doctype_name):
return return
# Child Table 1 # Child Table 1
table_1_name = 'Child 1 of ' + doctype_name table_1_name = "Child 1 of " + doctype_name
frappe.get_doc({ frappe.get_doc(
'doctype': 'DocType', {
'name': table_1_name, "doctype": "DocType",
'module': 'Custom', "name": table_1_name,
'custom': 1, "module": "Custom",
'istable': 1, "custom": 1,
'fields': [ "istable": 1,
{'label': 'Child Title', 'fieldname': 'child_title', 'reqd': 1, 'fieldtype': 'Data'}, "fields": [
{'label': 'Child Number', 'fieldname': 'child_number', 'fieldtype': 'Int'}, {"label": "Child Title", "fieldname": "child_title", "reqd": 1, "fieldtype": "Data"},
] {"label": "Child Number", "fieldname": "child_number", "fieldtype": "Int"},
}).insert() ],
}
).insert()
# Main Table # Main Table
frappe.get_doc({ frappe.get_doc(
'doctype': 'DocType', {
'name': doctype_name, "doctype": "DocType",
'module': 'Custom', "name": doctype_name,
'custom': 1, "module": "Custom",
'autoname': 'field:title', "custom": 1,
'fields': [ "autoname": "field:title",
{'label': 'Title', 'fieldname': 'title', 'reqd': 1, 'fieldtype': 'Data'}, "fields": [
{'label': 'Number', 'fieldname': 'number', 'fieldtype': 'Int'}, {"label": "Title", "fieldname": "title", "reqd": 1, "fieldtype": "Data"},
{'label': 'Table Field 1', 'fieldname': 'table_field_1', 'fieldtype': 'Table', 'options': table_1_name}, {"label": "Number", "fieldname": "number", "fieldtype": "Int"},
], {
'permissions': [ "label": "Table Field 1",
{'role': 'System Manager'} "fieldname": "table_field_1",
] "fieldtype": "Table",
}).insert() "options": table_1_name,
},
],
"permissions": [{"role": "System Manager"}],
}
).insert()
def create_test_data(self, force=False): def create_test_data(self, force=False):
""" """
@ -69,37 +78,38 @@ class TestDataExporter(unittest.TestCase):
table_field_1=[ table_field_1=[
{"child_title": "Child Title 1", "child_number": "50"}, {"child_title": "Child Title 1", "child_number": "50"},
{"child_title": "Child Title 2", "child_number": "51"}, {"child_title": "Child Title 2", "child_number": "51"},
] ],
).insert() ).insert()
else: else:
self.doc = frappe.get_doc(self.doctype_name, self.doc_name) self.doc = frappe.get_doc(self.doctype_name, self.doc_name)
def test_export_content(self): def test_export_content(self):
exp = DataExporter(doctype=self.doctype_name, file_type='CSV') exp = DataExporter(doctype=self.doctype_name, file_type="CSV")
exp.build_response() exp.build_response()
self.assertEqual(frappe.response['type'],'csv') self.assertEqual(frappe.response["type"], "csv")
self.assertEqual(frappe.response['doctype'], self.doctype_name) self.assertEqual(frappe.response["doctype"], self.doctype_name)
self.assertTrue(frappe.response['result']) self.assertTrue(frappe.response["result"])
self.assertIn('Child Title 1\",50',frappe.response['result']) self.assertIn('Child Title 1",50', frappe.response["result"])
self.assertIn('Child Title 2\",51',frappe.response['result']) self.assertIn('Child Title 2",51', frappe.response["result"])
def test_export_type(self): def test_export_type(self):
for type in ['csv', 'Excel']: for type in ["csv", "Excel"]:
with self.subTest(type=type): with self.subTest(type=type):
exp = DataExporter(doctype=self.doctype_name, file_type=type) exp = DataExporter(doctype=self.doctype_name, file_type=type)
exp.build_response() exp.build_response()
self.assertEqual(frappe.response['doctype'], self.doctype_name) self.assertEqual(frappe.response["doctype"], self.doctype_name)
self.assertTrue(frappe.response['result']) self.assertTrue(frappe.response["result"])
if type == 'csv': if type == "csv":
self.assertEqual(frappe.response['type'],'csv') self.assertEqual(frappe.response["type"], "csv")
elif type == 'Excel': elif type == "Excel":
self.assertEqual(frappe.response['type'],'binary') self.assertEqual(frappe.response["type"], "binary")
self.assertEqual(frappe.response['filename'], self.doctype_name+'.xlsx') # 'Test DocType for Export Tool.xlsx') self.assertEqual(
self.assertTrue(frappe.response['filecontent']) frappe.response["filename"], self.doctype_name + ".xlsx"
) # 'Test DocType for Export Tool.xlsx')
self.assertTrue(frappe.response["filecontent"])
def tearDown(self): def tearDown(self):
pass pass

View file

@ -64,9 +64,7 @@ class DataImport(Document):
from frappe.utils.scheduler import is_scheduler_inactive from frappe.utils.scheduler import is_scheduler_inactive
if is_scheduler_inactive() and not frappe.flags.in_test: if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw( frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive")
)
enqueued_jobs = [d.get("job_name") for d in get_info()] enqueued_jobs = [d.get("job_name") for d in get_info()]
@ -100,6 +98,7 @@ def get_preview_from_template(data_import, import_file=None, google_sheets_url=N
import_file, google_sheets_url import_file, google_sheets_url
) )
@frappe.whitelist() @frappe.whitelist()
def form_start_import(data_import): def form_start_import(data_import):
return frappe.get_doc("Data Import", data_import).start_import() return frappe.get_doc("Data Import", data_import).start_import()
@ -127,11 +126,11 @@ def download_template(
): ):
""" """
Download template from Exporter Download template from Exporter
:param doctype: Document Type :param doctype: Document Type
:param export_fields=None: Fields to export as dict {'Sales Invoice': ['name', 'customer'], 'Sales Invoice Item': ['item_code']} :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_records=None: One of 'all', 'by_filter', 'blank_template'
:param export_filters: Filter dict :param export_filters: Filter dict
:param file_type: File type to export into :param file_type: File type to export into
""" """
export_fields = frappe.parse_json(export_fields) export_fields = frappe.parse_json(export_fields)
@ -154,34 +153,38 @@ def download_errored_template(data_import_name):
data_import = frappe.get_doc("Data Import", data_import_name) data_import = frappe.get_doc("Data Import", data_import_name)
data_import.export_errored_rows() data_import.export_errored_rows()
@frappe.whitelist() @frappe.whitelist()
def download_import_log(data_import_name): def download_import_log(data_import_name):
data_import = frappe.get_doc("Data Import", data_import_name) data_import = frappe.get_doc("Data Import", data_import_name)
data_import.download_import_log() data_import.download_import_log()
@frappe.whitelist() @frappe.whitelist()
def get_import_status(data_import_name): def get_import_status(data_import_name):
import_status = {} import_status = {}
logs = frappe.get_all('Data Import Log', fields=['count(*) as count', 'success'], logs = frappe.get_all(
filters={'data_import': data_import_name}, "Data Import Log",
group_by='success') fields=["count(*) as count", "success"],
filters={"data_import": data_import_name},
group_by="success",
)
total_payload_count = frappe.db.get_value('Data Import', data_import_name, 'payload_count') total_payload_count = frappe.db.get_value("Data Import", data_import_name, "payload_count")
for log in logs: for log in logs:
if log.get('success'): if log.get("success"):
import_status['success'] = log.get('count') import_status["success"] = log.get("count")
else: else:
import_status['failed'] = log.get('count') import_status["failed"] = log.get("count")
import_status['total_records'] = total_payload_count import_status["total_records"] = total_payload_count
return import_status return import_status
def import_file(
doctype, file_path, import_type, submit_after_import=False, console=False def import_file(doctype, file_path, import_type, submit_after_import=False, console=False):
):
""" """
Import documents in from CSV or XLSX using data import. Import documents in from CSV or XLSX using data import.
@ -198,9 +201,7 @@ def import_file(
"Insert New Records" if import_type.lower() == "insert" else "Update Existing Records" "Insert New Records" if import_type.lower() == "insert" else "Update Existing Records"
) )
i = Importer( i = Importer(doctype=doctype, file_path=file_path, data_import=data_import, console=console)
doctype=doctype, file_path=file_path, data_import=data_import, console=console
)
i.import_data() i.import_data()
@ -214,11 +215,7 @@ def import_doc(path, pre_process=None):
if f.endswith(".json"): if f.endswith(".json"):
frappe.flags.mute_emails = True frappe.flags.mute_emails = True
import_file_by_path( import_file_by_path(
f, f, data_import=True, force=True, pre_process=pre_process, reset_permissions=True
data_import=True,
force=True,
pre_process=pre_process,
reset_permissions=True
) )
frappe.flags.mute_emails = False frappe.flags.mute_emails = False
frappe.db.commit() frappe.db.commit()
@ -226,9 +223,7 @@ def import_doc(path, pre_process=None):
raise NotImplementedError("Only .json files can be imported") raise NotImplementedError("Only .json files can be imported")
def export_json( def export_json(doctype, path, filters=None, or_filters=None, name=None, order_by="creation asc"):
doctype, path, filters=None, or_filters=None, name=None, order_by="creation asc"
):
def post_process(out): def post_process(out):
# Note on Tree DocTypes: # Note on Tree DocTypes:
# The tree structure is maintained in the database via the fields "lft" # The tree structure is maintained in the database via the fields "lft"

View file

@ -6,11 +6,8 @@ import typing
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model import ( from frappe.model import display_fieldtypes, no_value_fields
display_fieldtypes, from frappe.model import table_fields as table_fieldtypes
no_value_fields,
table_fields as table_fieldtypes,
)
from frappe.utils import flt, format_duration, groupby_metric from frappe.utils import flt, format_duration, groupby_metric
from frappe.utils.csvutils import build_csv_response from frappe.utils.csvutils import build_csv_response
from frappe.utils.xlsxutils import build_xlsx_response from frappe.utils.xlsxutils import build_xlsx_response
@ -28,11 +25,11 @@ class Exporter:
): ):
""" """
Exports records of a DocType for use with Importer Exports records of a DocType for use with Importer
:param doctype: Document Type to export :param doctype: Document Type to export
:param export_fields=None: One of 'All', 'Mandatory' or {'DocType': ['field1', 'field2'], 'Child DocType': ['childfield1']} :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_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 export_filters=None: The filters (dict or list) which is used to query the records
:param file_type: One of 'Excel' or 'CSV' :param file_type: One of 'Excel' or 'CSV'
""" """
self.doctype = doctype self.doctype = doctype
self.meta = frappe.get_meta(doctype) self.meta = frappe.get_meta(doctype)
@ -168,9 +165,7 @@ class Exporter:
else: else:
order_by = "`tab{0}`.`creation` DESC".format(self.doctype) order_by = "`tab{0}`.`creation` DESC".format(self.doctype)
parent_fields = [ parent_fields = [format_column_name(df) for df in self.fields if df.parent == self.doctype]
format_column_name(df) for df in self.fields if df.parent == self.doctype
]
parent_data = frappe.db.get_list( parent_data = frappe.db.get_list(
self.doctype, self.doctype,
filters=filters, filters=filters,
@ -188,9 +183,7 @@ class Exporter:
child_table_df = self.meta.get_field(key) child_table_df = self.meta.get_field(key)
child_table_doctype = child_table_df.options child_table_doctype = child_table_df.options
child_fields = ["name", "idx", "parent", "parentfield"] + list( child_fields = ["name", "idx", "parent", "parentfield"] + list(
set( set([format_column_name(df) for df in self.fields if df.parent == child_table_doctype])
[format_column_name(df) for df in self.fields if df.parent == child_table_doctype]
)
) )
data = frappe.db.get_all( data = frappe.db.get_all(
child_table_doctype, child_table_doctype,
@ -261,4 +254,4 @@ class Exporter:
build_xlsx_response(self.get_csv_array_for_export(), _(self.doctype)) build_xlsx_response(self.get_csv_array_for_export(), _(self.doctype))
def group_children_data_by_parent(self, children_data: typing.Dict[str, list]): def group_children_data_by_parent(self, children_data: typing.Dict[str, list]):
return groupby_metric(children_data, key='parent') return groupby_metric(children_data, key="parent")

View file

@ -1,21 +1,23 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import os
import io import io
import frappe
import timeit
import json import json
from datetime import datetime, date import os
import timeit
from datetime import date, datetime
import frappe
from frappe import _ from frappe import _
from frappe.utils import cint, flt, update_progress_bar, cstr, duration_to_seconds
from frappe.utils.csvutils import read_csv_content, get_csv_content_from_google_sheets
from frappe.utils.xlsxutils import (
read_xlsx_file_from_attached_file,
read_xls_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 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_xls_file_from_attached_file,
read_xlsx_file_from_attached_file,
)
INVALID_VALUES = ("", None) INVALID_VALUES = ("", None)
MAX_ROWS_IN_PREVIEW = 10 MAX_ROWS_IN_PREVIEW = 10
@ -24,9 +26,7 @@ UPDATE = "Update Existing Records"
class Importer: class Importer:
def __init__( def __init__(self, doctype, data_import=None, file_path=None, import_type=None, console=False):
self, doctype, data_import=None, file_path=None, import_type=None, console=False
):
self.doctype = doctype self.doctype = doctype
self.console = console self.console = console
@ -49,9 +49,13 @@ class Importer:
def get_data_for_import_preview(self): def get_data_for_import_preview(self):
out = self.import_file.get_data_for_import_preview() out = self.import_file.get_data_for_import_preview()
out.import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success"], out.import_log = frappe.db.get_all(
"Data Import Log",
fields=["row_indexes", "success"],
filters={"data_import": self.data_import.name}, filters={"data_import": self.data_import.name},
order_by="log_index", limit=10) order_by="log_index",
limit=10,
)
return out return out
@ -84,14 +88,23 @@ class Importer:
return return
# setup import log # setup import log
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "log_index"], import_log = (
filters={"data_import": self.data_import.name}, frappe.db.get_all(
order_by="log_index") or [] "Data Import Log",
fields=["row_indexes", "success", "log_index"],
filters={"data_import": self.data_import.name},
order_by="log_index",
)
or []
)
log_index = 0 log_index = 0
# Do not remove rows in case of retry after an error or pending data import # Do not remove rows in case of retry after an error or pending data import
if self.data_import.status == "Partial Success" and len(import_log) >= self.data_import.payload_count: if (
self.data_import.status == "Partial Success"
and len(import_log) >= self.data_import.payload_count
):
# remove previous failures from import log only in case of retry after partial success # remove previous failures from import log only in case of retry after partial success
import_log = [log for log in import_log if log.get("success")] import_log = [log for log in import_log if log.get("success")]
@ -108,9 +121,7 @@ class Importer:
total_payload_count = len(payloads) total_payload_count = len(payloads)
batch_size = frappe.conf.data_import_batch_size or 1000 batch_size = frappe.conf.data_import_batch_size or 1000
for batch_index, batched_payloads in enumerate( for batch_index, batched_payloads in enumerate(frappe.utils.create_batch(payloads, batch_size)):
frappe.utils.create_batch(payloads, batch_size)
):
for i, payload in enumerate(batched_payloads): for i, payload in enumerate(batched_payloads):
doc = payload.doc doc = payload.doc
row_indexes = [row.row_number for row in payload.rows] row_indexes = [row.row_number for row in payload.rows]
@ -156,11 +167,11 @@ class Importer:
}, },
) )
create_import_log(self.data_import.name, log_index, { create_import_log(
'success': True, self.data_import.name,
'docname': doc.name, log_index,
'row_indexes': row_indexes {"success": True, "docname": doc.name, "row_indexes": row_indexes},
}) )
log_index += 1 log_index += 1
@ -177,19 +188,29 @@ class Importer:
# rollback if exception # rollback if exception
frappe.db.rollback() frappe.db.rollback()
create_import_log(self.data_import.name, log_index, { create_import_log(
'success': False, self.data_import.name,
'exception': frappe.get_traceback(), log_index,
'messages': messages, {
'row_indexes': row_indexes "success": False,
}) "exception": frappe.get_traceback(),
"messages": messages,
"row_indexes": row_indexes,
},
)
log_index += 1 log_index += 1
# Logs are db inserted directly so will have to be fetched again # Logs are db inserted directly so will have to be fetched again
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "log_index"], import_log = (
filters={"data_import": self.data_import.name}, frappe.db.get_all(
order_by="log_index") or [] "Data Import Log",
fields=["row_indexes", "success", "log_index"],
filters={"data_import": self.data_import.name},
order_by="log_index",
)
or []
)
# set status # set status
failures = [log for log in import_log if not log.get("success")] failures = [log for log in import_log if not log.get("success")]
@ -274,9 +295,15 @@ class Importer:
if not self.data_import: if not self.data_import:
return return
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success"], import_log = (
filters={"data_import": self.data_import.name}, frappe.db.get_all(
order_by="log_index") or [] "Data Import Log",
fields=["row_indexes", "success"],
filters={"data_import": self.data_import.name},
order_by="log_index",
)
or []
)
failures = [log for log in import_log if not log.get("success")] failures = [log for log in import_log if not log.get("success")]
row_indexes = [] row_indexes = []
@ -299,9 +326,12 @@ class Importer:
if not self.data_import: if not self.data_import:
return return
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "messages", "exception", "docname"], import_log = frappe.db.get_all(
"Data Import Log",
fields=["row_indexes", "success", "messages", "exception", "docname"],
filters={"data_import": self.data_import.name}, filters={"data_import": self.data_import.name},
order_by="log_index") order_by="log_index",
)
header_row = ["Row Numbers", "Status", "Message", "Exception"] header_row = ["Row Numbers", "Status", "Message", "Exception"]
@ -309,10 +339,13 @@ class Importer:
for log in import_log: for log in import_log:
row_number = json.loads(log.get("row_indexes"))[0] row_number = json.loads(log.get("row_indexes"))[0]
status = "Success" if log.get('success') else "Failure" status = "Success" if log.get("success") else "Failure"
message = "Successfully Imported {0}".format(log.get('docname')) if log.get('success') else \ message = (
log.get("messages") "Successfully Imported {0}".format(log.get("docname"))
exception = frappe.utils.cstr(log.get("exception", '')) if log.get("success")
else log.get("messages")
)
exception = frappe.utils.cstr(log.get("exception", ""))
rows += [[row_number, status, message, exception]] rows += [[row_number, status, message, exception]]
build_csv_response(rows, self.doctype) build_csv_response(rows, self.doctype)
@ -324,9 +357,7 @@ class Importer:
if successful_records: if successful_records:
print() print()
print( print(
"Successfully imported {0} records out of {1}".format( "Successfully imported {0} records out of {1}".format(len(successful_records), len(import_log))
len(successful_records), len(import_log)
)
) )
if failed_records: if failed_records:
@ -363,9 +394,7 @@ class Importer:
class ImportFile: class ImportFile:
def __init__(self, doctype, file, template_options=None, import_type=None): def __init__(self, doctype, file, template_options=None, import_type=None):
self.doctype = doctype self.doctype = doctype
self.template_options = template_options or frappe._dict( self.template_options = template_options or frappe._dict(column_to_field_map=frappe._dict())
column_to_field_map=frappe._dict()
)
self.column_to_field_map = self.template_options.column_to_field_map self.column_to_field_map = self.template_options.column_to_field_map
self.import_type = import_type self.import_type = import_type
self.warnings = [] self.warnings = []
@ -556,9 +585,7 @@ class ImportFile:
def read_content(self, content, extension): def read_content(self, content, extension):
error_title = _("Template Error") error_title = _("Template Error")
if extension not in ("csv", "xlsx", "xls"): if extension not in ("csv", "xlsx", "xls"):
frappe.throw( frappe.throw(_("Import template should be of type .csv, .xlsx or .xls"), title=error_title)
_("Import template should be of type .csv, .xlsx or .xls"), title=error_title
)
if extension == "csv": if extension == "csv":
data = read_csv_content(content) data = read_csv_content(content)
@ -587,12 +614,13 @@ class Row:
if len_row != len_columns: if len_row != len_columns:
less_than_columns = len_row < len_columns less_than_columns = len_row < len_columns
message = ( message = (
"Row has less values than columns" "Row has less values than columns" if less_than_columns else "Row has more values than columns"
if less_than_columns
else "Row has more values than columns"
) )
self.warnings.append( self.warnings.append(
{"row": self.row_number, "message": message,} {
"row": self.row_number,
"message": message,
}
) )
def parse_doc(self, doctype, parent_doc=None, table_df=None): def parse_doc(self, doctype, parent_doc=None, table_df=None):
@ -662,18 +690,24 @@ class Row:
options_string = ", ".join(frappe.bold(d) for d in select_options) options_string = ", ".join(frappe.bold(d) for d in select_options)
msg = _("Value must be one of {0}").format(options_string) msg = _("Value must be one of {0}").format(options_string)
self.warnings.append( self.warnings.append(
{"row": self.row_number, "field": df_as_json(df), "message": msg,} {
"row": self.row_number,
"field": df_as_json(df),
"message": msg,
}
) )
return return
elif df.fieldtype == "Link": elif df.fieldtype == "Link":
exists = self.link_exists(value, df) exists = self.link_exists(value, df)
if not exists: if not exists:
msg = _("Value {0} missing for {1}").format( msg = _("Value {0} missing for {1}").format(frappe.bold(value), frappe.bold(df.options))
frappe.bold(value), frappe.bold(df.options)
)
self.warnings.append( self.warnings.append(
{"row": self.row_number, "field": df_as_json(df), "message": msg,} {
"row": self.row_number,
"field": df_as_json(df),
"message": msg,
}
) )
return return
elif df.fieldtype in ["Date", "Datetime"]: elif df.fieldtype in ["Date", "Datetime"]:
@ -693,6 +727,7 @@ class Row:
return return
elif df.fieldtype == "Duration": elif df.fieldtype == "Duration":
import re import re
is_valid_duration = re.match(r"^(?:(\d+d)?((^|\s)\d+h)?((^|\s)\d+m)?((^|\s)\d+s)?)$", value) is_valid_duration = re.match(r"^(?:(\d+d)?((^|\s)\d+h)?((^|\s)\d+m)?((^|\s)\d+s)?)$", value)
if not is_valid_duration: if not is_valid_duration:
self.warnings.append( self.warnings.append(
@ -702,7 +737,7 @@ class Row:
"field": df_as_json(df), "field": df_as_json(df),
"message": _("Value {0} must be in the valid duration format: d h m s").format( "message": _("Value {0} must be in the valid duration format: d h m s").format(
frappe.bold(value) frappe.bold(value)
) ),
} }
) )
@ -789,9 +824,7 @@ class Header(Row):
else: else:
doctypes.append((col.df.parent, col.df.child_table_df)) doctypes.append((col.df.parent, col.df.child_table_df))
self.doctypes = sorted( self.doctypes = sorted(list(set(doctypes)), key=lambda x: -1 if x[0] == self.doctype else 1)
list(set(doctypes)), key=lambda x: -1 if x[0] == self.doctype else 1
)
def get_column_indexes(self, doctype, tablefield=None): def get_column_indexes(self, doctype, tablefield=None):
def is_table_field(df): def is_table_field(df):
@ -802,10 +835,7 @@ class Header(Row):
return [ return [
col.index col.index
for col in self.columns for col in self.columns
if not col.skip_import if not col.skip_import and col.df and col.df.parent == doctype and is_table_field(col.df)
and col.df
and col.df.parent == doctype
and is_table_field(col.df)
] ]
def get_columns(self, indexes): def get_columns(self, indexes):
@ -893,9 +923,7 @@ class Column:
self.warnings.append( self.warnings.append(
{ {
"col": column_number, "col": column_number,
"message": _("Cannot match column {0} with any field").format( "message": _("Cannot match column {0} with any field").format(frappe.bold(header_title)),
frappe.bold(header_title)
),
"type": "info", "type": "info",
} }
) )
@ -958,9 +986,7 @@ class Column:
if self.df.fieldtype == "Link": if self.df.fieldtype == "Link":
# find all values that dont exist # find all values that dont exist
values = list({cstr(v) for v in self.column_values[1:] if v}) values = list({cstr(v) for v in self.column_values[1:] if v})
exists = [ exists = [d.name for d in frappe.db.get_all(self.df.options, filters={"name": ("in", values)})]
d.name for d in frappe.db.get_all(self.df.options, filters={"name": ("in", values)})
]
not_exists = list(set(values) - set(exists)) not_exists = list(set(values) - set(exists))
if not_exists: if not_exists:
missing_values = ", ".join(not_exists) missing_values = ", ".join(not_exists)
@ -968,9 +994,7 @@ class Column:
{ {
"col": self.column_number, "col": self.column_number,
"message": ( "message": (
"The following values do not exist for {}: {}".format( "The following values do not exist for {}: {}".format(self.df.options, missing_values)
self.df.options, missing_values
)
), ),
"type": "warning", "type": "warning",
} }
@ -983,7 +1007,9 @@ class Column:
self.warnings.append( self.warnings.append(
{ {
"col": self.column_number, "col": self.column_number,
"message": _("Date format could not be determined from the values in this column. Defaulting to yyyy-mm-dd."), "message": _(
"Date format could not be determined from the values in this column. Defaulting to yyyy-mm-dd."
),
"type": "info", "type": "info",
} }
) )
@ -1027,12 +1053,12 @@ def build_fields_dict_for_column_matching(parent_doctype):
Build a dict with various keys to match with column headers and value as docfield Build a dict with various keys to match with column headers and value as docfield
The keys can be label or fieldname The keys can be label or fieldname
{ {
'Customer': df1, 'Customer': df1,
'customer': df1, 'customer': df1,
'Due Date': df2, 'Due Date': df2,
'due_date': df2, 'due_date': df2,
'Item Code (Sales Invoice Item)': df3, 'Item Code (Sales Invoice Item)': df3,
'Sales Invoice Item:item_code': df3, 'Sales Invoice Item:item_code': df3,
} }
""" """
@ -1062,9 +1088,7 @@ def build_fields_dict_for_column_matching(parent_doctype):
out = {} out = {}
# doctypes and fieldname if it is a child doctype # doctypes and fieldname if it is a child doctype
doctypes = [(parent_doctype, None)] + [ doctypes = [(parent_doctype, None)] + [(df.options, df) for df in parent_meta.get_table_fields()]
(df.options, df) for df in parent_meta.get_table_fields()
]
for doctype, table_df in doctypes: for doctype, table_df in doctypes:
translated_table_label = _(table_df.label) if table_df else None translated_table_label = _(table_df.label) if table_df else None
@ -1082,15 +1106,15 @@ def build_fields_dict_for_column_matching(parent_doctype):
if doctype == parent_doctype: if doctype == parent_doctype:
name_headers = ( name_headers = (
"name", # fieldname "name", # fieldname
"ID", # label "ID", # label
_("ID"), # translated label _("ID"), # translated label
) )
else: else:
name_headers = ( name_headers = (
"{0}.name".format(table_df.fieldname), # fieldname "{0}.name".format(table_df.fieldname), # fieldname
"ID ({0})".format(table_df.label), # label "ID ({0})".format(table_df.label), # label
"{0} ({1})".format(_("ID"), translated_table_label), # translated label "{0} ({1})".format(_("ID"), translated_table_label), # translated label
) )
name_df.is_child_table_field = True name_df.is_child_table_field = True
@ -1122,7 +1146,7 @@ def build_fields_dict_for_column_matching(parent_doctype):
for header in ( for header in (
df.fieldname, df.fieldname,
f"{label} ({df.fieldname})", f"{label} ({df.fieldname})",
f"{translated_label} ({df.fieldname})" f"{translated_label} ({df.fieldname})",
): ):
out[header] = df out[header] = df
@ -1155,9 +1179,8 @@ def build_fields_dict_for_column_matching(parent_doctype):
autoname_field = get_autoname_field(parent_doctype) autoname_field = get_autoname_field(parent_doctype)
if autoname_field: if autoname_field:
for header in ( for header in (
"ID ({})".format(autoname_field.label), # label "ID ({})".format(autoname_field.label), # label
"{0} ({1})".format(_("ID"), _(autoname_field.label)), # translated label "{0} ({1})".format(_("ID"), _(autoname_field.label)), # translated label
# ID field should also map to the autoname field # ID field should also map to the autoname field
"ID", "ID",
_("ID"), _("ID"),
@ -1205,10 +1228,7 @@ def get_item_at_index(_list, i, default=None):
def get_user_format(date_format): def get_user_format(date_format):
return ( return (
date_format.replace("%Y", "yyyy") date_format.replace("%Y", "yyyy").replace("%y", "yy").replace("%m", "mm").replace("%d", "dd")
.replace("%y", "yy")
.replace("%m", "mm")
.replace("%d", "dd")
) )
@ -1226,16 +1246,17 @@ def df_as_json(df):
def get_select_options(df): def get_select_options(df):
return [d for d in (df.options or "").split("\n") if d] return [d for d in (df.options or "").split("\n") if d]
def create_import_log(data_import, log_index, log_details): def create_import_log(data_import, log_index, log_details):
frappe.get_doc({ frappe.get_doc(
'doctype': 'Data Import Log', {
'log_index': log_index, "doctype": "Data Import Log",
'success': log_details.get('success'), "log_index": log_index,
'data_import': data_import, "success": log_details.get("success"),
'row_indexes': json.dumps(log_details.get('row_indexes')), "data_import": data_import,
'docname': log_details.get('docname'), "row_indexes": json.dumps(log_details.get("row_indexes")),
'messages': json.dumps(log_details.get('messages', '[]')), "docname": log_details.get("docname"),
'exception': log_details.get('exception') "messages": json.dumps(log_details.get("messages", "[]")),
}).db_insert() "exception": log_details.get("exception"),
}
).db_insert()

View file

@ -4,5 +4,6 @@
# import frappe # import frappe
import unittest import unittest
class TestDataImport(unittest.TestCase): class TestDataImport(unittest.TestCase):
pass pass

View file

@ -2,13 +2,13 @@
# Copyright (c) 2019, Frappe Technologies and Contributors # Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import unittest import unittest
import frappe import frappe
from frappe.core.doctype.data_import.exporter import Exporter from frappe.core.doctype.data_import.exporter import Exporter
from frappe.core.doctype.data_import.test_importer import ( from frappe.core.doctype.data_import.test_importer import create_doctype_if_not_exists
create_doctype_if_not_exists,
) doctype_name = "DocType for Export"
doctype_name = 'DocType for Export'
class TestExporter(unittest.TestCase): class TestExporter(unittest.TestCase):
def setUp(self): def setUp(self):
@ -93,10 +93,10 @@ class TestExporter(unittest.TestCase):
doctype_name, doctype_name,
export_fields={doctype_name: ["title", "description"]}, export_fields={doctype_name: ["title", "description"]},
export_data=True, export_data=True,
file_type="CSV" file_type="CSV",
) )
e.build_response() e.build_response()
self.assertTrue(frappe.response['result']) self.assertTrue(frappe.response["result"])
self.assertEqual(frappe.response['doctype'], doctype_name) self.assertEqual(frappe.response["doctype"], doctype_name)
self.assertEqual(frappe.response['type'], "csv") self.assertEqual(frappe.response["type"], "csv")

View file

@ -2,53 +2,57 @@
# Copyright (c) 2019, Frappe Technologies and Contributors # Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import unittest import unittest
import frappe import frappe
from frappe.core.doctype.data_import.importer import Importer from frappe.core.doctype.data_import.importer import Importer
from frappe.tests.test_query_builder import db_type_is, run_only_if from frappe.tests.test_query_builder import db_type_is, run_only_if
from frappe.utils import getdate, format_duration from frappe.utils import format_duration, getdate
doctype_name = "DocType for Import"
doctype_name = 'DocType for Import'
class TestImporter(unittest.TestCase): class TestImporter(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
create_doctype_if_not_exists(doctype_name,) create_doctype_if_not_exists(
doctype_name,
)
def test_data_import_from_file(self): def test_data_import_from_file(self):
import_file = get_import_file('sample_import_file') import_file = get_import_file("sample_import_file")
data_import = self.get_importer(doctype_name, import_file) data_import = self.get_importer(doctype_name, import_file)
data_import.start_import() data_import.start_import()
doc1 = frappe.get_doc(doctype_name, 'Test') doc1 = frappe.get_doc(doctype_name, "Test")
doc2 = frappe.get_doc(doctype_name, 'Test 2') doc2 = frappe.get_doc(doctype_name, "Test 2")
doc3 = frappe.get_doc(doctype_name, 'Test 3') doc3 = frappe.get_doc(doctype_name, "Test 3")
self.assertEqual(doc1.description, 'test description') self.assertEqual(doc1.description, "test description")
self.assertEqual(doc1.number, 1) self.assertEqual(doc1.number, 1)
self.assertEqual(format_duration(doc1.duration), '3h') self.assertEqual(format_duration(doc1.duration), "3h")
self.assertEqual(doc1.table_field_1[0].child_title, 'child title') self.assertEqual(doc1.table_field_1[0].child_title, "child title")
self.assertEqual(doc1.table_field_1[0].child_description, 'child description') 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_title, "child title 2")
self.assertEqual(doc1.table_field_1[1].child_description, 'child description 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_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_date, getdate("2019-10-30"))
self.assertEqual(doc1.table_field_2[1].child_2_another_number, 5) self.assertEqual(doc1.table_field_2[1].child_2_another_number, 5)
self.assertEqual(doc1.table_field_1_again[0].child_title, 'child title again') self.assertEqual(doc1.table_field_1_again[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_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[1].child_date, getdate("2021-09-22"))
self.assertEqual(doc2.description, 'test description 2') self.assertEqual(doc2.description, "test description 2")
self.assertEqual(format_duration(doc2.duration), '4d 3h') self.assertEqual(format_duration(doc2.duration), "4d 3h")
self.assertEqual(doc3.another_number, 5) self.assertEqual(doc3.another_number, 5)
self.assertEqual(format_duration(doc3.duration), '5d 5h 45m') self.assertEqual(format_duration(doc3.duration), "5d 5h 45m")
def test_data_import_preview(self): def test_data_import_preview(self):
import_file = get_import_file('sample_import_file') import_file = get_import_file("sample_import_file")
data_import = self.get_importer(doctype_name, import_file) data_import = self.get_importer(doctype_name, import_file)
preview = data_import.get_preview_from_template() preview = data_import.get_preview_from_template()
@ -58,35 +62,49 @@ class TestImporter(unittest.TestCase):
# ignored on postgres because myisam doesn't exist on pg # ignored on postgres because myisam doesn't exist on pg
@run_only_if(db_type_is.MARIADB) @run_only_if(db_type_is.MARIADB)
def test_data_import_without_mandatory_values(self): def test_data_import_without_mandatory_values(self):
import_file = get_import_file('sample_import_file_without_mandatory') import_file = get_import_file("sample_import_file_without_mandatory")
data_import = self.get_importer(doctype_name, import_file) data_import = self.get_importer(doctype_name, import_file)
frappe.local.message_log = [] frappe.local.message_log = []
data_import.start_import() data_import.start_import()
data_import.reload() data_import.reload()
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "messages", "exception", "docname"], import_log = frappe.db.get_all(
"Data Import Log",
fields=["row_indexes", "success", "messages", "exception", "docname"],
filters={"data_import": data_import.name}, filters={"data_import": data_import.name},
order_by="log_index") order_by="log_index",
)
self.assertEqual(frappe.parse_json(import_log[0]['row_indexes']), [2,3]) 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" expected_error = (
self.assertEqual(frappe.parse_json(frappe.parse_json(import_log[0]['messages'])[0])['message'], expected_error) "Error: <strong>Child 1 of DocType for Import</strong> Row #1: Value missing for: Child Title"
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(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(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(frappe.parse_json(import_log[1]["messages"])[0])["message"],
"Title is required",
)
def test_data_import_update(self): def test_data_import_update(self):
existing_doc = frappe.get_doc( existing_doc = frappe.get_doc(
doctype=doctype_name, doctype=doctype_name,
title=frappe.generate_hash(doctype_name, 8), title=frappe.generate_hash(doctype_name, 8),
table_field_1=[{'child_title': 'child title to update'}] table_field_1=[{"child_title": "child title to update"}],
) )
existing_doc.save() existing_doc.save()
frappe.db.commit() frappe.db.commit()
import_file = get_import_file('sample_import_file_for_update') import_file = get_import_file("sample_import_file_for_update")
data_import = self.get_importer(doctype_name, import_file, update=True) data_import = self.get_importer(doctype_name, import_file, update=True)
i = Importer(data_import.reference_doctype, data_import=data_import) i = Importer(data_import.reference_doctype, data_import=data_import)
@ -104,15 +122,15 @@ class TestImporter(unittest.TestCase):
updated_doc = frappe.get_doc(doctype_name, existing_doc.name) updated_doc = frappe.get_doc(doctype_name, existing_doc.name)
self.assertEqual(existing_doc.title, updated_doc.title) self.assertEqual(existing_doc.title, updated_doc.title)
self.assertEqual(updated_doc.description, 'test description') self.assertEqual(updated_doc.description, "test description")
self.assertEqual(updated_doc.table_field_1[0].child_title, 'child title') self.assertEqual(updated_doc.table_field_1[0].child_title, "child title")
self.assertEqual(updated_doc.table_field_1[0].name, existing_doc.table_field_1[0].name) self.assertEqual(updated_doc.table_field_1[0].name, existing_doc.table_field_1[0].name)
self.assertEqual(updated_doc.table_field_1[0].child_description, 'child description') self.assertEqual(updated_doc.table_field_1[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_again[0].child_title, "child title again")
def get_importer(self, doctype, import_file, update=False): def get_importer(self, doctype, import_file, update=False):
data_import = frappe.new_doc('Data Import') data_import = frappe.new_doc("Data Import")
data_import.import_type = 'Insert New Records' if not update else 'Update Existing Records' data_import.import_type = "Insert New Records" if not update else "Update Existing Records"
data_import.reference_doctype = doctype data_import.reference_doctype = doctype
data_import.import_file = import_file.file_url data_import.import_file = import_file.file_url
data_import.insert() data_import.insert()
@ -121,88 +139,109 @@ class TestImporter(unittest.TestCase):
return data_import return data_import
def create_doctype_if_not_exists(doctype_name, force=False): def create_doctype_if_not_exists(doctype_name, force=False):
if force: if force:
frappe.delete_doc_if_exists('DocType', doctype_name) frappe.delete_doc_if_exists("DocType", doctype_name)
frappe.delete_doc_if_exists('DocType', 'Child 1 of ' + 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", "Child 2 of " + doctype_name)
if frappe.db.exists('DocType', doctype_name): if frappe.db.exists("DocType", doctype_name):
return return
# Child Table 1 # Child Table 1
table_1_name = 'Child 1 of ' + doctype_name table_1_name = "Child 1 of " + doctype_name
frappe.get_doc({ frappe.get_doc(
'doctype': 'DocType', {
'name': table_1_name, "doctype": "DocType",
'module': 'Custom', "name": table_1_name,
'custom': 1, "module": "Custom",
'istable': 1, "custom": 1,
'fields': [ "istable": 1,
{'label': 'Child Title', 'fieldname': 'child_title', 'reqd': 1, 'fieldtype': 'Data'}, "fields": [
{'label': 'Child Description', 'fieldname': 'child_description', 'fieldtype': 'Small Text'}, {"label": "Child Title", "fieldname": "child_title", "reqd": 1, "fieldtype": "Data"},
{'label': 'Child Date', 'fieldname': 'child_date', 'fieldtype': 'Date'}, {"label": "Child Description", "fieldname": "child_description", "fieldtype": "Small Text"},
{'label': 'Child Number', 'fieldname': 'child_number', 'fieldtype': 'Int'}, {"label": "Child Date", "fieldname": "child_date", "fieldtype": "Date"},
{'label': 'Child Number', 'fieldname': 'child_another_number', 'fieldtype': 'Int'}, {"label": "Child Number", "fieldname": "child_number", "fieldtype": "Int"},
] {"label": "Child Number", "fieldname": "child_another_number", "fieldtype": "Int"},
}).insert() ],
}
).insert()
# Child Table 2 # Child Table 2
table_2_name = 'Child 2 of ' + doctype_name table_2_name = "Child 2 of " + doctype_name
frappe.get_doc({ frappe.get_doc(
'doctype': 'DocType', {
'name': table_2_name, "doctype": "DocType",
'module': 'Custom', "name": table_2_name,
'custom': 1, "module": "Custom",
'istable': 1, "custom": 1,
'fields': [ "istable": 1,
{'label': 'Child 2 Title', 'fieldname': 'child_2_title', 'reqd': 1, 'fieldtype': 'Data'}, "fields": [
{'label': 'Child 2 Description', 'fieldname': 'child_2_description', 'fieldtype': 'Small Text'}, {"label": "Child 2 Title", "fieldname": "child_2_title", "reqd": 1, "fieldtype": "Data"},
{'label': 'Child 2 Date', 'fieldname': 'child_2_date', 'fieldtype': 'Date'}, {
{'label': 'Child 2 Number', 'fieldname': 'child_2_number', 'fieldtype': 'Int'}, "label": "Child 2 Description",
{'label': 'Child 2 Number', 'fieldname': 'child_2_another_number', 'fieldtype': 'Int'}, "fieldname": "child_2_description",
] "fieldtype": "Small Text",
}).insert() },
{"label": "Child 2 Date", "fieldname": "child_2_date", "fieldtype": "Date"},
{"label": "Child 2 Number", "fieldname": "child_2_number", "fieldtype": "Int"},
{"label": "Child 2 Number", "fieldname": "child_2_another_number", "fieldtype": "Int"},
],
}
).insert()
# Main Table # Main Table
frappe.get_doc({ frappe.get_doc(
'doctype': 'DocType', {
'name': doctype_name, "doctype": "DocType",
'module': 'Custom', "name": doctype_name,
'custom': 1, "module": "Custom",
'autoname': 'field:title', "custom": 1,
'fields': [ "autoname": "field:title",
{'label': 'Title', 'fieldname': 'title', 'reqd': 1, 'fieldtype': 'Data'}, "fields": [
{'label': 'Description', 'fieldname': 'description', 'fieldtype': 'Small Text'}, {"label": "Title", "fieldname": "title", "reqd": 1, "fieldtype": "Data"},
{'label': 'Date', 'fieldname': 'date', 'fieldtype': 'Date'}, {"label": "Description", "fieldname": "description", "fieldtype": "Small Text"},
{'label': 'Duration', 'fieldname': 'duration', 'fieldtype': 'Duration'}, {"label": "Date", "fieldname": "date", "fieldtype": "Date"},
{'label': 'Number', 'fieldname': 'number', 'fieldtype': 'Int'}, {"label": "Duration", "fieldname": "duration", "fieldtype": "Duration"},
{'label': 'Number', 'fieldname': 'another_number', 'fieldtype': 'Int'}, {"label": "Number", "fieldname": "number", "fieldtype": "Int"},
{'label': 'Table Field 1', 'fieldname': 'table_field_1', 'fieldtype': 'Table', 'options': table_1_name}, {"label": "Number", "fieldname": "another_number", "fieldtype": "Int"},
{'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}, "label": "Table Field 1",
], "fieldname": "table_field_1",
'permissions': [ "fieldtype": "Table",
{'role': 'System Manager'} "options": table_1_name,
] },
}).insert() {
"label": "Table Field 2",
"fieldname": "table_field_2",
"fieldtype": "Table",
"options": table_2_name,
},
{
"label": "Table Field 1 Again",
"fieldname": "table_field_1_again",
"fieldtype": "Table",
"options": table_1_name,
},
],
"permissions": [{"role": "System Manager"}],
}
).insert()
def get_import_file(csv_file_name, force=False): def get_import_file(csv_file_name, force=False):
file_name = csv_file_name + '.csv' file_name = csv_file_name + ".csv"
_file = frappe.db.exists('File', {'file_name': file_name}) _file = frappe.db.exists("File", {"file_name": file_name})
if force and _file: if force and _file:
frappe.delete_doc_if_exists('File', _file) frappe.delete_doc_if_exists("File", _file)
if frappe.db.exists('File', {'file_name': file_name}): if frappe.db.exists("File", {"file_name": file_name}):
f = frappe.get_doc('File', {'file_name': file_name}) f = frappe.get_doc("File", {"file_name": file_name})
else: else:
full_path = get_csv_file_path(file_name) full_path = get_csv_file_path(file_name)
f = frappe.get_doc( f = frappe.get_doc(
doctype='File', doctype="File", content=frappe.read_file(full_path), file_name=file_name, is_private=1
content=frappe.read_file(full_path),
file_name=file_name,
is_private=1
) )
f.save(ignore_permissions=True) f.save(ignore_permissions=True)
@ -210,4 +249,4 @@ def get_import_file(csv_file_name, force=False):
def get_csv_file_path(file_name): def get_csv_file_path(file_name):
return frappe.get_app_path('frappe', 'core', 'doctype', 'data_import', 'fixtures', file_name) return frappe.get_app_path("frappe", "core", "doctype", "data_import", "fixtures", file_name)

View file

@ -4,5 +4,6 @@
# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document
class DataImportLog(Document): class DataImportLog(Document):
pass pass

View file

@ -4,5 +4,6 @@
# import frappe # import frappe
import unittest import unittest
class TestDataImportLog(unittest.TestCase): class TestDataImportLog(unittest.TestCase):
pass pass

View file

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

View file

@ -2,19 +2,24 @@
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
class DefaultValue(Document): class DefaultValue(Document):
pass pass
def on_doctype_update(): def on_doctype_update():
"""Create indexes for `tabDefaultValue` on `(parent, defkey)`""" """Create indexes for `tabDefaultValue` on `(parent, defkey)`"""
frappe.db.commit() frappe.db.commit()
frappe.db.add_index(doctype='DefaultValue', frappe.db.add_index(
fields=['parent', 'defkey'], doctype="DefaultValue",
index_name='defaultvalue_parent_defkey_index') fields=["parent", "defkey"],
index_name="defaultvalue_parent_defkey_index",
)
frappe.db.add_index(doctype='DefaultValue', frappe.db.add_index(
fields=['parent', 'parenttype'], doctype="DefaultValue",
index_name='defaultvalue_parent_parenttype_index') fields=["parent", "parenttype"],
index_name="defaultvalue_parent_parenttype_index",
)

View file

@ -2,11 +2,12 @@
# Copyright (c) 2015, Frappe Technologies and contributors # Copyright (c) 2015, Frappe Technologies and contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe
import json import json
import frappe
from frappe import _
from frappe.desk.doctype.bulk_update.bulk_update import show_progress from frappe.desk.doctype.bulk_update.bulk_update import show_progress
from frappe.model.document import Document from frappe.model.document import Document
from frappe import _
class DeletedDocument(Document): class DeletedDocument(Document):
@ -15,7 +16,7 @@ class DeletedDocument(Document):
@frappe.whitelist() @frappe.whitelist()
def restore(name, alert=True): def restore(name, alert=True):
deleted = frappe.get_doc('Deleted Document', name) deleted = frappe.get_doc("Deleted Document", name)
if deleted.restored: if deleted.restored:
frappe.throw(_("Document {0} Already Restored").format(name), exc=frappe.DocumentAlreadyRestored) frappe.throw(_("Document {0} Already Restored").format(name), exc=frappe.DocumentAlreadyRestored)
@ -29,20 +30,20 @@ def restore(name, alert=True):
doc.docstatus = 0 doc.docstatus = 0
doc.insert() doc.insert()
doc.add_comment('Edit', _('restored {0} as {1}').format(deleted.deleted_name, doc.name)) doc.add_comment("Edit", _("restored {0} as {1}").format(deleted.deleted_name, doc.name))
deleted.new_name = doc.name deleted.new_name = doc.name
deleted.restored = 1 deleted.restored = 1
deleted.db_update() deleted.db_update()
if alert: if alert:
frappe.msgprint(_('Document Restored')) frappe.msgprint(_("Document Restored"))
@frappe.whitelist() @frappe.whitelist()
def bulk_restore(docnames): def bulk_restore(docnames):
docnames = frappe.parse_json(docnames) docnames = frappe.parse_json(docnames)
message = _('Restoring Deleted Document') message = _("Restoring Deleted Document")
restored, invalid, failed = [], [], [] restored, invalid, failed = [], [], []
for i, d in enumerate(docnames): for i, d in enumerate(docnames):
@ -61,8 +62,4 @@ def bulk_restore(docnames):
failed.append(d) failed.append(d)
frappe.db.rollback() frappe.db.rollback()
return { return {"restored": restored, "invalid": invalid, "failed": failed}
"restored": restored,
"invalid": invalid,
"failed": failed
}

View file

@ -1,10 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors # Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe
import unittest import unittest
import frappe
# test_records = frappe.get_test_records('Deleted Document') # test_records = frappe.get_test_records('Deleted Document')
class TestDeletedDocument(unittest.TestCase): class TestDeletedDocument(unittest.TestCase):
pass pass

View file

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

View file

@ -4,28 +4,28 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
class DocField(Document): class DocField(Document):
def get_link_doctype(self): def get_link_doctype(self):
'''Returns the Link doctype for the docfield (if applicable) """Returns the Link doctype for the docfield (if applicable)
if fieldtype is Link: Returns "options" if fieldtype is Link: Returns "options"
if fieldtype is Table MultiSelect: Returns "options" of the Link field in the Child Table if fieldtype is Table MultiSelect: Returns "options" of the Link field in the Child Table
''' """
if self.fieldtype == 'Link': if self.fieldtype == "Link":
return self.options return self.options
if self.fieldtype == 'Table MultiSelect': if self.fieldtype == "Table MultiSelect":
table_doctype = self.options table_doctype = self.options
link_doctype = frappe.db.get_value('DocField', { link_doctype = frappe.db.get_value(
'fieldtype': 'Link', "DocField",
'parenttype': 'DocType', {"fieldtype": "Link", "parenttype": "DocType", "parent": table_doctype, "in_list_view": 1},
'parent': table_doctype, "options",
'in_list_view': 1 )
}, 'options')
return link_doctype return link_doctype
def get_select_options(self): def get_select_options(self):
if self.fieldtype == 'Select': if self.fieldtype == "Select":
options = self.options or '' options = self.options or ""
return [d for d in options.split('\n') if d] return [d for d in options.split("\n") if d]

View file

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

View file

@ -2,8 +2,8 @@
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
class DocPerm(Document): class DocPerm(Document):
pass pass

View file

@ -2,12 +2,13 @@
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe import frappe
from frappe.model.document import Document
from frappe import _ from frappe import _
from frappe.utils import get_fullname, cint from frappe.model.document import Document
from frappe.utils import cint, get_fullname
exclude_from_linked_with = True exclude_from_linked_with = True
class DocShare(Document): class DocShare(Document):
no_feed_on_delete = True no_feed_on_delete = True
@ -36,15 +37,21 @@ class DocShare(Document):
frappe.throw(_("User is mandatory for Share"), frappe.MandatoryError) frappe.throw(_("User is mandatory for Share"), frappe.MandatoryError)
def check_share_permission(self): def check_share_permission(self):
if (not self.flags.ignore_share_permission and if not self.flags.ignore_share_permission and not frappe.has_permission(
not frappe.has_permission(self.share_doctype, "share", self.get_doc())): self.share_doctype, "share", self.get_doc()
):
frappe.throw(_('You need to have "Share" permission'), frappe.PermissionError) frappe.throw(_('You need to have "Share" permission'), frappe.PermissionError)
def check_is_submittable(self): def check_is_submittable(self):
if self.submit and not cint(frappe.db.get_value("DocType", self.share_doctype, "is_submittable")): if self.submit and not cint(
frappe.throw(_("Cannot share {0} with submit permission as the doctype {1} is not submittable").format( frappe.db.get_value("DocType", self.share_doctype, "is_submittable")
frappe.bold(self.share_name), frappe.bold(self.share_doctype))) ):
frappe.throw(
_("Cannot share {0} with submit permission as the doctype {1} is not submittable").format(
frappe.bold(self.share_name), frappe.bold(self.share_doctype)
)
)
def after_insert(self): def after_insert(self):
doc = self.get_doc() doc = self.get_doc()
@ -53,14 +60,21 @@ class DocShare(Document):
if self.everyone: if self.everyone:
doc.add_comment("Shared", _("{0} shared this document with everyone").format(owner)) doc.add_comment("Shared", _("{0} shared this document with everyone").format(owner))
else: else:
doc.add_comment("Shared", _("{0} shared this document with {1}").format(owner, get_fullname(self.user))) doc.add_comment(
"Shared", _("{0} shared this document with {1}").format(owner, get_fullname(self.user))
)
def on_trash(self): def on_trash(self):
if not self.flags.ignore_share_permission: if not self.flags.ignore_share_permission:
self.check_share_permission() self.check_share_permission()
self.get_doc().add_comment("Unshared", self.get_doc().add_comment(
_("{0} un-shared this document with {1}").format(get_fullname(self.owner), get_fullname(self.user))) "Unshared",
_("{0} un-shared this document with {1}").format(
get_fullname(self.owner), get_fullname(self.user)
),
)
def on_doctype_update(): def on_doctype_update():
"""Add index in `tabDocShare` for `(user, share_doctype)`""" """Add index in `tabDocShare` for `(user, share_doctype)`"""

View file

@ -1,20 +1,26 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import unittest
import frappe import frappe
import frappe.share import frappe.share
import unittest
from frappe.automation.doctype.auto_repeat.test_auto_repeat import create_submittable_doctype from frappe.automation.doctype.auto_repeat.test_auto_repeat import create_submittable_doctype
test_dependencies = ['User'] test_dependencies = ["User"]
class TestDocShare(unittest.TestCase): class TestDocShare(unittest.TestCase):
def setUp(self): def setUp(self):
self.user = "test@example.com" self.user = "test@example.com"
self.event = frappe.get_doc({"doctype": "Event", self.event = frappe.get_doc(
"subject": "test share event", {
"starts_on": "2015-01-01 10:00:00", "doctype": "Event",
"event_type": "Private"}).insert() "subject": "test share event",
"starts_on": "2015-01-01 10:00:00",
"event_type": "Private",
}
).insert()
def tearDown(self): def tearDown(self):
frappe.set_user("Administrator") frappe.set_user("Administrator")
@ -98,7 +104,9 @@ class TestDocShare(unittest.TestCase):
doctype = "Test DocShare with Submit" doctype = "Test DocShare with Submit"
create_submittable_doctype(doctype, submit_perms=0) create_submittable_doctype(doctype, submit_perms=0)
submittable_doc = frappe.get_doc(dict(doctype=doctype, test="test docshare with submit")).insert() submittable_doc = frappe.get_doc(
dict(doctype=doctype, test="test docshare with submit")
).insert()
frappe.set_user(self.user) frappe.set_user(self.user)
self.assertFalse(frappe.has_permission(doctype, "submit", user=self.user)) self.assertFalse(frappe.has_permission(doctype, "submit", user=self.user))
@ -107,10 +115,14 @@ class TestDocShare(unittest.TestCase):
frappe.share.add(doctype, submittable_doc.name, self.user, submit=1) frappe.share.add(doctype, submittable_doc.name, self.user, submit=1)
frappe.set_user(self.user) frappe.set_user(self.user)
self.assertTrue(frappe.has_permission(doctype, "submit", doc=submittable_doc.name, user=self.user)) self.assertTrue(
frappe.has_permission(doctype, "submit", doc=submittable_doc.name, user=self.user)
)
# test cascade # test cascade
self.assertTrue(frappe.has_permission(doctype, "read", doc=submittable_doc.name, user=self.user)) self.assertTrue(frappe.has_permission(doctype, "read", doc=submittable_doc.name, user=self.user))
self.assertTrue(frappe.has_permission(doctype, "write", doc=submittable_doc.name, user=self.user)) self.assertTrue(
frappe.has_permission(doctype, "write", doc=submittable_doc.name, user=self.user)
)
frappe.share.remove(doctype, submittable_doc.name, self.user) frappe.share.remove(doctype, submittable_doc.name, self.user)

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,8 @@
import frappe import frappe
from frappe.desk.utils import slug from frappe.desk.utils import slug
def execute(): def execute():
for doctype in frappe.get_all('DocType', ['name', 'route'], dict(istable=0)): for doctype in frappe.get_all("DocType", ["name", "route"], dict(istable=0)):
if not doctype.route: if not doctype.route:
frappe.db.set_value('DocType', doctype.name, 'route', slug(doctype.name), update_modified = False) frappe.db.set_value("DocType", doctype.name, "route", slug(doctype.name), update_modified=False)

View file

@ -1,21 +1,24 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe
import unittest import unittest
from frappe.core.doctype.doctype.doctype import (UniqueFieldnameError,
IllegalMandatoryError, import frappe
DoctypeLinkError, from frappe.core.doctype.doctype.doctype import (
WrongOptionsDoctypeLinkError,
HiddenAndMandatoryWithoutDefaultError,
CannotIndexedError, CannotIndexedError,
DoctypeLinkError,
HiddenAndMandatoryWithoutDefaultError,
IllegalMandatoryError,
InvalidFieldNameError, InvalidFieldNameError,
validate_links_table_fieldnames) UniqueFieldnameError,
WrongOptionsDoctypeLinkError,
validate_links_table_fieldnames,
)
# test_records = frappe.get_test_records('DocType') # test_records = frappe.get_test_records('DocType')
class TestDocType(unittest.TestCase):
class TestDocType(unittest.TestCase):
def tearDown(self): def tearDown(self):
frappe.db.rollback() frappe.db.rollback()
@ -23,7 +26,10 @@ class TestDocType(unittest.TestCase):
self.assertRaises(frappe.NameError, new_doctype("_Some DocType").insert) self.assertRaises(frappe.NameError, new_doctype("_Some DocType").insert)
self.assertRaises(frappe.NameError, new_doctype("8Some DocType").insert) self.assertRaises(frappe.NameError, new_doctype("8Some DocType").insert)
self.assertRaises(frappe.NameError, new_doctype("Some (DocType)").insert) self.assertRaises(frappe.NameError, new_doctype("Some (DocType)").insert)
self.assertRaises(frappe.NameError, new_doctype("Some Doctype with a name whose length is more than 61 characters").insert) self.assertRaises(
frappe.NameError,
new_doctype("Some Doctype with a name whose length is more than 61 characters").insert,
)
for name in ("Some DocType", "Some_DocType", "Some-DocType"): for name in ("Some DocType", "Some_DocType", "Some-DocType"):
if frappe.db.exists("DocType", name): if frappe.db.exists("DocType", name):
frappe.delete_doc("DocType", name) frappe.delete_doc("DocType", name)
@ -86,19 +92,33 @@ class TestDocType(unittest.TestCase):
def test_all_depends_on_fields_conditions(self): def test_all_depends_on_fields_conditions(self):
import re import re
docfields = frappe.get_all("DocField", docfields = frappe.get_all(
or_filters={ "DocField",
"ifnull(depends_on, '')": ("!=", ''), or_filters={
"ifnull(collapsible_depends_on, '')": ("!=", ''), "ifnull(depends_on, '')": ("!=", ""),
"ifnull(mandatory_depends_on, '')": ("!=", ''), "ifnull(collapsible_depends_on, '')": ("!=", ""),
"ifnull(read_only_depends_on, '')": ("!=", '') "ifnull(mandatory_depends_on, '')": ("!=", ""),
"ifnull(read_only_depends_on, '')": ("!=", ""),
}, },
fields=["parent", "depends_on", "collapsible_depends_on", "mandatory_depends_on",\ fields=[
"read_only_depends_on", "fieldname", "fieldtype"]) "parent",
"depends_on",
"collapsible_depends_on",
"mandatory_depends_on",
"read_only_depends_on",
"fieldname",
"fieldtype",
],
)
pattern = r'[\w\.:_]+\s*={1}\s*[\w\.@\'"]+' pattern = r'[\w\.:_]+\s*={1}\s*[\w\.@\'"]+'
for field in docfields: for field in docfields:
for depends_on in ["depends_on", "collapsible_depends_on", "mandatory_depends_on", "read_only_depends_on"]: for depends_on in [
"depends_on",
"collapsible_depends_on",
"mandatory_depends_on",
"read_only_depends_on",
]:
condition = field.get(depends_on) condition = field.get(depends_on)
if condition: if condition:
self.assertFalse(re.match(pattern, condition)) self.assertFalse(re.match(pattern, condition))
@ -108,18 +128,18 @@ class TestDocType(unittest.TestCase):
valid_data_field_options = frappe.model.data_field_options + ("",) valid_data_field_options = frappe.model.data_field_options + ("",)
invalid_data_field_options = ("Invalid Option 1", frappe.utils.random_string(5)) invalid_data_field_options = ("Invalid Option 1", frappe.utils.random_string(5))
for field_option in (valid_data_field_options + invalid_data_field_options): for field_option in valid_data_field_options + invalid_data_field_options:
test_doctype = frappe.get_doc({ test_doctype = frappe.get_doc(
"doctype": "DocType", {
"name": doctype_name, "doctype": "DocType",
"module": "Core", "name": doctype_name,
"custom": 1, "module": "Core",
"fields": [{ "custom": 1,
"fieldname": "{0}_field".format(field_option), "fields": [
"fieldtype": "Data", {"fieldname": "{0}_field".format(field_option), "fieldtype": "Data", "options": field_option}
"options": field_option ],
}] }
}) )
if field_option in invalid_data_field_options: if field_option in invalid_data_field_options:
# assert that only data options in frappe.model.data_field_options are valid # assert that only data options in frappe.model.data_field_options are valid
@ -130,45 +150,29 @@ class TestDocType(unittest.TestCase):
test_doctype.delete() test_doctype.delete()
def test_sync_field_order(self): def test_sync_field_order(self):
from frappe.modules.import_file import get_file_path
import os import os
from frappe.modules.import_file import get_file_path
# create test doctype # create test doctype
test_doctype = frappe.get_doc({ test_doctype = frappe.get_doc(
"doctype": "DocType", {
"module": "Core", "doctype": "DocType",
"fields": [ "module": "Core",
{ "fields": [
"label": "Field 1", {"label": "Field 1", "fieldname": "field_1", "fieldtype": "Data"},
"fieldname": "field_1", {"label": "Field 2", "fieldname": "field_2", "fieldtype": "Data"},
"fieldtype": "Data" {"label": "Field 3", "fieldname": "field_3", "fieldtype": "Data"},
}, {"label": "Field 4", "fieldname": "field_4", "fieldtype": "Data"},
{ ],
"label": "Field 2", "permissions": [{"role": "System Manager", "read": 1}],
"fieldname": "field_2", "name": "Test Field Order DocType",
"fieldtype": "Data" "__islocal": 1,
}, }
{ )
"label": "Field 3",
"fieldname": "field_3",
"fieldtype": "Data"
},
{
"label": "Field 4",
"fieldname": "field_4",
"fieldtype": "Data"
}
],
"permissions": [{
"role": "System Manager",
"read": 1
}],
"name": "Test Field Order DocType",
"__islocal": 1
})
path = get_file_path(test_doctype.module, test_doctype.doctype, test_doctype.name) path = get_file_path(test_doctype.module, test_doctype.doctype, test_doctype.name)
initial_fields_order = ['field_1', 'field_2', 'field_3', 'field_4'] initial_fields_order = ["field_1", "field_2", "field_3", "field_4"]
frappe.delete_doc_if_exists("DocType", "Test Field Order DocType") frappe.delete_doc_if_exists("DocType", "Test Field Order DocType")
if os.path.isfile(path): if os.path.isfile(path):
@ -181,14 +185,18 @@ class TestDocType(unittest.TestCase):
# assert that field_order list is being created with the default order # assert that field_order list is being created with the default order
test_doctype_json = frappe.get_file_json(path) test_doctype_json = frappe.get_file_json(path)
self.assertTrue(test_doctype_json.get("field_order")) self.assertTrue(test_doctype_json.get("field_order"))
self.assertEqual(len(test_doctype_json['fields']), len(test_doctype_json['field_order'])) self.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(
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], initial_fields_order) [f["fieldname"] for f in test_doctype_json["fields"]], test_doctype_json["field_order"]
self.assertListEqual(test_doctype_json['field_order'], initial_fields_order) )
self.assertListEqual(
[f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order
)
self.assertListEqual(test_doctype_json["field_order"], initial_fields_order)
# remove field_order to test reload_doc/sync/migrate is backwards compatible without field_order # remove field_order to test reload_doc/sync/migrate is backwards compatible without field_order
del test_doctype_json['field_order'] del test_doctype_json["field_order"]
with open(path, 'w+') as txtfile: with open(path, "w+") as txtfile:
txtfile.write(frappe.as_json(test_doctype_json)) txtfile.write(frappe.as_json(test_doctype_json))
# assert that field_order is actually removed from the json file # assert that field_order is actually removed from the json file
@ -203,10 +211,14 @@ class TestDocType(unittest.TestCase):
test_doctype.save() test_doctype.save()
test_doctype_json = frappe.get_file_json(path) test_doctype_json = frappe.get_file_json(path)
self.assertTrue(test_doctype_json.get("field_order")) self.assertTrue(test_doctype_json.get("field_order"))
self.assertEqual(len(test_doctype_json['fields']), len(test_doctype_json['field_order'])) self.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(
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], initial_fields_order) [f["fieldname"] for f in test_doctype_json["fields"]], test_doctype_json["field_order"]
self.assertListEqual(test_doctype_json['field_order'], initial_fields_order) )
self.assertListEqual(
[f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order
)
self.assertListEqual(test_doctype_json["field_order"], initial_fields_order)
# reorder fields: swap row 1 and 3 # reorder fields: swap row 1 and 3
test_doctype.fields[0], test_doctype.fields[2] = test_doctype.fields[2], test_doctype.fields[0] test_doctype.fields[0], test_doctype.fields[2] = test_doctype.fields[2], test_doctype.fields[0]
@ -216,25 +228,30 @@ class TestDocType(unittest.TestCase):
# assert that reordering fields only affects `field_order` rather than `fields` attr # assert that reordering fields only affects `field_order` rather than `fields` attr
test_doctype.save() test_doctype.save()
test_doctype_json = frappe.get_file_json(path) test_doctype_json = frappe.get_file_json(path)
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], initial_fields_order) self.assertListEqual(
self.assertListEqual(test_doctype_json['field_order'], ['field_3', 'field_2', 'field_1', 'field_4']) [f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order
)
self.assertListEqual(
test_doctype_json["field_order"], ["field_3", "field_2", "field_1", "field_4"]
)
# reorder `field_order` in the json file: swap row 2 and 4 # reorder `field_order` in the json file: swap row 2 and 4
test_doctype_json['field_order'][1], test_doctype_json['field_order'][3] = test_doctype_json['field_order'][3], test_doctype_json['field_order'][1] test_doctype_json["field_order"][1], test_doctype_json["field_order"][3] = (
with open(path, 'w+') as txtfile: test_doctype_json["field_order"][3],
test_doctype_json["field_order"][1],
)
with open(path, "w+") as txtfile:
txtfile.write(frappe.as_json(test_doctype_json)) txtfile.write(frappe.as_json(test_doctype_json))
# assert that reordering `field_order` from json file is reflected in DocType upon migrate/sync # assert that reordering `field_order` from json file is reflected in DocType upon migrate/sync
frappe.reload_doctype(test_doctype.name, force=True) frappe.reload_doctype(test_doctype.name, force=True)
test_doctype.reload() test_doctype.reload()
self.assertListEqual([f.fieldname for f in test_doctype.fields], ['field_3', 'field_4', 'field_1', 'field_2']) self.assertListEqual(
[f.fieldname for f in test_doctype.fields], ["field_3", "field_4", "field_1", "field_2"]
)
# insert row in the middle and remove first row (field 3) # insert row in the middle and remove first row (field 3)
test_doctype.append("fields", { test_doctype.append("fields", {"label": "Field 5", "fieldname": "field_5", "fieldtype": "Data"})
"label": "Field 5",
"fieldname": "field_5",
"fieldtype": "Data"
})
test_doctype.fields[4], test_doctype.fields[3] = test_doctype.fields[3], test_doctype.fields[4] test_doctype.fields[4], test_doctype.fields[3] = test_doctype.fields[3], test_doctype.fields[4]
test_doctype.fields[3], test_doctype.fields[2] = test_doctype.fields[2], test_doctype.fields[3] test_doctype.fields[3], test_doctype.fields[2] = test_doctype.fields[2], test_doctype.fields[3]
test_doctype.remove(test_doctype.fields[0]) test_doctype.remove(test_doctype.fields[0])
@ -243,115 +260,121 @@ class TestDocType(unittest.TestCase):
test_doctype.save() test_doctype.save()
test_doctype_json = frappe.get_file_json(path) test_doctype_json = frappe.get_file_json(path)
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], ['field_1', 'field_2', 'field_4', 'field_5']) self.assertListEqual(
self.assertListEqual(test_doctype_json['field_order'], ['field_4', 'field_5', 'field_1', 'field_2']) [f["fieldname"] for f in test_doctype_json["fields"]],
["field_1", "field_2", "field_4", "field_5"],
)
self.assertListEqual(
test_doctype_json["field_order"], ["field_4", "field_5", "field_1", "field_2"]
)
except: except:
raise raise
finally: finally:
frappe.flags.allow_doctype_export = 0 frappe.flags.allow_doctype_export = 0
def test_unique_field_name_for_two_fields(self): def test_unique_field_name_for_two_fields(self):
doc = new_doctype('Test Unique Field') doc = new_doctype("Test Unique Field")
field_1 = doc.append('fields', {}) field_1 = doc.append("fields", {})
field_1.fieldname = 'some_fieldname_1' field_1.fieldname = "some_fieldname_1"
field_1.fieldtype = 'Data' field_1.fieldtype = "Data"
field_2 = doc.append('fields', {}) field_2 = doc.append("fields", {})
field_2.fieldname = 'some_fieldname_1' field_2.fieldname = "some_fieldname_1"
field_2.fieldtype = 'Data' field_2.fieldtype = "Data"
self.assertRaises(UniqueFieldnameError, doc.insert) self.assertRaises(UniqueFieldnameError, doc.insert)
def test_fieldname_is_not_name(self): def test_fieldname_is_not_name(self):
doc = new_doctype('Test Name Field') doc = new_doctype("Test Name Field")
field_1 = doc.append('fields', {}) field_1 = doc.append("fields", {})
field_1.label = 'Name' field_1.label = "Name"
field_1.fieldtype = 'Data' field_1.fieldtype = "Data"
doc.insert() doc.insert()
self.assertEqual(doc.fields[1].fieldname, "name1") self.assertEqual(doc.fields[1].fieldname, "name1")
doc.fields[1].fieldname = 'name' doc.fields[1].fieldname = "name"
self.assertRaises(InvalidFieldNameError, doc.save) self.assertRaises(InvalidFieldNameError, doc.save)
def test_illegal_mandatory_validation(self): def test_illegal_mandatory_validation(self):
doc = new_doctype('Test Illegal mandatory') doc = new_doctype("Test Illegal mandatory")
field_1 = doc.append('fields', {}) field_1 = doc.append("fields", {})
field_1.fieldname = 'some_fieldname_1' field_1.fieldname = "some_fieldname_1"
field_1.fieldtype = 'Section Break' field_1.fieldtype = "Section Break"
field_1.reqd = 1 field_1.reqd = 1
self.assertRaises(IllegalMandatoryError, doc.insert) self.assertRaises(IllegalMandatoryError, doc.insert)
def test_link_with_wrong_and_no_options(self): def test_link_with_wrong_and_no_options(self):
doc = new_doctype('Test link') doc = new_doctype("Test link")
field_1 = doc.append('fields', {}) field_1 = doc.append("fields", {})
field_1.fieldname = 'some_fieldname_1' field_1.fieldname = "some_fieldname_1"
field_1.fieldtype = 'Link' field_1.fieldtype = "Link"
self.assertRaises(DoctypeLinkError, doc.insert) self.assertRaises(DoctypeLinkError, doc.insert)
field_1.options = 'wrongdoctype' field_1.options = "wrongdoctype"
self.assertRaises(WrongOptionsDoctypeLinkError, doc.insert) self.assertRaises(WrongOptionsDoctypeLinkError, doc.insert)
def test_hidden_and_mandatory_without_default(self): def test_hidden_and_mandatory_without_default(self):
doc = new_doctype('Test hidden and mandatory') doc = new_doctype("Test hidden and mandatory")
field_1 = doc.append('fields', {}) field_1 = doc.append("fields", {})
field_1.fieldname = 'some_fieldname_1' field_1.fieldname = "some_fieldname_1"
field_1.fieldtype = 'Data' field_1.fieldtype = "Data"
field_1.reqd = 1 field_1.reqd = 1
field_1.hidden = 1 field_1.hidden = 1
self.assertRaises(HiddenAndMandatoryWithoutDefaultError, doc.insert) self.assertRaises(HiddenAndMandatoryWithoutDefaultError, doc.insert)
def test_field_can_not_be_indexed_validation(self): def test_field_can_not_be_indexed_validation(self):
doc = new_doctype('Test index') doc = new_doctype("Test index")
field_1 = doc.append('fields', {}) field_1 = doc.append("fields", {})
field_1.fieldname = 'some_fieldname_1' field_1.fieldname = "some_fieldname_1"
field_1.fieldtype = 'Long Text' field_1.fieldtype = "Long Text"
field_1.search_index = 1 field_1.search_index = 1
self.assertRaises(CannotIndexedError, doc.insert) self.assertRaises(CannotIndexedError, doc.insert)
def test_cancel_link_doctype(self): def test_cancel_link_doctype(self):
import json import json
from frappe.desk.form.linked_with import get_submitted_linked_docs, cancel_all_linked_docs
#create doctype from frappe.desk.form.linked_with import cancel_all_linked_docs, get_submitted_linked_docs
link_doc = new_doctype('Test Linked Doctype')
# create doctype
link_doc = new_doctype("Test Linked Doctype")
link_doc.is_submittable = 1 link_doc.is_submittable = 1
for data in link_doc.get('permissions'): for data in link_doc.get("permissions"):
data.submit = 1 data.submit = 1
data.cancel = 1 data.cancel = 1
link_doc.insert() link_doc.insert()
doc = new_doctype('Test Doctype') doc = new_doctype("Test Doctype")
doc.is_submittable = 1 doc.is_submittable = 1
field_2 = doc.append('fields', {}) field_2 = doc.append("fields", {})
field_2.label = 'Test Linked Doctype' field_2.label = "Test Linked Doctype"
field_2.fieldname = 'test_linked_doctype' field_2.fieldname = "test_linked_doctype"
field_2.fieldtype = 'Link' field_2.fieldtype = "Link"
field_2.options = 'Test Linked Doctype' field_2.options = "Test Linked Doctype"
for data in link_doc.get('permissions'): for data in link_doc.get("permissions"):
data.submit = 1 data.submit = 1
data.cancel = 1 data.cancel = 1
doc.insert() doc.insert()
# create doctype data # create doctype data
data_link_doc = frappe.new_doc('Test Linked Doctype') data_link_doc = frappe.new_doc("Test Linked Doctype")
data_link_doc.some_fieldname = 'Data1' data_link_doc.some_fieldname = "Data1"
data_link_doc.insert() data_link_doc.insert()
data_link_doc.save() data_link_doc.save()
data_link_doc.submit() data_link_doc.submit()
data_doc = frappe.new_doc('Test Doctype') data_doc = frappe.new_doc("Test Doctype")
data_doc.some_fieldname = 'Data1' data_doc.some_fieldname = "Data1"
data_doc.test_linked_doctype = data_link_doc.name data_doc.test_linked_doctype = data_link_doc.name
data_doc.insert() data_doc.insert()
data_doc.save() data_doc.save()
data_doc.submit() data_doc.submit()
docs = get_submitted_linked_docs(link_doc.name, data_link_doc.name) docs = get_submitted_linked_docs(link_doc.name, data_link_doc.name)
dump_docs = json.dumps(docs.get('docs')) dump_docs = json.dumps(docs.get("docs"))
cancel_all_linked_docs(dump_docs) cancel_all_linked_docs(dump_docs)
data_link_doc.cancel() data_link_doc.cancel()
data_doc.load_from_db() data_doc.load_from_db()
@ -369,69 +392,70 @@ class TestDocType(unittest.TestCase):
def test_ignore_cancelation_of_linked_doctype_during_cancel(self): def test_ignore_cancelation_of_linked_doctype_during_cancel(self):
import json import json
from frappe.desk.form.linked_with import get_submitted_linked_docs, cancel_all_linked_docs
#create linked doctype from frappe.desk.form.linked_with import cancel_all_linked_docs, get_submitted_linked_docs
link_doc = new_doctype('Test Linked Doctype 1')
# create linked doctype
link_doc = new_doctype("Test Linked Doctype 1")
link_doc.is_submittable = 1 link_doc.is_submittable = 1
for data in link_doc.get('permissions'): for data in link_doc.get("permissions"):
data.submit = 1 data.submit = 1
data.cancel = 1 data.cancel = 1
link_doc.insert() link_doc.insert()
#create first parent doctype # create first parent doctype
test_doc_1 = new_doctype('Test Doctype 1') test_doc_1 = new_doctype("Test Doctype 1")
test_doc_1.is_submittable = 1 test_doc_1.is_submittable = 1
field_2 = test_doc_1.append('fields', {}) field_2 = test_doc_1.append("fields", {})
field_2.label = 'Test Linked Doctype 1' field_2.label = "Test Linked Doctype 1"
field_2.fieldname = 'test_linked_doctype_a' field_2.fieldname = "test_linked_doctype_a"
field_2.fieldtype = 'Link' field_2.fieldtype = "Link"
field_2.options = 'Test Linked Doctype 1' field_2.options = "Test Linked Doctype 1"
for data in test_doc_1.get('permissions'): for data in test_doc_1.get("permissions"):
data.submit = 1 data.submit = 1
data.cancel = 1 data.cancel = 1
test_doc_1.insert() test_doc_1.insert()
#crete second parent doctype # crete second parent doctype
doc = new_doctype('Test Doctype 2') doc = new_doctype("Test Doctype 2")
doc.is_submittable = 1 doc.is_submittable = 1
field_2 = doc.append('fields', {}) field_2 = doc.append("fields", {})
field_2.label = 'Test Linked Doctype 1' field_2.label = "Test Linked Doctype 1"
field_2.fieldname = 'test_linked_doctype_a' field_2.fieldname = "test_linked_doctype_a"
field_2.fieldtype = 'Link' field_2.fieldtype = "Link"
field_2.options = 'Test Linked Doctype 1' field_2.options = "Test Linked Doctype 1"
for data in link_doc.get('permissions'): for data in link_doc.get("permissions"):
data.submit = 1 data.submit = 1
data.cancel = 1 data.cancel = 1
doc.insert() doc.insert()
# create doctype data # create doctype data
data_link_doc_1 = frappe.new_doc('Test Linked Doctype 1') data_link_doc_1 = frappe.new_doc("Test Linked Doctype 1")
data_link_doc_1.some_fieldname = 'Data1' data_link_doc_1.some_fieldname = "Data1"
data_link_doc_1.insert() data_link_doc_1.insert()
data_link_doc_1.save() data_link_doc_1.save()
data_link_doc_1.submit() data_link_doc_1.submit()
data_doc_2 = frappe.new_doc('Test Doctype 1') data_doc_2 = frappe.new_doc("Test Doctype 1")
data_doc_2.some_fieldname = 'Data1' data_doc_2.some_fieldname = "Data1"
data_doc_2.test_linked_doctype_a = data_link_doc_1.name data_doc_2.test_linked_doctype_a = data_link_doc_1.name
data_doc_2.insert() data_doc_2.insert()
data_doc_2.save() data_doc_2.save()
data_doc_2.submit() data_doc_2.submit()
data_doc = frappe.new_doc('Test Doctype 2') data_doc = frappe.new_doc("Test Doctype 2")
data_doc.some_fieldname = 'Data1' data_doc.some_fieldname = "Data1"
data_doc.test_linked_doctype_a = data_link_doc_1.name data_doc.test_linked_doctype_a = data_link_doc_1.name
data_doc.insert() data_doc.insert()
data_doc.save() data_doc.save()
data_doc.submit() data_doc.submit()
docs = get_submitted_linked_docs(link_doc.name, data_link_doc_1.name) docs = get_submitted_linked_docs(link_doc.name, data_link_doc_1.name)
dump_docs = json.dumps(docs.get('docs')) dump_docs = json.dumps(docs.get("docs"))
cancel_all_linked_docs(dump_docs, ignore_doctypes_on_cancel_all=["Test Doctype 2"]) cancel_all_linked_docs(dump_docs, ignore_doctypes_on_cancel_all=["Test Doctype 2"])
@ -442,10 +466,10 @@ class TestDocType(unittest.TestCase):
data_doc_2.load_from_db() data_doc_2.load_from_db()
self.assertEqual(data_link_doc_1.docstatus, 2) self.assertEqual(data_link_doc_1.docstatus, 2)
#linked doc is canceled # linked doc is canceled
self.assertEqual(data_doc_2.docstatus, 2) self.assertEqual(data_doc_2.docstatus, 2)
#ignored doctype 2 during cancel # ignored doctype 2 during cancel
self.assertEqual(data_doc.docstatus, 1) self.assertEqual(data_doc.docstatus, 1)
# delete doctype record # delete doctype record
@ -464,42 +488,35 @@ class TestDocType(unittest.TestCase):
doc = new_doctype("Test Links Table Validation") doc = new_doctype("Test Links Table Validation")
# check valid data # check valid data
doc.append("links", { doc.append("links", {"link_doctype": "User", "link_fieldname": "first_name"})
'link_doctype': "User", validate_links_table_fieldnames(doc) # no error
'link_fieldname': "first_name" doc.links = [] # reset links table
})
validate_links_table_fieldnames(doc) # no error
doc.links = [] # reset links table
# check invalid doctype # check invalid doctype
doc.append("links", { doc.append("links", {"link_doctype": "User2", "link_fieldname": "first_name"})
'link_doctype': "User2",
'link_fieldname': "first_name"
})
self.assertRaises(frappe.DoesNotExistError, validate_links_table_fieldnames, doc) self.assertRaises(frappe.DoesNotExistError, validate_links_table_fieldnames, doc)
doc.links = [] # reset links table doc.links = [] # reset links table
# check invalid fieldname # check invalid fieldname
doc.append("links", { doc.append("links", {"link_doctype": "User", "link_fieldname": "a_field_that_does_not_exists"})
'link_doctype': "User",
'link_fieldname': "a_field_that_does_not_exists"
})
self.assertRaises(InvalidFieldNameError, validate_links_table_fieldnames, doc) self.assertRaises(InvalidFieldNameError, validate_links_table_fieldnames, doc)
def test_create_virtual_doctype(self): def test_create_virtual_doctype(self):
"""Test virtual DOcTYpe.""" """Test virtual DOcTYpe."""
virtual_doc = new_doctype('Test Virtual Doctype') virtual_doc = new_doctype("Test Virtual Doctype")
virtual_doc.is_virtual = 1 virtual_doc.is_virtual = 1
virtual_doc.insert() virtual_doc.insert()
virtual_doc.save() virtual_doc.save()
doc = frappe.get_doc("DocType", "Test Virtual Doctype") doc = frappe.get_doc("DocType", "Test Virtual Doctype")
self.assertEqual(doc.is_virtual, 1) self.assertEqual(doc.is_virtual, 1)
self.assertFalse(frappe.db.table_exists('Test Virtual Doctype')) self.assertFalse(frappe.db.table_exists("Test Virtual Doctype"))
def test_default_fieldname(self): def test_default_fieldname(self):
fields = [{"label": "title", "fieldname": "title", "fieldtype": "Data", "default": "{some_fieldname}"}] fields = [
{"label": "title", "fieldname": "title", "fieldtype": "Data", "default": "{some_fieldname}"}
]
dt = new_doctype("DT with default field", fields=fields) dt = new_doctype("DT with default field", fields=fields)
dt.insert() dt.insert()
@ -521,28 +538,34 @@ class TestDocType(unittest.TestCase):
dt.delete(ignore_permissions=True) dt.delete(ignore_permissions=True)
def new_doctype(name, unique=0, depends_on='', fields=None, autoincremented=False): def new_doctype(name, unique=0, depends_on="", fields=None, autoincremented=False):
doc = frappe.get_doc({ doc = frappe.get_doc(
"doctype": "DocType", {
"module": "Core", "doctype": "DocType",
"custom": 1, "module": "Core",
"fields": [{ "custom": 1,
"label": "Some Field", "fields": [
"fieldname": "some_fieldname", {
"fieldtype": "Data", "label": "Some Field",
"unique": unique, "fieldname": "some_fieldname",
"depends_on": depends_on, "fieldtype": "Data",
}], "unique": unique,
"permissions": [{ "depends_on": depends_on,
"role": "System Manager", }
"read": 1, ],
}], "permissions": [
"name": name, {
"autoname": "autoincrement" if autoincremented else "" "role": "System Manager",
}) "read": 1,
}
],
"name": name,
"autoname": "autoincrement" if autoincremented else "",
}
)
if fields: if fields:
for f in fields: for f in fields:
doc.append('fields', f) doc.append("fields", f)
return doc return doc

View file

@ -5,5 +5,6 @@
# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document
class DocTypeAction(Document): class DocTypeAction(Document):
pass pass

View file

@ -5,5 +5,6 @@
# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document
class DocTypeLink(Document): class DocTypeLink(Document):
pass pass

View file

@ -4,5 +4,6 @@
# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document
class DocTypeState(Document): class DocTypeState(Document):
pass pass

View file

@ -3,10 +3,11 @@
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe import frappe
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 import _
from frappe.model.document import Document
from frappe.model.naming import parse_naming_series
from frappe.utils.data import evaluate_filters
class DocumentNamingRule(Document): class DocumentNamingRule(Document):
def validate(self): def validate(self):
@ -17,23 +18,30 @@ class DocumentNamingRule(Document):
docfields = [x.fieldname for x in frappe.get_meta(self.document_type).fields] docfields = [x.fieldname for x in frappe.get_meta(self.document_type).fields]
for condition in self.conditions: for condition in self.conditions:
if condition.field not in docfields: if condition.field not in docfields:
frappe.throw(_("{0} is not a field of doctype {1}").format(frappe.bold(condition.field), frappe.bold(self.document_type))) frappe.throw(
_("{0} is not a field of doctype {1}").format(
frappe.bold(condition.field), frappe.bold(self.document_type)
)
)
def apply(self, doc): def apply(self, doc):
''' """
Apply naming rules for the given document. Will set `name` if the rule is matched. Apply naming rules for the given document. Will set `name` if the rule is matched.
''' """
if self.conditions: if self.conditions:
if not evaluate_filters(doc, [(self.document_type, d.field, d.condition, d.value) for d in self.conditions]): if not evaluate_filters(
doc, [(self.document_type, d.field, d.condition, d.value) for d in self.conditions]
):
return return
counter = frappe.db.get_value(self.doctype, self.name, 'counter', for_update=True) or 0 counter = frappe.db.get_value(self.doctype, self.name, "counter", for_update=True) or 0
naming_series = parse_naming_series(self.prefix, doc=doc) naming_series = parse_naming_series(self.prefix, doc=doc)
doc.name = naming_series + ('%0'+str(self.prefix_digits)+'d') % (counter + 1) doc.name = naming_series + ("%0" + str(self.prefix_digits) + "d") % (counter + 1)
frappe.db.set_value(self.doctype, self.name, 'counter', counter + 1) frappe.db.set_value(self.doctype, self.name, "counter", counter + 1)
@frappe.whitelist() @frappe.whitelist()
def update_current(name, new_counter): def update_current(name, new_counter):
frappe.only_for('System Manager') frappe.only_for("System Manager")
frappe.db.set_value('Document Naming Rule', name, 'counter', new_counter) frappe.db.set_value("Document Naming Rule", name, "counter", new_counter)

View file

@ -1,79 +1,68 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors # Copyright (c) 2020, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe
import unittest import unittest
import frappe
class TestDocumentNamingRule(unittest.TestCase): class TestDocumentNamingRule(unittest.TestCase):
def test_naming_rule_by_series(self): def test_naming_rule_by_series(self):
naming_rule = frappe.get_doc(dict( naming_rule = frappe.get_doc(
doctype = 'Document Naming Rule', dict(doctype="Document Naming Rule", document_type="ToDo", prefix="test-todo-", prefix_digits=5)
document_type = 'ToDo', ).insert()
prefix = 'test-todo-',
prefix_digits = 5
)).insert()
todo = frappe.get_doc(dict( todo = frappe.get_doc(
doctype = 'ToDo', dict(doctype="ToDo", description="Is this my name " + frappe.generate_hash())
description = 'Is this my name ' + frappe.generate_hash() ).insert()
)).insert()
self.assertEqual(todo.name, 'test-todo-00001') self.assertEqual(todo.name, "test-todo-00001")
naming_rule.delete() naming_rule.delete()
todo.delete() todo.delete()
def test_naming_rule_by_condition(self): def test_naming_rule_by_condition(self):
naming_rule = frappe.get_doc(dict( naming_rule = frappe.get_doc(
doctype = 'Document Naming Rule', dict(
document_type = 'ToDo', doctype="Document Naming Rule",
prefix = 'test-high-', document_type="ToDo",
prefix_digits = 5, prefix="test-high-",
priority = 10, prefix_digits=5,
conditions = [dict( priority=10,
field = 'priority', conditions=[dict(field="priority", condition="=", value="High")],
condition = '=', )
value = 'High' ).insert()
)]
)).insert()
# another rule # another rule
naming_rule_1 = frappe.copy_doc(naming_rule) naming_rule_1 = frappe.copy_doc(naming_rule)
naming_rule_1.prefix = 'test-medium-' naming_rule_1.prefix = "test-medium-"
naming_rule_1.conditions[0].value = 'Medium' naming_rule_1.conditions[0].value = "Medium"
naming_rule_1.insert() naming_rule_1.insert()
# default rule with low priority - should not get applied for rules # default rule with low priority - should not get applied for rules
# with higher priority # with higher priority
naming_rule_2 = frappe.copy_doc(naming_rule) naming_rule_2 = frappe.copy_doc(naming_rule)
naming_rule_2.prefix = 'test-low-' naming_rule_2.prefix = "test-low-"
naming_rule_2.priority = 0 naming_rule_2.priority = 0
naming_rule_2.conditions = [] naming_rule_2.conditions = []
naming_rule_2.insert() naming_rule_2.insert()
todo = frappe.get_doc(
dict(doctype="ToDo", priority="High", description="Is this my name " + frappe.generate_hash())
).insert()
todo = frappe.get_doc(dict( todo_1 = frappe.get_doc(
doctype = 'ToDo', dict(doctype="ToDo", priority="Medium", description="Is this my name " + frappe.generate_hash())
priority = 'High', ).insert()
description = 'Is this my name ' + frappe.generate_hash()
)).insert()
todo_1 = frappe.get_doc(dict( todo_2 = frappe.get_doc(
doctype = 'ToDo', dict(doctype="ToDo", priority="Low", description="Is this my name " + frappe.generate_hash())
priority = 'Medium', ).insert()
description = 'Is this my name ' + frappe.generate_hash()
)).insert()
todo_2 = frappe.get_doc(dict(
doctype = 'ToDo',
priority = 'Low',
description = 'Is this my name ' + frappe.generate_hash()
)).insert()
try: try:
self.assertEqual(todo.name, 'test-high-00001') self.assertEqual(todo.name, "test-high-00001")
self.assertEqual(todo_1.name, 'test-medium-00001') self.assertEqual(todo_1.name, "test-medium-00001")
self.assertEqual(todo_2.name, 'test-low-00001') self.assertEqual(todo_2.name, "test-low-00001")
finally: finally:
naming_rule.delete() naming_rule.delete()
naming_rule_1.delete() naming_rule_1.delete()

View file

@ -5,5 +5,6 @@
# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document
class DocumentNamingRuleCondition(Document): class DocumentNamingRuleCondition(Document):
pass pass

View file

@ -4,5 +4,6 @@
# import frappe # import frappe
import unittest import unittest
class TestDocumentNamingRuleCondition(unittest.TestCase): class TestDocumentNamingRuleCondition(unittest.TestCase):
pass pass

View file

@ -3,16 +3,17 @@
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe import frappe
from frappe.model.document import Document
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.model.document import Document
class Domain(Document): class Domain(Document):
'''Domain documents are created automatically when DocTypes """Domain documents are created automatically when DocTypes
with "Restricted" domains are imported during with "Restricted" domains are imported during
installation or migration''' installation or migration"""
def setup_domain(self): def setup_domain(self):
'''Setup domain icons, permissions, custom fields etc.''' """Setup domain icons, permissions, custom fields etc."""
self.setup_data() self.setup_data()
self.setup_roles() self.setup_roles()
self.setup_properties() self.setup_properties()
@ -31,20 +32,20 @@ class Domain(Document):
frappe.get_attr(self.data.on_setup)() frappe.get_attr(self.data.on_setup)()
def remove_domain(self): def remove_domain(self):
'''Unset domain settings''' """Unset domain settings"""
self.setup_data() self.setup_data()
if self.data.restricted_roles: if self.data.restricted_roles:
for role_name in self.data.restricted_roles: for role_name in self.data.restricted_roles:
if frappe.db.exists('Role', role_name): if frappe.db.exists("Role", role_name):
role = frappe.get_doc('Role', role_name) role = frappe.get_doc("Role", role_name)
role.disabled = 1 role.disabled = 1
role.save() role.save()
self.remove_custom_field() self.remove_custom_field()
def remove_custom_field(self): def remove_custom_field(self):
'''Remove custom_fields when disabling domain''' """Remove custom_fields when disabling domain"""
if self.data.custom_fields: if self.data.custom_fields:
for doctype in self.data.custom_fields: for doctype in self.data.custom_fields:
custom_fields = self.data.custom_fields[doctype] custom_fields = self.data.custom_fields[doctype]
@ -54,47 +55,48 @@ class Domain(Document):
custom_fields = [custom_fields] custom_fields = [custom_fields]
for custom_field_detail in custom_fields: for custom_field_detail in custom_fields:
custom_field_name = frappe.db.get_value('Custom Field', custom_field_name = frappe.db.get_value(
dict(dt=doctype, fieldname=custom_field_detail.get('fieldname'))) "Custom Field", dict(dt=doctype, fieldname=custom_field_detail.get("fieldname"))
)
if custom_field_name: if custom_field_name:
frappe.delete_doc('Custom Field', custom_field_name) frappe.delete_doc("Custom Field", custom_field_name)
def setup_roles(self): def setup_roles(self):
'''Enable roles that are restricted to this domain''' """Enable roles that are restricted to this domain"""
if self.data.restricted_roles: if self.data.restricted_roles:
user = frappe.get_doc("User", frappe.session.user) user = frappe.get_doc("User", frappe.session.user)
for role_name in self.data.restricted_roles: for role_name in self.data.restricted_roles:
user.append("roles", {"role": role_name}) user.append("roles", {"role": role_name})
if not frappe.db.get_value('Role', role_name): if not frappe.db.get_value("Role", role_name):
frappe.get_doc(dict(doctype='Role', role_name=role_name)).insert() frappe.get_doc(dict(doctype="Role", role_name=role_name)).insert()
continue continue
role = frappe.get_doc('Role', role_name) role = frappe.get_doc("Role", role_name)
role.disabled = 0 role.disabled = 0
role.save() role.save()
user.save() user.save()
def setup_data(self, domain=None): def setup_data(self, domain=None):
'''Load domain info via hooks''' """Load domain info via hooks"""
self.data = frappe.get_domain_data(self.name) self.data = frappe.get_domain_data(self.name)
def get_domain_data(self, module): def get_domain_data(self, module):
return frappe.get_attr(frappe.get_hooks('domains')[self.name] + '.data') return frappe.get_attr(frappe.get_hooks("domains")[self.name] + ".data")
def set_default_portal_role(self): def set_default_portal_role(self):
'''Set default portal role based on domain''' """Set default portal role based on domain"""
if self.data.get('default_portal_role'): if self.data.get("default_portal_role"):
frappe.db.set_value('Portal Settings', None, 'default_role', frappe.db.set_value(
self.data.get('default_portal_role')) "Portal Settings", None, "default_role", self.data.get("default_portal_role")
)
def setup_properties(self): def setup_properties(self):
if self.data.properties: if self.data.properties:
for args in self.data.properties: for args in self.data.properties:
frappe.make_property_setter(args) frappe.make_property_setter(args)
def set_values(self): def set_values(self):
'''set values based on `data.set_value`''' """set values based on `data.set_value`"""
if self.data.set_value: if self.data.set_value:
for args in self.data.set_value: for args in self.data.set_value:
frappe.reload_doctype(args[0]) frappe.reload_doctype(args[0])
@ -103,19 +105,27 @@ class Domain(Document):
doc.save() doc.save()
def setup_sidebar_items(self): def setup_sidebar_items(self):
'''Enable / disable sidebar items''' """Enable / disable sidebar items"""
if self.data.allow_sidebar_items: if self.data.allow_sidebar_items:
# disable all # disable all
frappe.db.sql('update `tabPortal Menu Item` set enabled=0') frappe.db.sql("update `tabPortal Menu Item` set enabled=0")
# enable # enable
frappe.db.sql('''update `tabPortal Menu Item` set enabled=1 frappe.db.sql(
where route in ({0})'''.format(', '.join('"{0}"'.format(d) for d in self.data.allow_sidebar_items))) """update `tabPortal Menu Item` set enabled=1
where route in ({0})""".format(
", ".join('"{0}"'.format(d) for d in self.data.allow_sidebar_items)
)
)
if self.data.remove_sidebar_items: if self.data.remove_sidebar_items:
# disable all # disable all
frappe.db.sql('update `tabPortal Menu Item` set enabled=1') frappe.db.sql("update `tabPortal Menu Item` set enabled=1")
# enable # enable
frappe.db.sql('''update `tabPortal Menu Item` set enabled=0 frappe.db.sql(
where route in ({0})'''.format(', '.join('"{0}"'.format(d) for d in self.data.remove_sidebar_items))) """update `tabPortal Menu Item` set enabled=0
where route in ({0})""".format(
", ".join('"{0}"'.format(d) for d in self.data.remove_sidebar_items)
)
)

View file

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

View file

@ -5,13 +5,14 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
class DomainSettings(Document): class DomainSettings(Document):
def set_active_domains(self, domains): def set_active_domains(self, domains):
active_domains = [d.domain for d in self.active_domains] active_domains = [d.domain for d in self.active_domains]
added = False added = False
for d in domains: for d in domains:
if not d in active_domains: if not d in active_domains:
self.append('active_domains', dict(domain=d)) self.append("active_domains", dict(domain=d))
added = True added = True
if added: if added:
@ -22,49 +23,52 @@ class DomainSettings(Document):
# set the flag to update the the desktop icons of all domains # set the flag to update the the desktop icons of all domains
if i >= 1: if i >= 1:
frappe.flags.keep_desktop_icons = True frappe.flags.keep_desktop_icons = True
domain = frappe.get_doc('Domain', d.domain) domain = frappe.get_doc("Domain", d.domain)
domain.setup_domain() domain.setup_domain()
self.restrict_roles_and_modules() self.restrict_roles_and_modules()
frappe.clear_cache() frappe.clear_cache()
def restrict_roles_and_modules(self): def restrict_roles_and_modules(self):
'''Disable all restricted roles and set `restrict_to_domain` property in Module Def''' """Disable all restricted roles and set `restrict_to_domain` property in Module Def"""
active_domains = frappe.get_active_domains() active_domains = frappe.get_active_domains()
all_domains = list((frappe.get_hooks('domains') or {})) all_domains = list((frappe.get_hooks("domains") or {}))
def remove_role(role): def remove_role(role):
frappe.db.delete("Has Role", {"role": role}) frappe.db.delete("Has Role", {"role": role})
frappe.set_value('Role', role, 'disabled', 1) frappe.set_value("Role", role, "disabled", 1)
for domain in all_domains: for domain in all_domains:
data = frappe.get_domain_data(domain) data = frappe.get_domain_data(domain)
if not frappe.db.get_value('Domain', domain): if not frappe.db.get_value("Domain", domain):
frappe.get_doc(dict(doctype='Domain', domain=domain)).insert() frappe.get_doc(dict(doctype="Domain", domain=domain)).insert()
if 'modules' in data: if "modules" in data:
for module in data.get('modules'): for module in data.get("modules"):
frappe.db.set_value('Module Def', module, 'restrict_to_domain', domain) frappe.db.set_value("Module Def", module, "restrict_to_domain", domain)
if 'restricted_roles' in data: if "restricted_roles" in data:
for role in data['restricted_roles']: for role in data["restricted_roles"]:
if not frappe.db.get_value('Role', role): if not frappe.db.get_value("Role", role):
frappe.get_doc(dict(doctype='Role', role_name=role)).insert() frappe.get_doc(dict(doctype="Role", role_name=role)).insert()
frappe.db.set_value('Role', role, 'restrict_to_domain', domain) frappe.db.set_value("Role", role, "restrict_to_domain", domain)
if domain not in active_domains: if domain not in active_domains:
remove_role(role) remove_role(role)
if 'custom_fields' in data: if "custom_fields" in data:
if domain not in active_domains: if domain not in active_domains:
inactive_domain = frappe.get_doc("Domain", domain) inactive_domain = frappe.get_doc("Domain", domain)
inactive_domain.setup_data() inactive_domain.setup_data()
inactive_domain.remove_custom_field() inactive_domain.remove_custom_field()
def get_active_domains(): def get_active_domains():
""" get the domains set in the Domain Settings as active domain """ """get the domains set in the Domain Settings as active domain"""
def _get_active_domains(): def _get_active_domains():
domains = frappe.get_all("Has Domain", filters={ "parent": "Domain Settings" }, domains = frappe.get_all(
fields=["domain"], distinct=True) "Has Domain", filters={"parent": "Domain Settings"}, fields=["domain"], distinct=True
)
active_domains = [row.get("domain") for row in domains] active_domains = [row.get("domain") for row in domains]
active_domains.append("") active_domains.append("")
@ -72,14 +76,16 @@ def get_active_domains():
return frappe.cache().get_value("active_domains", _get_active_domains) return frappe.cache().get_value("active_domains", _get_active_domains)
def get_active_modules(): def get_active_modules():
""" get the active modules from Module Def""" """get the active modules from Module Def"""
def _get_active_modules(): def _get_active_modules():
active_modules = [] active_modules = []
active_domains = get_active_domains() active_domains = get_active_domains()
for m in frappe.get_all("Module Def", fields=['name', 'restrict_to_domain']): for m in frappe.get_all("Module Def", fields=["name", "restrict_to_domain"]):
if (not m.restrict_to_domain) or (m.restrict_to_domain in active_domains): if (not m.restrict_to_domain) or (m.restrict_to_domain in active_domains):
active_modules.append(m.name) active_modules.append(m.name)
return active_modules return active_modules
return frappe.cache().get_value('active_modules', _get_active_modules) return frappe.cache().get_value("active_modules", _get_active_modules)

View file

@ -5,12 +5,15 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
class DynamicLink(Document): class DynamicLink(Document):
pass pass
def on_doctype_update(): def on_doctype_update():
frappe.db.add_index("Dynamic Link", ["link_doctype", "link_name"]) frappe.db.add_index("Dynamic Link", ["link_doctype", "link_name"])
def deduplicate_dynamic_links(doc): def deduplicate_dynamic_links(doc):
links, duplicate = [], False links, duplicate = [], False
for l in doc.links or []: for l in doc.links or []:
@ -23,4 +26,4 @@ def deduplicate_dynamic_links(doc):
if duplicate: if duplicate:
doc.links = [] doc.links = []
for l in links: for l in links:
doc.append('links', dict(link_doctype=l[0], link_name=l[1])) doc.append("links", dict(link_doctype=l[0], link_name=l[1]))

View file

@ -5,19 +5,24 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
class ErrorLog(Document): class ErrorLog(Document):
def onload(self): def onload(self):
if not self.seen: if not self.seen:
self.db_set('seen', 1, update_modified=0) self.db_set("seen", 1, update_modified=0)
frappe.db.commit() frappe.db.commit()
def set_old_logs_as_seen(): def set_old_logs_as_seen():
# set logs as seen # set logs as seen
frappe.db.sql("""UPDATE `tabError Log` SET `seen`=1 frappe.db.sql(
WHERE `seen`=0 AND `creation` < (NOW() - INTERVAL '7' DAY)""") """UPDATE `tabError Log` SET `seen`=1
WHERE `seen`=0 AND `creation` < (NOW() - INTERVAL '7' DAY)"""
)
@frappe.whitelist() @frappe.whitelist()
def clear_error_logs(): def clear_error_logs():
'''Flush all Error Logs''' """Flush all Error Logs"""
frappe.only_for('System Manager') frappe.only_for("System Manager")
frappe.db.truncate("Error Log") frappe.db.truncate("Error Log")

View file

@ -1,10 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors # Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe
import unittest import unittest
import frappe
# test_records = frappe.get_test_records('Error Log') # test_records = frappe.get_test_records('Error Log')
class TestErrorLog(unittest.TestCase): class TestErrorLog(unittest.TestCase):
pass pass

Some files were not shown because too many files have changed in this diff Show more