Co-authored-by: Frappe Bot <developers@frappe.io>version-14
@@ -19,3 +19,6 @@ fe20515c23a3ac41f1092bf0eaf0a0a452ec2e85 | |||
# Clean up whitespace | |||
b2fc959307c7c79f5584625569d5aed04133ba13 | |||
# Format codebase and sort imports | |||
cb6f68e8c106ee2d037dd4b39dbb6d7c68caf1c8 |
@@ -16,6 +16,17 @@ repos: | |||
- id: check-merge-conflict | |||
- id: check-ast | |||
- repo: https://github.com/adityahase/black | |||
rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119 | |||
hooks: | |||
- id: black | |||
additional_dependencies: ['click==8.0.4'] | |||
- repo: https://github.com/timothycrosley/isort | |||
rev: 5.9.1 | |||
hooks: | |||
- id: isort | |||
exclude: ".*setup.py$" | |||
ci: | |||
autoupdate_schedule: weekly | |||
@@ -9,8 +9,8 @@ import frappe | |||
import frappe.client | |||
import frappe.handler | |||
from frappe import _ | |||
from frappe.utils.response import build_response | |||
from frappe.utils.data import sbool | |||
from frappe.utils.response import build_response | |||
def handle(): | |||
@@ -22,22 +22,22 @@ def handle(): | |||
`/api/method/{methodname}` will call a whitelisted method | |||
`/api/resource/{doctype}` will query a table | |||
examples: | |||
- `?fields=["name", "owner"]` | |||
- `?filters=[["Task", "name", "like", "%005"]]` | |||
- `?limit_start=0` | |||
- `?limit_page_length=20` | |||
examples: | |||
- `?fields=["name", "owner"]` | |||
- `?filters=[["Task", "name", "like", "%005"]]` | |||
- `?limit_start=0` | |||
- `?limit_page_length=20` | |||
`/api/resource/{doctype}/{name}` will point to a resource | |||
`GET` will return doclist | |||
`POST` will insert | |||
`PUT` will update | |||
`DELETE` will delete | |||
`GET` will return doclist | |||
`POST` will insert | |||
`PUT` will update | |||
`DELETE` will delete | |||
`/api/resource/{doctype}/{name}?run_method={method}` will run a whitelisted controller method | |||
""" | |||
parts = frappe.request.path[1:].split("/",3) | |||
parts = frappe.request.path[1:].split("/", 3) | |||
call = doctype = name = None | |||
if len(parts) > 1: | |||
@@ -49,22 +49,22 @@ def handle(): | |||
if len(parts) > 3: | |||
name = parts[3] | |||
if call=="method": | |||
if call == "method": | |||
frappe.local.form_dict.cmd = doctype | |||
return frappe.handler.handle() | |||
elif call=="resource": | |||
elif call == "resource": | |||
if "run_method" in frappe.local.form_dict: | |||
method = frappe.local.form_dict.pop("run_method") | |||
doc = frappe.get_doc(doctype, name) | |||
doc.is_whitelisted(method) | |||
if frappe.local.request.method=="GET": | |||
if frappe.local.request.method == "GET": | |||
if not doc.has_permission("read"): | |||
frappe.throw(_("Not permitted"), frappe.PermissionError) | |||
frappe.local.response.update({"data": doc.run_method(method, **frappe.local.form_dict)}) | |||
if frappe.local.request.method=="POST": | |||
if frappe.local.request.method == "POST": | |||
if not doc.has_permission("write"): | |||
frappe.throw(_("Not permitted"), frappe.PermissionError) | |||
@@ -73,13 +73,13 @@ def handle(): | |||
else: | |||
if name: | |||
if frappe.local.request.method=="GET": | |||
if frappe.local.request.method == "GET": | |||
doc = frappe.get_doc(doctype, name) | |||
if not doc.has_permission("read"): | |||
raise frappe.PermissionError | |||
frappe.local.response.update({"data": doc}) | |||
if frappe.local.request.method=="PUT": | |||
if frappe.local.request.method == "PUT": | |||
data = get_request_form_data() | |||
doc = frappe.get_doc(doctype, name, for_update=True) | |||
@@ -90,9 +90,7 @@ def handle(): | |||
# Not checking permissions here because it's checked in doc.save | |||
doc.update(data) | |||
frappe.local.response.update({ | |||
"data": doc.save().as_dict() | |||
}) | |||
frappe.local.response.update({"data": doc.save().as_dict()}) | |||
# check for child table doctype | |||
if doc.get("parenttype"): | |||
@@ -183,7 +181,7 @@ def validate_oauth(authorization_header): | |||
Authenticate request using OAuth and set session user | |||
Args: | |||
authorization_header (list of str): The 'Authorization' header containing the prefix and token | |||
authorization_header (list of str): The 'Authorization' header containing the prefix and token | |||
""" | |||
from frappe.integrations.oauth2 import get_oauth_server | |||
@@ -194,7 +192,9 @@ def validate_oauth(authorization_header): | |||
req = frappe.request | |||
parsed_url = urlparse(req.url) | |||
access_token = {"access_token": token} | |||
uri = parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + "?" + urlencode(access_token) | |||
uri = ( | |||
parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + "?" + urlencode(access_token) | |||
) | |||
http_method = req.method | |||
headers = req.headers | |||
body = req.get_data() | |||
@@ -202,8 +202,12 @@ def validate_oauth(authorization_header): | |||
body = None | |||
try: | |||
required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(get_url_delimiter()) | |||
valid, oauthlib_request = get_oauth_server().verify_request(uri, http_method, body, headers, required_scopes) | |||
required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split( | |||
get_url_delimiter() | |||
) | |||
valid, oauthlib_request = get_oauth_server().verify_request( | |||
uri, http_method, body, headers, required_scopes | |||
) | |||
if valid: | |||
frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user")) | |||
frappe.local.form_dict = form_dict | |||
@@ -216,48 +220,43 @@ def validate_auth_via_api_keys(authorization_header): | |||
Authenticate request using API keys and set session user | |||
Args: | |||
authorization_header (list of str): The 'Authorization' header containing the prefix and token | |||
authorization_header (list of str): The 'Authorization' header containing the prefix and token | |||
""" | |||
try: | |||
auth_type, auth_token = authorization_header | |||
authorization_source = frappe.get_request_header("Frappe-Authorization-Source") | |||
if auth_type.lower() == 'basic': | |||
if auth_type.lower() == "basic": | |||
api_key, api_secret = frappe.safe_decode(base64.b64decode(auth_token)).split(":") | |||
validate_api_key_secret(api_key, api_secret, authorization_source) | |||
elif auth_type.lower() == 'token': | |||
elif auth_type.lower() == "token": | |||
api_key, api_secret = auth_token.split(":") | |||
validate_api_key_secret(api_key, api_secret, authorization_source) | |||
except binascii.Error: | |||
frappe.throw(_("Failed to decode token, please provide a valid base64-encoded token."), frappe.InvalidAuthorizationToken) | |||
frappe.throw( | |||
_("Failed to decode token, please provide a valid base64-encoded token."), | |||
frappe.InvalidAuthorizationToken, | |||
) | |||
except (AttributeError, TypeError, ValueError): | |||
pass | |||
def validate_api_key_secret(api_key, api_secret, frappe_authorization_source=None): | |||
"""frappe_authorization_source to provide api key and secret for a doctype apart from User""" | |||
doctype = frappe_authorization_source or 'User' | |||
doc = frappe.db.get_value( | |||
doctype=doctype, | |||
filters={"api_key": api_key}, | |||
fieldname=["name"] | |||
) | |||
doctype = frappe_authorization_source or "User" | |||
doc = frappe.db.get_value(doctype=doctype, filters={"api_key": api_key}, fieldname=["name"]) | |||
form_dict = frappe.local.form_dict | |||
doc_secret = frappe.utils.password.get_decrypted_password(doctype, doc, fieldname='api_secret') | |||
doc_secret = frappe.utils.password.get_decrypted_password(doctype, doc, fieldname="api_secret") | |||
if api_secret == doc_secret: | |||
if doctype == 'User': | |||
user = frappe.db.get_value( | |||
doctype="User", | |||
filters={"api_key": api_key}, | |||
fieldname=["name"] | |||
) | |||
if doctype == "User": | |||
user = frappe.db.get_value(doctype="User", filters={"api_key": api_key}, fieldname=["name"]) | |||
else: | |||
user = frappe.db.get_value(doctype, doc, 'user') | |||
if frappe.local.login_manager.user in ('', 'Guest'): | |||
user = frappe.db.get_value(doctype, doc, "user") | |||
if frappe.local.login_manager.user in ("", "Guest"): | |||
frappe.set_user(user) | |||
frappe.local.form_dict = form_dict | |||
def validate_auth_via_hooks(): | |||
for auth_hook in frappe.get_hooks('auth_hooks', []): | |||
for auth_hook in frappe.get_hooks("auth_hooks", []): | |||
frappe.get_attr(auth_hook)() |
@@ -2,37 +2,37 @@ | |||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
import os | |||
import logging | |||
import os | |||
from werkzeug.local import LocalManager | |||
from werkzeug.wrappers import Request, Response | |||
from werkzeug.exceptions import HTTPException, NotFound | |||
from werkzeug.local import LocalManager | |||
from werkzeug.middleware.profiler import ProfilerMiddleware | |||
from werkzeug.middleware.shared_data import SharedDataMiddleware | |||
from werkzeug.wrappers import Request, Response | |||
import frappe | |||
import frappe.handler | |||
import frappe.auth | |||
import frappe.api | |||
import frappe.auth | |||
import frappe.handler | |||
import frappe.monitor | |||
import frappe.rate_limiter | |||
import frappe.recorder | |||
import frappe.utils.response | |||
from frappe.utils import get_site_name, sanitize_html | |||
from frappe import _ | |||
from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request | |||
from frappe.middlewares import StaticDataMiddleware | |||
from frappe.website.serve import get_response | |||
from frappe.utils import get_site_name, sanitize_html | |||
from frappe.utils.error import make_error_snapshot | |||
from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request | |||
from frappe import _ | |||
import frappe.recorder | |||
import frappe.monitor | |||
import frappe.rate_limiter | |||
from frappe.website.serve import get_response | |||
local_manager = LocalManager([frappe.local]) | |||
_site = None | |||
_sites_path = os.environ.get("SITES_PATH", ".") | |||
class RequestContext(object): | |||
class RequestContext(object): | |||
def __init__(self, environ): | |||
self.request = Request(environ) | |||
@@ -42,6 +42,7 @@ class RequestContext(object): | |||
def __exit__(self, type, value, traceback): | |||
frappe.destroy() | |||
@Request.application | |||
def application(request): | |||
response = None | |||
@@ -65,13 +66,13 @@ def application(request): | |||
elif request.path.startswith("/api/"): | |||
response = frappe.api.handle() | |||
elif request.path.startswith('/backups'): | |||
elif request.path.startswith("/backups"): | |||
response = frappe.utils.response.download_backup(request.path) | |||
elif request.path.startswith('/private/files/'): | |||
elif request.path.startswith("/private/files/"): | |||
response = frappe.utils.response.download_private_file(request.path) | |||
elif request.method in ('GET', 'HEAD', 'POST'): | |||
elif request.method in ("GET", "HEAD", "POST"): | |||
response = get_response() | |||
else: | |||
@@ -103,41 +104,45 @@ def application(request): | |||
return response | |||
def init_request(request): | |||
frappe.local.request = request | |||
frappe.local.is_ajax = frappe.get_request_header("X-Requested-With")=="XMLHttpRequest" | |||
frappe.local.is_ajax = frappe.get_request_header("X-Requested-With") == "XMLHttpRequest" | |||
site = _site or request.headers.get('X-Frappe-Site-Name') or get_site_name(request.host) | |||
site = _site or request.headers.get("X-Frappe-Site-Name") or get_site_name(request.host) | |||
frappe.init(site=site, sites_path=_sites_path) | |||
if not (frappe.local.conf and frappe.local.conf.db_name): | |||
# site does not exist | |||
raise NotFound | |||
if frappe.local.conf.get('maintenance_mode'): | |||
if frappe.local.conf.get("maintenance_mode"): | |||
frappe.connect() | |||
raise frappe.SessionStopped('Session Stopped') | |||
raise frappe.SessionStopped("Session Stopped") | |||
else: | |||
frappe.connect(set_admin_as_user=False) | |||
request.max_content_length = frappe.local.conf.get('max_file_size') or 10 * 1024 * 1024 | |||
request.max_content_length = frappe.local.conf.get("max_file_size") or 10 * 1024 * 1024 | |||
make_form_dict(request) | |||
if request.method != "OPTIONS": | |||
frappe.local.http_request = frappe.auth.HTTPRequest() | |||
def log_request(request, response): | |||
if hasattr(frappe.local, 'conf') and frappe.local.conf.enable_frappe_logger: | |||
frappe.logger("frappe.web", allow_site=frappe.local.site).info({ | |||
"site": get_site_name(request.host), | |||
"remote_addr": getattr(request, "remote_addr", "NOTFOUND"), | |||
"base_url": getattr(request, "base_url", "NOTFOUND"), | |||
"full_path": getattr(request, "full_path", "NOTFOUND"), | |||
"method": getattr(request, "method", "NOTFOUND"), | |||
"scheme": getattr(request, "scheme", "NOTFOUND"), | |||
"http_status_code": getattr(response, "status_code", "NOTFOUND") | |||
}) | |||
if hasattr(frappe.local, "conf") and frappe.local.conf.enable_frappe_logger: | |||
frappe.logger("frappe.web", allow_site=frappe.local.site).info( | |||
{ | |||
"site": get_site_name(request.host), | |||
"remote_addr": getattr(request, "remote_addr", "NOTFOUND"), | |||
"base_url": getattr(request, "base_url", "NOTFOUND"), | |||
"full_path": getattr(request, "full_path", "NOTFOUND"), | |||
"method": getattr(request, "method", "NOTFOUND"), | |||
"scheme": getattr(request, "scheme", "NOTFOUND"), | |||
"http_status_code": getattr(response, "status_code", "NOTFOUND"), | |||
} | |||
) | |||
def process_response(response): | |||
@@ -145,19 +150,20 @@ def process_response(response): | |||
return | |||
# set cookies | |||
if hasattr(frappe.local, 'cookie_manager'): | |||
if hasattr(frappe.local, "cookie_manager"): | |||
frappe.local.cookie_manager.flush_cookies(response=response) | |||
# rate limiter headers | |||
if hasattr(frappe.local, 'rate_limiter'): | |||
if hasattr(frappe.local, "rate_limiter"): | |||
response.headers.extend(frappe.local.rate_limiter.headers()) | |||
# CORS headers | |||
if hasattr(frappe.local, 'conf') and frappe.conf.allow_cors: | |||
if hasattr(frappe.local, "conf") and frappe.conf.allow_cors: | |||
set_cors_headers(response) | |||
def set_cors_headers(response): | |||
origin = frappe.request.headers.get('Origin') | |||
origin = frappe.request.headers.get("Origin") | |||
allow_cors = frappe.conf.allow_cors | |||
if not (origin and allow_cors): | |||
return | |||
@@ -169,20 +175,25 @@ def set_cors_headers(response): | |||
if origin not in allow_cors: | |||
return | |||
response.headers.extend({ | |||
'Access-Control-Allow-Origin': origin, | |||
'Access-Control-Allow-Credentials': 'true', | |||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', | |||
'Access-Control-Allow-Headers': ('Authorization,DNT,X-Mx-ReqToken,' | |||
'Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,' | |||
'Cache-Control,Content-Type') | |||
}) | |||
response.headers.extend( | |||
{ | |||
"Access-Control-Allow-Origin": origin, | |||
"Access-Control-Allow-Credentials": "true", | |||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", | |||
"Access-Control-Allow-Headers": ( | |||
"Authorization,DNT,X-Mx-ReqToken," | |||
"Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since," | |||
"Cache-Control,Content-Type" | |||
), | |||
} | |||
) | |||
def make_form_dict(request): | |||
import json | |||
request_data = request.get_data(as_text=True) | |||
if 'application/json' in (request.content_type or '') and request_data: | |||
if "application/json" in (request.content_type or "") and request_data: | |||
args = json.loads(request_data) | |||
else: | |||
args = {} | |||
@@ -198,20 +209,19 @@ def make_form_dict(request): | |||
# _ is passed by $.ajax so that the request is not cached by the browser. So, remove _ from form_dict | |||
frappe.local.form_dict.pop("_") | |||
def handle_exception(e): | |||
response = None | |||
http_status_code = getattr(e, "http_status_code", 500) | |||
return_as_message = False | |||
accept_header = frappe.get_request_header("Accept") or "" | |||
respond_as_json = ( | |||
frappe.get_request_header('Accept') | |||
and (frappe.local.is_ajax or 'application/json' in accept_header) | |||
or ( | |||
frappe.local.request.path.startswith("/api/") and not accept_header.startswith("text") | |||
) | |||
frappe.get_request_header("Accept") | |||
and (frappe.local.is_ajax or "application/json" in accept_header) | |||
or (frappe.local.request.path.startswith("/api/") and not accept_header.startswith("text")) | |||
) | |||
if frappe.conf.get('developer_mode'): | |||
if frappe.conf.get("developer_mode"): | |||
# don't fail silently | |||
print(frappe.get_traceback()) | |||
@@ -220,27 +230,38 @@ def handle_exception(e): | |||
# if the request is ajax, send back the trace or error message | |||
response = frappe.utils.response.report_error(http_status_code) | |||
elif (http_status_code==500 | |||
elif ( | |||
http_status_code == 500 | |||
and (frappe.db and isinstance(e, frappe.db.InternalError)) | |||
and (frappe.db and (frappe.db.is_deadlocked(e) or frappe.db.is_timedout(e)))): | |||
http_status_code = 508 | |||
and (frappe.db and (frappe.db.is_deadlocked(e) or frappe.db.is_timedout(e))) | |||
): | |||
http_status_code = 508 | |||
elif http_status_code==401: | |||
frappe.respond_as_web_page(_("Session Expired"), | |||
elif http_status_code == 401: | |||
frappe.respond_as_web_page( | |||
_("Session Expired"), | |||
_("Your session has expired, please login again to continue."), | |||
http_status_code=http_status_code, indicator_color='red') | |||
http_status_code=http_status_code, | |||
indicator_color="red", | |||
) | |||
return_as_message = True | |||
elif http_status_code==403: | |||
frappe.respond_as_web_page(_("Not Permitted"), | |||
elif http_status_code == 403: | |||
frappe.respond_as_web_page( | |||
_("Not Permitted"), | |||
_("You do not have enough permissions to complete the action"), | |||
http_status_code=http_status_code, indicator_color='red') | |||
http_status_code=http_status_code, | |||
indicator_color="red", | |||
) | |||
return_as_message = True | |||
elif http_status_code==404: | |||
frappe.respond_as_web_page(_("Not Found"), | |||
elif http_status_code == 404: | |||
frappe.respond_as_web_page( | |||
_("Not Found"), | |||
_("The resource you are looking for is not available"), | |||
http_status_code=http_status_code, indicator_color='red') | |||
http_status_code=http_status_code, | |||
indicator_color="red", | |||
) | |||
return_as_message = True | |||
elif http_status_code == 429: | |||
@@ -252,9 +273,9 @@ def handle_exception(e): | |||
if frappe.local.flags.disable_traceback and not frappe.local.dev_server: | |||
traceback = "" | |||
frappe.respond_as_web_page("Server Error", | |||
traceback, http_status_code=http_status_code, | |||
indicator_color='red', width=640) | |||
frappe.respond_as_web_page( | |||
"Server Error", traceback, http_status_code=http_status_code, indicator_color="red", width=640 | |||
) | |||
return_as_message = True | |||
if e.__class__ == frappe.AuthenticationError: | |||
@@ -269,6 +290,7 @@ def handle_exception(e): | |||
return response | |||
def after_request(rollback): | |||
if (frappe.local.request.method in ("POST", "PUT") or frappe.local.flags.commit) and frappe.db: | |||
if frappe.db.transaction_writes: | |||
@@ -286,41 +308,47 @@ def after_request(rollback): | |||
return rollback | |||
application = local_manager.make_middleware(application) | |||
def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=None, sites_path='.'): | |||
def serve( | |||
port=8000, profile=False, no_reload=False, no_threading=False, site=None, sites_path="." | |||
): | |||
global application, _site, _sites_path | |||
_site = site | |||
_sites_path = sites_path | |||
from werkzeug.serving import run_simple | |||
if profile or os.environ.get('USE_PROFILER'): | |||
application = ProfilerMiddleware(application, sort_by=('cumtime', 'calls')) | |||
if profile or os.environ.get("USE_PROFILER"): | |||
application = ProfilerMiddleware(application, sort_by=("cumtime", "calls")) | |||
if not os.environ.get('NO_STATICS'): | |||
application = SharedDataMiddleware(application, { | |||
str('/assets'): str(os.path.join(sites_path, 'assets')) | |||
}) | |||
if not os.environ.get("NO_STATICS"): | |||
application = SharedDataMiddleware( | |||
application, {str("/assets"): str(os.path.join(sites_path, "assets"))} | |||
) | |||
application = StaticDataMiddleware(application, { | |||
str('/files'): str(os.path.abspath(sites_path)) | |||
}) | |||
application = StaticDataMiddleware( | |||
application, {str("/files"): str(os.path.abspath(sites_path))} | |||
) | |||
application.debug = True | |||
application.config = { | |||
'SERVER_NAME': 'localhost:8000' | |||
} | |||
application.config = {"SERVER_NAME": "localhost:8000"} | |||
log = logging.getLogger('werkzeug') | |||
log = logging.getLogger("werkzeug") | |||
log.propagate = False | |||
in_test_env = os.environ.get('CI') | |||
in_test_env = os.environ.get("CI") | |||
if in_test_env: | |||
log.setLevel(logging.ERROR) | |||
run_simple('0.0.0.0', int(port), application, | |||
run_simple( | |||
"0.0.0.0", | |||
int(port), | |||
application, | |||
use_reloader=False if in_test_env else not no_reload, | |||
use_debugger=not in_test_env, | |||
use_evalex=not in_test_env, | |||
threaded=not no_threading) | |||
threaded=not no_threading, | |||
) |
@@ -11,7 +11,12 @@ from frappe.core.doctype.activity_log.activity_log import add_authentication_log | |||
from frappe.modules.patch_handler import check_session_stopped | |||
from frappe.sessions import Session, clear_sessions, delete_session | |||
from frappe.translate import get_language | |||
from frappe.twofactor import authenticate_for_2factor, confirm_otp_token, get_cached_user_pass, should_run_2fa | |||
from frappe.twofactor import ( | |||
authenticate_for_2factor, | |||
confirm_otp_token, | |||
get_cached_user_pass, | |||
should_run_2fa, | |||
) | |||
from frappe.utils import cint, date_diff, datetime, get_datetime, today | |||
from frappe.utils.password import check_password | |||
from frappe.website.utils import get_home_page | |||
@@ -47,20 +52,20 @@ class HTTPRequest: | |||
def domain(self): | |||
if not getattr(self, "_domain", None): | |||
self._domain = frappe.request.host | |||
if self._domain and self._domain.startswith('www.'): | |||
if self._domain and self._domain.startswith("www."): | |||
self._domain = self._domain[4:] | |||
return self._domain | |||
def set_request_ip(self): | |||
if frappe.get_request_header('X-Forwarded-For'): | |||
frappe.local.request_ip = (frappe.get_request_header('X-Forwarded-For').split(",")[0]).strip() | |||
if frappe.get_request_header("X-Forwarded-For"): | |||
frappe.local.request_ip = (frappe.get_request_header("X-Forwarded-For").split(",")[0]).strip() | |||
elif frappe.get_request_header('REMOTE_ADDR'): | |||
frappe.local.request_ip = frappe.get_request_header('REMOTE_ADDR') | |||
elif frappe.get_request_header("REMOTE_ADDR"): | |||
frappe.local.request_ip = frappe.get_request_header("REMOTE_ADDR") | |||
else: | |||
frappe.local.request_ip = '127.0.0.1' | |||
frappe.local.request_ip = "127.0.0.1" | |||
def set_cookies(self): | |||
frappe.local.cookie_manager = CookieManager() | |||
@@ -75,7 +80,7 @@ class HTTPRequest: | |||
if ( | |||
not frappe.local.session.data.csrf_token | |||
or frappe.local.session.data.device == "mobile" | |||
or frappe.conf.get('ignore_csrf', None) | |||
or frappe.conf.get("ignore_csrf", None) | |||
): | |||
# not via boot | |||
return | |||
@@ -99,10 +104,10 @@ class HTTPRequest: | |||
def connect(self): | |||
"""connect to db, from ac_name or db_name""" | |||
frappe.local.db = frappe.database.get_db( | |||
user=self.get_db_name(), | |||
password=getattr(conf, 'db_password', '') | |||
user=self.get_db_name(), password=getattr(conf, "db_password", "") | |||
) | |||
class LoginManager: | |||
def __init__(self): | |||
self.user = None | |||
@@ -110,13 +115,15 @@ class LoginManager: | |||
self.full_name = None | |||
self.user_type = None | |||
if frappe.local.form_dict.get('cmd')=='login' or frappe.local.request.path=="/api/method/login": | |||
if ( | |||
frappe.local.form_dict.get("cmd") == "login" or frappe.local.request.path == "/api/method/login" | |||
): | |||
if self.login() is False: | |||
return | |||
self.resume = False | |||
# run login triggers | |||
self.run_trigger('on_session_creation') | |||
self.run_trigger("on_session_creation") | |||
else: | |||
try: | |||
self.resume = True | |||
@@ -131,12 +138,14 @@ class LoginManager: | |||
def login(self): | |||
# clear cache | |||
frappe.clear_cache(user = frappe.form_dict.get('usr')) | |||
frappe.clear_cache(user=frappe.form_dict.get("usr")) | |||
user, pwd = get_cached_user_pass() | |||
self.authenticate(user=user, pwd=pwd) | |||
if self.force_user_to_reset_password(): | |||
doc = frappe.get_doc("User", self.user) | |||
frappe.local.response["redirect_to"] = doc.reset_password(send_email=False, password_expired=True) | |||
frappe.local.response["redirect_to"] = doc.reset_password( | |||
send_email=False, password_expired=True | |||
) | |||
frappe.local.response["message"] = "Password Reset" | |||
return False | |||
@@ -147,7 +156,7 @@ class LoginManager: | |||
self.post_login() | |||
def post_login(self): | |||
self.run_trigger('on_login') | |||
self.run_trigger("on_login") | |||
validate_ip_address(self.user) | |||
self.validate_hour() | |||
self.get_user_info() | |||
@@ -156,8 +165,9 @@ class LoginManager: | |||
self.set_user_info() | |||
def get_user_info(self): | |||
self.info = frappe.db.get_value("User", self.user, | |||
["user_type", "first_name", "last_name", "user_image"], as_dict=1) | |||
self.info = frappe.db.get_value( | |||
"User", self.user, ["user_type", "first_name", "last_name", "user_image"], as_dict=1 | |||
) | |||
self.user_type = self.info.user_type | |||
@@ -170,28 +180,27 @@ class LoginManager: | |||
# set sid again | |||
frappe.local.cookie_manager.init_cookies() | |||
self.full_name = " ".join(filter(None, [self.info.first_name, | |||
self.info.last_name])) | |||
self.full_name = " ".join(filter(None, [self.info.first_name, self.info.last_name])) | |||
if self.info.user_type=="Website User": | |||
if self.info.user_type == "Website User": | |||
frappe.local.cookie_manager.set_cookie("system_user", "no") | |||
if not resume: | |||
frappe.local.response["message"] = "No App" | |||
frappe.local.response["home_page"] = '/' + get_home_page() | |||
frappe.local.response["home_page"] = "/" + get_home_page() | |||
else: | |||
frappe.local.cookie_manager.set_cookie("system_user", "yes") | |||
if not resume: | |||
frappe.local.response['message'] = 'Logged In' | |||
frappe.local.response["message"] = "Logged In" | |||
frappe.local.response["home_page"] = "/app" | |||
if not resume: | |||
frappe.response["full_name"] = self.full_name | |||
# redirect information | |||
redirect_to = frappe.cache().hget('redirect_after_login', self.user) | |||
redirect_to = frappe.cache().hget("redirect_after_login", self.user) | |||
if redirect_to: | |||
frappe.local.response["redirect_to"] = redirect_to | |||
frappe.cache().hdel('redirect_after_login', self.user) | |||
frappe.cache().hdel("redirect_after_login", self.user) | |||
frappe.local.cookie_manager.set_cookie("full_name", self.full_name) | |||
frappe.local.cookie_manager.set_cookie("user_id", self.user) | |||
@@ -202,8 +211,9 @@ class LoginManager: | |||
def make_session(self, resume=False): | |||
# start session | |||
frappe.local.session_obj = Session(user=self.user, resume=resume, | |||
full_name=self.full_name, user_type=self.user_type) | |||
frappe.local.session_obj = Session( | |||
user=self.user, resume=resume, full_name=self.full_name, user_type=self.user_type | |||
) | |||
# reset user if changed to Guest | |||
self.user = frappe.local.session_obj.user | |||
@@ -212,7 +222,10 @@ class LoginManager: | |||
def clear_active_sessions(self): | |||
"""Clear other sessions of the current user if `deny_multiple_sessions` is not set""" | |||
if not (cint(frappe.conf.get("deny_multiple_sessions")) or cint(frappe.db.get_system_setting('deny_multiple_sessions'))): | |||
if not ( | |||
cint(frappe.conf.get("deny_multiple_sessions")) | |||
or cint(frappe.db.get_system_setting("deny_multiple_sessions")) | |||
): | |||
return | |||
if frappe.session.user != "Guest": | |||
@@ -222,27 +235,27 @@ class LoginManager: | |||
from frappe.core.doctype.user.user import User | |||
if not (user and pwd): | |||
user, pwd = frappe.form_dict.get('usr'), frappe.form_dict.get('pwd') | |||
user, pwd = frappe.form_dict.get("usr"), frappe.form_dict.get("pwd") | |||
if not (user and pwd): | |||
self.fail(_('Incomplete login details'), user=user) | |||
self.fail(_("Incomplete login details"), user=user) | |||
user = User.find_by_credentials(user, pwd) | |||
if not user: | |||
self.fail('Invalid login credentials') | |||
self.fail("Invalid login credentials") | |||
# Current login flow uses cached credentials for authentication while checking OTP. | |||
# Incase of OTP check, tracker for auth needs to be disabled(If not, it can remove tracker history as it is going to succeed anyway) | |||
# Tracker is activated for 2FA incase of OTP. | |||
ignore_tracker = should_run_2fa(user.name) and ('otp' in frappe.form_dict) | |||
ignore_tracker = should_run_2fa(user.name) and ("otp" in frappe.form_dict) | |||
tracker = None if ignore_tracker else get_login_attempt_tracker(user.name) | |||
if not user.is_authenticated: | |||
tracker and tracker.add_failure_attempt() | |||
self.fail('Invalid login credentials', user=user.name) | |||
elif not (user.name == 'Administrator' or user.enabled): | |||
self.fail("Invalid login credentials", user=user.name) | |||
elif not (user.name == "Administrator" or user.enabled): | |||
tracker and tracker.add_failure_attempt() | |||
self.fail('User disabled or missing', user=user.name) | |||
self.fail("User disabled or missing", user=user.name) | |||
else: | |||
tracker and tracker.add_success_attempt() | |||
self.user = user.name | |||
@@ -254,12 +267,14 @@ class LoginManager: | |||
if self.user in frappe.STANDARD_USERS: | |||
return False | |||
reset_pwd_after_days = cint(frappe.db.get_single_value("System Settings", | |||
"force_user_to_reset_password")) | |||
reset_pwd_after_days = cint( | |||
frappe.db.get_single_value("System Settings", "force_user_to_reset_password") | |||
) | |||
if reset_pwd_after_days: | |||
last_password_reset_date = frappe.db.get_value("User", | |||
self.user, "last_password_reset_date") or today() | |||
last_password_reset_date = ( | |||
frappe.db.get_value("User", self.user, "last_password_reset_date") or today() | |||
) | |||
last_pwd_reset_days = date_diff(today(), last_password_reset_date) | |||
@@ -272,30 +287,31 @@ class LoginManager: | |||
# returns user in correct case | |||
return check_password(user, pwd) | |||
except frappe.AuthenticationError: | |||
self.fail('Incorrect password', user=user) | |||
self.fail("Incorrect password", user=user) | |||
def fail(self, message, user=None): | |||
if not user: | |||
user = _('Unknown User') | |||
frappe.local.response['message'] = message | |||
user = _("Unknown User") | |||
frappe.local.response["message"] = message | |||
add_authentication_log(message, user, status="Failed") | |||
frappe.db.commit() | |||
raise frappe.AuthenticationError | |||
def run_trigger(self, event='on_login'): | |||
def run_trigger(self, event="on_login"): | |||
for method in frappe.get_hooks().get(event, []): | |||
frappe.call(frappe.get_attr(method), login_manager=self) | |||
def validate_hour(self): | |||
"""check if user is logging in during restricted hours""" | |||
login_before = int(frappe.db.get_value('User', self.user, 'login_before', ignore=True) or 0) | |||
login_after = int(frappe.db.get_value('User', self.user, 'login_after', ignore=True) or 0) | |||
login_before = int(frappe.db.get_value("User", self.user, "login_before", ignore=True) or 0) | |||
login_after = int(frappe.db.get_value("User", self.user, "login_after", ignore=True) or 0) | |||
if not (login_before or login_after): | |||
return | |||
from frappe.utils import now_datetime | |||
current_hour = int(now_datetime().strftime('%H')) | |||
current_hour = int(now_datetime().strftime("%H")) | |||
if login_before and current_hour > login_before: | |||
frappe.throw(_("Login not allowed at this time"), frappe.AuthenticationError) | |||
@@ -311,9 +327,10 @@ class LoginManager: | |||
self.user = user | |||
self.post_login() | |||
def logout(self, arg='', user=None): | |||
if not user: user = frappe.session.user | |||
self.run_trigger('on_logout') | |||
def logout(self, arg="", user=None): | |||
if not user: | |||
user = frappe.session.user | |||
self.run_trigger("on_logout") | |||
if user == frappe.session.user: | |||
delete_session(frappe.session.sid, user=user, reason="User Manually Logged Out") | |||
@@ -324,13 +341,15 @@ class LoginManager: | |||
def clear_cookies(self): | |||
clear_cookies() | |||
class CookieManager: | |||
def __init__(self): | |||
self.cookies = {} | |||
self.to_delete = [] | |||
def init_cookies(self): | |||
if not frappe.local.session.get('sid'): return | |||
if not frappe.local.session.get("sid"): | |||
return | |||
# sid expires in 3 days | |||
expires = datetime.datetime.now() + datetime.timedelta(days=3) | |||
@@ -340,7 +359,7 @@ class CookieManager: | |||
self.set_cookie("country", frappe.session.session_country) | |||
def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Lax"): | |||
if not secure and hasattr(frappe.local, 'request'): | |||
if not secure and hasattr(frappe.local, "request"): | |||
secure = frappe.local.request.scheme == "https" | |||
# Cordova does not work with Lax | |||
@@ -352,7 +371,7 @@ class CookieManager: | |||
"expires": expires, | |||
"secure": secure, | |||
"httponly": httponly, | |||
"samesite": samesite | |||
"samesite": samesite, | |||
} | |||
def delete_cookie(self, to_delete): | |||
@@ -363,11 +382,14 @@ class CookieManager: | |||
def flush_cookies(self, response): | |||
for key, opts in self.cookies.items(): | |||
response.set_cookie(key, quote((opts.get("value") or "").encode('utf-8')), | |||
response.set_cookie( | |||
key, | |||
quote((opts.get("value") or "").encode("utf-8")), | |||
expires=opts.get("expires"), | |||
secure=opts.get("secure"), | |||
httponly=opts.get("httponly"), | |||
samesite=opts.get("samesite")) | |||
samesite=opts.get("samesite"), | |||
) | |||
# expires yesterday! | |||
expires = datetime.datetime.now() + datetime.timedelta(days=-1) | |||
@@ -379,19 +401,29 @@ class CookieManager: | |||
def get_logged_user(): | |||
return frappe.session.user | |||
def clear_cookies(): | |||
if hasattr(frappe.local, "session"): | |||
frappe.session.sid = "" | |||
frappe.local.cookie_manager.delete_cookie(["full_name", "user_id", "sid", "user_image", "system_user"]) | |||
frappe.local.cookie_manager.delete_cookie( | |||
["full_name", "user_id", "sid", "user_image", "system_user"] | |||
) | |||
def validate_ip_address(user): | |||
"""check if IP Address is valid""" | |||
user = frappe.get_cached_doc("User", user) if not frappe.flags.in_test else frappe.get_doc("User", user) | |||
user = ( | |||
frappe.get_cached_doc("User", user) if not frappe.flags.in_test else frappe.get_doc("User", user) | |||
) | |||
ip_list = user.get_restricted_ip_list() | |||
if not ip_list: | |||
return | |||
system_settings = frappe.get_cached_doc("System Settings") if not frappe.flags.in_test else frappe.get_single("System Settings") | |||
system_settings = ( | |||
frappe.get_cached_doc("System Settings") | |||
if not frappe.flags.in_test | |||
else frappe.get_single("System Settings") | |||
) | |||
# check if bypass restrict ip is enabled for all users | |||
bypass_restrict_ip_check = system_settings.bypass_restrict_ip_check_if_2fa_enabled | |||
@@ -406,6 +438,7 @@ def validate_ip_address(user): | |||
frappe.throw(_("Access not allowed from this IP Address"), frappe.AuthenticationError) | |||
def get_login_attempt_tracker(user_name: str, raise_locked_exception: bool = True): | |||
"""Get login attempt tracker instance. | |||
@@ -413,18 +446,22 @@ def get_login_attempt_tracker(user_name: str, raise_locked_exception: bool = Tru | |||
:param raise_locked_exception: If set, raises an exception incase of user not allowed to login | |||
""" | |||
sys_settings = frappe.get_doc("System Settings") | |||
track_login_attempts = (sys_settings.allow_consecutive_login_attempts >0) | |||
track_login_attempts = sys_settings.allow_consecutive_login_attempts > 0 | |||
tracker_kwargs = {} | |||
if track_login_attempts: | |||
tracker_kwargs['lock_interval'] = sys_settings.allow_login_after_fail | |||
tracker_kwargs['max_consecutive_login_attempts'] = sys_settings.allow_consecutive_login_attempts | |||
tracker_kwargs["lock_interval"] = sys_settings.allow_login_after_fail | |||
tracker_kwargs["max_consecutive_login_attempts"] = sys_settings.allow_consecutive_login_attempts | |||
tracker = LoginAttemptTracker(user_name, **tracker_kwargs) | |||
if raise_locked_exception and track_login_attempts and not tracker.is_user_allowed(): | |||
frappe.throw(_("Your account has been locked and will resume after {0} seconds") | |||
.format(sys_settings.allow_login_after_fail), frappe.SecurityException) | |||
frappe.throw( | |||
_("Your account has been locked and will resume after {0} seconds").format( | |||
sys_settings.allow_login_after_fail | |||
), | |||
frappe.SecurityException, | |||
) | |||
return tracker | |||
@@ -433,8 +470,11 @@ class LoginAttemptTracker(object): | |||
Lock the account for s number of seconds if there have been n consecutive unsuccessful attempts to log in. | |||
""" | |||
def __init__(self, user_name: str, max_consecutive_login_attempts: int=3, lock_interval:int = 5*60): | |||
""" Initialize the tracker. | |||
def __init__( | |||
self, user_name: str, max_consecutive_login_attempts: int = 3, lock_interval: int = 5 * 60 | |||
): | |||
"""Initialize the tracker. | |||
:param user_name: Name of the loggedin user | |||
:param max_consecutive_login_attempts: Maximum allowed consecutive failed login attempts | |||
@@ -446,15 +486,15 @@ class LoginAttemptTracker(object): | |||
@property | |||
def login_failed_count(self): | |||
return frappe.cache().hget('login_failed_count', self.user_name) | |||
return frappe.cache().hget("login_failed_count", self.user_name) | |||
@login_failed_count.setter | |||
def login_failed_count(self, count): | |||
frappe.cache().hset('login_failed_count', self.user_name, count) | |||
frappe.cache().hset("login_failed_count", self.user_name, count) | |||
@login_failed_count.deleter | |||
def login_failed_count(self): | |||
frappe.cache().hdel('login_failed_count', self.user_name) | |||
frappe.cache().hdel("login_failed_count", self.user_name) | |||
@property | |||
def login_failed_time(self): | |||
@@ -462,23 +502,23 @@ class LoginAttemptTracker(object): | |||
For every user we track only First failed login attempt time within lock interval of time. | |||
""" | |||
return frappe.cache().hget('login_failed_time', self.user_name) | |||
return frappe.cache().hget("login_failed_time", self.user_name) | |||
@login_failed_time.setter | |||
def login_failed_time(self, timestamp): | |||
frappe.cache().hset('login_failed_time', self.user_name, timestamp) | |||
frappe.cache().hset("login_failed_time", self.user_name, timestamp) | |||
@login_failed_time.deleter | |||
def login_failed_time(self): | |||
frappe.cache().hdel('login_failed_time', self.user_name) | |||
frappe.cache().hdel("login_failed_time", self.user_name) | |||
def add_failure_attempt(self): | |||
""" Log user failure attempts into the system. | |||
"""Log user failure attempts into the system. | |||
Increase the failure count if new failure is with in current lock interval time period, if not reset the login failure count. | |||
""" | |||
login_failed_time = self.login_failed_time | |||
login_failed_count = self.login_failed_count # Consecutive login failure count | |||
login_failed_count = self.login_failed_count # Consecutive login failure count | |||
current_time = get_datetime() | |||
if not (login_failed_time and login_failed_count): | |||
@@ -493,8 +533,7 @@ class LoginAttemptTracker(object): | |||
self.login_failed_count = login_failed_count | |||
def add_success_attempt(self): | |||
"""Reset login failures. | |||
""" | |||
"""Reset login failures.""" | |||
del self.login_failed_count | |||
del self.login_failed_time | |||
@@ -507,6 +546,10 @@ class LoginAttemptTracker(object): | |||
login_failed_count = self.login_failed_count or 0 | |||
current_time = get_datetime() | |||
if login_failed_time and login_failed_time + self.lock_interval > current_time and login_failed_count > self.max_failed_logins: | |||
if ( | |||
login_failed_time | |||
and login_failed_time + self.lock_interval > current_time | |||
and login_failed_count > self.max_failed_logins | |||
): | |||
return False | |||
return True |
@@ -24,9 +24,7 @@ class AssignmentRule(Document): | |||
def validate_document_types(self): | |||
if self.document_type == "ToDo": | |||
frappe.throw( | |||
_('Assignment Rule is not allowed on {0} document type').format( | |||
frappe.bold("ToDo") | |||
) | |||
_("Assignment Rule is not allowed on {0} document type").format(frappe.bold("ToDo")) | |||
) | |||
def validate_assignment_days(self): | |||
@@ -38,70 +36,70 @@ class AssignmentRule(Document): | |||
frappe.throw( | |||
_("Assignment Day{0} {1} has been repeated.").format( | |||
plural, | |||
frappe.bold(", ".join(repeated_days)) | |||
plural, frappe.bold(", ".join(repeated_days)) | |||
) | |||
) | |||
def apply_unassign(self, doc, assignments): | |||
if (self.unassign_condition and | |||
self.name in [d.assignment_rule for d in assignments]): | |||
if self.unassign_condition and self.name in [d.assignment_rule for d in assignments]: | |||
return self.clear_assignment(doc) | |||
return False | |||
def apply_assign(self, doc): | |||
if self.safe_eval('assign_condition', doc): | |||
if self.safe_eval("assign_condition", doc): | |||
return self.do_assignment(doc) | |||
def do_assignment(self, doc): | |||
# clear existing assignment, to reassign | |||
assign_to.clear(doc.get('doctype'), doc.get('name')) | |||
assign_to.clear(doc.get("doctype"), doc.get("name")) | |||
user = self.get_user(doc) | |||
if user: | |||
assign_to.add(dict( | |||
assign_to = [user], | |||
doctype = doc.get('doctype'), | |||
name = doc.get('name'), | |||
description = frappe.render_template(self.description, doc), | |||
assignment_rule = self.name, | |||
notify = True, | |||
date = doc.get(self.due_date_based_on) if self.due_date_based_on else None | |||
)) | |||
assign_to.add( | |||
dict( | |||
assign_to=[user], | |||
doctype=doc.get("doctype"), | |||
name=doc.get("name"), | |||
description=frappe.render_template(self.description, doc), | |||
assignment_rule=self.name, | |||
notify=True, | |||
date=doc.get(self.due_date_based_on) if self.due_date_based_on else None, | |||
) | |||
) | |||
# set for reference in round robin | |||
self.db_set('last_user', user) | |||
self.db_set("last_user", user) | |||
return True | |||
return False | |||
def clear_assignment(self, doc): | |||
'''Clear assignments''' | |||
if self.safe_eval('unassign_condition', doc): | |||
return assign_to.clear(doc.get('doctype'), doc.get('name')) | |||
"""Clear assignments""" | |||
if self.safe_eval("unassign_condition", doc): | |||
return assign_to.clear(doc.get("doctype"), doc.get("name")) | |||
def close_assignments(self, doc): | |||
'''Close assignments''' | |||
if self.safe_eval('close_condition', doc): | |||
return assign_to.close_all_assignments(doc.get('doctype'), doc.get('name')) | |||
"""Close assignments""" | |||
if self.safe_eval("close_condition", doc): | |||
return assign_to.close_all_assignments(doc.get("doctype"), doc.get("name")) | |||
def get_user(self, doc): | |||
''' | |||
""" | |||
Get the next user for assignment | |||
''' | |||
if self.rule == 'Round Robin': | |||
""" | |||
if self.rule == "Round Robin": | |||
return self.get_user_round_robin() | |||
elif self.rule == 'Load Balancing': | |||
elif self.rule == "Load Balancing": | |||
return self.get_user_load_balancing() | |||
elif self.rule == 'Based on Field': | |||
elif self.rule == "Based on Field": | |||
return self.get_user_based_on_field(doc) | |||
def get_user_round_robin(self): | |||
''' | |||
""" | |||
Get next user based on round robin | |||
''' | |||
""" | |||
# first time, or last in list, pick the first | |||
if not self.last_user or self.last_user == self.users[-1].user: | |||
@@ -110,32 +108,33 @@ class AssignmentRule(Document): | |||
# find out the next user in the list | |||
for i, d in enumerate(self.users): | |||
if self.last_user == d.user: | |||
return self.users[i+1].user | |||
return self.users[i + 1].user | |||
# bad last user, assign to the first one | |||
return self.users[0].user | |||
def get_user_load_balancing(self): | |||
'''Assign to the user with least number of open assignments''' | |||
"""Assign to the user with least number of open assignments""" | |||
counts = [] | |||
for d in self.users: | |||
counts.append(dict( | |||
user = d.user, | |||
count = frappe.db.count('ToDo', dict( | |||
reference_type = self.document_type, | |||
allocated_to = d.user, | |||
status = "Open")) | |||
)) | |||
counts.append( | |||
dict( | |||
user=d.user, | |||
count=frappe.db.count( | |||
"ToDo", dict(reference_type=self.document_type, allocated_to=d.user, status="Open") | |||
), | |||
) | |||
) | |||
# sort by dict value | |||
sorted_counts = sorted(counts, key = lambda k: k['count']) | |||
sorted_counts = sorted(counts, key=lambda k: k["count"]) | |||
# pick the first user | |||
return sorted_counts[0].get('user') | |||
return sorted_counts[0].get("user") | |||
def get_user_based_on_field(self, doc): | |||
val = doc.get(self.field) | |||
if frappe.db.exists('User', val): | |||
if frappe.db.exists("User", val): | |||
return val | |||
def safe_eval(self, fieldname, doc): | |||
@@ -145,12 +144,12 @@ class AssignmentRule(Document): | |||
except Exception as e: | |||
# when assignment fails, don't block the document as it may be | |||
# a part of the email pulling | |||
frappe.msgprint(frappe._('Auto assignment failed: {0}').format(str(e)), indicator = 'orange') | |||
frappe.msgprint(frappe._("Auto assignment failed: {0}").format(str(e)), indicator="orange") | |||
return False | |||
def get_assignment_days(self): | |||
return [d.day for d in self.get('assignment_days', [])] | |||
return [d.day for d in self.get("assignment_days", [])] | |||
def is_rule_not_applicable_today(self): | |||
today = frappe.flags.assignment_day or frappe.utils.get_weekday() | |||
@@ -159,11 +158,14 @@ class AssignmentRule(Document): | |||
def get_assignments(doc) -> List[Dict]: | |||
return frappe.get_all('ToDo', fields = ['name', 'assignment_rule'], filters = dict( | |||
reference_type = doc.get('doctype'), | |||
reference_name = doc.get('name'), | |||
status = ('!=', 'Cancelled') | |||
), limit=5) | |||
return frappe.get_all( | |||
"ToDo", | |||
fields=["name", "assignment_rule"], | |||
filters=dict( | |||
reference_type=doc.get("doctype"), reference_name=doc.get("name"), status=("!=", "Cancelled") | |||
), | |||
limit=5, | |||
) | |||
@frappe.whitelist() | |||
@@ -173,21 +175,30 @@ def bulk_apply(doctype, docnames): | |||
for name in docnames: | |||
if background: | |||
frappe.enqueue('frappe.automation.doctype.assignment_rule.assignment_rule.apply', doc=None, doctype=doctype, name=name) | |||
frappe.enqueue( | |||
"frappe.automation.doctype.assignment_rule.assignment_rule.apply", | |||
doc=None, | |||
doctype=doctype, | |||
name=name, | |||
) | |||
else: | |||
apply(doctype=doctype, name=name) | |||
def reopen_closed_assignment(doc): | |||
todo_list = frappe.get_all("ToDo", filters={ | |||
"reference_type": doc.doctype, | |||
"reference_name": doc.name, | |||
"status": "Closed", | |||
}, pluck="name") | |||
todo_list = frappe.get_all( | |||
"ToDo", | |||
filters={ | |||
"reference_type": doc.doctype, | |||
"reference_name": doc.name, | |||
"status": "Closed", | |||
}, | |||
pluck="name", | |||
) | |||
for todo in todo_list: | |||
todo_doc = frappe.get_doc('ToDo', todo) | |||
todo_doc.status = 'Open' | |||
todo_doc = frappe.get_doc("ToDo", todo) | |||
todo_doc.status = "Open" | |||
todo_doc.save(ignore_permissions=True) | |||
return bool(todo_list) | |||
@@ -209,13 +220,16 @@ def apply(doc=None, method=None, doctype=None, name=None): | |||
if not doc and doctype and name: | |||
doc = frappe.get_doc(doctype, name) | |||
assignment_rules = get_doctype_map("Assignment Rule", doc.doctype, filters={ | |||
"document_type": doc.doctype, "disabled": 0 | |||
}, order_by="priority desc") | |||
assignment_rules = get_doctype_map( | |||
"Assignment Rule", | |||
doc.doctype, | |||
filters={"document_type": doc.doctype, "disabled": 0}, | |||
order_by="priority desc", | |||
) | |||
# multiple auto assigns | |||
assignment_rule_docs: List[AssignmentRule] = [ | |||
frappe.get_cached_doc("Assignment Rule", d.get('name')) for d in assignment_rules | |||
frappe.get_cached_doc("Assignment Rule", d.get("name")) for d in assignment_rules | |||
] | |||
if not assignment_rule_docs: | |||
@@ -224,8 +238,8 @@ def apply(doc=None, method=None, doctype=None, name=None): | |||
doc = doc.as_dict() | |||
assignments = get_assignments(doc) | |||
clear = True # are all assignments cleared | |||
new_apply = False # are new assignments applied | |||
clear = True # are all assignments cleared | |||
new_apply = False # are new assignments applied | |||
if assignments: | |||
# first unassign | |||
@@ -260,14 +274,18 @@ def apply(doc=None, method=None, doctype=None, name=None): | |||
if not new_apply: | |||
# only reopen if close condition is not satisfied | |||
to_close_todos = assignment_rule.safe_eval('close_condition', doc) | |||
to_close_todos = assignment_rule.safe_eval("close_condition", doc) | |||
if to_close_todos: | |||
# close todo status | |||
todos_to_close = frappe.get_all("ToDo", filters={ | |||
"reference_type": doc.doctype, | |||
"reference_name": doc.name, | |||
}, pluck="name") | |||
todos_to_close = frappe.get_all( | |||
"ToDo", | |||
filters={ | |||
"reference_type": doc.doctype, | |||
"reference_name": doc.name, | |||
}, | |||
pluck="name", | |||
) | |||
for todo in todos_to_close: | |||
_todo = frappe.get_doc("ToDo", todo) | |||
@@ -286,8 +304,7 @@ def apply(doc=None, method=None, doctype=None, name=None): | |||
def update_due_date(doc, state=None): | |||
"""Run on_update on every Document (via hooks.py) | |||
""" | |||
"""Run on_update on every Document (via hooks.py)""" | |||
skip_document_update = ( | |||
frappe.flags.in_migrate | |||
or frappe.flags.in_patch | |||
@@ -306,7 +323,7 @@ def update_due_date(doc, state=None): | |||
"due_date_based_on": ["is", "set"], | |||
"document_type": doc.doctype, | |||
"disabled": 0, | |||
} | |||
}, | |||
) | |||
for rule in assignment_rules: | |||
@@ -319,20 +336,24 @@ def update_due_date(doc, state=None): | |||
) | |||
if field_updated: | |||
assignment_todos = frappe.get_all("ToDo", filters={ | |||
"assignment_rule": rule.get("name"), | |||
"reference_type": doc.doctype, | |||
"reference_name": doc.name, | |||
"status": "Open", | |||
}, pluck="name") | |||
assignment_todos = frappe.get_all( | |||
"ToDo", | |||
filters={ | |||
"assignment_rule": rule.get("name"), | |||
"reference_type": doc.doctype, | |||
"reference_name": doc.name, | |||
"status": "Open", | |||
}, | |||
pluck="name", | |||
) | |||
for todo in assignment_todos: | |||
todo_doc = frappe.get_doc('ToDo', todo) | |||
todo_doc = frappe.get_doc("ToDo", todo) | |||
todo_doc.date = doc.get(due_date_field) | |||
todo_doc.flags.updater_reference = { | |||
'doctype': 'Assignment Rule', | |||
'docname': rule.get('name'), | |||
'label': _('via Assignment Rule') | |||
"doctype": "Assignment Rule", | |||
"docname": rule.get("name"), | |||
"label": _("via Assignment Rule"), | |||
} | |||
todo_doc.save(ignore_permissions=True) | |||
@@ -20,13 +20,13 @@ class TestAutoAssign(unittest.TestCase): | |||
def setUp(self): | |||
make_test_records("User") | |||
days = [ | |||
dict(day = 'Sunday'), | |||
dict(day = 'Monday'), | |||
dict(day = 'Tuesday'), | |||
dict(day = 'Wednesday'), | |||
dict(day = 'Thursday'), | |||
dict(day = 'Friday'), | |||
dict(day = 'Saturday'), | |||
dict(day="Sunday"), | |||
dict(day="Monday"), | |||
dict(day="Tuesday"), | |||
dict(day="Wednesday"), | |||
dict(day="Thursday"), | |||
dict(day="Friday"), | |||
dict(day="Saturday"), | |||
] | |||
self.days = days | |||
self.assignment_rule = get_assignment_rule([days, days]) | |||
@@ -36,20 +36,22 @@ class TestAutoAssign(unittest.TestCase): | |||
note = make_note(dict(public=1)) | |||
# check if auto assigned to first user | |||
self.assertEqual(frappe.db.get_value('ToDo', dict( | |||
reference_type = 'Note', | |||
reference_name = note.name, | |||
status = 'Open' | |||
), 'allocated_to'), 'test@example.com') | |||
self.assertEqual( | |||
frappe.db.get_value( | |||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" | |||
), | |||
"test@example.com", | |||
) | |||
note = make_note(dict(public=1)) | |||
# check if auto assigned to second user | |||
self.assertEqual(frappe.db.get_value('ToDo', dict( | |||
reference_type = 'Note', | |||
reference_name = note.name, | |||
status = 'Open' | |||
), 'allocated_to'), 'test1@example.com') | |||
self.assertEqual( | |||
frappe.db.get_value( | |||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" | |||
), | |||
"test1@example.com", | |||
) | |||
clear_assignments() | |||
@@ -57,35 +59,41 @@ class TestAutoAssign(unittest.TestCase): | |||
# check if auto assigned to third user, even if | |||
# previous assignments where closed | |||
self.assertEqual(frappe.db.get_value('ToDo', dict( | |||
reference_type = 'Note', | |||
reference_name = note.name, | |||
status = 'Open' | |||
), 'allocated_to'), 'test2@example.com') | |||
self.assertEqual( | |||
frappe.db.get_value( | |||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" | |||
), | |||
"test2@example.com", | |||
) | |||
# check loop back to first user | |||
note = make_note(dict(public=1)) | |||
self.assertEqual(frappe.db.get_value('ToDo', dict( | |||
reference_type = 'Note', | |||
reference_name = note.name, | |||
status = 'Open' | |||
), 'allocated_to'), 'test@example.com') | |||
self.assertEqual( | |||
frappe.db.get_value( | |||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" | |||
), | |||
"test@example.com", | |||
) | |||
def test_load_balancing(self): | |||
self.assignment_rule.rule = 'Load Balancing' | |||
self.assignment_rule.rule = "Load Balancing" | |||
self.assignment_rule.save() | |||
for _ in range(30): | |||
note = make_note(dict(public=1)) | |||
# check if each user has 10 assignments (?) | |||
for user in ('test@example.com', 'test1@example.com', 'test2@example.com'): | |||
self.assertEqual(len(frappe.get_all('ToDo', dict(allocated_to = user, reference_type = 'Note'))), 10) | |||
for user in ("test@example.com", "test1@example.com", "test2@example.com"): | |||
self.assertEqual( | |||
len(frappe.get_all("ToDo", dict(allocated_to=user, reference_type="Note"))), 10 | |||
) | |||
# clear 5 assignments for first user | |||
# can't do a limit in "delete" since postgres does not support it | |||
for d in frappe.get_all('ToDo', dict(reference_type = 'Note', allocated_to = 'test@example.com'), limit=5): | |||
for d in frappe.get_all( | |||
"ToDo", dict(reference_type="Note", allocated_to="test@example.com"), limit=5 | |||
): | |||
frappe.db.delete("ToDo", {"name": d.name}) | |||
# add 5 more assignments | |||
@@ -93,56 +101,59 @@ class TestAutoAssign(unittest.TestCase): | |||
make_note(dict(public=1)) | |||
# check if each user still has 10 assignments | |||
for user in ('test@example.com', 'test1@example.com', 'test2@example.com'): | |||
self.assertEqual(len(frappe.get_all('ToDo', dict(allocated_to = user, reference_type = 'Note'))), 10) | |||
for user in ("test@example.com", "test1@example.com", "test2@example.com"): | |||
self.assertEqual( | |||
len(frappe.get_all("ToDo", dict(allocated_to=user, reference_type="Note"))), 10 | |||
) | |||
def test_based_on_field(self): | |||
self.assignment_rule.rule = 'Based on Field' | |||
self.assignment_rule.field = 'owner' | |||
self.assignment_rule.rule = "Based on Field" | |||
self.assignment_rule.field = "owner" | |||
self.assignment_rule.save() | |||
frappe.set_user('test1@example.com') | |||
frappe.set_user("test1@example.com") | |||
note = make_note(dict(public=1)) | |||
# check if auto assigned to doc owner, test1@example.com | |||
self.assertEqual(frappe.db.get_value('ToDo', dict( | |||
reference_type = 'Note', | |||
reference_name = note.name, | |||
status = 'Open' | |||
), 'owner'), 'test1@example.com') | |||
frappe.set_user('test2@example.com') | |||
self.assertEqual( | |||
frappe.db.get_value( | |||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "owner" | |||
), | |||
"test1@example.com", | |||
) | |||
frappe.set_user("test2@example.com") | |||
note = make_note(dict(public=1)) | |||
# check if auto assigned to doc owner, test2@example.com | |||
self.assertEqual(frappe.db.get_value('ToDo', dict( | |||
reference_type = 'Note', | |||
reference_name = note.name, | |||
status = 'Open' | |||
), 'owner'), 'test2@example.com') | |||
self.assertEqual( | |||
frappe.db.get_value( | |||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "owner" | |||
), | |||
"test2@example.com", | |||
) | |||
frappe.set_user('Administrator') | |||
frappe.set_user("Administrator") | |||
def test_assign_condition(self): | |||
# check condition | |||
note = make_note(dict(public=0)) | |||
self.assertEqual(frappe.db.get_value('ToDo', dict( | |||
reference_type = 'Note', | |||
reference_name = note.name, | |||
status = 'Open' | |||
), 'allocated_to'), None) | |||
self.assertEqual( | |||
frappe.db.get_value( | |||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" | |||
), | |||
None, | |||
) | |||
def test_clear_assignment(self): | |||
note = make_note(dict(public=1)) | |||
# check if auto assigned to first user | |||
todo = frappe.get_list('ToDo', dict( | |||
reference_type = 'Note', | |||
reference_name = note.name, | |||
status = 'Open' | |||
), limit=1)[0] | |||
todo = frappe.get_list( | |||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), limit=1 | |||
)[0] | |||
todo = frappe.get_doc('ToDo', todo['name']) | |||
self.assertEqual(todo.allocated_to, 'test@example.com') | |||
todo = frappe.get_doc("ToDo", todo["name"]) | |||
self.assertEqual(todo.allocated_to, "test@example.com") | |||
# test auto unassign | |||
note.public = 0 | |||
@@ -151,99 +162,101 @@ class TestAutoAssign(unittest.TestCase): | |||
todo.load_from_db() | |||
# check if todo is cancelled | |||
self.assertEqual(todo.status, 'Cancelled') | |||
self.assertEqual(todo.status, "Cancelled") | |||
def test_close_assignment(self): | |||
note = make_note(dict(public=1, content="valid")) | |||
# check if auto assigned | |||
todo = frappe.get_list('ToDo', dict( | |||
reference_type = 'Note', | |||
reference_name = note.name, | |||
status = 'Open' | |||
), limit=1)[0] | |||
todo = frappe.get_list( | |||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), limit=1 | |||
)[0] | |||
todo = frappe.get_doc('ToDo', todo['name']) | |||
self.assertEqual(todo.allocated_to, 'test@example.com') | |||
todo = frappe.get_doc("ToDo", todo["name"]) | |||
self.assertEqual(todo.allocated_to, "test@example.com") | |||
note.content="Closed" | |||
note.content = "Closed" | |||
note.save() | |||
todo.load_from_db() | |||
# check if todo is closed | |||
self.assertEqual(todo.status, 'Closed') | |||
self.assertEqual(todo.status, "Closed") | |||
# check if closed todo retained assignment | |||
self.assertEqual(todo.allocated_to, 'test@example.com') | |||
self.assertEqual(todo.allocated_to, "test@example.com") | |||
def check_multiple_rules(self): | |||
note = make_note(dict(public=1, notify_on_login=1)) | |||
# check if auto assigned to test3 (2nd rule is applied, as it has higher priority) | |||
self.assertEqual(frappe.db.get_value('ToDo', dict( | |||
reference_type = 'Note', | |||
reference_name = note.name, | |||
status = 'Open' | |||
), 'allocated_to'), 'test@example.com') | |||
self.assertEqual( | |||
frappe.db.get_value( | |||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" | |||
), | |||
"test@example.com", | |||
) | |||
def check_assignment_rule_scheduling(self): | |||
frappe.db.delete("Assignment Rule") | |||
days_1 = [dict(day = 'Sunday'), dict(day = 'Monday'), dict(day = 'Tuesday')] | |||
days_1 = [dict(day="Sunday"), dict(day="Monday"), dict(day="Tuesday")] | |||
days_2 = [dict(day = 'Wednesday'), dict(day = 'Thursday'), dict(day = 'Friday'), dict(day = 'Saturday')] | |||
days_2 = [dict(day="Wednesday"), dict(day="Thursday"), dict(day="Friday"), dict(day="Saturday")] | |||
get_assignment_rule([days_1, days_2], ['public == 1', 'public == 1']) | |||
get_assignment_rule([days_1, days_2], ["public == 1", "public == 1"]) | |||
frappe.flags.assignment_day = "Monday" | |||
note = make_note(dict(public=1)) | |||
self.assertIn(frappe.db.get_value('ToDo', dict( | |||
reference_type = 'Note', | |||
reference_name = note.name, | |||
status = 'Open' | |||
), 'allocated_to'), ['test@example.com', 'test1@example.com', 'test2@example.com']) | |||
self.assertIn( | |||
frappe.db.get_value( | |||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" | |||
), | |||
["test@example.com", "test1@example.com", "test2@example.com"], | |||
) | |||
frappe.flags.assignment_day = "Friday" | |||
note = make_note(dict(public=1)) | |||
self.assertIn(frappe.db.get_value('ToDo', dict( | |||
reference_type = 'Note', | |||
reference_name = note.name, | |||
status = 'Open' | |||
), 'allocated_to'), ['test3@example.com']) | |||
self.assertIn( | |||
frappe.db.get_value( | |||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" | |||
), | |||
["test3@example.com"], | |||
) | |||
def test_assignment_rule_condition(self): | |||
frappe.db.delete("Assignment Rule") | |||
# Add expiry_date custom field | |||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field | |||
df = dict(fieldname='expiry_date', label='Expiry Date', fieldtype='Date') | |||
create_custom_field('Note', df) | |||
assignment_rule = frappe.get_doc(dict( | |||
name = 'Assignment with Due Date', | |||
doctype = 'Assignment Rule', | |||
document_type = 'Note', | |||
assign_condition = 'public == 0', | |||
due_date_based_on = 'expiry_date', | |||
assignment_days = self.days, | |||
users = [ | |||
dict(user = 'test@example.com'), | |||
] | |||
)).insert() | |||
df = dict(fieldname="expiry_date", label="Expiry Date", fieldtype="Date") | |||
create_custom_field("Note", df) | |||
assignment_rule = frappe.get_doc( | |||
dict( | |||
name="Assignment with Due Date", | |||
doctype="Assignment Rule", | |||
document_type="Note", | |||
assign_condition="public == 0", | |||
due_date_based_on="expiry_date", | |||
assignment_days=self.days, | |||
users=[ | |||
dict(user="test@example.com"), | |||
], | |||
) | |||
).insert() | |||
expiry_date = frappe.utils.add_days(frappe.utils.nowdate(), 2) | |||
note1 = make_note({'expiry_date': expiry_date}) | |||
note2 = make_note({'expiry_date': expiry_date}) | |||
note1 = make_note({"expiry_date": expiry_date}) | |||
note2 = make_note({"expiry_date": expiry_date}) | |||
note1_todo = frappe.get_all('ToDo', filters=dict( | |||
reference_type = 'Note', | |||
reference_name = note1.name, | |||
status = 'Open' | |||
))[0] | |||
note1_todo = frappe.get_all( | |||
"ToDo", filters=dict(reference_type="Note", reference_name=note1.name, status="Open") | |||
)[0] | |||
note1_todo_doc = frappe.get_doc('ToDo', note1_todo.name) | |||
note1_todo_doc = frappe.get_doc("ToDo", note1_todo.name) | |||
self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), expiry_date) | |||
# due date should be updated if the reference doc's date is updated. | |||
@@ -253,66 +266,67 @@ class TestAutoAssign(unittest.TestCase): | |||
self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), note1.expiry_date) | |||
# saving one note's expiry should not update other note todo's due date | |||
note2_todo = frappe.get_all('ToDo', filters=dict( | |||
reference_type = 'Note', | |||
reference_name = note2.name, | |||
status = 'Open' | |||
), fields=['name', 'date'])[0] | |||
note2_todo = frappe.get_all( | |||
"ToDo", | |||
filters=dict(reference_type="Note", reference_name=note2.name, status="Open"), | |||
fields=["name", "date"], | |||
)[0] | |||
self.assertNotEqual(frappe.utils.get_date_str(note2_todo.date), note1.expiry_date) | |||
self.assertEqual(frappe.utils.get_date_str(note2_todo.date), expiry_date) | |||
assignment_rule.delete() | |||
def clear_assignments(): | |||
frappe.db.delete("ToDo", {"reference_type": "Note"}) | |||
def get_assignment_rule(days, assign=None): | |||
frappe.delete_doc_if_exists('Assignment Rule', 'For Note 1') | |||
frappe.delete_doc_if_exists("Assignment Rule", "For Note 1") | |||
if not assign: | |||
assign = ['public == 1', 'notify_on_login == 1'] | |||
assignment_rule = frappe.get_doc(dict( | |||
name = 'For Note 1', | |||
doctype = 'Assignment Rule', | |||
priority = 0, | |||
document_type = 'Note', | |||
assign_condition = assign[0], | |||
unassign_condition = 'public == 0 or notify_on_login == 1', | |||
close_condition = '"Closed" in content', | |||
rule = 'Round Robin', | |||
assignment_days = days[0], | |||
users = [ | |||
dict(user = 'test@example.com'), | |||
dict(user = 'test1@example.com'), | |||
dict(user = 'test2@example.com'), | |||
] | |||
)).insert() | |||
frappe.delete_doc_if_exists('Assignment Rule', 'For Note 2') | |||
assign = ["public == 1", "notify_on_login == 1"] | |||
assignment_rule = frappe.get_doc( | |||
dict( | |||
name="For Note 1", | |||
doctype="Assignment Rule", | |||
priority=0, | |||
document_type="Note", | |||
assign_condition=assign[0], | |||
unassign_condition="public == 0 or notify_on_login == 1", | |||
close_condition='"Closed" in content', | |||
rule="Round Robin", | |||
assignment_days=days[0], | |||
users=[ | |||
dict(user="test@example.com"), | |||
dict(user="test1@example.com"), | |||
dict(user="test2@example.com"), | |||
], | |||
) | |||
).insert() | |||
frappe.delete_doc_if_exists("Assignment Rule", "For Note 2") | |||
# 2nd rule | |||
frappe.get_doc(dict( | |||
name = 'For Note 2', | |||
doctype = 'Assignment Rule', | |||
priority = 1, | |||
document_type = 'Note', | |||
assign_condition = assign[1], | |||
unassign_condition = 'notify_on_login == 0', | |||
rule = 'Round Robin', | |||
assignment_days = days[1], | |||
users = [ | |||
dict(user = 'test3@example.com') | |||
] | |||
)).insert() | |||
frappe.get_doc( | |||
dict( | |||
name="For Note 2", | |||
doctype="Assignment Rule", | |||
priority=1, | |||
document_type="Note", | |||
assign_condition=assign[1], | |||
unassign_condition="notify_on_login == 0", | |||
rule="Round Robin", | |||
assignment_days=days[1], | |||
users=[dict(user="test3@example.com")], | |||
) | |||
).insert() | |||
return assignment_rule | |||
def make_note(values=None): | |||
note = frappe.get_doc(dict( | |||
doctype = 'Note', | |||
title = random_string(10), | |||
content = random_string(20) | |||
)) | |||
note = frappe.get_doc(dict(doctype="Note", title=random_string(10), content=random_string(20))) | |||
if values: | |||
note.update(values) | |||
@@ -5,5 +5,6 @@ | |||
# import frappe | |||
from frappe.model.document import Document | |||
class AssignmentRuleDay(Document): | |||
pass |
@@ -5,5 +5,6 @@ | |||
# import frappe | |||
from frappe.model.document import Document | |||
class AssignmentRuleUser(Document): | |||
pass |
@@ -2,23 +2,45 @@ | |||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors | |||
# License: MIT. See LICENSE | |||
from datetime import timedelta | |||
from dateutil.relativedelta import relativedelta | |||
import frappe | |||
from frappe import _ | |||
from datetime import timedelta | |||
from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeated | |||
from frappe.contacts.doctype.contact.contact import ( | |||
get_contacts_linked_from, | |||
get_contacts_linking_to, | |||
) | |||
from frappe.core.doctype.communication.email import make | |||
from frappe.desk.form import assign_to | |||
from frappe.utils.jinja import validate_template | |||
from dateutil.relativedelta import relativedelta | |||
from frappe.utils.user import get_system_managers | |||
from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_day, get_first_day, month_diff | |||
from frappe.model.document import Document | |||
from frappe.core.doctype.communication.email import make | |||
from frappe.utils import ( | |||
add_days, | |||
cstr, | |||
get_first_day, | |||
get_last_day, | |||
getdate, | |||
month_diff, | |||
split_emails, | |||
today, | |||
) | |||
from frappe.utils.background_jobs import get_jobs | |||
from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeated | |||
from frappe.contacts.doctype.contact.contact import get_contacts_linked_from | |||
from frappe.contacts.doctype.contact.contact import get_contacts_linking_to | |||
from frappe.utils.jinja import validate_template | |||
from frappe.utils.user import get_system_managers | |||
month_map = {"Monthly": 1, "Quarterly": 3, "Half-yearly": 6, "Yearly": 12} | |||
week_map = { | |||
"Monday": 0, | |||
"Tuesday": 1, | |||
"Wednesday": 2, | |||
"Thursday": 3, | |||
"Friday": 4, | |||
"Saturday": 5, | |||
"Sunday": 6, | |||
} | |||
month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12} | |||
week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6} | |||
class AutoRepeat(Document): | |||
def validate(self): | |||
@@ -46,7 +68,7 @@ class AutoRepeat(Document): | |||
frappe.get_doc(self.reference_doctype, self.reference_document).notify_update() | |||
def on_trash(self): | |||
frappe.db.set_value(self.reference_doctype, self.reference_document, 'auto_repeat', '') | |||
frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", "") | |||
frappe.get_doc(self.reference_doctype, self.reference_document).notify_update() | |||
def set_dates(self): | |||
@@ -56,29 +78,36 @@ class AutoRepeat(Document): | |||
self.next_schedule_date = self.get_next_schedule_date(schedule_date=self.start_date) | |||
def unlink_if_applicable(self): | |||
if self.status == 'Completed' or self.disabled: | |||
frappe.db.set_value(self.reference_doctype, self.reference_document, 'auto_repeat', '') | |||
if self.status == "Completed" or self.disabled: | |||
frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", "") | |||
def validate_reference_doctype(self): | |||
if frappe.flags.in_test or frappe.flags.in_patch: | |||
return | |||
if not frappe.get_meta(self.reference_doctype).allow_auto_repeat: | |||
frappe.throw(_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(self.reference_doctype)) | |||
frappe.throw( | |||
_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format( | |||
self.reference_doctype | |||
) | |||
) | |||
def validate_submit_on_creation(self): | |||
if self.submit_on_creation and not frappe.get_meta(self.reference_doctype).is_submittable: | |||
frappe.throw(_('Cannot enable {0} for a non-submittable doctype').format( | |||
frappe.bold('Submit on Creation'))) | |||
frappe.throw( | |||
_("Cannot enable {0} for a non-submittable doctype").format(frappe.bold("Submit on Creation")) | |||
) | |||
def validate_dates(self): | |||
if frappe.flags.in_patch: | |||
return | |||
if self.end_date: | |||
self.validate_from_to_dates('start_date', 'end_date') | |||
self.validate_from_to_dates("start_date", "end_date") | |||
if self.end_date == self.start_date: | |||
frappe.throw(_('{0} should not be same as {1}').format(frappe.bold('End Date'), frappe.bold('Start Date'))) | |||
frappe.throw( | |||
_("{0} should not be same as {1}").format(frappe.bold("End Date"), frappe.bold("Start Date")) | |||
) | |||
def validate_email_id(self): | |||
if self.notify_by_email: | |||
@@ -100,17 +129,17 @@ class AutoRepeat(Document): | |||
frappe.throw( | |||
_("Auto Repeat Day{0} {1} has been repeated.").format( | |||
plural, | |||
frappe.bold(", ".join(repeated_days)) | |||
plural, frappe.bold(", ".join(repeated_days)) | |||
) | |||
) | |||
def update_auto_repeat_id(self): | |||
#check if document is already on auto repeat | |||
# check if document is already on auto repeat | |||
auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat") | |||
if auto_repeat and auto_repeat != self.name and not frappe.flags.in_patch: | |||
frappe.throw(_("The {0} is already on auto repeat {1}").format(self.reference_document, auto_repeat)) | |||
frappe.throw( | |||
_("The {0} is already on auto repeat {1}").format(self.reference_document, auto_repeat) | |||
) | |||
else: | |||
frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", self.name) | |||
@@ -136,18 +165,18 @@ class AutoRepeat(Document): | |||
row = { | |||
"reference_document": self.reference_document, | |||
"frequency": self.frequency, | |||
"next_scheduled_date": next_date | |||
"next_scheduled_date": next_date, | |||
} | |||
schedule_details.append(row) | |||
if self.end_date: | |||
next_date = self.get_next_schedule_date(schedule_date=start_date, for_full_schedule=True) | |||
while (getdate(next_date) < getdate(end_date)): | |||
while getdate(next_date) < getdate(end_date): | |||
row = { | |||
"reference_document" : self.reference_document, | |||
"frequency" : self.frequency, | |||
"next_scheduled_date" : next_date | |||
"reference_document": self.reference_document, | |||
"frequency": self.frequency, | |||
"next_scheduled_date": next_date, | |||
} | |||
schedule_details.append(row) | |||
next_date = self.get_next_schedule_date(schedule_date=next_date, for_full_schedule=True) | |||
@@ -169,9 +198,9 @@ class AutoRepeat(Document): | |||
def make_new_document(self): | |||
reference_doc = frappe.get_doc(self.reference_doctype, self.reference_document) | |||
new_doc = frappe.copy_doc(reference_doc, ignore_no_copy = False) | |||
new_doc = frappe.copy_doc(reference_doc, ignore_no_copy=False) | |||
self.update_doc(new_doc, reference_doc) | |||
new_doc.insert(ignore_permissions = True) | |||
new_doc.insert(ignore_permissions=True) | |||
if self.submit_on_creation: | |||
new_doc.submit() | |||
@@ -180,61 +209,72 @@ class AutoRepeat(Document): | |||
def update_doc(self, new_doc, reference_doc): | |||
new_doc.docstatus = 0 | |||
if new_doc.meta.get_field('set_posting_time'): | |||
new_doc.set('set_posting_time', 1) | |||
if new_doc.meta.get_field('auto_repeat'): | |||
new_doc.set('auto_repeat', self.name) | |||
for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'user_remark', 'remarks', 'owner']: | |||
if new_doc.meta.get_field("set_posting_time"): | |||
new_doc.set("set_posting_time", 1) | |||
if new_doc.meta.get_field("auto_repeat"): | |||
new_doc.set("auto_repeat", self.name) | |||
for fieldname in [ | |||
"naming_series", | |||
"ignore_pricing_rule", | |||
"posting_time", | |||
"select_print_heading", | |||
"user_remark", | |||
"remarks", | |||
"owner", | |||
]: | |||
if new_doc.meta.get_field(fieldname): | |||
new_doc.set(fieldname, reference_doc.get(fieldname)) | |||
for data in new_doc.meta.fields: | |||
if data.fieldtype == 'Date' and data.reqd: | |||
if data.fieldtype == "Date" and data.reqd: | |||
new_doc.set(data.fieldname, self.next_schedule_date) | |||
self.set_auto_repeat_period(new_doc) | |||
auto_repeat_doc = frappe.get_doc('Auto Repeat', self.name) | |||
auto_repeat_doc = frappe.get_doc("Auto Repeat", self.name) | |||
#for any action that needs to take place after the recurring document creation | |||
#on recurring method of that doctype is triggered | |||
new_doc.run_method('on_recurring', reference_doc = reference_doc, auto_repeat_doc = auto_repeat_doc) | |||
# for any action that needs to take place after the recurring document creation | |||
# on recurring method of that doctype is triggered | |||
new_doc.run_method("on_recurring", reference_doc=reference_doc, auto_repeat_doc=auto_repeat_doc) | |||
def set_auto_repeat_period(self, new_doc): | |||
mcount = month_map.get(self.frequency) | |||
if mcount and new_doc.meta.get_field('from_date') and new_doc.meta.get_field('to_date'): | |||
last_ref_doc = frappe.db.get_all(doctype = self.reference_doctype, | |||
fields = ['name', 'from_date', 'to_date'], | |||
filters = [ | |||
['auto_repeat', '=', self.name], | |||
['docstatus', '<', 2], | |||
if mcount and new_doc.meta.get_field("from_date") and new_doc.meta.get_field("to_date"): | |||
last_ref_doc = frappe.db.get_all( | |||
doctype=self.reference_doctype, | |||
fields=["name", "from_date", "to_date"], | |||
filters=[ | |||
["auto_repeat", "=", self.name], | |||
["docstatus", "<", 2], | |||
], | |||
order_by = 'creation desc', | |||
limit = 1) | |||
order_by="creation desc", | |||
limit=1, | |||
) | |||
if not last_ref_doc: | |||
return | |||
from_date = get_next_date(last_ref_doc[0].from_date, mcount) | |||
if (cstr(get_first_day(last_ref_doc[0].from_date)) == cstr(last_ref_doc[0].from_date)) and \ | |||
(cstr(get_last_day(last_ref_doc[0].to_date)) == cstr(last_ref_doc[0].to_date)): | |||
if (cstr(get_first_day(last_ref_doc[0].from_date)) == cstr(last_ref_doc[0].from_date)) and ( | |||
cstr(get_last_day(last_ref_doc[0].to_date)) == cstr(last_ref_doc[0].to_date) | |||
): | |||
to_date = get_last_day(get_next_date(last_ref_doc[0].to_date, mcount)) | |||
else: | |||
to_date = get_next_date(last_ref_doc[0].to_date, mcount) | |||
new_doc.set('from_date', from_date) | |||
new_doc.set('to_date', to_date) | |||
new_doc.set("from_date", from_date) | |||
new_doc.set("to_date", to_date) | |||
def get_next_schedule_date(self, schedule_date, for_full_schedule=False): | |||
""" | |||
Returns the next schedule date for auto repeat after a recurring document has been created. | |||
Adds required offset to the schedule_date param and returns the next schedule date. | |||
Returns the next schedule date for auto repeat after a recurring document has been created. | |||
Adds required offset to the schedule_date param and returns the next schedule date. | |||
:param schedule_date: The date when the last recurring document was created. | |||
:param for_full_schedule: If True, returns the immediate next schedule date, else the full schedule. | |||
:param schedule_date: The date when the last recurring document was created. | |||
:param for_full_schedule: If True, returns the immediate next schedule date, else the full schedule. | |||
""" | |||
if month_map.get(self.frequency): | |||
month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1 | |||
@@ -295,60 +335,75 @@ class AutoRepeat(Document): | |||
return 7 | |||
def get_auto_repeat_days(self): | |||
return [d.day for d in self.get('repeat_on_days', [])] | |||
return [d.day for d in self.get("repeat_on_days", [])] | |||
def send_notification(self, new_doc): | |||
"""Notify concerned people about recurring document generation""" | |||
subject = self.subject or '' | |||
message = self.message or '' | |||
subject = self.subject or "" | |||
message = self.message or "" | |||
if not self.subject: | |||
subject = _("New {0}: {1}").format(new_doc.doctype, new_doc.name) | |||
elif "{" in self.subject: | |||
subject = frappe.render_template(self.subject, {'doc': new_doc}) | |||
subject = frappe.render_template(self.subject, {"doc": new_doc}) | |||
print_format = self.print_format or 'Standard' | |||
print_format = self.print_format or "Standard" | |||
error_string = None | |||
try: | |||
attachments = [frappe.attach_print(new_doc.doctype, new_doc.name, | |||
file_name=new_doc.name, print_format=print_format)] | |||
attachments = [ | |||
frappe.attach_print( | |||
new_doc.doctype, new_doc.name, file_name=new_doc.name, print_format=print_format | |||
) | |||
] | |||
except frappe.PermissionError: | |||
error_string = _("A recurring {0} {1} has been created for you via Auto Repeat {2}.").format(new_doc.doctype, new_doc.name, self.name) | |||
error_string = _("A recurring {0} {1} has been created for you via Auto Repeat {2}.").format( | |||
new_doc.doctype, new_doc.name, self.name | |||
) | |||
error_string += "<br><br>" | |||
error_string += _("{0}: Failed to attach new recurring document. To enable attaching document in the auto repeat notification email, enable {1} in Print Settings").format( | |||
frappe.bold(_('Note')), | |||
frappe.bold(_('Allow Print for Draft')) | |||
) | |||
attachments = '[]' | |||
error_string += _( | |||
"{0}: Failed to attach new recurring document. To enable attaching document in the auto repeat notification email, enable {1} in Print Settings" | |||
).format(frappe.bold(_("Note")), frappe.bold(_("Allow Print for Draft"))) | |||
attachments = "[]" | |||
if error_string: | |||
message = error_string | |||
elif not self.message: | |||
message = _("Please find attached {0}: {1}").format(new_doc.doctype, new_doc.name) | |||
elif "{" in self.message: | |||
message = frappe.render_template(self.message, {'doc': new_doc}) | |||
message = frappe.render_template(self.message, {"doc": new_doc}) | |||
recipients = self.recipients.split('\n') | |||
recipients = self.recipients.split("\n") | |||
make(doctype=new_doc.doctype, name=new_doc.name, recipients=recipients, | |||
subject=subject, content=message, attachments=attachments, send_email=1) | |||
make( | |||
doctype=new_doc.doctype, | |||
name=new_doc.name, | |||
recipients=recipients, | |||
subject=subject, | |||
content=message, | |||
attachments=attachments, | |||
send_email=1, | |||
) | |||
@frappe.whitelist() | |||
def fetch_linked_contacts(self): | |||
if self.reference_doctype and self.reference_document: | |||
res = get_contacts_linking_to(self.reference_doctype, self.reference_document, fields=['email_id']) | |||
res += get_contacts_linked_from(self.reference_doctype, self.reference_document, fields=['email_id']) | |||
res = get_contacts_linking_to( | |||
self.reference_doctype, self.reference_document, fields=["email_id"] | |||
) | |||
res += get_contacts_linked_from( | |||
self.reference_doctype, self.reference_document, fields=["email_id"] | |||
) | |||
email_ids = {d.email_id for d in res} | |||
if not email_ids: | |||
frappe.msgprint(_('No contacts linked to document'), alert=True) | |||
frappe.msgprint(_("No contacts linked to document"), alert=True) | |||
else: | |||
self.recipients = ', '.join(email_ids) | |||
self.recipients = ", ".join(email_ids) | |||
def disable_auto_repeat(self): | |||
frappe.db.set_value('Auto Repeat', self.name, 'disabled', 1) | |||
frappe.db.set_value("Auto Repeat", self.name, "disabled", 1) | |||
def notify_error_to_user(self, error_log): | |||
recipients = list(get_system_managers(only_name=True)) | |||
@@ -356,20 +411,17 @@ class AutoRepeat(Document): | |||
subject = _("Auto Repeat Document Creation Failed") | |||
form_link = frappe.utils.get_link_to_form(self.reference_doctype, self.reference_document) | |||
auto_repeat_failed_for = _('Auto Repeat failed for {0}').format(form_link) | |||
auto_repeat_failed_for = _("Auto Repeat failed for {0}").format(form_link) | |||
error_log_link = frappe.utils.get_link_to_form('Error Log', error_log.name) | |||
error_log_message = _('Check the Error Log for more information: {0}').format(error_log_link) | |||
error_log_link = frappe.utils.get_link_to_form("Error Log", error_log.name) | |||
error_log_message = _("Check the Error Log for more information: {0}").format(error_log_link) | |||
frappe.sendmail( | |||
recipients=recipients, | |||
subject=subject, | |||
template="auto_repeat_fail", | |||
args={ | |||
'auto_repeat_failed_for': auto_repeat_failed_for, | |||
'error_log_message': error_log_message | |||
}, | |||
header=[subject, 'red'] | |||
args={"auto_repeat_failed_for": auto_repeat_failed_for, "error_log_message": error_log_message}, | |||
header=[subject, "red"], | |||
) | |||
@@ -382,18 +434,18 @@ def get_next_date(dt, mcount, day=None): | |||
def get_next_weekday(current_schedule_day, weekdays): | |||
days = list(week_map.keys()) | |||
if current_schedule_day > 0: | |||
days = days[(current_schedule_day + 1):] + days[:current_schedule_day] | |||
days = days[(current_schedule_day + 1) :] + days[:current_schedule_day] | |||
else: | |||
days = days[(current_schedule_day + 1):] | |||
days = days[(current_schedule_day + 1) :] | |||
for entry in days: | |||
if entry in weekdays: | |||
return entry | |||
#called through hooks | |||
# called through hooks | |||
def make_auto_repeat_entry(): | |||
enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries' | |||
enqueued_method = "frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries" | |||
jobs = get_jobs() | |||
if not jobs or enqueued_method not in jobs[frappe.local.site]: | |||
@@ -404,7 +456,7 @@ def make_auto_repeat_entry(): | |||
def create_repeated_entries(data): | |||
for d in data: | |||
doc = frappe.get_doc('Auto Repeat', d.name) | |||
doc = frappe.get_doc("Auto Repeat", d.name) | |||
current_date = getdate(today()) | |||
schedule_date = getdate(doc.next_schedule_date) | |||
@@ -413,33 +465,32 @@ def create_repeated_entries(data): | |||
doc.create_documents() | |||
schedule_date = doc.get_next_schedule_date(schedule_date=schedule_date) | |||
if schedule_date and not doc.disabled: | |||
frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date) | |||
frappe.db.set_value("Auto Repeat", doc.name, "next_schedule_date", schedule_date) | |||
def get_auto_repeat_entries(date=None): | |||
if not date: | |||
date = getdate(today()) | |||
return frappe.db.get_all('Auto Repeat', filters=[ | |||
['next_schedule_date', '<=', date], | |||
['status', '=', 'Active'] | |||
]) | |||
return frappe.db.get_all( | |||
"Auto Repeat", filters=[["next_schedule_date", "<=", date], ["status", "=", "Active"]] | |||
) | |||
#called through hooks | |||
# called through hooks | |||
def set_auto_repeat_as_completed(): | |||
auto_repeat = frappe.get_all("Auto Repeat", filters = {'status': ['!=', 'Disabled']}) | |||
auto_repeat = frappe.get_all("Auto Repeat", filters={"status": ["!=", "Disabled"]}) | |||
for entry in auto_repeat: | |||
doc = frappe.get_doc("Auto Repeat", entry.name) | |||
if doc.is_completed(): | |||
doc.status = 'Completed' | |||
doc.status = "Completed" | |||
doc.save() | |||
@frappe.whitelist() | |||
def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, end_date = None): | |||
def make_auto_repeat(doctype, docname, frequency="Daily", start_date=None, end_date=None): | |||
if not start_date: | |||
start_date = getdate(today()) | |||
doc = frappe.new_doc('Auto Repeat') | |||
doc = frappe.new_doc("Auto Repeat") | |||
doc.reference_doctype = doctype | |||
doc.reference_document = docname | |||
doc.frequency = frequency | |||
@@ -449,24 +500,34 @@ def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, e | |||
doc.save() | |||
return doc | |||
# method for reference_doctype filter | |||
@frappe.whitelist() | |||
@frappe.validate_and_sanitize_search_inputs | |||
def get_auto_repeat_doctypes(doctype, txt, searchfield, start, page_len, filters): | |||
res = frappe.db.get_all('Property Setter', { | |||
'property': 'allow_auto_repeat', | |||
'value': '1', | |||
}, ['doc_type']) | |||
res = frappe.db.get_all( | |||
"Property Setter", | |||
{ | |||
"property": "allow_auto_repeat", | |||
"value": "1", | |||
}, | |||
["doc_type"], | |||
) | |||
docs = [r.doc_type for r in res] | |||
res = frappe.db.get_all('DocType', { | |||
'allow_auto_repeat': 1, | |||
}, ['name']) | |||
res = frappe.db.get_all( | |||
"DocType", | |||
{ | |||
"allow_auto_repeat": 1, | |||
}, | |||
["name"], | |||
) | |||
docs += [r.name for r in res] | |||
docs = set(list(docs)) | |||
return [[d] for d in docs] | |||
@frappe.whitelist() | |||
def update_reference(docname, reference): | |||
result = "" | |||
@@ -478,13 +539,14 @@ def update_reference(docname, reference): | |||
raise e | |||
return result | |||
@frappe.whitelist() | |||
def generate_message_preview(reference_dt, reference_doc, message=None, subject=None): | |||
frappe.has_permission("Auto Repeat", "write", throw=True) | |||
doc = frappe.get_doc(reference_dt, reference_doc) | |||
subject_preview = _("Please add a subject to your email") | |||
msg_preview = frappe.render_template(message, {'doc': doc}) | |||
msg_preview = frappe.render_template(message, {"doc": doc}) | |||
if subject: | |||
subject_preview = frappe.render_template(subject, {'doc': doc}) | |||
subject_preview = frappe.render_template(subject, {"doc": doc}) | |||
return {'message': msg_preview, 'subject': subject_preview} | |||
return {"message": msg_preview, "subject": subject_preview} |
@@ -4,24 +4,40 @@ | |||
import unittest | |||
import frappe | |||
from frappe.automation.doctype.auto_repeat.auto_repeat import ( | |||
create_repeated_entries, | |||
get_auto_repeat_entries, | |||
week_map, | |||
) | |||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field | |||
from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries, week_map | |||
from frappe.utils import today, add_days, getdate, add_months | |||
from frappe.utils import add_days, add_months, getdate, today | |||
def add_custom_fields(): | |||
df = dict( | |||
fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', insert_after='sender', | |||
options='Auto Repeat', hidden=1, print_hide=1, read_only=1) | |||
create_custom_field('ToDo', df) | |||
fieldname="auto_repeat", | |||
label="Auto Repeat", | |||
fieldtype="Link", | |||
insert_after="sender", | |||
options="Auto Repeat", | |||
hidden=1, | |||
print_hide=1, | |||
read_only=1, | |||
) | |||
create_custom_field("ToDo", df) | |||
class TestAutoRepeat(unittest.TestCase): | |||
def setUp(self): | |||
if not frappe.db.sql("SELECT `fieldname` FROM `tabCustom Field` WHERE `fieldname`='auto_repeat' and `dt`=%s", "Todo"): | |||
if not frappe.db.sql( | |||
"SELECT `fieldname` FROM `tabCustom Field` WHERE `fieldname`='auto_repeat' and `dt`=%s", "Todo" | |||
): | |||
add_custom_fields() | |||
def test_daily_auto_repeat(self): | |||
todo = frappe.get_doc( | |||
dict(doctype='ToDo', description='test recurring todo', assigned_by='Administrator')).insert() | |||
dict(doctype="ToDo", description="test recurring todo", assigned_by="Administrator") | |||
).insert() | |||
doc = make_auto_repeat(reference_document=todo.name) | |||
self.assertEqual(doc.next_schedule_date, today()) | |||
@@ -32,19 +48,25 @@ class TestAutoRepeat(unittest.TestCase): | |||
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document) | |||
self.assertEqual(todo.auto_repeat, doc.name) | |||
new_todo = frappe.db.get_value('ToDo', | |||
{'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name') | |||
new_todo = frappe.db.get_value( | |||
"ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name" | |||
) | |||
new_todo = frappe.get_doc('ToDo', new_todo) | |||
new_todo = frappe.get_doc("ToDo", new_todo) | |||
self.assertEqual(todo.get('description'), new_todo.get('description')) | |||
self.assertEqual(todo.get("description"), new_todo.get("description")) | |||
def test_weekly_auto_repeat(self): | |||
todo = frappe.get_doc( | |||
dict(doctype='ToDo', description='test weekly todo', assigned_by='Administrator')).insert() | |||
dict(doctype="ToDo", description="test weekly todo", assigned_by="Administrator") | |||
).insert() | |||
doc = make_auto_repeat(reference_doctype='ToDo', | |||
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7)) | |||
doc = make_auto_repeat( | |||
reference_doctype="ToDo", | |||
frequency="Weekly", | |||
reference_document=todo.name, | |||
start_date=add_days(today(), -7), | |||
) | |||
self.assertEqual(doc.next_schedule_date, today()) | |||
data = get_auto_repeat_entries(getdate(today())) | |||
@@ -54,25 +76,29 @@ class TestAutoRepeat(unittest.TestCase): | |||
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document) | |||
self.assertEqual(todo.auto_repeat, doc.name) | |||
new_todo = frappe.db.get_value('ToDo', | |||
{'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name') | |||
new_todo = frappe.db.get_value( | |||
"ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name" | |||
) | |||
new_todo = frappe.get_doc('ToDo', new_todo) | |||
new_todo = frappe.get_doc("ToDo", new_todo) | |||
self.assertEqual(todo.get('description'), new_todo.get('description')) | |||
self.assertEqual(todo.get("description"), new_todo.get("description")) | |||
def test_weekly_auto_repeat_with_weekdays(self): | |||
todo = frappe.get_doc( | |||
dict(doctype='ToDo', description='test auto repeat with weekdays', assigned_by='Administrator')).insert() | |||
dict(doctype="ToDo", description="test auto repeat with weekdays", assigned_by="Administrator") | |||
).insert() | |||
weekdays = list(week_map.keys()) | |||
current_weekday = getdate().weekday() | |||
days = [ | |||
{'day': weekdays[current_weekday]}, | |||
{'day': weekdays[(current_weekday + 2) % 7]} | |||
] | |||
doc = make_auto_repeat(reference_doctype='ToDo', | |||
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7), days=days) | |||
days = [{"day": weekdays[current_weekday]}, {"day": weekdays[(current_weekday + 2) % 7]}] | |||
doc = make_auto_repeat( | |||
reference_doctype="ToDo", | |||
frequency="Weekly", | |||
reference_document=todo.name, | |||
start_date=add_days(today(), -7), | |||
days=days, | |||
) | |||
self.assertEqual(doc.next_schedule_date, today()) | |||
data = get_auto_repeat_entries(getdate(today())) | |||
@@ -90,136 +116,173 @@ class TestAutoRepeat(unittest.TestCase): | |||
end_date = add_months(start_date, 12) | |||
todo = frappe.get_doc( | |||
dict(doctype='ToDo', description='test recurring todo', assigned_by='Administrator')).insert() | |||
dict(doctype="ToDo", description="test recurring todo", assigned_by="Administrator") | |||
).insert() | |||
self.monthly_auto_repeat('ToDo', todo.name, start_date, end_date) | |||
#test without end_date | |||
todo = frappe.get_doc(dict(doctype='ToDo', description='test recurring todo without end_date', assigned_by='Administrator')).insert() | |||
self.monthly_auto_repeat('ToDo', todo.name, start_date) | |||
self.monthly_auto_repeat("ToDo", todo.name, start_date, end_date) | |||
# test without end_date | |||
todo = frappe.get_doc( | |||
dict( | |||
doctype="ToDo", description="test recurring todo without end_date", assigned_by="Administrator" | |||
) | |||
).insert() | |||
self.monthly_auto_repeat("ToDo", todo.name, start_date) | |||
def monthly_auto_repeat(self, doctype, docname, start_date, end_date = None): | |||
def monthly_auto_repeat(self, doctype, docname, start_date, end_date=None): | |||
def get_months(start, end): | |||
diff = (12 * end.year + end.month) - (12 * start.year + start.month) | |||
return diff + 1 | |||
doc = make_auto_repeat( | |||
reference_doctype=doctype, frequency='Monthly', reference_document=docname, start_date=start_date, | |||
end_date=end_date) | |||
reference_doctype=doctype, | |||
frequency="Monthly", | |||
reference_document=docname, | |||
start_date=start_date, | |||
end_date=end_date, | |||
) | |||
doc.disable_auto_repeat() | |||
data = get_auto_repeat_entries(getdate(today())) | |||
create_repeated_entries(data) | |||
docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name}) | |||
docnames = frappe.get_all(doc.reference_doctype, {"auto_repeat": doc.name}) | |||
self.assertEqual(len(docnames), 1) | |||
doc = frappe.get_doc('Auto Repeat', doc.name) | |||
doc.db_set('disabled', 0) | |||
doc = frappe.get_doc("Auto Repeat", doc.name) | |||
doc.db_set("disabled", 0) | |||
months = get_months(getdate(start_date), getdate(today())) | |||
data = get_auto_repeat_entries(getdate(today())) | |||
create_repeated_entries(data) | |||
docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name}) | |||
docnames = frappe.get_all(doc.reference_doctype, {"auto_repeat": doc.name}) | |||
self.assertEqual(len(docnames), months) | |||
def test_notification_is_attached(self): | |||
todo = frappe.get_doc( | |||
dict(doctype='ToDo', description='Test recurring notification attachment', assigned_by='Administrator')).insert() | |||
dict( | |||
doctype="ToDo", | |||
description="Test recurring notification attachment", | |||
assigned_by="Administrator", | |||
) | |||
).insert() | |||
doc = make_auto_repeat(reference_document=todo.name, notify=1, recipients="test@domain.com", subject="New ToDo", | |||
message="A new ToDo has just been created for you") | |||
doc = make_auto_repeat( | |||
reference_document=todo.name, | |||
notify=1, | |||
recipients="test@domain.com", | |||
subject="New ToDo", | |||
message="A new ToDo has just been created for you", | |||
) | |||
data = get_auto_repeat_entries(getdate(today())) | |||
create_repeated_entries(data) | |||
frappe.db.commit() | |||
new_todo = frappe.db.get_value('ToDo', | |||
{'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name') | |||
new_todo = frappe.db.get_value( | |||
"ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name" | |||
) | |||
linked_comm = frappe.db.exists("Communication", dict(reference_doctype="ToDo", reference_name=new_todo)) | |||
linked_comm = frappe.db.exists( | |||
"Communication", dict(reference_doctype="ToDo", reference_name=new_todo) | |||
) | |||
self.assertTrue(linked_comm) | |||
def test_next_schedule_date(self): | |||
current_date = getdate(today()) | |||
todo = frappe.get_doc( | |||
dict(doctype='ToDo', description='test next schedule date for monthly', assigned_by='Administrator')).insert() | |||
doc = make_auto_repeat(frequency='Monthly', reference_document=todo.name, start_date=add_months(today(), -2)) | |||
dict( | |||
doctype="ToDo", description="test next schedule date for monthly", assigned_by="Administrator" | |||
) | |||
).insert() | |||
doc = make_auto_repeat( | |||
frequency="Monthly", reference_document=todo.name, start_date=add_months(today(), -2) | |||
) | |||
# next_schedule_date is set as on or after current date | |||
# it should not be a previous month's date | |||
self.assertTrue((doc.next_schedule_date >= current_date)) | |||
todo = frappe.get_doc( | |||
dict(doctype='ToDo', description='test next schedule date for daily', assigned_by='Administrator')).insert() | |||
doc = make_auto_repeat(frequency='Daily', reference_document=todo.name, start_date=add_days(today(), -2)) | |||
dict( | |||
doctype="ToDo", description="test next schedule date for daily", assigned_by="Administrator" | |||
) | |||
).insert() | |||
doc = make_auto_repeat( | |||
frequency="Daily", reference_document=todo.name, start_date=add_days(today(), -2) | |||
) | |||
self.assertEqual(getdate(doc.next_schedule_date), current_date) | |||
def test_submit_on_creation(self): | |||
doctype = 'Test Submittable DocType' | |||
doctype = "Test Submittable DocType" | |||
create_submittable_doctype(doctype) | |||
current_date = getdate() | |||
submittable_doc = frappe.get_doc(dict(doctype=doctype, test='test submit on creation')).insert() | |||
submittable_doc = frappe.get_doc(dict(doctype=doctype, test="test submit on creation")).insert() | |||
submittable_doc.submit() | |||
doc = make_auto_repeat(frequency='Daily', reference_doctype=doctype, reference_document=submittable_doc.name, | |||
start_date=add_days(current_date, -1), submit_on_creation=1) | |||
doc = make_auto_repeat( | |||
frequency="Daily", | |||
reference_doctype=doctype, | |||
reference_document=submittable_doc.name, | |||
start_date=add_days(current_date, -1), | |||
submit_on_creation=1, | |||
) | |||
data = get_auto_repeat_entries(current_date) | |||
create_repeated_entries(data) | |||
docnames = frappe.db.get_all(doc.reference_doctype, | |||
filters={'auto_repeat': doc.name}, | |||
fields=['docstatus'], | |||
limit=1 | |||
docnames = frappe.db.get_all( | |||
doc.reference_doctype, filters={"auto_repeat": doc.name}, fields=["docstatus"], limit=1 | |||
) | |||
self.assertEqual(docnames[0].docstatus, 1) | |||
def make_auto_repeat(**args): | |||
args = frappe._dict(args) | |||
doc = frappe.get_doc({ | |||
'doctype': 'Auto Repeat', | |||
'reference_doctype': args.reference_doctype or 'ToDo', | |||
'reference_document': args.reference_document or frappe.db.get_value('ToDo', 'name'), | |||
'submit_on_creation': args.submit_on_creation or 0, | |||
'frequency': args.frequency or 'Daily', | |||
'start_date': args.start_date or add_days(today(), -1), | |||
'end_date': args.end_date or "", | |||
'notify_by_email': args.notify or 0, | |||
'recipients': args.recipients or "", | |||
'subject': args.subject or "", | |||
'message': args.message or "", | |||
'repeat_on_days': args.days or [] | |||
}).insert(ignore_permissions=True) | |||
doc = frappe.get_doc( | |||
{ | |||
"doctype": "Auto Repeat", | |||
"reference_doctype": args.reference_doctype or "ToDo", | |||
"reference_document": args.reference_document or frappe.db.get_value("ToDo", "name"), | |||
"submit_on_creation": args.submit_on_creation or 0, | |||
"frequency": args.frequency or "Daily", | |||
"start_date": args.start_date or add_days(today(), -1), | |||
"end_date": args.end_date or "", | |||
"notify_by_email": args.notify or 0, | |||
"recipients": args.recipients or "", | |||
"subject": args.subject or "", | |||
"message": args.message or "", | |||
"repeat_on_days": args.days or [], | |||
} | |||
).insert(ignore_permissions=True) | |||
return doc | |||
def create_submittable_doctype(doctype, submit_perms=1): | |||
if frappe.db.exists('DocType', doctype): | |||
if frappe.db.exists("DocType", doctype): | |||
return | |||
else: | |||
doc = frappe.get_doc({ | |||
'doctype': 'DocType', | |||
'__newname': doctype, | |||
'module': 'Custom', | |||
'custom': 1, | |||
'is_submittable': 1, | |||
'fields': [{ | |||
'fieldname': 'test', | |||
'label': 'Test', | |||
'fieldtype': 'Data' | |||
}], | |||
'permissions': [{ | |||
'role': 'System Manager', | |||
'read': 1, | |||
'write': 1, | |||
'create': 1, | |||
'delete': 1, | |||
'submit': submit_perms, | |||
'cancel': submit_perms, | |||
'amend': submit_perms | |||
}] | |||
}).insert() | |||
doc = frappe.get_doc( | |||
{ | |||
"doctype": "DocType", | |||
"__newname": doctype, | |||
"module": "Custom", | |||
"custom": 1, | |||
"is_submittable": 1, | |||
"fields": [{"fieldname": "test", "label": "Test", "fieldtype": "Data"}], | |||
"permissions": [ | |||
{ | |||
"role": "System Manager", | |||
"read": 1, | |||
"write": 1, | |||
"create": 1, | |||
"delete": 1, | |||
"submit": submit_perms, | |||
"cancel": submit_perms, | |||
"amend": submit_perms, | |||
} | |||
], | |||
} | |||
).insert() | |||
doc.allow_auto_repeat = 1 | |||
doc.save() | |||
doc.save() |
@@ -5,5 +5,6 @@ | |||
# import frappe | |||
from frappe.model.document import Document | |||
class AutoRepeatDay(Document): | |||
pass |
@@ -5,8 +5,10 @@ | |||
import frappe | |||
from frappe.model.document import Document | |||
class Milestone(Document): | |||
pass | |||
def on_doctype_update(): | |||
frappe.db.add_index("Milestone", ["reference_type", "reference_name"]) |
@@ -1,8 +1,9 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright (c) 2019, Frappe Technologies and Contributors | |||
# License: MIT. See LICENSE | |||
#import frappe | |||
# import frappe | |||
import unittest | |||
class TestMilestone(unittest.TestCase): | |||
pass |
@@ -3,43 +3,50 @@ | |||
# License: MIT. See LICENSE | |||
import frappe | |||
from frappe.model.document import Document | |||
import frappe.cache_manager | |||
from frappe.model import log_types | |||
from frappe.model.document import Document | |||
class MilestoneTracker(Document): | |||
def on_update(self): | |||
frappe.cache_manager.clear_doctype_map('Milestone Tracker', self.document_type) | |||
frappe.cache_manager.clear_doctype_map("Milestone Tracker", self.document_type) | |||
def on_trash(self): | |||
frappe.cache_manager.clear_doctype_map('Milestone Tracker', self.document_type) | |||
frappe.cache_manager.clear_doctype_map("Milestone Tracker", self.document_type) | |||
def apply(self, doc): | |||
before_save = doc.get_doc_before_save() | |||
from_value = before_save and before_save.get(self.track_field) or None | |||
if from_value != doc.get(self.track_field): | |||
frappe.get_doc(dict( | |||
doctype = 'Milestone', | |||
reference_type = doc.doctype, | |||
reference_name = doc.name, | |||
track_field = self.track_field, | |||
from_value = from_value, | |||
value = doc.get(self.track_field), | |||
milestone_tracker = self.name, | |||
)).insert(ignore_permissions=True) | |||
frappe.get_doc( | |||
dict( | |||
doctype="Milestone", | |||
reference_type=doc.doctype, | |||
reference_name=doc.name, | |||
track_field=self.track_field, | |||
from_value=from_value, | |||
value=doc.get(self.track_field), | |||
milestone_tracker=self.name, | |||
) | |||
).insert(ignore_permissions=True) | |||
def evaluate_milestone(doc, event): | |||
if (frappe.flags.in_install | |||
if ( | |||
frappe.flags.in_install | |||
or frappe.flags.in_migrate | |||
or frappe.flags.in_setup_wizard | |||
or doc.doctype in log_types): | |||
or doc.doctype in log_types | |||
): | |||
return | |||
# track milestones related to this doctype | |||
for d in get_milestone_trackers(doc.doctype): | |||
frappe.get_doc('Milestone Tracker', d.get('name')).apply(doc) | |||
frappe.get_doc("Milestone Tracker", d.get("name")).apply(doc) | |||
def get_milestone_trackers(doctype): | |||
return frappe.cache_manager.get_doctype_map('Milestone Tracker', doctype, | |||
dict(document_type = doctype, disabled=0)) | |||
def get_milestone_trackers(doctype): | |||
return frappe.cache_manager.get_doctype_map( | |||
"Milestone Tracker", doctype, dict(document_type=doctype, disabled=0) | |||
) |
@@ -1,48 +1,48 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright (c) 2019, Frappe Technologies and Contributors | |||
# License: MIT. See LICENSE | |||
import unittest | |||
import frappe | |||
import frappe.cache_manager | |||
import unittest | |||
class TestMilestoneTracker(unittest.TestCase): | |||
def test_milestone(self): | |||
frappe.db.delete("Milestone Tracker") | |||
frappe.cache().delete_key('milestone_tracker_map') | |||
frappe.cache().delete_key("milestone_tracker_map") | |||
milestone_tracker = frappe.get_doc(dict( | |||
doctype = 'Milestone Tracker', | |||
document_type = 'ToDo', | |||
track_field = 'status' | |||
)).insert() | |||
milestone_tracker = frappe.get_doc( | |||
dict(doctype="Milestone Tracker", document_type="ToDo", track_field="status") | |||
).insert() | |||
todo = frappe.get_doc(dict( | |||
doctype = 'ToDo', | |||
description = 'test milestone', | |||
status = 'Open' | |||
)).insert() | |||
todo = frappe.get_doc(dict(doctype="ToDo", description="test milestone", status="Open")).insert() | |||
milestones = frappe.get_all('Milestone', | |||
fields = ['track_field', 'value', 'milestone_tracker'], | |||
filters = dict(reference_type = todo.doctype, reference_name=todo.name)) | |||
milestones = frappe.get_all( | |||
"Milestone", | |||
fields=["track_field", "value", "milestone_tracker"], | |||
filters=dict(reference_type=todo.doctype, reference_name=todo.name), | |||
) | |||
self.assertEqual(len(milestones), 1) | |||
self.assertEqual(milestones[0].track_field, 'status') | |||
self.assertEqual(milestones[0].value, 'Open') | |||
self.assertEqual(milestones[0].track_field, "status") | |||
self.assertEqual(milestones[0].value, "Open") | |||
todo.status = 'Closed' | |||
todo.status = "Closed" | |||
todo.save() | |||
milestones = frappe.get_all('Milestone', | |||
fields = ['track_field', 'value', 'milestone_tracker'], | |||
filters = dict(reference_type = todo.doctype, reference_name=todo.name), | |||
order_by = 'modified desc') | |||
milestones = frappe.get_all( | |||
"Milestone", | |||
fields=["track_field", "value", "milestone_tracker"], | |||
filters=dict(reference_type=todo.doctype, reference_name=todo.name), | |||
order_by="modified desc", | |||
) | |||
self.assertEqual(len(milestones), 2) | |||
self.assertEqual(milestones[0].track_field, 'status') | |||
self.assertEqual(milestones[0].value, 'Closed') | |||
self.assertEqual(milestones[0].track_field, "status") | |||
self.assertEqual(milestones[0].value, "Closed") | |||
# cleanup | |||
frappe.db.delete("Milestone") | |||
milestone_tracker.delete() | |||
milestone_tracker.delete() |
@@ -7,20 +7,22 @@ bootstrap client session | |||
import frappe | |||
import frappe.defaults | |||
import frappe.desk.desk_page | |||
from frappe.core.doctype.navbar_settings.navbar_settings import get_app_logo, get_navbar_settings | |||
from frappe.desk.doctype.route_history.route_history import frequently_visited_links | |||
from frappe.desk.form.load import get_meta_bundle | |||
from frappe.utils.change_log import get_versions | |||
from frappe.translate import get_lang_dict | |||
from frappe.email.inbox import get_email_accounts | |||
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled | |||
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled | |||
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points | |||
from frappe.model.base_document import get_controller | |||
from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings, get_app_logo | |||
from frappe.utils import get_time_zone, add_user_info | |||
from frappe.query_builder import DocType | |||
from frappe.query_builder.functions import Count | |||
from frappe.query_builder.terms import subqry | |||
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points | |||
from frappe.social.doctype.energy_point_settings.energy_point_settings import ( | |||
is_energy_point_enabled, | |||
) | |||
from frappe.translate import get_lang_dict | |||
from frappe.utils import add_user_info, get_time_zone | |||
from frappe.utils.change_log import get_versions | |||
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled | |||
def get_bootinfo(): | |||
@@ -38,9 +40,9 @@ def get_bootinfo(): | |||
bootinfo.sysdefaults = frappe.defaults.get_defaults() | |||
bootinfo.server_date = frappe.utils.nowdate() | |||
if frappe.session['user'] != 'Guest': | |||
if frappe.session["user"] != "Guest": | |||
bootinfo.user_info = get_user_info() | |||
bootinfo.sid = frappe.session['sid'] | |||
bootinfo.sid = frappe.session["sid"] | |||
bootinfo.modules = {} | |||
bootinfo.module_list = [] | |||
@@ -51,8 +53,10 @@ def get_bootinfo(): | |||
add_layouts(bootinfo) | |||
bootinfo.module_app = frappe.local.module_app | |||
bootinfo.single_types = [d.name for d in frappe.get_all('DocType', {'issingle': 1})] | |||
bootinfo.nested_set_doctypes = [d.parent for d in frappe.get_all('DocField', {'fieldname': 'lft'}, ['parent'])] | |||
bootinfo.single_types = [d.name for d in frappe.get_all("DocType", {"issingle": 1})] | |||
bootinfo.nested_set_doctypes = [ | |||
d.parent for d in frappe.get_all("DocField", {"fieldname": "lft"}, ["parent"]) | |||
] | |||
add_home_page(bootinfo, doclist) | |||
bootinfo.page_info = get_allowed_pages() | |||
load_translations(bootinfo) | |||
@@ -66,8 +70,8 @@ def get_bootinfo(): | |||
set_time_zone(bootinfo) | |||
# ipinfo | |||
if frappe.session.data.get('ipinfo'): | |||
bootinfo.ipinfo = frappe.session['data']['ipinfo'] | |||
if frappe.session.data.get("ipinfo"): | |||
bootinfo.ipinfo = frappe.session["data"]["ipinfo"] | |||
# add docs | |||
bootinfo.docs = doclist | |||
@@ -77,7 +81,7 @@ def get_bootinfo(): | |||
if bootinfo.lang: | |||
bootinfo.lang = str(bootinfo.lang) | |||
bootinfo.versions = {k: v['version'] for k, v in get_versions().items()} | |||
bootinfo.versions = {k: v["version"] for k, v in get_versions().items()} | |||
bootinfo.error_report_email = frappe.conf.error_report_email | |||
bootinfo.calendars = sorted(frappe.get_hooks("calendars")) | |||
@@ -97,37 +101,47 @@ def get_bootinfo(): | |||
return bootinfo | |||
def get_letter_heads(): | |||
letter_heads = {} | |||
for letter_head in frappe.get_all("Letter Head", fields = ["name", "content", "footer"]): | |||
letter_heads.setdefault(letter_head.name, | |||
{'header': letter_head.content, 'footer': letter_head.footer}) | |||
for letter_head in frappe.get_all("Letter Head", fields=["name", "content", "footer"]): | |||
letter_heads.setdefault( | |||
letter_head.name, {"header": letter_head.content, "footer": letter_head.footer} | |||
) | |||
return letter_heads | |||
def load_conf_settings(bootinfo): | |||
from frappe import conf | |||
bootinfo.max_file_size = conf.get('max_file_size') or 10485760 | |||
for key in ('developer_mode', 'socketio_port', 'file_watcher_port'): | |||
if key in conf: bootinfo[key] = conf.get(key) | |||
bootinfo.max_file_size = conf.get("max_file_size") or 10485760 | |||
for key in ("developer_mode", "socketio_port", "file_watcher_port"): | |||
if key in conf: | |||
bootinfo[key] = conf.get(key) | |||
def load_desktop_data(bootinfo): | |||
from frappe.desk.desktop import get_workspace_sidebar_items | |||
bootinfo.allowed_workspaces = get_workspace_sidebar_items().get('pages') | |||
bootinfo.allowed_workspaces = get_workspace_sidebar_items().get("pages") | |||
bootinfo.module_page_map = get_controller("Workspace").get_module_page_map() | |||
bootinfo.dashboards = frappe.get_all("Dashboard") | |||
def get_allowed_pages(cache=False): | |||
return get_user_pages_or_reports('Page', cache=cache) | |||
return get_user_pages_or_reports("Page", cache=cache) | |||
def get_allowed_reports(cache=False): | |||
return get_user_pages_or_reports('Report', cache=cache) | |||
return get_user_pages_or_reports("Report", cache=cache) | |||
def get_user_pages_or_reports(parent, cache=False): | |||
_cache = frappe.cache() | |||
if cache: | |||
has_role = _cache.get_value('has_role:' + parent, user=frappe.session.user) | |||
has_role = _cache.get_value("has_role:" + parent, user=frappe.session.user) | |||
if has_role: | |||
return has_role | |||
@@ -140,8 +154,7 @@ def get_user_pages_or_reports(parent, cache=False): | |||
if parent == "Report": | |||
columns = (report.name.as_("title"), report.ref_doctype, report.report_type) | |||
else: | |||
columns = (page.title.as_("title"), ) | |||
columns = (page.title.as_("title"),) | |||
customRole = DocType("Custom Role") | |||
hasRole = DocType("Has Role") | |||
@@ -149,31 +162,39 @@ def get_user_pages_or_reports(parent, cache=False): | |||
# get pages or reports set on custom role | |||
pages_with_custom_roles = ( | |||
frappe.qb.from_(customRole).from_(hasRole).from_(parentTable) | |||
.select(customRole[parent.lower()].as_("name"), customRole.modified, customRole.ref_doctype, *columns) | |||
frappe.qb.from_(customRole) | |||
.from_(hasRole) | |||
.from_(parentTable) | |||
.select( | |||
customRole[parent.lower()].as_("name"), customRole.modified, customRole.ref_doctype, *columns | |||
) | |||
.where( | |||
(hasRole.parent == customRole.name) | |||
& (parentTable.name == customRole[parent.lower()]) | |||
& (customRole[parent.lower()].isnotnull()) | |||
& (hasRole.role.isin(roles))) | |||
& (hasRole.role.isin(roles)) | |||
) | |||
).run(as_dict=True) | |||
for p in pages_with_custom_roles: | |||
has_role[p.name] = {"modified":p.modified, "title": p.title, "ref_doctype": p.ref_doctype} | |||
has_role[p.name] = {"modified": p.modified, "title": p.title, "ref_doctype": p.ref_doctype} | |||
subq = ( | |||
frappe.qb.from_(customRole).select(customRole[parent.lower()]) | |||
frappe.qb.from_(customRole) | |||
.select(customRole[parent.lower()]) | |||
.where(customRole[parent.lower()].isnotnull()) | |||
) | |||
pages_with_standard_roles = ( | |||
frappe.qb.from_(hasRole).from_(parentTable) | |||
frappe.qb.from_(hasRole) | |||
.from_(parentTable) | |||
.select(parentTable.name.as_("name"), parentTable.modified, *columns) | |||
.where( | |||
(hasRole.role.isin(roles)) | |||
& (hasRole.parent == parentTable.name) | |||
& (parentTable.name.notin(subq)) | |||
).distinct() | |||
) | |||
.distinct() | |||
) | |||
if parent == "Report": | |||
@@ -183,18 +204,20 @@ def get_user_pages_or_reports(parent, cache=False): | |||
for p in pages_with_standard_roles: | |||
if p.name not in has_role: | |||
has_role[p.name] = {"modified":p.modified, "title": p.title} | |||
has_role[p.name] = {"modified": p.modified, "title": p.title} | |||
if parent == "Report": | |||
has_role[p.name].update({'ref_doctype': p.ref_doctype}) | |||
has_role[p.name].update({"ref_doctype": p.ref_doctype}) | |||
no_of_roles = (frappe.qb.from_(hasRole).select(Count("*")) | |||
.where(hasRole.parent == parentTable.name) | |||
no_of_roles = ( | |||
frappe.qb.from_(hasRole).select(Count("*")).where(hasRole.parent == parentTable.name) | |||
) | |||
# pages with no role are allowed | |||
if parent =="Page": | |||
if parent == "Page": | |||
pages_with_no_roles = (frappe.qb.from_(parentTable).select(parentTable.name, parentTable.modified, *columns) | |||
pages_with_no_roles = ( | |||
frappe.qb.from_(parentTable) | |||
.select(parentTable.name, parentTable.modified, *columns) | |||
.where(subqry(no_of_roles) == 0) | |||
).run(as_dict=True) | |||
@@ -203,18 +226,20 @@ def get_user_pages_or_reports(parent, cache=False): | |||
has_role[p.name] = {"modified": p.modified, "title": p.title} | |||
elif parent == "Report": | |||
reports = frappe.get_all("Report", | |||
reports = frappe.get_all( | |||
"Report", | |||
fields=["name", "report_type"], | |||
filters={"name": ("in", has_role.keys())}, | |||
ignore_ifnull=True | |||
ignore_ifnull=True, | |||
) | |||
for report in reports: | |||
has_role[report.name]["report_type"] = report.report_type | |||
# Expire every six hours | |||
_cache.set_value('has_role:' + parent, has_role, frappe.session.user, 21600) | |||
_cache.set_value("has_role:" + parent, has_role, frappe.session.user, 21600) | |||
return has_role | |||
def load_translations(bootinfo): | |||
messages = frappe.get_lang_dict("boot") | |||
@@ -225,27 +250,30 @@ def load_translations(bootinfo): | |||
messages[name] = frappe._(name) | |||
# only untranslated | |||
messages = {k: v for k, v in messages.items() if k!=v} | |||
messages = {k: v for k, v in messages.items() if k != v} | |||
bootinfo["__messages"] = messages | |||
def get_user_info(): | |||
# get info for current user | |||
user_info = frappe._dict() | |||
add_user_info(frappe.session.user, user_info) | |||
if frappe.session.user == 'Administrator' and user_info.Administrator.email: | |||
if frappe.session.user == "Administrator" and user_info.Administrator.email: | |||
user_info[user_info.Administrator.email] = user_info.Administrator | |||
return user_info | |||
def get_user(bootinfo): | |||
"""get user info""" | |||
bootinfo.user = frappe.get_user().load_user() | |||
def add_home_page(bootinfo, docs): | |||
"""load home page""" | |||
if frappe.session.user=="Guest": | |||
if frappe.session.user == "Guest": | |||
return | |||
home_page = frappe.db.get_default("desktop:home_page") | |||
@@ -255,50 +283,65 @@ def add_home_page(bootinfo, docs): | |||
try: | |||
page = frappe.desk.desk_page.get(home_page) | |||
docs.append(page) | |||
bootinfo['home_page'] = page.name | |||
bootinfo["home_page"] = page.name | |||
except (frappe.DoesNotExistError, frappe.PermissionError): | |||
if frappe.message_log: | |||
frappe.message_log.pop() | |||
bootinfo['home_page'] = 'Workspaces' | |||
bootinfo["home_page"] = "Workspaces" | |||
def add_timezone_info(bootinfo): | |||
system = bootinfo.sysdefaults.get("time_zone") | |||
import frappe.utils.momentjs | |||
bootinfo.timezone_info = {"zones":{}, "rules":{}, "links":{}} | |||
bootinfo.timezone_info = {"zones": {}, "rules": {}, "links": {}} | |||
frappe.utils.momentjs.update(system, bootinfo.timezone_info) | |||
def load_print(bootinfo, doclist): | |||
print_settings = frappe.db.get_singles_dict("Print Settings") | |||
print_settings.doctype = ":Print Settings" | |||
doclist.append(print_settings) | |||
load_print_css(bootinfo, print_settings) | |||
def load_print_css(bootinfo, print_settings): | |||
import frappe.www.printview | |||
bootinfo.print_css = frappe.www.printview.get_print_style(print_settings.print_style or "Redesign", for_legacy=True) | |||
bootinfo.print_css = frappe.www.printview.get_print_style( | |||
print_settings.print_style or "Redesign", for_legacy=True | |||
) | |||
def get_unseen_notes(): | |||
note = DocType("Note") | |||
nsb = DocType("Note Seen By").as_("nsb") | |||
return ( | |||
frappe.qb.from_(note).select(note.name, note.title, note.content, note.notify_on_every_login) | |||
frappe.qb.from_(note) | |||
.select(note.name, note.title, note.content, note.notify_on_every_login) | |||
.where( | |||
(note.notify_on_every_login == 1) | |||
& (note.expire_notification_on > frappe.utils.now()) | |||
& (subqry(frappe.qb.from_(nsb).select(nsb.user).where(nsb.parent == note.name)).notin([frappe.session.user]))) | |||
).run(as_dict=1) | |||
& ( | |||
subqry(frappe.qb.from_(nsb).select(nsb.user).where(nsb.parent == note.name)).notin( | |||
[frappe.session.user] | |||
) | |||
) | |||
) | |||
).run(as_dict=1) | |||
def get_success_action(): | |||
return frappe.get_all("Success Action", fields=["*"]) | |||
def get_link_preview_doctypes(): | |||
from frappe.utils import cint | |||
link_preview_doctypes = [d.name for d in frappe.db.get_all('DocType', {'show_preview_popup': 1})] | |||
customizations = frappe.get_all("Property Setter", | |||
fields=['doc_type', 'value'], | |||
filters={'property': 'show_preview_popup'} | |||
link_preview_doctypes = [d.name for d in frappe.db.get_all("DocType", {"show_preview_popup": 1})] | |||
customizations = frappe.get_all( | |||
"Property Setter", fields=["doc_type", "value"], filters={"property": "show_preview_popup"} | |||
) | |||
for custom in customizations: | |||
@@ -309,22 +352,23 @@ def get_link_preview_doctypes(): | |||
return link_preview_doctypes | |||
def get_additional_filters_from_hooks(): | |||
filter_config = frappe._dict() | |||
filter_hooks = frappe.get_hooks('filters_config') | |||
filter_hooks = frappe.get_hooks("filters_config") | |||
for hook in filter_hooks: | |||
filter_config.update(frappe.get_attr(hook)()) | |||
return filter_config | |||
def add_layouts(bootinfo): | |||
# add routes for readable doctypes | |||
bootinfo.doctype_layouts = frappe.get_all('DocType Layout', ['name', 'route', 'document_type']) | |||
bootinfo.doctype_layouts = frappe.get_all("DocType Layout", ["name", "route", "document_type"]) | |||
def get_desk_settings(): | |||
role_list = frappe.get_all('Role', fields=['*'], filters=dict( | |||
name=['in', frappe.get_roles()] | |||
)) | |||
role_list = frappe.get_all("Role", fields=["*"], filters=dict(name=["in", frappe.get_roles()])) | |||
desk_settings = {} | |||
from frappe.core.doctype.role.role import desk_properties | |||
@@ -335,8 +379,10 @@ def get_desk_settings(): | |||
return desk_settings | |||
def get_notification_settings(): | |||
return frappe.get_cached_doc('Notification Settings', frappe.session.user) | |||
return frappe.get_cached_doc("Notification Settings", frappe.session.user) | |||
@frappe.whitelist() | |||
def get_link_title_doctypes(): | |||
@@ -348,8 +394,10 @@ def get_link_title_doctypes(): | |||
) | |||
return [d.name for d in dts + custom_dts if d] | |||
def set_time_zone(bootinfo): | |||
bootinfo.time_zone = { | |||
"system": get_time_zone(), | |||
"user": bootinfo.get("user_info", {}).get(frappe.session.user, {}).get("time_zone", None) or get_time_zone() | |||
"user": bootinfo.get("user_info", {}).get(frappe.session.user, {}).get("time_zone", None) | |||
or get_time_zone(), | |||
} |
@@ -1,8 +1,8 @@ | |||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
import os | |||
import shutil | |||
import re | |||
import shutil | |||
import subprocess | |||
from distutils.spawn import find_executable | |||
from subprocess import getoutput | |||
@@ -25,6 +25,7 @@ sites_path = os.path.abspath(os.getcwd()) | |||
class AssetsNotDownloadedError(Exception): | |||
pass | |||
class AssetsDontExistError(HTTPError): | |||
pass | |||
@@ -43,7 +44,7 @@ def download_file(url, prefix): | |||
def build_missing_files(): | |||
'''Check which files dont exist yet from the assets.json and run build for those files''' | |||
"""Check which files dont exist yet from the assets.json and run build for those files""" | |||
missing_assets = [] | |||
current_asset_files = [] | |||
@@ -60,7 +61,7 @@ def build_missing_files(): | |||
assets_json = frappe.parse_json(assets_json) | |||
for bundle_file, output_file in assets_json.items(): | |||
if not output_file.startswith('/assets/frappe'): | |||
if not output_file.startswith("/assets/frappe"): | |||
continue | |||
if os.path.basename(output_file) not in current_asset_files: | |||
@@ -78,8 +79,7 @@ def build_missing_files(): | |||
def get_assets_link(frappe_head) -> str: | |||
tag = getoutput( | |||
r"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*" | |||
r" refs/tags/,,' -e 's/\^{}//'" | |||
% frappe_head | |||
r" refs/tags/,,' -e 's/\^{}//'" % frappe_head | |||
) | |||
if tag: | |||
@@ -111,6 +111,7 @@ def fetch_assets(url, frappe_head): | |||
def setup_assets(assets_archive): | |||
import tarfile | |||
directories_created = set() | |||
click.secho("\nExtracting assets...\n", fg="yellow") | |||
@@ -221,7 +222,16 @@ def setup(): | |||
assets_path = os.path.join(frappe.local.sites_path, "assets") | |||
def bundle(mode, apps=None, hard_link=False, make_copy=False, restore=False, verbose=False, skip_frappe=False, files=None): | |||
def bundle( | |||
mode, | |||
apps=None, | |||
hard_link=False, | |||
make_copy=False, | |||
restore=False, | |||
verbose=False, | |||
skip_frappe=False, | |||
files=None, | |||
): | |||
"""concat / minify js files""" | |||
setup() | |||
make_asset_dirs(hard_link=hard_link) | |||
@@ -236,7 +246,7 @@ def bundle(mode, apps=None, hard_link=False, make_copy=False, restore=False, ver | |||
command += " --skip_frappe" | |||
if files: | |||
command += " --files {files}".format(files=','.join(files)) | |||
command += " --files {files}".format(files=",".join(files)) | |||
command += " --run-build-command" | |||
@@ -253,9 +263,7 @@ def watch(apps=None): | |||
if apps: | |||
command += " --apps {apps}".format(apps=apps) | |||
live_reload = frappe.utils.cint( | |||
os.environ.get("LIVE_RELOAD", frappe.conf.live_reload) | |||
) | |||
live_reload = frappe.utils.cint(os.environ.get("LIVE_RELOAD", frappe.conf.live_reload)) | |||
if live_reload: | |||
command += " --live-reload" | |||
@@ -266,8 +274,8 @@ def watch(apps=None): | |||
def check_node_executable(): | |||
node_version = Version(subprocess.getoutput('node -v')[1:]) | |||
warn = '⚠️ ' | |||
node_version = Version(subprocess.getoutput("node -v")[1:]) | |||
warn = "⚠️ " | |||
if node_version.major < 14: | |||
click.echo(f"{warn} Please update your node version to 14") | |||
if not find_executable("yarn"): | |||
@@ -276,9 +284,7 @@ def check_node_executable(): | |||
def get_node_env(): | |||
node_env = { | |||
"NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}" | |||
} | |||
node_env = {"NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}"} | |||
return node_env | |||
@@ -345,8 +351,7 @@ def clear_broken_symlinks(): | |||
def unstrip(message: str) -> str: | |||
"""Pads input string on the right side until the last available column in the terminal | |||
""" | |||
"""Pads input string on the right side until the last available column in the terminal""" | |||
_len = len(message) | |||
try: | |||
max_str = os.get_terminal_size().columns | |||
@@ -367,7 +372,9 @@ def make_asset_dirs(hard_link=False): | |||
symlinks = generate_assets_map() | |||
for source, target in symlinks.items(): | |||
start_message = unstrip(f"{'Copying assets from' if hard_link else 'Linking'} {source} to {target}") | |||
start_message = unstrip( | |||
f"{'Copying assets from' if hard_link else 'Linking'} {source} to {target}" | |||
) | |||
fail_message = unstrip(f"Cannot {'copy' if hard_link else 'link'} {source} to {target}") | |||
# Used '\r' instead of '\x1b[1K\r' to print entire lines in smaller terminal sizes | |||
@@ -404,10 +411,11 @@ def scrub_html_template(content): | |||
# strip comments | |||
content = re.sub(r"(<!--.*?-->)", "", content) | |||
return content.replace("'", "\'") | |||
return content.replace("'", "'") | |||
def html_to_js_template(path, content): | |||
"""returns HTML template content as Javascript code, adding it to `frappe.templates`""" | |||
return """frappe.templates["{key}"] = '{content}';\n""".format( | |||
key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content)) | |||
key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content) | |||
) |
@@ -1,33 +1,75 @@ | |||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
import frappe, json | |||
import json | |||
import frappe | |||
from frappe.desk.notifications import clear_notifications, delete_notification_count_for | |||
from frappe.model.document import Document | |||
from frappe.desk.notifications import (delete_notification_count_for, | |||
clear_notifications) | |||
common_default_keys = ["__default", "__global"] | |||
doctype_map_keys = ('energy_point_rule_map', 'assignment_rule_map', | |||
'milestone_tracker_map', 'event_consumer_document_type_map') | |||
bench_cache_keys = ('assets_json',) | |||
global_cache_keys = ("app_hooks", "installed_apps", 'all_apps', | |||
"app_modules", "module_app", "system_settings", | |||
'scheduler_events', 'time_zone', 'webhooks', 'active_domains', | |||
'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version', | |||
'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts', | |||
'sitemap_routes', 'db_tables', 'server_script_autocompletion_items') + doctype_map_keys | |||
user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang", | |||
"defaults", "user_permissions", "home_page", "linked_with", | |||
"desktop_icons", 'portal_menu_items', 'user_perm_can_read', | |||
"has_role:Page", "has_role:Report", "desk_sidebar_items") | |||
doctype_map_keys = ( | |||
"energy_point_rule_map", | |||
"assignment_rule_map", | |||
"milestone_tracker_map", | |||
"event_consumer_document_type_map", | |||
) | |||
bench_cache_keys = ("assets_json",) | |||
global_cache_keys = ( | |||
"app_hooks", | |||
"installed_apps", | |||
"all_apps", | |||
"app_modules", | |||
"module_app", | |||
"system_settings", | |||
"scheduler_events", | |||
"time_zone", | |||
"webhooks", | |||
"active_domains", | |||
"active_modules", | |||
"assignment_rule", | |||
"server_script_map", | |||
"wkhtmltopdf_version", | |||
"domain_restricted_doctypes", | |||
"domain_restricted_pages", | |||
"information_schema:counts", | |||
"sitemap_routes", | |||
"db_tables", | |||
"server_script_autocompletion_items", | |||
) + doctype_map_keys | |||
user_cache_keys = ( | |||
"bootinfo", | |||
"user_recent", | |||
"roles", | |||
"user_doc", | |||
"lang", | |||
"defaults", | |||
"user_permissions", | |||
"home_page", | |||
"linked_with", | |||
"desktop_icons", | |||
"portal_menu_items", | |||
"user_perm_can_read", | |||
"has_role:Page", | |||
"has_role:Report", | |||
"desk_sidebar_items", | |||
) | |||
doctype_cache_keys = ( | |||
"meta", | |||
"form_meta", | |||
"table_columns", | |||
"last_modified", | |||
"linked_doctypes", | |||
"notifications", | |||
"workflow", | |||
"data_import_column_header_map", | |||
) + doctype_map_keys | |||
doctype_cache_keys = ("meta", "form_meta", "table_columns", "last_modified", | |||
"linked_doctypes", 'notifications', 'workflow' , | |||
'data_import_column_header_map') + doctype_map_keys | |||
def clear_user_cache(user=None): | |||
cache = frappe.cache() | |||
@@ -47,11 +89,13 @@ def clear_user_cache(user=None): | |||
clear_defaults_cache() | |||
clear_global_cache() | |||
def clear_domain_cache(user=None): | |||
cache = frappe.cache() | |||
domain_cache_keys = ('domain_restricted_doctypes', 'domain_restricted_pages') | |||
domain_cache_keys = ("domain_restricted_doctypes", "domain_restricted_pages") | |||
cache.delete_value(domain_cache_keys) | |||
def clear_global_cache(): | |||
from frappe.website.utils import clear_website_cache | |||
@@ -61,21 +105,23 @@ def clear_global_cache(): | |||
frappe.cache().delete_value(bench_cache_keys) | |||
frappe.setup_module_map() | |||
def clear_defaults_cache(user=None): | |||
if user: | |||
for p in ([user] + common_default_keys): | |||
for p in [user] + common_default_keys: | |||
frappe.cache().hdel("defaults", p) | |||
elif frappe.flags.in_install!="frappe": | |||
elif frappe.flags.in_install != "frappe": | |||
frappe.cache().delete_key("defaults") | |||
def clear_doctype_cache(doctype=None): | |||
clear_controller_cache(doctype) | |||
cache = frappe.cache() | |||
if getattr(frappe.local, 'meta_cache') and (doctype in frappe.local.meta_cache): | |||
if getattr(frappe.local, "meta_cache") and (doctype in frappe.local.meta_cache): | |||
del frappe.local.meta_cache[doctype] | |||
for key in ('is_table', 'doctype_modules', 'document_cache'): | |||
for key in ("is_table", "doctype_modules", "document_cache"): | |||
cache.delete_value(key) | |||
frappe.local.document_cache = {} | |||
@@ -89,8 +135,9 @@ def clear_doctype_cache(doctype=None): | |||
# clear all parent doctypes | |||
for dt in frappe.db.get_all('DocField', 'parent', | |||
dict(fieldtype=['in', frappe.model.table_fields], options=doctype)): | |||
for dt in frappe.db.get_all( | |||
"DocField", "parent", dict(fieldtype=["in", frappe.model.table_fields], options=doctype) | |||
): | |||
clear_single(dt.parent) | |||
# clear all notifications | |||
@@ -101,6 +148,7 @@ def clear_doctype_cache(doctype=None): | |||
for name in doctype_cache_keys: | |||
cache.delete_value(name) | |||
def clear_controller_cache(doctype=None): | |||
if not doctype: | |||
del frappe.controllers | |||
@@ -110,9 +158,10 @@ def clear_controller_cache(doctype=None): | |||
for site_controllers in frappe.controllers.values(): | |||
site_controllers.pop(doctype, None) | |||
def get_doctype_map(doctype, name, filters=None, order_by=None): | |||
cache = frappe.cache() | |||
cache_key = frappe.scrub(doctype) + '_map' | |||
cache_key = frappe.scrub(doctype) + "_map" | |||
doctype_map = cache.hget(cache_key, name) | |||
if doctype_map is not None: | |||
@@ -121,7 +170,7 @@ def get_doctype_map(doctype, name, filters=None, order_by=None): | |||
else: | |||
# non cached, build cache | |||
try: | |||
items = frappe.get_all(doctype, filters=filters, order_by = order_by) | |||
items = frappe.get_all(doctype, filters=filters, order_by=order_by) | |||
cache.hset(cache_key, name, json.dumps(items)) | |||
except frappe.db.TableMissingError: | |||
# executed from inside patch, ignore | |||
@@ -129,15 +178,19 @@ def get_doctype_map(doctype, name, filters=None, order_by=None): | |||
return items | |||
def clear_doctype_map(doctype, name): | |||
frappe.cache().hdel(frappe.scrub(doctype) + '_map', name) | |||
frappe.cache().hdel(frappe.scrub(doctype) + "_map", name) | |||
def build_table_count_cache(): | |||
if (frappe.flags.in_patch | |||
if ( | |||
frappe.flags.in_patch | |||
or frappe.flags.in_install | |||
or frappe.flags.in_migrate | |||
or frappe.flags.in_import | |||
or frappe.flags.in_setup_wizard): | |||
or frappe.flags.in_setup_wizard | |||
): | |||
return | |||
_cache = frappe.cache() | |||
@@ -145,39 +198,45 @@ def build_table_count_cache(): | |||
table_rows = frappe.qb.Field("table_rows").as_("count") | |||
information_schema = frappe.qb.Schema("information_schema") | |||
data = ( | |||
frappe.qb.from_(information_schema.tables).select(table_name, table_rows) | |||
).run(as_dict=True) | |||
counts = {d.get('name').replace('tab', '', 1): d.get('count', None) for d in data} | |||
data = (frappe.qb.from_(information_schema.tables).select(table_name, table_rows)).run( | |||
as_dict=True | |||
) | |||
counts = {d.get("name").replace("tab", "", 1): d.get("count", None) for d in data} | |||
_cache.set_value("information_schema:counts", counts) | |||
return counts | |||
def build_domain_restriced_doctype_cache(*args, **kwargs): | |||
if (frappe.flags.in_patch | |||
if ( | |||
frappe.flags.in_patch | |||
or frappe.flags.in_install | |||
or frappe.flags.in_migrate | |||
or frappe.flags.in_import | |||
or frappe.flags.in_setup_wizard): | |||
or frappe.flags.in_setup_wizard | |||
): | |||
return | |||
_cache = frappe.cache() | |||
active_domains = frappe.get_active_domains() | |||
doctypes = frappe.get_all("DocType", filters={'restrict_to_domain': ('IN', active_domains)}) | |||
doctypes = frappe.get_all("DocType", filters={"restrict_to_domain": ("IN", active_domains)}) | |||
doctypes = [doc.name for doc in doctypes] | |||
_cache.set_value("domain_restricted_doctypes", doctypes) | |||
return doctypes | |||
def build_domain_restriced_page_cache(*args, **kwargs): | |||
if (frappe.flags.in_patch | |||
if ( | |||
frappe.flags.in_patch | |||
or frappe.flags.in_install | |||
or frappe.flags.in_migrate | |||
or frappe.flags.in_import | |||
or frappe.flags.in_setup_wizard): | |||
or frappe.flags.in_setup_wizard | |||
): | |||
return | |||
_cache = frappe.cache() | |||
active_domains = frappe.get_active_domains() | |||
pages = frappe.get_all("Page", filters={'restrict_to_domain': ('IN', active_domains)}) | |||
pages = frappe.get_all("Page", filters={"restrict_to_domain": ("IN", active_domains)}) | |||
pages = [page.name for page in pages] | |||
_cache.set_value("domain_restricted_pages", pages) | |||
@@ -1,32 +1,44 @@ | |||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
import json | |||
import os | |||
import frappe | |||
from frappe import _ | |||
import frappe.model | |||
import frappe.utils | |||
import json, os | |||
from frappe.utils import get_safe_filters | |||
from frappe import _ | |||
from frappe.desk.reportview import validate_args | |||
from frappe.model.db_query import check_parent_permission | |||
from frappe.utils import get_safe_filters | |||
''' | |||
""" | |||
Handle RESTful requests that are mapped to the `/api/resource` route. | |||
Requests via FrappeClient are also handled here. | |||
''' | |||
""" | |||
@frappe.whitelist() | |||
def get_list(doctype, fields=None, filters=None, order_by=None, | |||
limit_start=None, limit_page_length=20, parent=None, debug=False, as_dict=True, or_filters=None): | |||
'''Returns a list of records by filters, fields, ordering and limit | |||
def get_list( | |||
doctype, | |||
fields=None, | |||
filters=None, | |||
order_by=None, | |||
limit_start=None, | |||
limit_page_length=20, | |||
parent=None, | |||
debug=False, | |||
as_dict=True, | |||
or_filters=None, | |||
): | |||
"""Returns a list of records by filters, fields, ordering and limit | |||
:param doctype: DocType of the data to be queried | |||
:param fields: fields to be returned. Default is `name` | |||
:param filters: filter list by this dict | |||
:param order_by: Order by this fieldname | |||
:param limit_start: Start at this index | |||
:param limit_page_length: Number of records to be returned (default 20)''' | |||
:param limit_page_length: Number of records to be returned (default 20)""" | |||
if frappe.is_table(doctype): | |||
check_parent_permission(parent, doctype) | |||
@@ -40,23 +52,25 @@ def get_list(doctype, fields=None, filters=None, order_by=None, | |||
limit_start=limit_start, | |||
limit_page_length=limit_page_length, | |||
debug=debug, | |||
as_list=not as_dict | |||
as_list=not as_dict, | |||
) | |||
validate_args(args) | |||
return frappe.get_list(**args) | |||
@frappe.whitelist() | |||
def get_count(doctype, filters=None, debug=False, cache=False): | |||
return frappe.db.count(doctype, get_safe_filters(filters), debug, cache) | |||
@frappe.whitelist() | |||
def get(doctype, name=None, filters=None, parent=None): | |||
'''Returns a document by name or filters | |||
"""Returns a document by name or filters | |||
:param doctype: DocType of the document to be returned | |||
:param name: return document of this `name` | |||
:param filters: If name is not set, filter by these values and return the first match''' | |||
:param filters: If name is not set, filter by these values and return the first match""" | |||
if frappe.is_table(doctype): | |||
check_parent_permission(parent, doctype) | |||
@@ -71,13 +85,14 @@ def get(doctype, name=None, filters=None, parent=None): | |||
return frappe.get_doc(doctype, name).as_dict() | |||
@frappe.whitelist() | |||
def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, parent=None): | |||
'''Returns a value form a document | |||
"""Returns a value form a document | |||
:param doctype: DocType to be queried | |||
:param fieldname: Field to be returned (default `name`) | |||
:param filters: dict or string for identifying the record''' | |||
:param filters: dict or string for identifying the record""" | |||
if frappe.is_table(doctype): | |||
check_parent_permission(parent, doctype) | |||
@@ -102,7 +117,15 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, paren | |||
if frappe.get_meta(doctype).issingle: | |||
value = frappe.db.get_values_from_single(fields, filters, doctype, as_dict=as_dict, debug=debug) | |||
else: | |||
value = get_list(doctype, filters=filters, fields=fields, debug=debug, limit_page_length=1, parent=parent, as_dict=as_dict) | |||
value = get_list( | |||
doctype, | |||
filters=filters, | |||
fields=fields, | |||
debug=debug, | |||
limit_page_length=1, | |||
parent=parent, | |||
as_dict=as_dict, | |||
) | |||
if as_dict: | |||
return value[0] if value else {} | |||
@@ -112,6 +135,7 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, paren | |||
return value[0] if len(fields) > 1 else value[0][0] | |||
@frappe.whitelist() | |||
def get_single_value(doctype, field): | |||
if not frappe.has_permission(doctype): | |||
@@ -119,14 +143,15 @@ def get_single_value(doctype, field): | |||
value = frappe.db.get_single_value(doctype, field) | |||
return value | |||
@frappe.whitelist(methods=['POST', 'PUT']) | |||
@frappe.whitelist(methods=["POST", "PUT"]) | |||
def set_value(doctype, name, fieldname, value=None): | |||
'''Set a value using get_doc, group of values | |||
"""Set a value using get_doc, group of values | |||
:param doctype: DocType of the document | |||
:param name: name of the document | |||
:param fieldname: fieldname string or JSON / dict with key value pair | |||
:param value: value if fieldname is JSON / dict''' | |||
:param value: value if fieldname is JSON / dict""" | |||
if fieldname in (frappe.model.default_fields + frappe.model.child_table_fields): | |||
frappe.throw(_("Cannot edit standard fields")) | |||
@@ -137,7 +162,7 @@ def set_value(doctype, name, fieldname, value=None): | |||
try: | |||
values = json.loads(fieldname) | |||
except ValueError: | |||
values = {fieldname: ''} | |||
values = {fieldname: ""} | |||
else: | |||
values = {fieldname: value} | |||
@@ -155,11 +180,12 @@ def set_value(doctype, name, fieldname, value=None): | |||
return doc.as_dict() | |||
@frappe.whitelist(methods=['POST', 'PUT']) | |||
@frappe.whitelist(methods=["POST", "PUT"]) | |||
def insert(doc=None): | |||
'''Insert a document | |||
"""Insert a document | |||
:param doc: JSON or dict object to be inserted''' | |||
:param doc: JSON or dict object to be inserted""" | |||
if isinstance(doc, str): | |||
doc = json.loads(doc) | |||
@@ -173,18 +199,19 @@ def insert(doc=None): | |||
doc = frappe.get_doc(doc).insert() | |||
return doc.as_dict() | |||
@frappe.whitelist(methods=['POST', 'PUT']) | |||
@frappe.whitelist(methods=["POST", "PUT"]) | |||
def insert_many(docs=None): | |||
'''Insert multiple documents | |||
"""Insert multiple documents | |||
:param docs: JSON or list of dict objects to be inserted in one request''' | |||
:param docs: JSON or list of dict objects to be inserted in one request""" | |||
if isinstance(docs, str): | |||
docs = json.loads(docs) | |||
out = [] | |||
if len(docs) > 200: | |||
frappe.throw(_('Only 200 inserts allowed in one request')) | |||
frappe.throw(_("Only 200 inserts allowed in one request")) | |||
for doc in docs: | |||
if doc.get("parenttype"): | |||
@@ -199,11 +226,12 @@ def insert_many(docs=None): | |||
return out | |||
@frappe.whitelist(methods=['POST', 'PUT']) | |||
@frappe.whitelist(methods=["POST", "PUT"]) | |||
def save(doc): | |||
'''Update (save) an existing document | |||
"""Update (save) an existing document | |||
:param doc: JSON or dict object with the properties of the document to be updated''' | |||
:param doc: JSON or dict object with the properties of the document to be updated""" | |||
if isinstance(doc, str): | |||
doc = json.loads(doc) | |||
@@ -212,21 +240,23 @@ def save(doc): | |||
return doc.as_dict() | |||
@frappe.whitelist(methods=['POST', 'PUT']) | |||
@frappe.whitelist(methods=["POST", "PUT"]) | |||
def rename_doc(doctype, old_name, new_name, merge=False): | |||
'''Rename document | |||
"""Rename document | |||
:param doctype: DocType of the document to be renamed | |||
:param old_name: Current `name` of the document to be renamed | |||
:param new_name: New `name` to be set''' | |||
:param new_name: New `name` to be set""" | |||
new_name = frappe.rename_doc(doctype, old_name, new_name, merge=merge) | |||
return new_name | |||
@frappe.whitelist(methods=['POST', 'PUT']) | |||
@frappe.whitelist(methods=["POST", "PUT"]) | |||
def submit(doc): | |||
'''Submit a document | |||
"""Submit a document | |||
:param doc: JSON or dict object to be submitted remotely''' | |||
:param doc: JSON or dict object to be submitted remotely""" | |||
if isinstance(doc, str): | |||
doc = json.loads(doc) | |||
@@ -235,52 +265,57 @@ def submit(doc): | |||
return doc.as_dict() | |||
@frappe.whitelist(methods=['POST', 'PUT']) | |||
@frappe.whitelist(methods=["POST", "PUT"]) | |||
def cancel(doctype, name): | |||
'''Cancel a document | |||
"""Cancel a document | |||
:param doctype: DocType of the document to be cancelled | |||
:param name: name of the document to be cancelled''' | |||
:param name: name of the document to be cancelled""" | |||
wrapper = frappe.get_doc(doctype, name) | |||
wrapper.cancel() | |||
return wrapper.as_dict() | |||
@frappe.whitelist(methods=['DELETE', 'POST']) | |||
@frappe.whitelist(methods=["DELETE", "POST"]) | |||
def delete(doctype, name): | |||
'''Delete a remote document | |||
"""Delete a remote document | |||
:param doctype: DocType of the document to be deleted | |||
:param name: name of the document to be deleted''' | |||
:param name: name of the document to be deleted""" | |||
frappe.delete_doc(doctype, name, ignore_missing=False) | |||
@frappe.whitelist(methods=['POST', 'PUT']) | |||
@frappe.whitelist(methods=["POST", "PUT"]) | |||
def set_default(key, value, parent=None): | |||
"""set a user default value""" | |||
frappe.db.set_default(key, value, parent or frappe.session.user) | |||
frappe.clear_cache(user=frappe.session.user) | |||
@frappe.whitelist() | |||
def get_default(key, parent=None): | |||
"""set a user default value""" | |||
return frappe.db.get_default(key, parent) | |||
@frappe.whitelist(methods=['POST', 'PUT']) | |||
@frappe.whitelist(methods=["POST", "PUT"]) | |||
def make_width_property_setter(doc): | |||
'''Set width Property Setter | |||
"""Set width Property Setter | |||
:param doc: Property Setter document with `width` property''' | |||
:param doc: Property Setter document with `width` property""" | |||
if isinstance(doc, str): | |||
doc = json.loads(doc) | |||
if doc["doctype"]=="Property Setter" and doc["property"]=="width": | |||
frappe.get_doc(doc).insert(ignore_permissions = True) | |||
if doc["doctype"] == "Property Setter" and doc["property"] == "width": | |||
frappe.get_doc(doc).insert(ignore_permissions=True) | |||
@frappe.whitelist(methods=['POST', 'PUT']) | |||
@frappe.whitelist(methods=["POST", "PUT"]) | |||
def bulk_update(docs): | |||
'''Bulk update documents | |||
"""Bulk update documents | |||
:param docs: JSON list of documents to be updated remotely. Each document must have `docname` property''' | |||
:param docs: JSON list of documents to be updated remotely. Each document must have `docname` property""" | |||
docs = json.loads(docs) | |||
failed_docs = [] | |||
for doc in docs: | |||
@@ -290,41 +325,40 @@ def bulk_update(docs): | |||
existing_doc.update(doc) | |||
existing_doc.save() | |||
except Exception: | |||
failed_docs.append({ | |||
'doc': doc, | |||
'exc': frappe.utils.get_traceback() | |||
}) | |||
failed_docs.append({"doc": doc, "exc": frappe.utils.get_traceback()}) | |||
return {"failed_docs": failed_docs} | |||
return {'failed_docs': failed_docs} | |||
@frappe.whitelist() | |||
def has_permission(doctype, docname, perm_type="read"): | |||
'''Returns a JSON with data whether the document has the requested permission | |||
"""Returns a JSON with data whether the document has the requested permission | |||
:param doctype: DocType of the document to be checked | |||
:param docname: `name` of the document to be checked | |||
:param perm_type: one of `read`, `write`, `create`, `submit`, `cancel`, `report`. Default is `read`''' | |||
:param perm_type: one of `read`, `write`, `create`, `submit`, `cancel`, `report`. Default is `read`""" | |||
# perm_type can be one of read, write, create, submit, cancel, report | |||
return {"has_permission": frappe.has_permission(doctype, perm_type.lower(), docname)} | |||
@frappe.whitelist() | |||
def get_password(doctype, name, fieldname): | |||
'''Return a password type property. Only applicable for System Managers | |||
"""Return a password type property. Only applicable for System Managers | |||
:param doctype: DocType of the document that holds the password | |||
:param name: `name` of the document that holds the password | |||
:param fieldname: `fieldname` of the password property | |||
''' | |||
""" | |||
frappe.only_for("System Manager") | |||
return frappe.get_doc(doctype, name).get_password(fieldname) | |||
@frappe.whitelist() | |||
def get_js(items): | |||
'''Load JS code files. Will also append translations | |||
"""Load JS code files. Will also append translations | |||
and extend `frappe._messages` | |||
:param items: JSON list of paths of the js files to be loaded.''' | |||
:param items: JSON list of paths of the js files to be loaded.""" | |||
items = json.loads(items) | |||
out = [] | |||
for src in items: | |||
@@ -346,14 +380,25 @@ def get_js(items): | |||
return out | |||
@frappe.whitelist(allow_guest=True) | |||
def get_time_zone(): | |||
'''Returns default time zone''' | |||
"""Returns default time zone""" | |||
return {"time_zone": frappe.defaults.get_defaults().get("time_zone")} | |||
@frappe.whitelist(methods=['POST', 'PUT']) | |||
def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder=None, decode_base64=False, is_private=None, docfield=None): | |||
'''Attach a file to Document (POST) | |||
@frappe.whitelist(methods=["POST", "PUT"]) | |||
def attach_file( | |||
filename=None, | |||
filedata=None, | |||
doctype=None, | |||
docname=None, | |||
folder=None, | |||
decode_base64=False, | |||
is_private=None, | |||
docfield=None, | |||
): | |||
"""Attach a file to Document (POST) | |||
:param filename: filename e.g. test-file.txt | |||
:param filedata: base64 encode filedata which must be urlencoded | |||
@@ -362,7 +407,7 @@ def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder | |||
:param folder: Folder to add File into | |||
:param decode_base64: decode filedata from base64 encode, default is False | |||
:param is_private: Attach file as private file (1 or 0) | |||
:param docfield: file to attach to (optional)''' | |||
:param docfield: file to attach to (optional)""" | |||
request_method = frappe.local.request.environ.get("REQUEST_METHOD") | |||
@@ -374,16 +419,19 @@ def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder | |||
if not doc.has_permission(): | |||
frappe.throw(_("Not permitted"), frappe.PermissionError) | |||
_file = frappe.get_doc({ | |||
"doctype": "File", | |||
"file_name": filename, | |||
"attached_to_doctype": doctype, | |||
"attached_to_name": docname, | |||
"attached_to_field": docfield, | |||
"folder": folder, | |||
"is_private": is_private, | |||
"content": filedata, | |||
"decode": decode_base64}) | |||
_file = frappe.get_doc( | |||
{ | |||
"doctype": "File", | |||
"file_name": filename, | |||
"attached_to_doctype": doctype, | |||
"attached_to_name": docname, | |||
"attached_to_field": docfield, | |||
"folder": folder, | |||
"is_private": is_private, | |||
"content": filedata, | |||
"decode": decode_base64, | |||
} | |||
) | |||
_file.save() | |||
if docfield and doctype: | |||
@@ -392,22 +440,23 @@ def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder | |||
return _file.as_dict() | |||
@frappe.whitelist() | |||
def get_hooks(hook, app_name=None): | |||
return frappe.get_hooks(hook, app_name) | |||
@frappe.whitelist() | |||
def is_document_amended(doctype, docname): | |||
if frappe.permissions.has_permission(doctype): | |||
try: | |||
return frappe.db.exists(doctype, { | |||
'amended_from': docname | |||
}) | |||
return frappe.db.exists(doctype, {"amended_from": docname}) | |||
except frappe.db.InternalError: | |||
pass | |||
return False | |||
@frappe.whitelist() | |||
def validate_link(doctype: str, docname: str, fields=None): | |||
if not isinstance(doctype, str): | |||
@@ -417,13 +466,11 @@ def validate_link(doctype: str, docname: str, fields=None): | |||
frappe.throw(_("Document Name must be a string")) | |||
if doctype != "DocType" and not ( | |||
frappe.has_permission(doctype, "select") | |||
or frappe.has_permission(doctype, "read") | |||
frappe.has_permission(doctype, "select") or frappe.has_permission(doctype, "read") | |||
): | |||
frappe.throw( | |||
_("You do not have Read or Select Permissions for {}") | |||
.format(frappe.bold(doctype)), | |||
frappe.PermissionError | |||
_("You do not have Read or Select Permissions for {}").format(frappe.bold(doctype)), | |||
frappe.PermissionError, | |||
) | |||
values = frappe._dict() | |||
@@ -438,14 +485,11 @@ def validate_link(doctype: str, docname: str, fields=None): | |||
except frappe.PermissionError: | |||
frappe.clear_last_message() | |||
frappe.msgprint( | |||
_("You need {0} permission to fetch values from {1} {2}") | |||
.format( | |||
frappe.bold(_("Read")), | |||
frappe.bold(doctype), | |||
frappe.bold(docname) | |||
_("You need {0} permission to fetch values from {1} {2}").format( | |||
frappe.bold(_("Read")), frappe.bold(doctype), frappe.bold(docname) | |||
), | |||
title=_("Cannot Fetch Values"), | |||
indicator="orange" | |||
indicator="orange", | |||
) | |||
return values |
@@ -1,23 +1,26 @@ | |||
# Copyright (c) 2015, Web Notes Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
import sys | |||
import click | |||
import cProfile | |||
import pstats | |||
import frappe | |||
import frappe.utils | |||
import subprocess # nosec | |||
import subprocess # nosec | |||
import sys | |||
from functools import wraps | |||
from io import StringIO | |||
from os import environ | |||
import click | |||
import frappe | |||
import frappe.utils | |||
click.disable_unicode_literals_warning = True | |||
def pass_context(f): | |||
@wraps(f) | |||
def _func(ctx, *args, **kwargs): | |||
profile = ctx.obj['profile'] | |||
profile = ctx.obj["profile"] | |||
if profile: | |||
pr = cProfile.Profile() | |||
pr.enable() | |||
@@ -25,18 +28,17 @@ def pass_context(f): | |||
try: | |||
ret = f(frappe._dict(ctx.obj), *args, **kwargs) | |||
except frappe.exceptions.SiteNotSpecifiedError as e: | |||
click.secho(str(e), fg='yellow') | |||
click.secho(str(e), fg="yellow") | |||
sys.exit(1) | |||
except frappe.exceptions.IncorrectSitePath: | |||
site = ctx.obj.get("sites", "")[0] | |||
click.secho(f'Site {site} does not exist!', fg='yellow') | |||
click.secho(f"Site {site} does not exist!", fg="yellow") | |||
sys.exit(1) | |||
if profile: | |||
pr.disable() | |||
s = StringIO() | |||
ps = pstats.Stats(pr, stream=s)\ | |||
.sort_stats('cumtime', 'tottime', 'ncalls') | |||
ps = pstats.Stats(pr, stream=s).sort_stats("cumtime", "tottime", "ncalls") | |||
ps.print_stats() | |||
# print the top-100 | |||
@@ -47,6 +49,7 @@ def pass_context(f): | |||
return click.pass_context(_func) | |||
def get_site(context, raise_err=True): | |||
try: | |||
site = context.sites[0] | |||
@@ -56,17 +59,19 @@ def get_site(context, raise_err=True): | |||
raise frappe.SiteNotSpecifiedError | |||
return None | |||
def popen(command, *args, **kwargs): | |||
output = kwargs.get('output', True) | |||
cwd = kwargs.get('cwd') | |||
shell = kwargs.get('shell', True) | |||
raise_err = kwargs.get('raise_err') | |||
env = kwargs.get('env') | |||
output = kwargs.get("output", True) | |||
cwd = kwargs.get("cwd") | |||
shell = kwargs.get("shell", True) | |||
raise_err = kwargs.get("raise_err") | |||
env = kwargs.get("env") | |||
if env: | |||
env = dict(environ, **env) | |||
def set_low_prio(): | |||
import psutil | |||
if psutil.LINUX: | |||
psutil.Process().nice(19) | |||
psutil.Process().ionice(psutil.IOPRIO_CLASS_IDLE) | |||
@@ -77,13 +82,14 @@ def popen(command, *args, **kwargs): | |||
psutil.Process().nice(19) | |||
# ionice not supported | |||
proc = subprocess.Popen(command, | |||
proc = subprocess.Popen( | |||
command, | |||
stdout=None if output else subprocess.PIPE, | |||
stderr=None if output else subprocess.PIPE, | |||
shell=shell, | |||
cwd=cwd, | |||
preexec_fn=set_low_prio, | |||
env=env | |||
env=env, | |||
) | |||
return_ = proc.wait() | |||
@@ -93,26 +99,22 @@ def popen(command, *args, **kwargs): | |||
return return_ | |||
def call_command(cmd, context): | |||
return click.Context(cmd, obj=context).forward(cmd) | |||
def get_commands(): | |||
# prevent circular imports | |||
from .redis_utils import commands as redis_commands | |||
from .scheduler import commands as scheduler_commands | |||
from .site import commands as site_commands | |||
from .translate import commands as translate_commands | |||
from .utils import commands as utils_commands | |||
from .redis_utils import commands as redis_commands | |||
clickable_link = ( | |||
"\x1b]8;;https://frappeframework.com/docs\afrappeframework.com\x1b]8;;\a" | |||
) | |||
clickable_link = "\x1b]8;;https://frappeframework.com/docs\afrappeframework.com\x1b]8;;\a" | |||
all_commands = ( | |||
scheduler_commands | |||
+ site_commands | |||
+ translate_commands | |||
+ utils_commands | |||
+ redis_commands | |||
scheduler_commands + site_commands + translate_commands + utils_commands + redis_commands | |||
) | |||
for command in all_commands: | |||
@@ -3,51 +3,71 @@ import os | |||
import click | |||
import frappe | |||
from frappe.utils.redis_queue import RedisQueue | |||
from frappe.installer import update_site_config | |||
from frappe.utils.redis_queue import RedisQueue | |||
@click.command('create-rq-users') | |||
@click.option('--set-admin-password', is_flag=True, default=False, help='Set new Redis admin(default user) password') | |||
@click.option('--use-rq-auth', is_flag=True, default=False, help='Enable Redis authentication for sites') | |||
@click.command("create-rq-users") | |||
@click.option( | |||
"--set-admin-password", | |||
is_flag=True, | |||
default=False, | |||
help="Set new Redis admin(default user) password", | |||
) | |||
@click.option( | |||
"--use-rq-auth", is_flag=True, default=False, help="Enable Redis authentication for sites" | |||
) | |||
def create_rq_users(set_admin_password=False, use_rq_auth=False): | |||
"""Create Redis Queue users and add to acl and app configs. | |||
acl config file will be used by redis server while starting the server | |||
and app config is used by app while connecting to redis server. | |||
""" | |||
acl_file_path = os.path.abspath('../config/redis_queue.acl') | |||
acl_file_path = os.path.abspath("../config/redis_queue.acl") | |||
with frappe.init_site(): | |||
acl_list, user_credentials = RedisQueue.gen_acl_list( | |||
set_admin_password=set_admin_password) | |||
acl_list, user_credentials = RedisQueue.gen_acl_list(set_admin_password=set_admin_password) | |||
with open(acl_file_path, 'w') as f: | |||
f.writelines([acl+'\n' for acl in acl_list]) | |||
with open(acl_file_path, "w") as f: | |||
f.writelines([acl + "\n" for acl in acl_list]) | |||
sites_path = os.getcwd() | |||
common_site_config_path = os.path.join(sites_path, 'common_site_config.json') | |||
update_site_config("rq_username", user_credentials['bench'][0], validate=False, | |||
site_config_path=common_site_config_path) | |||
update_site_config("rq_password", user_credentials['bench'][1], validate=False, | |||
site_config_path=common_site_config_path) | |||
update_site_config("use_rq_auth", use_rq_auth, validate=False, | |||
site_config_path=common_site_config_path) | |||
click.secho('* ACL and site configs are updated with new user credentials. ' | |||
'Please restart Redis Queue server to enable namespaces.', | |||
fg='green') | |||
common_site_config_path = os.path.join(sites_path, "common_site_config.json") | |||
update_site_config( | |||
"rq_username", | |||
user_credentials["bench"][0], | |||
validate=False, | |||
site_config_path=common_site_config_path, | |||
) | |||
update_site_config( | |||
"rq_password", | |||
user_credentials["bench"][1], | |||
validate=False, | |||
site_config_path=common_site_config_path, | |||
) | |||
update_site_config( | |||
"use_rq_auth", use_rq_auth, validate=False, site_config_path=common_site_config_path | |||
) | |||
click.secho( | |||
"* ACL and site configs are updated with new user credentials. " | |||
"Please restart Redis Queue server to enable namespaces.", | |||
fg="green", | |||
) | |||
if set_admin_password: | |||
env_key = 'RQ_ADMIN_PASWORD' | |||
click.secho('* Redis admin password is successfully set up. ' | |||
'Include below line in .bashrc file for system to use', | |||
fg='green') | |||
env_key = "RQ_ADMIN_PASWORD" | |||
click.secho( | |||
"* Redis admin password is successfully set up. " | |||
"Include below line in .bashrc file for system to use", | |||
fg="green", | |||
) | |||
click.secho(f"`export {env_key}={user_credentials['default'][1]}`") | |||
click.secho('NOTE: Please save the admin password as you ' | |||
'can not access redis server without the password', | |||
fg='yellow') | |||
click.secho( | |||
"NOTE: Please save the admin password as you " | |||
"can not access redis server without the password", | |||
fg="yellow", | |||
) | |||
commands = [ | |||
create_rq_users | |||
] | |||
commands = [create_rq_users] |
@@ -1,15 +1,20 @@ | |||
import click | |||
import sys | |||
import click | |||
import frappe | |||
from frappe.utils import cint | |||
from frappe.commands import pass_context, get_site | |||
from frappe.commands import get_site, pass_context | |||
from frappe.exceptions import SiteNotSpecifiedError | |||
from frappe.utils import cint | |||
def _is_scheduler_enabled(): | |||
enable_scheduler = False | |||
try: | |||
frappe.connect() | |||
enable_scheduler = cint(frappe.db.get_single_value("System Settings", "enable_scheduler")) and True or False | |||
enable_scheduler = ( | |||
cint(frappe.db.get_single_value("System Settings", "enable_scheduler")) and True or False | |||
) | |||
except: | |||
pass | |||
finally: | |||
@@ -44,11 +49,12 @@ def trigger_scheduler_event(context, event): | |||
sys.exit(exit_code) | |||
@click.command('enable-scheduler') | |||
@click.command("enable-scheduler") | |||
@pass_context | |||
def enable_scheduler(context): | |||
"Enable scheduler" | |||
import frappe.utils.scheduler | |||
for site in context.sites: | |||
try: | |||
frappe.init(site=site) | |||
@@ -61,11 +67,13 @@ def enable_scheduler(context): | |||
if not context.sites: | |||
raise SiteNotSpecifiedError | |||
@click.command('disable-scheduler') | |||
@click.command("disable-scheduler") | |||
@pass_context | |||
def disable_scheduler(context): | |||
"Disable scheduler" | |||
import frappe.utils.scheduler | |||
for site in context.sites: | |||
try: | |||
frappe.init(site=site) | |||
@@ -79,13 +87,13 @@ def disable_scheduler(context): | |||
raise SiteNotSpecifiedError | |||
@click.command('scheduler') | |||
@click.option('--site', help='site name') | |||
@click.argument('state', type=click.Choice(['pause', 'resume', 'disable', 'enable'])) | |||
@click.command("scheduler") | |||
@click.option("--site", help="site name") | |||
@click.argument("state", type=click.Choice(["pause", "resume", "disable", "enable"])) | |||
@pass_context | |||
def scheduler(context, state, site=None): | |||
from frappe.installer import update_site_config | |||
import frappe.utils.scheduler | |||
from frappe.installer import update_site_config | |||
if not site: | |||
site = get_site(context) | |||
@@ -93,58 +101,64 @@ def scheduler(context, state, site=None): | |||
try: | |||
frappe.init(site=site) | |||
if state == 'pause': | |||
update_site_config('pause_scheduler', 1) | |||
elif state == 'resume': | |||
update_site_config('pause_scheduler', 0) | |||
elif state == 'disable': | |||
if state == "pause": | |||
update_site_config("pause_scheduler", 1) | |||
elif state == "resume": | |||
update_site_config("pause_scheduler", 0) | |||
elif state == "disable": | |||
frappe.connect() | |||
frappe.utils.scheduler.disable_scheduler() | |||
frappe.db.commit() | |||
elif state == 'enable': | |||
elif state == "enable": | |||
frappe.connect() | |||
frappe.utils.scheduler.enable_scheduler() | |||
frappe.db.commit() | |||
print('Scheduler {0}d for site {1}'.format(state, site)) | |||
print("Scheduler {0}d for site {1}".format(state, site)) | |||
finally: | |||
frappe.destroy() | |||
@click.command('set-maintenance-mode') | |||
@click.option('--site', help='site name') | |||
@click.argument('state', type=click.Choice(['on', 'off'])) | |||
@click.command("set-maintenance-mode") | |||
@click.option("--site", help="site name") | |||
@click.argument("state", type=click.Choice(["on", "off"])) | |||
@pass_context | |||
def set_maintenance_mode(context, state, site=None): | |||
from frappe.installer import update_site_config | |||
if not site: | |||
site = get_site(context) | |||
try: | |||
frappe.init(site=site) | |||
update_site_config('maintenance_mode', 1 if (state == 'on') else 0) | |||
update_site_config("maintenance_mode", 1 if (state == "on") else 0) | |||
finally: | |||
frappe.destroy() | |||
@click.command('doctor') #Passing context always gets a site and if there is no use site it breaks | |||
@click.option('--site', help='site name') | |||
@click.command( | |||
"doctor" | |||
) # Passing context always gets a site and if there is no use site it breaks | |||
@click.option("--site", help="site name") | |||
@pass_context | |||
def doctor(context, site=None): | |||
"Get diagnostic info about background workers" | |||
from frappe.utils.doctor import doctor as _doctor | |||
if not site: | |||
site = get_site(context, raise_err=False) | |||
return _doctor(site=site) | |||
@click.command('show-pending-jobs') | |||
@click.option('--site', help='site name') | |||
@click.command("show-pending-jobs") | |||
@click.option("--site", help="site name") | |||
@pass_context | |||
def show_pending_jobs(context, site=None): | |||
"Get diagnostic info about background jobs" | |||
from frappe.utils.doctor import pending_jobs as _pending_jobs | |||
if not site: | |||
site = get_site(context) | |||
@@ -153,35 +167,45 @@ def show_pending_jobs(context, site=None): | |||
return pending_jobs | |||
@click.command('purge-jobs') | |||
@click.option('--site', help='site name') | |||
@click.option('--queue', default=None, help='one of "low", "default", "high') | |||
@click.option('--event', default=None, help='one of "all", "weekly", "monthly", "hourly", "daily", "weekly_long", "daily_long"') | |||
@click.command("purge-jobs") | |||
@click.option("--site", help="site name") | |||
@click.option("--queue", default=None, help='one of "low", "default", "high') | |||
@click.option( | |||
"--event", | |||
default=None, | |||
help='one of "all", "weekly", "monthly", "hourly", "daily", "weekly_long", "daily_long"', | |||
) | |||
def purge_jobs(site=None, queue=None, event=None): | |||
"Purge any pending periodic tasks, if event option is not given, it will purge everything for the site" | |||
from frappe.utils.doctor import purge_pending_jobs | |||
frappe.init(site or '') | |||
frappe.init(site or "") | |||
count = purge_pending_jobs(event=event, site=site, queue=queue) | |||
print("Purged {} jobs".format(count)) | |||
@click.command('schedule') | |||
@click.command("schedule") | |||
def start_scheduler(): | |||
from frappe.utils.scheduler import start_scheduler | |||
start_scheduler() | |||
@click.command('worker') | |||
@click.option('--queue', type=str) | |||
@click.option('--quiet', is_flag = True, default = False, help = 'Hide Log Outputs') | |||
@click.option('-u', '--rq-username', default=None, help='Redis ACL user') | |||
@click.option('-p', '--rq-password', default=None, help='Redis ACL user password') | |||
def start_worker(queue, quiet = False, rq_username=None, rq_password=None): | |||
"""Site is used to find redis credentals. | |||
""" | |||
@click.command("worker") | |||
@click.option("--queue", type=str) | |||
@click.option("--quiet", is_flag=True, default=False, help="Hide Log Outputs") | |||
@click.option("-u", "--rq-username", default=None, help="Redis ACL user") | |||
@click.option("-p", "--rq-password", default=None, help="Redis ACL user password") | |||
def start_worker(queue, quiet=False, rq_username=None, rq_password=None): | |||
"""Site is used to find redis credentals.""" | |||
from frappe.utils.background_jobs import start_worker | |||
start_worker(queue, quiet = quiet, rq_username=rq_username, rq_password=rq_password) | |||
@click.command('ready-for-migration') | |||
@click.option('--site', help='site name') | |||
start_worker(queue, quiet=quiet, rq_username=rq_username, rq_password=rq_password) | |||
@click.command("ready-for-migration") | |||
@click.option("--site", help="site name") | |||
@pass_context | |||
def ready_for_migration(context, site=None): | |||
from frappe.utils.doctor import get_pending_jobs | |||
@@ -194,16 +218,17 @@ def ready_for_migration(context, site=None): | |||
pending_jobs = get_pending_jobs(site=site) | |||
if pending_jobs: | |||
print('NOT READY for migration: site {0} has pending background jobs'.format(site)) | |||
print("NOT READY for migration: site {0} has pending background jobs".format(site)) | |||
sys.exit(1) | |||
else: | |||
print('READY for migration: site {0} does not have any background jobs'.format(site)) | |||
print("READY for migration: site {0} does not have any background jobs".format(site)) | |||
return 0 | |||
finally: | |||
frappe.destroy() | |||
commands = [ | |||
disable_scheduler, | |||
doctor, | |||
@@ -1,13 +1,16 @@ | |||
import click | |||
from frappe.commands import pass_context, get_site | |||
from frappe.commands import get_site, pass_context | |||
from frappe.exceptions import SiteNotSpecifiedError | |||
# translation | |||
@click.command('build-message-files') | |||
@click.command("build-message-files") | |||
@pass_context | |||
def build_message_files(context): | |||
"Build message files for translation" | |||
import frappe.translate | |||
for site in context.sites: | |||
try: | |||
frappe.init(site=site) | |||
@@ -18,32 +21,41 @@ def build_message_files(context): | |||
if not context.sites: | |||
raise SiteNotSpecifiedError | |||
@click.command('new-language') #, help="Create lang-code.csv for given app") | |||
@click.command("new-language") # , help="Create lang-code.csv for given app") | |||
@pass_context | |||
@click.argument('lang_code') #, help="Language code eg. en") | |||
@click.argument('app') #, help="App name eg. frappe") | |||
@click.argument("lang_code") # , help="Language code eg. en") | |||
@click.argument("app") # , help="App name eg. frappe") | |||
def new_language(context, lang_code, app): | |||
"""Create lang-code.csv for given app""" | |||
import frappe.translate | |||
if not context['sites']: | |||
raise Exception('--site is required') | |||
if not context["sites"]: | |||
raise Exception("--site is required") | |||
# init site | |||
frappe.connect(site=context['sites'][0]) | |||
frappe.connect(site=context["sites"][0]) | |||
frappe.translate.write_translations_file(app, lang_code) | |||
print("File created at ./apps/{app}/{app}/translations/{lang_code}.csv".format(app=app, lang_code=lang_code)) | |||
print("You will need to add the language in frappe/geo/languages.json, if you haven't done it already.") | |||
print( | |||
"File created at ./apps/{app}/{app}/translations/{lang_code}.csv".format( | |||
app=app, lang_code=lang_code | |||
) | |||
) | |||
print( | |||
"You will need to add the language in frappe/geo/languages.json, if you haven't done it already." | |||
) | |||
@click.command('get-untranslated') | |||
@click.argument('lang') | |||
@click.argument('untranslated_file') | |||
@click.option('--all', default=False, is_flag=True, help='Get all message strings') | |||
@click.command("get-untranslated") | |||
@click.argument("lang") | |||
@click.argument("untranslated_file") | |||
@click.option("--all", default=False, is_flag=True, help="Get all message strings") | |||
@pass_context | |||
def get_untranslated(context, lang, untranslated_file, all=None): | |||
"Get untranslated strings for language" | |||
import frappe.translate | |||
site = get_site(context) | |||
try: | |||
frappe.init(site=site) | |||
@@ -52,14 +64,16 @@ def get_untranslated(context, lang, untranslated_file, all=None): | |||
finally: | |||
frappe.destroy() | |||
@click.command('update-translations') | |||
@click.argument('lang') | |||
@click.argument('untranslated_file') | |||
@click.argument('translated-file') | |||
@click.command("update-translations") | |||
@click.argument("lang") | |||
@click.argument("untranslated_file") | |||
@click.argument("translated-file") | |||
@pass_context | |||
def update_translations(context, lang, untranslated_file, translated_file): | |||
"Update translated strings" | |||
import frappe.translate | |||
site = get_site(context) | |||
try: | |||
frappe.init(site=site) | |||
@@ -68,13 +82,15 @@ def update_translations(context, lang, untranslated_file, translated_file): | |||
finally: | |||
frappe.destroy() | |||
@click.command('import-translations') | |||
@click.argument('lang') | |||
@click.argument('path') | |||
@click.command("import-translations") | |||
@click.argument("lang") | |||
@click.argument("path") | |||
@pass_context | |||
def import_translations(context, lang, path): | |||
"Update translated strings" | |||
import frappe.translate | |||
site = get_site(context) | |||
try: | |||
frappe.init(site=site) | |||
@@ -83,6 +99,7 @@ def import_translations(context, lang, path): | |||
finally: | |||
frappe.destroy() | |||
commands = [ | |||
build_message_files, | |||
get_untranslated, | |||
@@ -1,14 +1,20 @@ | |||
import frappe | |||
from frappe import _ | |||
from frappe.desk.moduleview import (get_data, get_onboard_items, config_exists, get_module_link_items_from_list) | |||
from frappe.desk.moduleview import ( | |||
config_exists, | |||
get_data, | |||
get_module_link_items_from_list, | |||
get_onboard_items, | |||
) | |||
def get_modules_from_all_apps_for_user(user=None): | |||
if not user: | |||
user = frappe.session.user | |||
all_modules = get_modules_from_all_apps() | |||
global_blocked_modules = frappe.get_doc('User', 'Administrator').get_blocked_modules() | |||
user_blocked_modules = frappe.get_doc('User', user).get_blocked_modules() | |||
global_blocked_modules = frappe.get_doc("User", "Administrator").get_blocked_modules() | |||
user_blocked_modules = frappe.get_doc("User", user).get_blocked_modules() | |||
blocked_modules = global_blocked_modules + user_blocked_modules | |||
allowed_modules_list = [m for m in all_modules if m.get("module_name") not in blocked_modules] | |||
@@ -22,31 +28,31 @@ def get_modules_from_all_apps_for_user(user=None): | |||
module["onboard_present"] = 1 | |||
# Set defaults links | |||
module["links"] = get_onboard_items(module["app"], frappe.scrub(module_name))[:5] | |||
module["links"] = get_onboard_items(module["app"], frappe.scrub(module_name))[:5] | |||
return allowed_modules_list | |||
def get_modules_from_all_apps(): | |||
modules_list = [] | |||
for app in frappe.get_installed_apps(): | |||
modules_list += get_modules_from_app(app) | |||
return modules_list | |||
def get_modules_from_app(app): | |||
return frappe.get_all('Module Def', | |||
filters={'app_name': app}, | |||
fields=['module_name', 'app_name as app'] | |||
return frappe.get_all( | |||
"Module Def", filters={"app_name": app}, fields=["module_name", "app_name as app"] | |||
) | |||
def get_all_empty_tables_by_module(): | |||
table_rows = frappe.qb.Field("table_rows") | |||
table_name = frappe.qb.Field("table_name") | |||
information_schema = frappe.qb.Schema("information_schema") | |||
empty_tables = ( | |||
frappe.qb.from_(information_schema.tables) | |||
.select(table_name) | |||
.where(table_rows == 0) | |||
frappe.qb.from_(information_schema.tables).select(table_name).where(table_rows == 0) | |||
).run() | |||
empty_tables = {r[0] for r in empty_tables} | |||
@@ -62,8 +68,10 @@ def get_all_empty_tables_by_module(): | |||
empty_tables_by_module[module] = [doctype] | |||
return empty_tables_by_module | |||
def is_domain(module): | |||
return module.get("category") == "Domains" | |||
def is_module(module): | |||
return module.get("type") == "module" | |||
return module.get("type") == "module" |
@@ -1,12 +1,13 @@ | |||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
import frappe | |||
from frappe import _ | |||
import functools | |||
import re | |||
import frappe | |||
from frappe import _ | |||
def load_address_and_contact(doc, key=None): | |||
"""Loads address list and contact list in `__onload`""" | |||
from frappe.contacts.doctype.address.address import get_address_display, get_condensed_address | |||
@@ -18,15 +19,18 @@ def load_address_and_contact(doc, key=None): | |||
] | |||
address_list = frappe.get_list("Address", filters=filters, fields=["*"]) | |||
address_list = [a.update({"display": get_address_display(a)}) | |||
for a in address_list] | |||
address_list = [a.update({"display": get_address_display(a)}) for a in address_list] | |||
address_list = sorted(address_list, | |||
key = functools.cmp_to_key(lambda a, b: | |||
(int(a.is_primary_address - b.is_primary_address)) or | |||
(1 if a.modified - b.modified else 0)), reverse=True) | |||
address_list = sorted( | |||
address_list, | |||
key=functools.cmp_to_key( | |||
lambda a, b: (int(a.is_primary_address - b.is_primary_address)) | |||
or (1 if a.modified - b.modified else 0) | |||
), | |||
reverse=True, | |||
) | |||
doc.set_onload('addr_list', address_list) | |||
doc.set_onload("addr_list", address_list) | |||
contact_list = [] | |||
filters = [ | |||
@@ -37,29 +41,38 @@ def load_address_and_contact(doc, key=None): | |||
contact_list = frappe.get_list("Contact", filters=filters, fields=["*"]) | |||
for contact in contact_list: | |||
contact["email_ids"] = frappe.get_all("Contact Email", filters={ | |||
"parenttype": "Contact", | |||
"parent": contact.name, | |||
"is_primary": 0 | |||
}, fields=["email_id"]) | |||
contact["phone_nos"] = frappe.get_all("Contact Phone", filters={ | |||
contact["email_ids"] = frappe.get_all( | |||
"Contact Email", | |||
filters={"parenttype": "Contact", "parent": contact.name, "is_primary": 0}, | |||
fields=["email_id"], | |||
) | |||
contact["phone_nos"] = frappe.get_all( | |||
"Contact Phone", | |||
filters={ | |||
"parenttype": "Contact", | |||
"parent": contact.name, | |||
"is_primary_phone": 0, | |||
"is_primary_mobile_no": 0 | |||
}, fields=["phone"]) | |||
"is_primary_mobile_no": 0, | |||
}, | |||
fields=["phone"], | |||
) | |||
if contact.address: | |||
address = frappe.get_doc("Address", contact.address) | |||
contact["address"] = get_condensed_address(address) | |||
contact_list = sorted(contact_list, | |||
key = functools.cmp_to_key(lambda a, b: | |||
(int(a.is_primary_contact - b.is_primary_contact)) or | |||
(1 if a.modified - b.modified else 0)), reverse=True) | |||
contact_list = sorted( | |||
contact_list, | |||
key=functools.cmp_to_key( | |||
lambda a, b: (int(a.is_primary_contact - b.is_primary_contact)) | |||
or (1 if a.modified - b.modified else 0) | |||
), | |||
reverse=True, | |||
) | |||
doc.set_onload("contact_list", contact_list) | |||
doc.set_onload('contact_list', contact_list) | |||
def has_permission(doc, ptype, user): | |||
links = get_permitted_and_not_permitted_links(doc.doctype) | |||
@@ -69,7 +82,7 @@ def has_permission(doc, ptype, user): | |||
# True if any one is True or all are empty | |||
names = [] | |||
for df in (links.get("permitted_links") + links.get("not_permitted_links")): | |||
for df in links.get("permitted_links") + links.get("not_permitted_links"): | |||
doctype = df.options | |||
name = doc.get(df.fieldname) | |||
names.append(name) | |||
@@ -81,12 +94,15 @@ def has_permission(doc, ptype, user): | |||
return True | |||
return False | |||
def get_permission_query_conditions_for_contact(user): | |||
return get_permission_query_conditions("Contact") | |||
def get_permission_query_conditions_for_address(user): | |||
return get_permission_query_conditions("Address") | |||
def get_permission_query_conditions(doctype): | |||
links = get_permitted_and_not_permitted_links(doctype) | |||
@@ -100,7 +116,9 @@ def get_permission_query_conditions(doctype): | |||
# when everything is not permitted | |||
for df in links.get("not_permitted_links"): | |||
# like ifnull(customer, '')='' and ifnull(supplier, '')='' | |||
conditions.append("ifnull(`tab{doctype}`.`{fieldname}`, '')=''".format(doctype=doctype, fieldname=df.fieldname)) | |||
conditions.append( | |||
"ifnull(`tab{doctype}`.`{fieldname}`, '')=''".format(doctype=doctype, fieldname=df.fieldname) | |||
) | |||
return "( " + " and ".join(conditions) + " )" | |||
@@ -109,10 +127,13 @@ def get_permission_query_conditions(doctype): | |||
for df in links.get("permitted_links"): | |||
# like ifnull(customer, '')!='' or ifnull(supplier, '')!='' | |||
conditions.append("ifnull(`tab{doctype}`.`{fieldname}`, '')!=''".format(doctype=doctype, fieldname=df.fieldname)) | |||
conditions.append( | |||
"ifnull(`tab{doctype}`.`{fieldname}`, '')!=''".format(doctype=doctype, fieldname=df.fieldname) | |||
) | |||
return "( " + " or ".join(conditions) + " )" | |||
def get_permitted_and_not_permitted_links(doctype): | |||
permitted_links = [] | |||
not_permitted_links = [] | |||
@@ -129,40 +150,40 @@ def get_permitted_and_not_permitted_links(doctype): | |||
else: | |||
not_permitted_links.append(df) | |||
return { | |||
"permitted_links": permitted_links, | |||
"not_permitted_links": not_permitted_links | |||
} | |||
return {"permitted_links": permitted_links, "not_permitted_links": not_permitted_links} | |||
def delete_contact_and_address(doctype, docname): | |||
for parenttype in ('Contact', 'Address'): | |||
items = frappe.db.sql_list("""select parent from `tabDynamic Link` | |||
for parenttype in ("Contact", "Address"): | |||
items = frappe.db.sql_list( | |||
"""select parent from `tabDynamic Link` | |||
where parenttype=%s and link_doctype=%s and link_name=%s""", | |||
(parenttype, doctype, docname)) | |||
(parenttype, doctype, docname), | |||
) | |||
for name in items: | |||
doc = frappe.get_doc(parenttype, name) | |||
if len(doc.links)==1: | |||
if len(doc.links) == 1: | |||
doc.delete() | |||
@frappe.whitelist() | |||
@frappe.validate_and_sanitize_search_inputs | |||
def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, filters): | |||
if not txt: txt = "" | |||
if not txt: | |||
txt = "" | |||
doctypes = frappe.db.get_all("DocField", filters=filters, fields=["parent"], | |||
distinct=True, as_list=True) | |||
doctypes = frappe.db.get_all( | |||
"DocField", filters=filters, fields=["parent"], distinct=True, as_list=True | |||
) | |||
doctypes = tuple(d for d in doctypes if re.search(txt+".*", _(d[0]), re.IGNORECASE)) | |||
doctypes = tuple(d for d in doctypes if re.search(txt + ".*", _(d[0]), re.IGNORECASE)) | |||
filters.update({ | |||
"dt": ("not in", [d[0] for d in doctypes]) | |||
}) | |||
filters.update({"dt": ("not in", [d[0] for d in doctypes])}) | |||
_doctypes = frappe.db.get_all("Custom Field", filters=filters, fields=["dt"], | |||
as_list=True) | |||
_doctypes = frappe.db.get_all("Custom Field", filters=filters, fields=["dt"], as_list=True) | |||
_doctypes = tuple([d for d in _doctypes if re.search(txt+".*", _(d[0]), re.IGNORECASE)]) | |||
_doctypes = tuple([d for d in _doctypes if re.search(txt + ".*", _(d[0]), re.IGNORECASE)]) | |||
all_doctypes = [d[0] for d in doctypes + _doctypes] | |||
allowed_doctypes = frappe.permissions.get_doctypes_with_read() | |||
@@ -172,6 +193,7 @@ def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, fil | |||
return valid_doctypes | |||
def set_link_title(doc): | |||
if not doc.links: | |||
return | |||
@@ -2,16 +2,15 @@ | |||
# Copyright (c) 2015, Frappe Technologies and contributors | |||
# License: MIT. See LICENSE | |||
import frappe | |||
from frappe import throw, _ | |||
from frappe.utils import cstr | |||
from jinja2 import TemplateSyntaxError | |||
import frappe | |||
from frappe import _, throw | |||
from frappe.contacts.address_and_contact import set_link_title | |||
from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links | |||
from frappe.model.document import Document | |||
from jinja2 import TemplateSyntaxError | |||
from frappe.model.naming import make_autoname | |||
from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links | |||
from frappe.contacts.address_and_contact import set_link_title | |||
from frappe.utils import cstr | |||
class Address(Document): | |||
@@ -24,10 +23,11 @@ class Address(Document): | |||
self.address_title = self.links[0].link_name | |||
if self.address_title: | |||
self.name = (cstr(self.address_title).strip() + "-" + cstr(_(self.address_type)).strip()) | |||
self.name = cstr(self.address_title).strip() + "-" + cstr(_(self.address_type)).strip() | |||
if frappe.db.exists("Address", self.name): | |||
self.name = make_autoname(cstr(self.address_title).strip() + "-" + | |||
cstr(self.address_type).strip() + "-.#") | |||
self.name = make_autoname( | |||
cstr(self.address_title).strip() + "-" + cstr(self.address_type).strip() + "-.#" | |||
) | |||
else: | |||
throw(_("Address Title is mandatory.")) | |||
@@ -42,15 +42,15 @@ class Address(Document): | |||
if not self.links: | |||
contact_name = frappe.db.get_value("Contact", {"email_id": self.owner}) | |||
if contact_name: | |||
contact = frappe.get_cached_doc('Contact', contact_name) | |||
contact = frappe.get_cached_doc("Contact", contact_name) | |||
for link in contact.links: | |||
self.append('links', dict(link_doctype=link.link_doctype, link_name=link.link_name)) | |||
self.append("links", dict(link_doctype=link.link_doctype, link_name=link.link_name)) | |||
return True | |||
return False | |||
def validate_preferred_address(self): | |||
preferred_fields = ['is_primary_address', 'is_shipping_address'] | |||
preferred_fields = ["is_primary_address", "is_shipping_address"] | |||
for field in preferred_fields: | |||
if self.get(field): | |||
@@ -76,9 +76,11 @@ class Address(Document): | |||
return False | |||
def get_preferred_address(doctype, name, preferred_key='is_primary_address'): | |||
if preferred_key in ['is_shipping_address', 'is_primary_address']: | |||
address = frappe.db.sql(""" SELECT | |||
def get_preferred_address(doctype, name, preferred_key="is_primary_address"): | |||
if preferred_key in ["is_shipping_address", "is_primary_address"]: | |||
address = frappe.db.sql( | |||
""" SELECT | |||
addr.name | |||
FROM | |||
`tabAddress` addr, `tabDynamic Link` dl | |||
@@ -86,27 +88,37 @@ def get_preferred_address(doctype, name, preferred_key='is_primary_address'): | |||
dl.parent = addr.name and dl.link_doctype = %s and | |||
dl.link_name = %s and ifnull(addr.disabled, 0) = 0 and | |||
%s = %s | |||
""" % ('%s', '%s', preferred_key, '%s'), (doctype, name, 1), as_dict=1) | |||
""" | |||
% ("%s", "%s", preferred_key, "%s"), | |||
(doctype, name, 1), | |||
as_dict=1, | |||
) | |||
if address: | |||
return address[0].name | |||
return | |||
@frappe.whitelist() | |||
def get_default_address(doctype, name, sort_key='is_primary_address'): | |||
'''Returns default Address name for the given doctype, name''' | |||
if sort_key not in ['is_shipping_address', 'is_primary_address']: | |||
def get_default_address(doctype, name, sort_key="is_primary_address"): | |||
"""Returns default Address name for the given doctype, name""" | |||
if sort_key not in ["is_shipping_address", "is_primary_address"]: | |||
return None | |||
out = frappe.db.sql(""" SELECT | |||
out = frappe.db.sql( | |||
""" SELECT | |||
addr.name, addr.%s | |||
FROM | |||
`tabAddress` addr, `tabDynamic Link` dl | |||
WHERE | |||
dl.parent = addr.name and dl.link_doctype = %s and | |||
dl.link_name = %s and ifnull(addr.disabled, 0) = 0 | |||
""" %(sort_key, '%s', '%s'), (doctype, name), as_dict=True) | |||
""" | |||
% (sort_key, "%s", "%s"), | |||
(doctype, name), | |||
as_dict=True, | |||
) | |||
if out: | |||
for contact in out: | |||
@@ -150,84 +162,96 @@ def get_territory_from_address(address): | |||
return territory | |||
def get_list_context(context=None): | |||
return { | |||
"title": _("Addresses"), | |||
"get_list": get_address_list, | |||
"row_template": "templates/includes/address_row.html", | |||
'no_breadcrumbs': True, | |||
"no_breadcrumbs": True, | |||
} | |||
def get_address_list(doctype, txt, filters, limit_start, limit_page_length = 20, order_by = None): | |||
def get_address_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by=None): | |||
from frappe.www.list import get_list | |||
user = frappe.session.user | |||
ignore_permissions = True | |||
if not filters: filters = [] | |||
if not filters: | |||
filters = [] | |||
filters.append(("Address", "owner", "=", user)) | |||
return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions) | |||
return get_list( | |||
doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions | |||
) | |||
def has_website_permission(doc, ptype, user, verbose=False): | |||
"""Returns true if there is a related lead or contact related to this document""" | |||
contact_name = frappe.db.get_value("Contact", {"email_id": frappe.session.user}) | |||
if contact_name: | |||
contact = frappe.get_doc('Contact', contact_name) | |||
contact = frappe.get_doc("Contact", contact_name) | |||
return contact.has_common_link(doc) | |||
return False | |||
def get_address_templates(address): | |||
result = frappe.db.get_value("Address Template", \ | |||
{"country": address.get("country")}, ["name", "template"]) | |||
result = frappe.db.get_value( | |||
"Address Template", {"country": address.get("country")}, ["name", "template"] | |||
) | |||
if not result: | |||
result = frappe.db.get_value("Address Template", \ | |||
{"is_default": 1}, ["name", "template"]) | |||
result = frappe.db.get_value("Address Template", {"is_default": 1}, ["name", "template"]) | |||
if not result: | |||
frappe.throw(_("No default Address Template found. Please create a new one from Setup > Printing and Branding > Address Template.")) | |||
frappe.throw( | |||
_( | |||
"No default Address Template found. Please create a new one from Setup > Printing and Branding > Address Template." | |||
) | |||
) | |||
else: | |||
return result | |||
def get_company_address(company): | |||
ret = frappe._dict() | |||
ret.company_address = get_default_address('Company', company) | |||
ret.company_address = get_default_address("Company", company) | |||
ret.company_address_display = get_address_display(ret.company_address) | |||
return ret | |||
@frappe.whitelist() | |||
@frappe.validate_and_sanitize_search_inputs | |||
def address_query(doctype, txt, searchfield, start, page_len, filters): | |||
from frappe.desk.reportview import get_match_cond | |||
link_doctype = filters.pop('link_doctype') | |||
link_name = filters.pop('link_name') | |||
link_doctype = filters.pop("link_doctype") | |||
link_name = filters.pop("link_name") | |||
condition = "" | |||
meta = frappe.get_meta("Address") | |||
for fieldname, value in filters.items(): | |||
if meta.get_field(fieldname) or fieldname in frappe.db.DEFAULT_COLUMNS: | |||
condition += " and {field}={value}".format( | |||
field=fieldname, | |||
value=frappe.db.escape(value)) | |||
condition += " and {field}={value}".format(field=fieldname, value=frappe.db.escape(value)) | |||
searchfields = meta.get_search_fields() | |||
if searchfield and (meta.get_field(searchfield)\ | |||
or searchfield in frappe.db.DEFAULT_COLUMNS): | |||
if searchfield and (meta.get_field(searchfield) or searchfield in frappe.db.DEFAULT_COLUMNS): | |||
searchfields.append(searchfield) | |||
search_condition = '' | |||
search_condition = "" | |||
for field in searchfields: | |||
if search_condition == '': | |||
search_condition += '`tabAddress`.`{field}` like %(txt)s'.format(field=field) | |||
if search_condition == "": | |||
search_condition += "`tabAddress`.`{field}` like %(txt)s".format(field=field) | |||
else: | |||
search_condition += ' or `tabAddress`.`{field}` like %(txt)s'.format(field=field) | |||
search_condition += " or `tabAddress`.`{field}` like %(txt)s".format(field=field) | |||
return frappe.db.sql("""select | |||
return frappe.db.sql( | |||
"""select | |||
`tabAddress`.name, `tabAddress`.city, `tabAddress`.country | |||
from | |||
`tabAddress`, `tabDynamic Link` | |||
@@ -245,19 +269,24 @@ def address_query(doctype, txt, searchfield, start, page_len, filters): | |||
limit %(start)s, %(page_len)s """.format( | |||
mcond=get_match_cond(doctype), | |||
key=searchfield, | |||
search_condition = search_condition, | |||
condition=condition or ""), { | |||
'txt': '%' + txt + '%', | |||
'_txt': txt.replace("%", ""), | |||
'start': start, | |||
'page_len': page_len, | |||
'link_name': link_name, | |||
'link_doctype': link_doctype | |||
}) | |||
search_condition=search_condition, | |||
condition=condition or "", | |||
), | |||
{ | |||
"txt": "%" + txt + "%", | |||
"_txt": txt.replace("%", ""), | |||
"start": start, | |||
"page_len": page_len, | |||
"link_name": link_name, | |||
"link_doctype": link_doctype, | |||
}, | |||
) | |||
def get_condensed_address(doc): | |||
fields = ["address_title", "address_line1", "address_line2", "city", "county", "state", "country"] | |||
return ", ".join(doc.get(d) for d in fields if doc.get(d)) | |||
def update_preferred_address(address, field): | |||
frappe.db.set_value('Address', address, field, 0) | |||
frappe.db.set_value("Address", address, field, 0) |
@@ -1,31 +1,32 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright (c) 2015, Frappe Technologies and Contributors | |||
# License: MIT. See LICENSE | |||
import frappe, unittest | |||
import unittest | |||
import frappe | |||
from frappe.contacts.doctype.address.address import get_address_display | |||
class TestAddress(unittest.TestCase): | |||
def test_template_works(self): | |||
if not frappe.db.exists('Address Template', 'India'): | |||
frappe.get_doc({ | |||
"doctype": "Address Template", | |||
"country": 'India', | |||
"is_default": 1 | |||
}).insert() | |||
if not frappe.db.exists("Address Template", "India"): | |||
frappe.get_doc({"doctype": "Address Template", "country": "India", "is_default": 1}).insert() | |||
if not frappe.db.exists('Address', '_Test Address-Office'): | |||
frappe.get_doc({ | |||
"address_line1": "_Test Address Line 1", | |||
"address_title": "_Test Address", | |||
"address_type": "Office", | |||
"city": "_Test City", | |||
"state": "Test State", | |||
"country": "India", | |||
"doctype": "Address", | |||
"is_primary_address": 1, | |||
"phone": "+91 0000000000" | |||
}).insert() | |||
if not frappe.db.exists("Address", "_Test Address-Office"): | |||
frappe.get_doc( | |||
{ | |||
"address_line1": "_Test Address Line 1", | |||
"address_title": "_Test Address", | |||
"address_type": "Office", | |||
"city": "_Test City", | |||
"state": "Test State", | |||
"country": "India", | |||
"doctype": "Address", | |||
"is_primary_address": 1, | |||
"phone": "+91 0000000000", | |||
} | |||
).insert() | |||
address = frappe.get_list("Address")[0].name | |||
display = get_address_display(frappe.get_doc("Address", address).as_dict()) | |||
self.assertTrue(display) | |||
self.assertTrue(display) |
@@ -3,21 +3,24 @@ | |||
# License: MIT. See LICENSE | |||
import frappe | |||
from frappe import _ | |||
from frappe.model.document import Document | |||
from frappe.utils import cint | |||
from frappe.utils.jinja import validate_template | |||
from frappe import _ | |||
class AddressTemplate(Document): | |||
def validate(self): | |||
if not self.template: | |||
self.template = get_default_address_template() | |||
self.defaults = frappe.db.get_values("Address Template", {"is_default":1, "name":("!=", self.name)}) | |||
self.defaults = frappe.db.get_values( | |||
"Address Template", {"is_default": 1, "name": ("!=", self.name)} | |||
) | |||
if not self.is_default: | |||
if not self.defaults: | |||
self.is_default = 1 | |||
if cint(frappe.db.get_single_value('System Settings', 'setup_complete')): | |||
if cint(frappe.db.get_single_value("System Settings", "setup_complete")): | |||
frappe.msgprint(_("Setting this Address Template as default as there is no other default")) | |||
validate_template(self.template) | |||
@@ -31,14 +34,23 @@ class AddressTemplate(Document): | |||
if self.is_default: | |||
frappe.throw(_("Default Address Template cannot be deleted")) | |||
@frappe.whitelist() | |||
def get_default_address_template(): | |||
'''Get default address template (translated)''' | |||
return '''{{ address_line1 }}<br>{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}\ | |||
"""Get default address template (translated)""" | |||
return ( | |||
"""{{ address_line1 }}<br>{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}\ | |||
{{ city }}<br> | |||
{% if state %}{{ state }}<br>{% endif -%} | |||
{% if pincode %}{{ pincode }}<br>{% endif -%} | |||
{{ country }}<br> | |||
{% if phone %}'''+_('Phone')+''': {{ phone }}<br>{% endif -%} | |||
{% if fax %}'''+_('Fax')+''': {{ fax }}<br>{% endif -%} | |||
{% if email_id %}'''+_('Email')+''': {{ email_id }}<br>{% endif -%}''' | |||
{% if phone %}""" | |||
+ _("Phone") | |||
+ """: {{ phone }}<br>{% endif -%} | |||
{% if fax %}""" | |||
+ _("Fax") | |||
+ """: {{ fax }}<br>{% endif -%} | |||
{% if email_id %}""" | |||
+ _("Email") | |||
+ """: {{ email_id }}<br>{% endif -%}""" | |||
) |
@@ -1,7 +1,10 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright (c) 2015, Frappe Technologies and Contributors | |||
# License: MIT. See LICENSE | |||
import frappe, unittest | |||
import unittest | |||
import frappe | |||
class TestAddressTemplate(unittest.TestCase): | |||
def setUp(self): | |||
@@ -27,17 +30,12 @@ class TestAddressTemplate(unittest.TestCase): | |||
def make_default_address_template(self): | |||
template = """{{ address_line1 }}<br>{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}{{ city }}<br>{% if state %}{{ state }}<br>{% endif -%}{% if pincode %}{{ pincode }}<br>{% endif -%}{{ country }}<br>{% if phone %}Phone: {{ phone }}<br>{% endif -%}{% if fax %}Fax: {{ fax }}<br>{% endif -%}{% if email_id %}Email: {{ email_id }}<br>{% endif -%}""" | |||
if not frappe.db.exists('Address Template', 'India'): | |||
frappe.get_doc({ | |||
"doctype": "Address Template", | |||
"country": 'India', | |||
"is_default": 1, | |||
"template": template | |||
}).insert() | |||
if not frappe.db.exists('Address Template', 'Brazil'): | |||
frappe.get_doc({ | |||
"doctype": "Address Template", | |||
"country": 'Brazil', | |||
"template": template | |||
}).insert() | |||
if not frappe.db.exists("Address Template", "India"): | |||
frappe.get_doc( | |||
{"doctype": "Address Template", "country": "India", "is_default": 1, "template": template} | |||
).insert() | |||
if not frappe.db.exists("Address Template", "Brazil"): | |||
frappe.get_doc( | |||
{"doctype": "Address Template", "country": "Brazil", "template": template} | |||
).insert() |
@@ -1,26 +1,27 @@ | |||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
import frappe | |||
from frappe.utils import cstr, has_gravatar | |||
from frappe import _ | |||
from frappe.model.document import Document | |||
from frappe.contacts.address_and_contact import set_link_title | |||
from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links | |||
from frappe.model.document import Document | |||
from frappe.model.naming import append_number_if_name_exists | |||
from frappe.contacts.address_and_contact import set_link_title | |||
from frappe.utils import cstr, has_gravatar | |||
class Contact(Document): | |||
def autoname(self): | |||
# concat first and last name | |||
self.name = " ".join(filter(None, | |||
[cstr(self.get(f)).strip() for f in ["first_name", "last_name"]])) | |||
self.name = " ".join( | |||
filter(None, [cstr(self.get(f)).strip() for f in ["first_name", "last_name"]]) | |||
) | |||
if frappe.db.exists("Contact", self.name): | |||
self.name = append_number_if_name_exists('Contact', self.name) | |||
self.name = append_number_if_name_exists("Contact", self.name) | |||
# concat party name if reqd | |||
for link in self.links: | |||
self.name = self.name + '-' + link.link_name.strip() | |||
self.name = self.name + "-" + link.link_name.strip() | |||
break | |||
def validate(self): | |||
@@ -45,7 +46,7 @@ class Contact(Document): | |||
self.user = frappe.db.get_value("User", {"email": self.email_id}) | |||
def get_link_for(self, link_doctype): | |||
'''Return the link name, if exists for the given link DocType''' | |||
"""Return the link name, if exists for the given link DocType""" | |||
for link in self.links: | |||
if link.link_doctype == link_doctype: | |||
return link.link_name | |||
@@ -65,21 +66,21 @@ class Contact(Document): | |||
def add_email(self, email_id, is_primary=0, autosave=False): | |||
if not frappe.db.exists("Contact Email", {"email_id": email_id, "parent": self.name}): | |||
self.append("email_ids", { | |||
"email_id": email_id, | |||
"is_primary": is_primary | |||
}) | |||
self.append("email_ids", {"email_id": email_id, "is_primary": is_primary}) | |||
if autosave: | |||
self.save(ignore_permissions=True) | |||
def add_phone(self, phone, is_primary_phone=0, is_primary_mobile_no=0, autosave=False): | |||
if not frappe.db.exists("Contact Phone", {"phone": phone, "parent": self.name}): | |||
self.append("phone_nos", { | |||
"phone": phone, | |||
"is_primary_phone": is_primary_phone, | |||
"is_primary_mobile_no": is_primary_mobile_no | |||
}) | |||
self.append( | |||
"phone_nos", | |||
{ | |||
"phone": phone, | |||
"is_primary_phone": is_primary_phone, | |||
"is_primary_mobile_no": is_primary_mobile_no, | |||
}, | |||
) | |||
if autosave: | |||
self.save(ignore_permissions=True) | |||
@@ -113,7 +114,9 @@ class Contact(Document): | |||
is_primary = [phone.phone for phone in self.phone_nos if phone.get(field_name)] | |||
if len(is_primary) > 1: | |||
frappe.throw(_("Only one {0} can be set as primary.").format(frappe.bold(frappe.unscrub(fieldname)))) | |||
frappe.throw( | |||
_("Only one {0} can be set as primary.").format(frappe.bold(frappe.unscrub(fieldname))) | |||
) | |||
primary_number_exists = False | |||
for d in self.phone_nos: | |||
@@ -125,9 +128,11 @@ class Contact(Document): | |||
if not primary_number_exists: | |||
setattr(self, fieldname, "") | |||
def get_default_contact(doctype, name): | |||
'''Returns default contact for the given doctype, name''' | |||
out = frappe.db.sql('''select parent, | |||
"""Returns default contact for the given doctype, name""" | |||
out = frappe.db.sql( | |||
'''select parent, | |||
IFNULL((select is_primary_contact from tabContact c where c.name = dl.parent), 0) | |||
as is_primary_contact | |||
from | |||
@@ -135,7 +140,10 @@ def get_default_contact(doctype, name): | |||
where | |||
dl.link_doctype=%s and | |||
dl.link_name=%s and | |||
dl.parenttype = "Contact"''', (doctype, name), as_dict=True) | |||
dl.parenttype = "Contact"''', | |||
(doctype, name), | |||
as_dict=True, | |||
) | |||
if out: | |||
for contact in out: | |||
@@ -145,6 +153,7 @@ def get_default_contact(doctype, name): | |||
else: | |||
return None | |||
@frappe.whitelist() | |||
def invite_user(contact): | |||
contact = frappe.get_doc("Contact", contact) | |||
@@ -153,34 +162,39 @@ def invite_user(contact): | |||
frappe.throw(_("Please set Email Address")) | |||
if contact.has_permission("write"): | |||
user = frappe.get_doc({ | |||
"doctype": "User", | |||
"first_name": contact.first_name, | |||
"last_name": contact.last_name, | |||
"email": contact.email_id, | |||
"user_type": "Website User", | |||
"send_welcome_email": 1 | |||
}).insert(ignore_permissions = True) | |||
user = frappe.get_doc( | |||
{ | |||
"doctype": "User", | |||
"first_name": contact.first_name, | |||
"last_name": contact.last_name, | |||
"email": contact.email_id, | |||
"user_type": "Website User", | |||
"send_welcome_email": 1, | |||
} | |||
).insert(ignore_permissions=True) | |||
return user.name | |||
@frappe.whitelist() | |||
def get_contact_details(contact): | |||
contact = frappe.get_doc("Contact", contact) | |||
out = { | |||
"contact_person": contact.get("name"), | |||
"contact_display": " ".join(filter(None, | |||
[contact.get("salutation"), contact.get("first_name"), contact.get("last_name")])), | |||
"contact_display": " ".join( | |||
filter(None, [contact.get("salutation"), contact.get("first_name"), contact.get("last_name")]) | |||
), | |||
"contact_email": contact.get("email_id"), | |||
"contact_mobile": contact.get("mobile_no"), | |||
"contact_phone": contact.get("phone"), | |||
"contact_designation": contact.get("designation"), | |||
"contact_department": contact.get("department") | |||
"contact_department": contact.get("department"), | |||
} | |||
return out | |||
def update_contact(doc, method): | |||
'''Update contact when user is updated, if contact is found. Called via hooks''' | |||
"""Update contact when user is updated, if contact is found. Called via hooks""" | |||
contact_name = frappe.db.get_value("Contact", {"email_id": doc.name}) | |||
if contact_name: | |||
contact = frappe.get_doc("Contact", contact_name) | |||
@@ -190,19 +204,23 @@ def update_contact(doc, method): | |||
contact.flags.ignore_mandatory = True | |||
contact.save(ignore_permissions=True) | |||
@frappe.whitelist() | |||
@frappe.validate_and_sanitize_search_inputs | |||
def contact_query(doctype, txt, searchfield, start, page_len, filters): | |||
from frappe.desk.reportview import get_match_cond | |||
if not frappe.get_meta("Contact").get_field(searchfield)\ | |||
and searchfield not in frappe.db.DEFAULT_COLUMNS: | |||
if ( | |||
not frappe.get_meta("Contact").get_field(searchfield) | |||
and searchfield not in frappe.db.DEFAULT_COLUMNS | |||
): | |||
return [] | |||
link_doctype = filters.pop('link_doctype') | |||
link_name = filters.pop('link_name') | |||
link_doctype = filters.pop("link_doctype") | |||
link_name = filters.pop("link_name") | |||
return frappe.db.sql("""select | |||
return frappe.db.sql( | |||
"""select | |||
`tabContact`.name, `tabContact`.first_name, `tabContact`.last_name | |||
from | |||
`tabContact`, `tabDynamic Link` | |||
@@ -216,68 +234,90 @@ def contact_query(doctype, txt, searchfield, start, page_len, filters): | |||
order by | |||
if(locate(%(_txt)s, `tabContact`.name), locate(%(_txt)s, `tabContact`.name), 99999), | |||
`tabContact`.idx desc, `tabContact`.name | |||
limit %(start)s, %(page_len)s """.format(mcond=get_match_cond(doctype), key=searchfield), { | |||
'txt': '%' + txt + '%', | |||
'_txt': txt.replace("%", ""), | |||
'start': start, | |||
'page_len': page_len, | |||
'link_name': link_name, | |||
'link_doctype': link_doctype | |||
}) | |||
limit %(start)s, %(page_len)s """.format( | |||
mcond=get_match_cond(doctype), key=searchfield | |||
), | |||
{ | |||
"txt": "%" + txt + "%", | |||
"_txt": txt.replace("%", ""), | |||
"start": start, | |||
"page_len": page_len, | |||
"link_name": link_name, | |||
"link_doctype": link_doctype, | |||
}, | |||
) | |||
@frappe.whitelist() | |||
def address_query(links): | |||
import json | |||
links = [{"link_doctype": d.get("link_doctype"), "link_name": d.get("link_name")} for d in json.loads(links)] | |||
links = [ | |||
{"link_doctype": d.get("link_doctype"), "link_name": d.get("link_name")} | |||
for d in json.loads(links) | |||
] | |||
result = [] | |||
for link in links: | |||
if not frappe.has_permission(doctype=link.get("link_doctype"), ptype="read", doc=link.get("link_name")): | |||
if not frappe.has_permission( | |||
doctype=link.get("link_doctype"), ptype="read", doc=link.get("link_name") | |||
): | |||
continue | |||
res = frappe.db.sql(""" | |||
res = frappe.db.sql( | |||
""" | |||
SELECT `tabAddress`.name | |||
FROM `tabAddress`, `tabDynamic Link` | |||
WHERE `tabDynamic Link`.parenttype='Address' | |||
AND `tabDynamic Link`.parent=`tabAddress`.name | |||
AND `tabDynamic Link`.link_doctype = %(link_doctype)s | |||
AND `tabDynamic Link`.link_name = %(link_name)s | |||
""", { | |||
"link_doctype": link.get("link_doctype"), | |||
"link_name": link.get("link_name"), | |||
}, as_dict=True) | |||
""", | |||
{ | |||
"link_doctype": link.get("link_doctype"), | |||
"link_name": link.get("link_name"), | |||
}, | |||
as_dict=True, | |||
) | |||
result.extend([l.name for l in res]) | |||
return result | |||
def get_contact_with_phone_number(number): | |||
if not number: return | |||
if not number: | |||
return | |||
contacts = frappe.get_all('Contact Phone', filters=[ | |||
['phone', 'like', '%{0}'.format(number)] | |||
], fields=["parent"], limit=1) | |||
contacts = frappe.get_all( | |||
"Contact Phone", filters=[["phone", "like", "%{0}".format(number)]], fields=["parent"], limit=1 | |||
) | |||
return contacts[0].parent if contacts else None | |||
def get_contact_name(email_id): | |||
contact = frappe.get_all("Contact Email", filters={"email_id": email_id}, fields=["parent"], limit=1) | |||
contact = frappe.get_all( | |||
"Contact Email", filters={"email_id": email_id}, fields=["parent"], limit=1 | |||
) | |||
return contact[0].parent if contact else None | |||
def get_contacts_linking_to(doctype, docname, fields=None): | |||
"""Return a list of contacts containing a link to the given document.""" | |||
return frappe.get_list('Contact', fields=fields, filters=[ | |||
['Dynamic Link', 'link_doctype', '=', doctype], | |||
['Dynamic Link', 'link_name', '=', docname] | |||
]) | |||
return frappe.get_list( | |||
"Contact", | |||
fields=fields, | |||
filters=[ | |||
["Dynamic Link", "link_doctype", "=", doctype], | |||
["Dynamic Link", "link_name", "=", docname], | |||
], | |||
) | |||
def get_contacts_linked_from(doctype, docname, fields=None): | |||
"""Return a list of contacts that are contained in (linked from) the given document.""" | |||
link_fields = frappe.get_meta(doctype).get('fields', { | |||
'fieldtype': 'Link', | |||
'options': 'Contact' | |||
}) | |||
link_fields = frappe.get_meta(doctype).get("fields", {"fieldtype": "Link", "options": "Contact"}) | |||
if not link_fields: | |||
return [] | |||
@@ -285,6 +325,4 @@ def get_contacts_linked_from(doctype, docname, fields=None): | |||
if not contact_names: | |||
return [] | |||
return frappe.get_list('Contact', fields=fields, filters={ | |||
'name': ('in', contact_names) | |||
}) | |||
return frappe.get_list("Contact", fields=fields, filters={"name": ("in", contact_names)}) |
@@ -1,13 +1,14 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright (c) 2017, Frappe Technologies and Contributors | |||
# License: MIT. See LICENSE | |||
import frappe | |||
import unittest | |||
test_dependencies = ['Contact', 'Salutation'] | |||
import frappe | |||
class TestContact(unittest.TestCase): | |||
test_dependencies = ["Contact", "Salutation"] | |||
class TestContact(unittest.TestCase): | |||
def test_check_default_email(self): | |||
emails = [ | |||
{"email": "test1@example.com", "is_primary": 0}, | |||
@@ -32,13 +33,11 @@ class TestContact(unittest.TestCase): | |||
self.assertEqual(contact.phone, "+91 0000000002") | |||
self.assertEqual(contact.mobile_no, "+91 0000000003") | |||
def create_contact(name, salutation, emails=None, phones=None, save=True): | |||
doc = frappe.get_doc({ | |||
"doctype": "Contact", | |||
"first_name": name, | |||
"status": "Open", | |||
"salutation": salutation | |||
}) | |||
doc = frappe.get_doc( | |||
{"doctype": "Contact", "first_name": name, "status": "Open", "salutation": salutation} | |||
) | |||
if emails: | |||
for d in emails: | |||
@@ -5,5 +5,6 @@ | |||
# import frappe | |||
from frappe.model.document import Document | |||
class ContactEmail(Document): | |||
pass |
@@ -5,5 +5,6 @@ | |||
# import frappe | |||
from frappe.model.document import Document | |||
class ContactPhone(Document): | |||
pass |
@@ -4,5 +4,6 @@ | |||
from frappe.model.document import Document | |||
class Gender(Document): | |||
pass |
@@ -3,5 +3,6 @@ | |||
# License: MIT. See LICENSE | |||
import unittest | |||
class TestGender(unittest.TestCase): | |||
pass |
@@ -4,5 +4,6 @@ | |||
from frappe.model.document import Document | |||
class Salutation(Document): | |||
pass |
@@ -3,5 +3,6 @@ | |||
# License: MIT. See LICENSE | |||
import unittest | |||
class TestSalutation(unittest.TestCase): | |||
pass |
@@ -4,17 +4,37 @@ import frappe | |||
from frappe import _ | |||
field_map = { | |||
"Contact": ["first_name", "last_name", "address", "phone", "mobile_no", "email_id", "is_primary_contact"], | |||
"Address": ["address_line1", "address_line2", "city", "state", "pincode", "country", "is_primary_address"] | |||
"Contact": [ | |||
"first_name", | |||
"last_name", | |||
"address", | |||
"phone", | |||
"mobile_no", | |||
"email_id", | |||
"is_primary_contact", | |||
], | |||
"Address": [ | |||
"address_line1", | |||
"address_line2", | |||
"city", | |||
"state", | |||
"pincode", | |||
"country", | |||
"is_primary_address", | |||
], | |||
} | |||
def execute(filters=None): | |||
columns, data = get_columns(filters), get_data(filters) | |||
return columns, data | |||
def get_columns(filters): | |||
return [ | |||
"{reference_doctype}:Link/{reference_doctype}".format(reference_doctype=filters.get("reference_doctype")), | |||
"{reference_doctype}:Link/{reference_doctype}".format( | |||
reference_doctype=filters.get("reference_doctype") | |||
), | |||
"Address Line 1", | |||
"Address Line 2", | |||
"City", | |||
@@ -27,9 +47,10 @@ def get_columns(filters): | |||
"Address", | |||
"Phone", | |||
"Email Id", | |||
"Is Primary Contact:Check" | |||
"Is Primary Contact:Check", | |||
] | |||
def get_data(filters): | |||
data = [] | |||
reference_doctype = filters.get("reference_doctype") | |||
@@ -37,6 +58,7 @@ def get_data(filters): | |||
return get_reference_addresses_and_contact(reference_doctype, reference_name) | |||
def get_reference_addresses_and_contact(reference_doctype, reference_name): | |||
data = [] | |||
filters = None | |||
@@ -48,16 +70,22 @@ def get_reference_addresses_and_contact(reference_doctype, reference_name): | |||
if reference_name: | |||
filters = {"name": reference_name} | |||
reference_list = [d[0] for d in frappe.get_list(reference_doctype, filters=filters, fields=["name"], as_list=True)] | |||
reference_list = [ | |||
d[0] for d in frappe.get_list(reference_doctype, filters=filters, fields=["name"], as_list=True) | |||
] | |||
for d in reference_list: | |||
reference_details.setdefault(d, frappe._dict()) | |||
reference_details = get_reference_details(reference_doctype, "Address", reference_list, reference_details) | |||
reference_details = get_reference_details(reference_doctype, "Contact", reference_list, reference_details) | |||
reference_details = get_reference_details( | |||
reference_doctype, "Address", reference_list, reference_details | |||
) | |||
reference_details = get_reference_details( | |||
reference_doctype, "Contact", reference_list, reference_details | |||
) | |||
for reference_name, details in reference_details.items(): | |||
addresses = details.get("address", []) | |||
contacts = details.get("contact", []) | |||
contacts = details.get("contact", []) | |||
if not any([addresses, contacts]): | |||
result = [reference_name] | |||
result.extend(add_blank_columns_for("Address")) | |||
@@ -78,10 +106,11 @@ def get_reference_addresses_and_contact(reference_doctype, reference_name): | |||
return data | |||
def get_reference_details(reference_doctype, doctype, reference_list, reference_details): | |||
filters = [ | |||
filters = [ | |||
["Dynamic Link", "link_doctype", "=", reference_doctype], | |||
["Dynamic Link", "link_name", "in", reference_list] | |||
["Dynamic Link", "link_name", "in", reference_list], | |||
] | |||
fields = ["`tabDynamic Link`.link_name"] + field_map.get(doctype, []) | |||
@@ -97,5 +126,6 @@ def get_reference_details(reference_doctype, doctype, reference_list, reference_ | |||
reference_details[reference_list[0]][frappe.scrub(doctype)] = temp_records | |||
return reference_details | |||
def add_blank_columns_for(doctype): | |||
return ["" for field in field_map.get(doctype, [])] |
@@ -1,95 +1,87 @@ | |||
import unittest | |||
import frappe | |||
import frappe.defaults | |||
import unittest | |||
from frappe.contacts.report.addresses_and_contacts.addresses_and_contacts import get_data | |||
def get_custom_linked_doctype(): | |||
if bool(frappe.get_all("DocType", filters={'name':'Test Custom Doctype'})): | |||
if bool(frappe.get_all("DocType", filters={"name": "Test Custom Doctype"})): | |||
return | |||
doc = frappe.get_doc({ | |||
"doctype": "DocType", | |||
"module": "Core", | |||
"custom": 1, | |||
"fields": [{ | |||
"label": "Test Field", | |||
"fieldname": "test_field", | |||
"fieldtype": "Data" | |||
}, | |||
{ | |||
"label": "Contact HTML", | |||
"fieldname": "contact_html", | |||
"fieldtype": "HTML" | |||
}, | |||
doc = frappe.get_doc( | |||
{ | |||
"label": "Address HTML", | |||
"fieldname": "address_html", | |||
"fieldtype": "HTML" | |||
}], | |||
"permissions": [{ | |||
"role": "System Manager", | |||
"read": 1 | |||
}], | |||
"name": "Test Custom Doctype", | |||
}) | |||
"doctype": "DocType", | |||
"module": "Core", | |||
"custom": 1, | |||
"fields": [ | |||
{"label": "Test Field", "fieldname": "test_field", "fieldtype": "Data"}, | |||
{"label": "Contact HTML", "fieldname": "contact_html", "fieldtype": "HTML"}, | |||
{"label": "Address HTML", "fieldname": "address_html", "fieldtype": "HTML"}, | |||
], | |||
"permissions": [{"role": "System Manager", "read": 1}], | |||
"name": "Test Custom Doctype", | |||
} | |||
) | |||
doc.insert() | |||
def get_custom_doc_for_address_and_contacts(): | |||
get_custom_linked_doctype() | |||
linked_doc = frappe.get_doc({ | |||
"doctype": "Test Custom Doctype", | |||
"test_field": "Hello", | |||
}).insert() | |||
linked_doc = frappe.get_doc( | |||
{ | |||
"doctype": "Test Custom Doctype", | |||
"test_field": "Hello", | |||
} | |||
).insert() | |||
return linked_doc | |||
def create_linked_address(link_list): | |||
if frappe.flags.test_address_created: | |||
return | |||
address = frappe.get_doc({ | |||
"doctype": "Address", | |||
"address_title": "_Test Address", | |||
"address_type": "Billing", | |||
"address_line1": "test address line 1", | |||
"address_line2": "test address line 2", | |||
"city": "Milan", | |||
"country": "Italy" | |||
}) | |||
address = frappe.get_doc( | |||
{ | |||
"doctype": "Address", | |||
"address_title": "_Test Address", | |||
"address_type": "Billing", | |||
"address_line1": "test address line 1", | |||
"address_line2": "test address line 2", | |||
"city": "Milan", | |||
"country": "Italy", | |||
} | |||
) | |||
for name in link_list: | |||
address.append("links",{ | |||
'link_doctype': 'Test Custom Doctype', | |||
'link_name': name | |||
}) | |||
address.append("links", {"link_doctype": "Test Custom Doctype", "link_name": name}) | |||
address.insert() | |||
frappe.flags.test_address_created = True | |||
return address.name | |||
def create_linked_contact(link_list, address): | |||
if frappe.flags.test_contact_created: | |||
return | |||
contact = frappe.get_doc({ | |||
"doctype": "Contact", | |||
"salutation": "Mr", | |||
"first_name": "_Test First Name", | |||
"last_name": "_Test Last Name", | |||
"is_primary_contact": 1, | |||
"address": address, | |||
"status": "Open" | |||
}) | |||
contact = frappe.get_doc( | |||
{ | |||
"doctype": "Contact", | |||
"salutation": "Mr", | |||
"first_name": "_Test First Name", | |||
"last_name": "_Test Last Name", | |||
"is_primary_contact": 1, | |||
"address": address, | |||
"status": "Open", | |||
} | |||
) | |||
contact.add_email("test_contact@example.com", is_primary=True) | |||
contact.add_phone("+91 0000000000", is_primary_phone=True) | |||
for name in link_list: | |||
contact.append("links",{ | |||
'link_doctype': 'Test Custom Doctype', | |||
'link_name': name | |||
}) | |||
contact.append("links", {"link_doctype": "Test Custom Doctype", "link_name": name}) | |||
contact.insert(ignore_permissions=True) | |||
frappe.flags.test_contact_created = True | |||
@@ -103,7 +95,23 @@ class TestAddressesAndContacts(unittest.TestCase): | |||
create_linked_contact(links_list, d) | |||
report_data = get_data({"reference_doctype": "Test Custom Doctype"}) | |||
for idx, link in enumerate(links_list): | |||
test_item = [link, 'test address line 1', 'test address line 2', 'Milan', None, None, 'Italy', 0, '_Test First Name', '_Test Last Name', '_Test Address-Billing', '+91 0000000000', '', 'test_contact@example.com', 1] | |||
test_item = [ | |||
link, | |||
"test address line 1", | |||
"test address line 2", | |||
"Milan", | |||
None, | |||
None, | |||
"Italy", | |||
0, | |||
"_Test First Name", | |||
"_Test Last Name", | |||
"_Test Address-Billing", | |||
"+91 0000000000", | |||
"", | |||
"test_contact@example.com", | |||
1, | |||
] | |||
self.assertListEqual(test_item, report_data[idx]) | |||
def tearDown(self): | |||
@@ -1,3 +1,2 @@ | |||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
@@ -1,9 +1,10 @@ | |||
# Copyright (c) 2021, Frappe Technologies and contributors | |||
# License: MIT. See LICENSE | |||
import frappe | |||
from frappe.utils import cstr | |||
from tenacity import retry, retry_if_exception_type, stop_after_attempt | |||
import frappe | |||
from frappe.model.document import Document | |||
from frappe.utils import cstr | |||
class AccessLog(Document): | |||
@@ -22,14 +23,19 @@ def make_access_log( | |||
columns=None, | |||
): | |||
_make_access_log( | |||
doctype, document, method, file_type, report_name, filters, page, columns, | |||
doctype, | |||
document, | |||
method, | |||
file_type, | |||
report_name, | |||
filters, | |||
page, | |||
columns, | |||
) | |||
@frappe.write_only() | |||
@retry( | |||
stop=stop_after_attempt(3), retry=retry_if_exception_type(frappe.DuplicateEntryError) | |||
) | |||
@retry(stop=stop_after_attempt(3), retry=retry_if_exception_type(frappe.DuplicateEntryError)) | |||
def _make_access_log( | |||
doctype=None, | |||
document=None, | |||
@@ -43,18 +49,20 @@ def _make_access_log( | |||
user = frappe.session.user | |||
in_request = frappe.request and frappe.request.method == "GET" | |||
frappe.get_doc({ | |||
"doctype": "Access Log", | |||
"user": user, | |||
"export_from": doctype, | |||
"reference_document": document, | |||
"file_type": file_type, | |||
"report_name": report_name, | |||
"page": page, | |||
"method": method, | |||
"filters": cstr(filters) or None, | |||
"columns": columns, | |||
}).db_insert() | |||
frappe.get_doc( | |||
{ | |||
"doctype": "Access Log", | |||
"user": user, | |||
"export_from": doctype, | |||
"reference_document": document, | |||
"file_type": file_type, | |||
"report_name": report_name, | |||
"page": page, | |||
"method": method, | |||
"filters": cstr(filters) or None, | |||
"columns": columns, | |||
} | |||
).db_insert() | |||
# `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview` | |||
# dont commit in test mode. It must be tempting to put this block along with the in_request in the | |||
@@ -2,20 +2,21 @@ | |||
# Copyright (c) 2019, Frappe Technologies and Contributors | |||
# License: MIT. See LICENSE | |||
# imports - standard imports | |||
import unittest | |||
import base64 | |||
import os | |||
# imports - standard imports | |||
import unittest | |||
# imports - third party imports | |||
import requests | |||
# imports - module imports | |||
import frappe | |||
from frappe.core.doctype.access_log.access_log import make_access_log | |||
from frappe.utils import cstr, get_site_url | |||
from frappe.core.doctype.data_import.data_import import export_csv | |||
from frappe.core.doctype.user.user import generate_keys | |||
# imports - third party imports | |||
import requests | |||
from frappe.utils import cstr, get_site_url | |||
class TestAccessLog(unittest.TestCase): | |||
@@ -23,8 +24,9 @@ class TestAccessLog(unittest.TestCase): | |||
# generate keys for current user to send requests for the following tests | |||
generate_keys(frappe.session.user) | |||
frappe.db.commit() | |||
generated_secret = frappe.utils.password.get_decrypted_password("User", | |||
frappe.session.user, fieldname='api_secret') | |||
generated_secret = frappe.utils.password.get_decrypted_password( | |||
"User", frappe.session.user, fieldname="api_secret" | |||
) | |||
api_key = frappe.db.get_value("User", "Administrator", "api_key") | |||
self.header = {"Authorization": "token {}:{}".format(api_key, generated_secret)} | |||
@@ -101,54 +103,55 @@ class TestAccessLog(unittest.TestCase): | |||
"party": [], | |||
"group_by": "Group by Voucher (Consolidated)", | |||
"cost_center": [], | |||
"project": [] | |||
"project": [], | |||
} | |||
self.test_doctype = 'File' | |||
self.test_document = 'Test Document' | |||
self.test_report_name = 'General Ledger' | |||
self.test_file_type = 'CSV' | |||
self.test_method = 'Test Method' | |||
self.file_name = frappe.utils.random_string(10) + '.txt' | |||
self.test_doctype = "File" | |||
self.test_document = "Test Document" | |||
self.test_report_name = "General Ledger" | |||
self.test_file_type = "CSV" | |||
self.test_method = "Test Method" | |||
self.file_name = frappe.utils.random_string(10) + ".txt" | |||
self.test_content = frappe.utils.random_string(1024) | |||
def test_make_full_access_log(self): | |||
self.maxDiff = None | |||
# test if all fields maintain data: html page and filters are converted? | |||
make_access_log(doctype=self.test_doctype, | |||
make_access_log( | |||
doctype=self.test_doctype, | |||
document=self.test_document, | |||
report_name=self.test_report_name, | |||
page=self.test_html_template, | |||
file_type=self.test_file_type, | |||
method=self.test_method, | |||
filters=self.test_filters) | |||
filters=self.test_filters, | |||
) | |||
last_doc = frappe.get_last_doc('Access Log') | |||
last_doc = frappe.get_last_doc("Access Log") | |||
self.assertEqual(last_doc.filters, cstr(self.test_filters)) | |||
self.assertEqual(self.test_doctype, last_doc.export_from) | |||
self.assertEqual(self.test_document, last_doc.reference_document) | |||
def test_make_export_log(self): | |||
# export data and delete temp file generated on disk | |||
export_csv(self.test_doctype, self.file_name) | |||
os.remove(self.file_name) | |||
# test if the exported data is logged | |||
last_doc = frappe.get_last_doc('Access Log') | |||
last_doc = frappe.get_last_doc("Access Log") | |||
self.assertEqual(self.test_doctype, last_doc.export_from) | |||
def test_private_file_download(self): | |||
# create new private file | |||
new_private_file = frappe.get_doc({ | |||
'doctype': self.test_doctype, | |||
'file_name': self.file_name, | |||
'content': base64.b64encode(self.test_content.encode('utf-8')), | |||
'is_private': 1, | |||
}) | |||
new_private_file = frappe.get_doc( | |||
{ | |||
"doctype": self.test_doctype, | |||
"file_name": self.file_name, | |||
"content": base64.b64encode(self.test_content.encode("utf-8")), | |||
"is_private": 1, | |||
} | |||
) | |||
new_private_file.insert() | |||
# access the created file | |||
@@ -156,7 +159,7 @@ class TestAccessLog(unittest.TestCase): | |||
try: | |||
request = requests.post(private_file_link, headers=self.header) | |||
last_doc = frappe.get_last_doc('Access Log') | |||
last_doc = frappe.get_last_doc("Access Log") | |||
if request.ok: | |||
# check for the access log of downloaded file | |||
@@ -169,6 +172,5 @@ class TestAccessLog(unittest.TestCase): | |||
# cleanup | |||
new_private_file.delete() | |||
def tearDown(self): | |||
pass |
@@ -26,20 +26,25 @@ class ActivityLog(Document): | |||
if self.reference_doctype and self.reference_name: | |||
self.status = "Linked" | |||
def on_doctype_update(): | |||
"""Add indexes in `tabActivity Log`""" | |||
frappe.db.add_index("Activity Log", ["reference_doctype", "reference_name"]) | |||
frappe.db.add_index("Activity Log", ["timeline_doctype", "timeline_name"]) | |||
frappe.db.add_index("Activity Log", ["link_doctype", "link_name"]) | |||
def add_authentication_log(subject, user, operation="Login", status="Success"): | |||
frappe.get_doc({ | |||
"doctype": "Activity Log", | |||
"user": user, | |||
"status": status, | |||
"subject": subject, | |||
"operation": operation, | |||
}).insert(ignore_permissions=True, ignore_links=True) | |||
frappe.get_doc( | |||
{ | |||
"doctype": "Activity Log", | |||
"user": user, | |||
"status": status, | |||
"subject": subject, | |||
"operation": operation, | |||
} | |||
).insert(ignore_permissions=True, ignore_links=True) | |||
def clear_activity_logs(days=None): | |||
"""clear 90 day old authentication logs or configured in log settings""" | |||
@@ -47,6 +52,4 @@ def clear_activity_logs(days=None): | |||
if not days: | |||
days = 90 | |||
doctype = DocType("Activity Log") | |||
frappe.db.delete(doctype, filters=( | |||
doctype.creation < (Now() - Interval(days=days)) | |||
)) | |||
frappe.db.delete(doctype, filters=(doctype.creation < (Now() - Interval(days=days)))) |
@@ -3,15 +3,16 @@ | |||
import frappe | |||
import frappe.permissions | |||
from frappe.utils import get_fullname | |||
from frappe import _ | |||
from frappe.core.doctype.activity_log.activity_log import add_authentication_log | |||
from frappe.utils import get_fullname | |||
def update_feed(doc, method=None): | |||
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import: | |||
return | |||
if doc._action!="save" or doc.flags.ignore_feed: | |||
if doc._action != "save" or doc.flags.ignore_feed: | |||
return | |||
if doc.doctype == "Activity Log" or doc.meta.issingle: | |||
@@ -29,65 +30,75 @@ def update_feed(doc, method=None): | |||
name = feed.name or doc.name | |||
# delete earlier feed | |||
frappe.db.delete("Activity Log", { | |||
"reference_doctype": doctype, | |||
"reference_name": name, | |||
"link_doctype": feed.link_doctype | |||
}) | |||
frappe.get_doc({ | |||
"doctype": "Activity Log", | |||
"reference_doctype": doctype, | |||
"reference_name": name, | |||
"subject": feed.subject, | |||
"full_name": get_fullname(doc.owner), | |||
"reference_owner": frappe.db.get_value(doctype, name, "owner"), | |||
"link_doctype": feed.link_doctype, | |||
"link_name": feed.link_name | |||
}).insert(ignore_permissions=True) | |||
frappe.db.delete( | |||
"Activity Log", | |||
{"reference_doctype": doctype, "reference_name": name, "link_doctype": feed.link_doctype}, | |||
) | |||
frappe.get_doc( | |||
{ | |||
"doctype": "Activity Log", | |||
"reference_doctype": doctype, | |||
"reference_name": name, | |||
"subject": feed.subject, | |||
"full_name": get_fullname(doc.owner), | |||
"reference_owner": frappe.db.get_value(doctype, name, "owner"), | |||
"link_doctype": feed.link_doctype, | |||
"link_name": feed.link_name, | |||
} | |||
).insert(ignore_permissions=True) | |||
def login_feed(login_manager): | |||
if login_manager.user != "Guest": | |||
subject = _("{0} logged in").format(get_fullname(login_manager.user)) | |||
add_authentication_log(subject, login_manager.user) | |||
def logout_feed(user, reason): | |||
if user and user != "Guest": | |||
subject = _("{0} logged out: {1}").format(get_fullname(user), frappe.bold(reason)) | |||
add_authentication_log(subject, user, operation="Logout") | |||
def get_feed_match_conditions(user=None, doctype='Comment'): | |||
if not user: user = frappe.session.user | |||
conditions = ['`tab{doctype}`.owner={user} or `tab{doctype}`.reference_owner={user}'.format( | |||
user = frappe.db.escape(user), | |||
doctype = doctype | |||
)] | |||
def get_feed_match_conditions(user=None, doctype="Comment"): | |||
if not user: | |||
user = frappe.session.user | |||
conditions = [ | |||
"`tab{doctype}`.owner={user} or `tab{doctype}`.reference_owner={user}".format( | |||
user=frappe.db.escape(user), doctype=doctype | |||
) | |||
] | |||
user_permissions = frappe.permissions.get_user_permissions(user) | |||
can_read = frappe.get_user().get_can_read() | |||
can_read_doctypes = ["'{}'".format(dt) for dt in | |||
list(set(can_read) - set(list(user_permissions)))] | |||
can_read_doctypes = [ | |||
"'{}'".format(dt) for dt in list(set(can_read) - set(list(user_permissions))) | |||
] | |||
if can_read_doctypes: | |||
conditions += ["""(`tab{doctype}`.reference_doctype is null | |||
conditions += [ | |||
"""(`tab{doctype}`.reference_doctype is null | |||
or `tab{doctype}`.reference_doctype = '' | |||
or `tab{doctype}`.reference_doctype | |||
in ({values}))""".format( | |||
doctype = doctype, | |||
values =", ".join(can_read_doctypes) | |||
)] | |||
doctype=doctype, values=", ".join(can_read_doctypes) | |||
) | |||
] | |||
if user_permissions: | |||
can_read_docs = [] | |||
for dt, obj in user_permissions.items(): | |||
for n in obj: | |||
can_read_docs.append('{}|{}'.format(frappe.db.escape(dt), frappe.db.escape(n.get('doc', '')))) | |||
can_read_docs.append("{}|{}".format(frappe.db.escape(dt), frappe.db.escape(n.get("doc", "")))) | |||
if can_read_docs: | |||
conditions.append("concat_ws('|', `tab{doctype}`.reference_doctype, `tab{doctype}`.reference_name) in ({values})".format( | |||
doctype = doctype, | |||
values = ", ".join(can_read_docs))) | |||
conditions.append( | |||
"concat_ws('|', `tab{doctype}`.reference_doctype, `tab{doctype}`.reference_name) in ({values})".format( | |||
doctype=doctype, values=", ".join(can_read_docs) | |||
) | |||
) | |||
return "(" + " or ".join(conditions) + ")" | |||
return "(" + " or ".join(conditions) + ")" |
@@ -1,77 +1,74 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright (c) 2015, Frappe Technologies and Contributors | |||
# License: MIT. See LICENSE | |||
import frappe | |||
import unittest | |||
import time | |||
from frappe.auth import LoginManager, CookieManager | |||
import unittest | |||
import frappe | |||
from frappe.auth import CookieManager, LoginManager | |||
class TestActivityLog(unittest.TestCase): | |||
def test_activity_log(self): | |||
# test user login log | |||
frappe.local.form_dict = frappe._dict({ | |||
'cmd': 'login', | |||
'sid': 'Guest', | |||
'pwd': 'admin', | |||
'usr': 'Administrator' | |||
}) | |||
frappe.local.form_dict = frappe._dict( | |||
{"cmd": "login", "sid": "Guest", "pwd": "admin", "usr": "Administrator"} | |||
) | |||
frappe.local.cookie_manager = CookieManager() | |||
frappe.local.login_manager = LoginManager() | |||
auth_log = self.get_auth_log() | |||
self.assertEqual(auth_log.status, 'Success') | |||
self.assertEqual(auth_log.status, "Success") | |||
# test user logout log | |||
frappe.local.login_manager.logout() | |||
auth_log = self.get_auth_log(operation='Logout') | |||
self.assertEqual(auth_log.status, 'Success') | |||
auth_log = self.get_auth_log(operation="Logout") | |||
self.assertEqual(auth_log.status, "Success") | |||
# test invalid login | |||
frappe.form_dict.update({ 'pwd': 'password' }) | |||
frappe.form_dict.update({"pwd": "password"}) | |||
self.assertRaises(frappe.AuthenticationError, LoginManager) | |||
auth_log = self.get_auth_log() | |||
self.assertEqual(auth_log.status, 'Failed') | |||
self.assertEqual(auth_log.status, "Failed") | |||
frappe.local.form_dict = frappe._dict() | |||
def get_auth_log(self, operation='Login'): | |||
names = frappe.db.get_all('Activity Log', filters={ | |||
'user': 'Administrator', | |||
'operation': operation, | |||
}, order_by='`creation` DESC') | |||
def get_auth_log(self, operation="Login"): | |||
names = frappe.db.get_all( | |||
"Activity Log", | |||
filters={ | |||
"user": "Administrator", | |||
"operation": operation, | |||
}, | |||
order_by="`creation` DESC", | |||
) | |||
name = names[0] | |||
auth_log = frappe.get_doc('Activity Log', name) | |||
auth_log = frappe.get_doc("Activity Log", name) | |||
return auth_log | |||
def test_brute_security(self): | |||
update_system_settings({ | |||
'allow_consecutive_login_attempts': 3, | |||
'allow_login_after_fail': 5 | |||
}) | |||
frappe.local.form_dict = frappe._dict({ | |||
'cmd': 'login', | |||
'sid': 'Guest', | |||
'pwd': 'admin', | |||
'usr': 'Administrator' | |||
}) | |||
update_system_settings({"allow_consecutive_login_attempts": 3, "allow_login_after_fail": 5}) | |||
frappe.local.form_dict = frappe._dict( | |||
{"cmd": "login", "sid": "Guest", "pwd": "admin", "usr": "Administrator"} | |||
) | |||
frappe.local.cookie_manager = CookieManager() | |||
frappe.local.login_manager = LoginManager() | |||
auth_log = self.get_auth_log() | |||
self.assertEqual(auth_log.status, 'Success') | |||
self.assertEqual(auth_log.status, "Success") | |||
# test user logout log | |||
frappe.local.login_manager.logout() | |||
auth_log = self.get_auth_log(operation='Logout') | |||
self.assertEqual(auth_log.status, 'Success') | |||
auth_log = self.get_auth_log(operation="Logout") | |||
self.assertEqual(auth_log.status, "Success") | |||
# test invalid login | |||
frappe.form_dict.update({ 'pwd': 'password' }) | |||
frappe.form_dict.update({"pwd": "password"}) | |||
self.assertRaises(frappe.AuthenticationError, LoginManager) | |||
self.assertRaises(frappe.AuthenticationError, LoginManager) | |||
self.assertRaises(frappe.AuthenticationError, LoginManager) | |||
@@ -85,8 +82,9 @@ class TestActivityLog(unittest.TestCase): | |||
frappe.local.form_dict = frappe._dict() | |||
def update_system_settings(args): | |||
doc = frappe.get_doc('System Settings') | |||
doc = frappe.get_doc("System Settings") | |||
doc.update(args) | |||
doc.flags.ignore_mandatory = 1 | |||
doc.save() |
@@ -5,5 +5,6 @@ | |||
import frappe | |||
from frappe.model.document import Document | |||
class BlockModule(Document): | |||
pass |
@@ -1,22 +1,27 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright (c) 2019, Frappe Technologies and contributors | |||
# License: MIT. See LICENSE | |||
import json | |||
import frappe | |||
from frappe import _ | |||
import json | |||
from frappe.model.document import Document | |||
from frappe.core.doctype.user.user import extract_mentions | |||
from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification,\ | |||
get_title, get_title_html | |||
from frappe.utils import get_fullname | |||
from frappe.website.utils import clear_cache | |||
from frappe.database.schema import add_column | |||
from frappe.desk.doctype.notification_log.notification_log import ( | |||
enqueue_create_notification, | |||
get_title, | |||
get_title_html, | |||
) | |||
from frappe.exceptions import ImplicitCommitError | |||
from frappe.model.document import Document | |||
from frappe.utils import get_fullname | |||
from frappe.website.utils import clear_cache | |||
class Comment(Document): | |||
def after_insert(self): | |||
self.notify_mentions() | |||
self.notify_change('add') | |||
self.notify_change("add") | |||
def validate(self): | |||
if not self.comment_email: | |||
@@ -26,34 +31,35 @@ class Comment(Document): | |||
def on_update(self): | |||
update_comment_in_doc(self) | |||
if self.is_new(): | |||
self.notify_change('update') | |||
self.notify_change("update") | |||
def on_trash(self): | |||
self.remove_comment_from_cache() | |||
self.notify_change('delete') | |||
self.notify_change("delete") | |||
def notify_change(self, action): | |||
key_map = { | |||
'Like': 'like_logs', | |||
'Assigned': 'assignment_logs', | |||
'Assignment Completed': 'assignment_logs', | |||
'Comment': 'comments', | |||
'Attachment': 'attachment_logs', | |||
'Attachment Removed': 'attachment_logs', | |||
"Like": "like_logs", | |||
"Assigned": "assignment_logs", | |||
"Assignment Completed": "assignment_logs", | |||
"Comment": "comments", | |||
"Attachment": "attachment_logs", | |||
"Attachment Removed": "attachment_logs", | |||
} | |||
key = key_map.get(self.comment_type) | |||
if not key: return | |||
if not key: | |||
return | |||
frappe.publish_realtime('update_docinfo_for_{}_{}'.format(self.reference_doctype, self.reference_name), { | |||
'doc': self.as_dict(), | |||
'key': key, | |||
'action': action | |||
}, after_commit=True) | |||
frappe.publish_realtime( | |||
"update_docinfo_for_{}_{}".format(self.reference_doctype, self.reference_name), | |||
{"doc": self.as_dict(), "key": key, "action": action}, | |||
after_commit=True, | |||
) | |||
def remove_comment_from_cache(self): | |||
_comments = get_comments_from_parent(self) | |||
for c in _comments: | |||
if c.get("name")==self.name: | |||
if c.get("name") == self.name: | |||
_comments.remove(c) | |||
update_comments_in_parent(self.reference_doctype, self.reference_name, _comments) | |||
@@ -68,19 +74,26 @@ class Comment(Document): | |||
sender_fullname = get_fullname(frappe.session.user) | |||
title = get_title(self.reference_doctype, self.reference_name) | |||
recipients = [frappe.db.get_value("User", {"enabled": 1, "name": name, "user_type": "System User", "allowed_in_mentions": 1}, "email") | |||
for name in mentions] | |||
recipients = [ | |||
frappe.db.get_value( | |||
"User", | |||
{"enabled": 1, "name": name, "user_type": "System User", "allowed_in_mentions": 1}, | |||
"email", | |||
) | |||
for name in mentions | |||
] | |||
notification_message = _('''{0} mentioned you in a comment in {1} {2}''')\ | |||
.format(frappe.bold(sender_fullname), frappe.bold(self.reference_doctype), get_title_html(title)) | |||
notification_message = _("""{0} mentioned you in a comment in {1} {2}""").format( | |||
frappe.bold(sender_fullname), frappe.bold(self.reference_doctype), get_title_html(title) | |||
) | |||
notification_doc = { | |||
'type': 'Mention', | |||
'document_type': self.reference_doctype, | |||
'document_name': self.reference_name, | |||
'subject': notification_message, | |||
'from_user': frappe.session.user, | |||
'email_content': self.content | |||
"type": "Mention", | |||
"document_type": self.reference_doctype, | |||
"document_name": self.reference_name, | |||
"subject": notification_message, | |||
"from_user": frappe.session.user, | |||
"email_content": self.content, | |||
} | |||
enqueue_create_notification(recipients, notification_doc) | |||
@@ -99,45 +112,46 @@ def update_comment_in_doc(doc): | |||
`_comments` format | |||
{ | |||
"comment": [String], | |||
"by": [user], | |||
"name": [Comment Document name] | |||
}""" | |||
{ | |||
"comment": [String], | |||
"by": [user], | |||
"name": [Comment Document name] | |||
}""" | |||
# only comments get updates, not likes, assignments etc. | |||
if doc.doctype == 'Comment' and doc.comment_type != 'Comment': | |||
if doc.doctype == "Comment" and doc.comment_type != "Comment": | |||
return | |||
def get_truncated(content): | |||
return (content[:97] + '...') if len(content) > 100 else content | |||
return (content[:97] + "...") if len(content) > 100 else content | |||
if doc.reference_doctype and doc.reference_name and doc.content: | |||
_comments = get_comments_from_parent(doc) | |||
updated = False | |||
for c in _comments: | |||
if c.get("name")==doc.name: | |||
if c.get("name") == doc.name: | |||
c["comment"] = get_truncated(doc.content) | |||
updated = True | |||
if not updated: | |||
_comments.append({ | |||
"comment": get_truncated(doc.content), | |||
# "comment_email" for Comment and "sender" for Communication | |||
"by": getattr(doc, 'comment_email', None) or getattr(doc, 'sender', None) or doc.owner, | |||
"name": doc.name | |||
}) | |||
_comments.append( | |||
{ | |||
"comment": get_truncated(doc.content), | |||
# "comment_email" for Comment and "sender" for Communication | |||
"by": getattr(doc, "comment_email", None) or getattr(doc, "sender", None) or doc.owner, | |||
"name": doc.name, | |||
} | |||
) | |||
update_comments_in_parent(doc.reference_doctype, doc.reference_name, _comments) | |||
def get_comments_from_parent(doc): | |||
''' | |||
""" | |||
get the list of comments cached in the document record in the column | |||
`_comments` | |||
''' | |||
""" | |||
try: | |||
_comments = frappe.db.get_value(doc.reference_doctype, doc.reference_name, "_comments") or "[]" | |||
@@ -153,23 +167,32 @@ def get_comments_from_parent(doc): | |||
except ValueError: | |||
return [] | |||
def update_comments_in_parent(reference_doctype, reference_name, _comments): | |||
"""Updates `_comments` property in parent Document with given dict. | |||
:param _comments: Dict of comments.""" | |||
if not reference_doctype or not reference_name or frappe.db.get_value("DocType", reference_doctype, "issingle") or frappe.db.get_value("DocType", reference_doctype, "is_virtual"): | |||
if ( | |||
not reference_doctype | |||
or not reference_name | |||
or frappe.db.get_value("DocType", reference_doctype, "issingle") | |||
or frappe.db.get_value("DocType", reference_doctype, "is_virtual") | |||
): | |||
return | |||
try: | |||
# use sql, so that we do not mess with the timestamp | |||
frappe.db.sql("""update `tab{0}` set `_comments`=%s where name=%s""".format(reference_doctype), # nosec | |||
(json.dumps(_comments[-100:]), reference_name)) | |||
frappe.db.sql( | |||
"""update `tab{0}` set `_comments`=%s where name=%s""".format(reference_doctype), # nosec | |||
(json.dumps(_comments[-100:]), reference_name), | |||
) | |||
except Exception as e: | |||
if frappe.db.is_column_missing(e) and getattr(frappe.local, 'request', None): | |||
if frappe.db.is_column_missing(e) and getattr(frappe.local, "request", None): | |||
# missing column and in request, add column and update after commit | |||
frappe.local._comments = (getattr(frappe.local, "_comments", []) | |||
+ [(reference_doctype, reference_name, _comments)]) | |||
frappe.local._comments = getattr(frappe.local, "_comments", []) + [ | |||
(reference_doctype, reference_name, _comments) | |||
] | |||
elif frappe.db.is_data_too_long(e): | |||
raise frappe.DataTooLongException | |||
@@ -183,6 +206,7 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments): | |||
if getattr(reference_doc, "route", None): | |||
clear_cache(reference_doc.route) | |||
def update_comments_in_parent_after_request(): | |||
"""update _comments in parent if _comments column is missing""" | |||
if hasattr(frappe.local, "_comments"): | |||
@@ -1,9 +1,12 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright (c) 2019, Frappe Technologies and Contributors | |||
# License: MIT. See LICENSE | |||
import frappe, json | |||
import json | |||
import unittest | |||
import frappe | |||
class TestComment(unittest.TestCase): | |||
def tearDown(self): | |||
frappe.form_dict.comment = None | |||
@@ -15,75 +18,88 @@ class TestComment(unittest.TestCase): | |||
frappe.local.request_ip = None | |||
def test_comment_creation(self): | |||
test_doc = frappe.get_doc(dict(doctype = 'ToDo', description = 'test')) | |||
test_doc = frappe.get_doc(dict(doctype="ToDo", description="test")) | |||
test_doc.insert() | |||
comment = test_doc.add_comment('Comment', 'test comment') | |||
comment = test_doc.add_comment("Comment", "test comment") | |||
test_doc.reload() | |||
# check if updated in _comments cache | |||
comments = json.loads(test_doc.get('_comments')) | |||
self.assertEqual(comments[0].get('name'), comment.name) | |||
self.assertEqual(comments[0].get('comment'), comment.content) | |||
comments = json.loads(test_doc.get("_comments")) | |||
self.assertEqual(comments[0].get("name"), comment.name) | |||
self.assertEqual(comments[0].get("comment"), comment.content) | |||
# check document creation | |||
comment_1 = frappe.get_all('Comment', fields = ['*'], filters = dict( | |||
reference_doctype = test_doc.doctype, | |||
reference_name = test_doc.name | |||
))[0] | |||
comment_1 = frappe.get_all( | |||
"Comment", | |||
fields=["*"], | |||
filters=dict(reference_doctype=test_doc.doctype, reference_name=test_doc.name), | |||
)[0] | |||
self.assertEqual(comment_1.content, 'test comment') | |||
self.assertEqual(comment_1.content, "test comment") | |||
# test via blog | |||
def test_public_comment(self): | |||
from frappe.website.doctype.blog_post.test_blog_post import make_test_blog | |||
test_blog = make_test_blog() | |||
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"}) | |||
from frappe.templates.includes.comments.comments import add_comment | |||
frappe.form_dict.comment = 'Good comment with 10 chars' | |||
frappe.form_dict.comment_email = 'test@test.com' | |||
frappe.form_dict.comment_by = 'Good Tester' | |||
frappe.form_dict.reference_doctype = 'Blog Post' | |||
frappe.form_dict.comment = "Good comment with 10 chars" | |||
frappe.form_dict.comment_email = "test@test.com" | |||
frappe.form_dict.comment_by = "Good Tester" | |||
frappe.form_dict.reference_doctype = "Blog Post" | |||
frappe.form_dict.reference_name = test_blog.name | |||
frappe.form_dict.route = test_blog.route | |||
frappe.local.request_ip = '127.0.0.1' | |||
frappe.local.request_ip = "127.0.0.1" | |||
add_comment() | |||
self.assertEqual(frappe.get_all('Comment', fields = ['*'], filters = dict( | |||
reference_doctype = test_blog.doctype, | |||
reference_name = test_blog.name | |||
))[0].published, 1) | |||
self.assertEqual( | |||
frappe.get_all( | |||
"Comment", | |||
fields=["*"], | |||
filters=dict(reference_doctype=test_blog.doctype, reference_name=test_blog.name), | |||
)[0].published, | |||
1, | |||
) | |||
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"}) | |||
frappe.form_dict.comment = 'pleez vizits my site http://mysite.com' | |||
frappe.form_dict.comment_by = 'bad commentor' | |||
frappe.form_dict.comment = "pleez vizits my site http://mysite.com" | |||
frappe.form_dict.comment_by = "bad commentor" | |||
add_comment() | |||
self.assertEqual(len(frappe.get_all('Comment', fields = ['*'], filters = dict( | |||
reference_doctype = test_blog.doctype, | |||
reference_name = test_blog.name | |||
))), 0) | |||
self.assertEqual( | |||
len( | |||
frappe.get_all( | |||
"Comment", | |||
fields=["*"], | |||
filters=dict(reference_doctype=test_blog.doctype, reference_name=test_blog.name), | |||
) | |||
), | |||
0, | |||
) | |||
# test for filtering html and css injection elements | |||
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"}) | |||
frappe.form_dict.comment = '<script>alert(1)</script>Comment' | |||
frappe.form_dict.comment_by = 'hacker' | |||
frappe.form_dict.comment = "<script>alert(1)</script>Comment" | |||
frappe.form_dict.comment_by = "hacker" | |||
add_comment() | |||
self.assertEqual(frappe.get_all('Comment', fields = ['content'], filters = dict( | |||
reference_doctype = test_blog.doctype, | |||
reference_name = test_blog.name | |||
))[0]['content'], 'Comment') | |||
self.assertEqual( | |||
frappe.get_all( | |||
"Comment", | |||
fields=["content"], | |||
filters=dict(reference_doctype=test_blog.doctype, reference_name=test_blog.name), | |||
)[0]["content"], | |||
"Comment", | |||
) | |||
test_blog.delete() | |||
@@ -1,3 +1,2 @@ | |||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
@@ -2,49 +2,67 @@ | |||
# License: MIT. See LICENSE | |||
from collections import Counter | |||
from email.utils import getaddresses | |||
from typing import List | |||
from urllib.parse import unquote | |||
from parse import compile | |||
import frappe | |||
from frappe import _ | |||
from frappe.model.document import Document | |||
from frappe.utils import validate_email_address, strip_html, cstr, time_diff_in_seconds | |||
from frappe.automation.doctype.assignment_rule.assignment_rule import ( | |||
apply as apply_assignment_rule, | |||
) | |||
from frappe.contacts.doctype.contact.contact import get_contact_name | |||
from frappe.core.doctype.comment.comment import update_comment_in_doc | |||
from frappe.core.doctype.communication.email import validate_email | |||
from frappe.core.doctype.communication.mixins import CommunicationEmailMixin | |||
from frappe.core.utils import get_parent_doc | |||
from frappe.utils import parse_addr, split_emails | |||
from frappe.core.doctype.comment.comment import update_comment_in_doc | |||
from email.utils import getaddresses | |||
from urllib.parse import unquote | |||
from frappe.model.document import Document | |||
from frappe.utils import ( | |||
cstr, | |||
parse_addr, | |||
split_emails, | |||
strip_html, | |||
time_diff_in_seconds, | |||
validate_email_address, | |||
) | |||
from frappe.utils.user import is_system_user | |||
from frappe.contacts.doctype.contact.contact import get_contact_name | |||
from frappe.automation.doctype.assignment_rule.assignment_rule import apply as apply_assignment_rule | |||
from parse import compile | |||
exclude_from_linked_with = True | |||
class Communication(Document, CommunicationEmailMixin): | |||
"""Communication represents an external communication like Email. | |||
""" | |||
"""Communication represents an external communication like Email.""" | |||
no_feed_on_delete = True | |||
DOCTYPE = 'Communication' | |||
DOCTYPE = "Communication" | |||
def onload(self): | |||
"""create email flag queue""" | |||
if self.communication_type == "Communication" and self.communication_medium == "Email" \ | |||
and self.sent_or_received == "Received" and self.uid and self.uid != -1: | |||
email_flag_queue = frappe.db.get_value("Email Flag Queue", { | |||
"communication": self.name, | |||
"is_completed": 0}) | |||
if ( | |||
self.communication_type == "Communication" | |||
and self.communication_medium == "Email" | |||
and self.sent_or_received == "Received" | |||
and self.uid | |||
and self.uid != -1 | |||
): | |||
email_flag_queue = frappe.db.get_value( | |||
"Email Flag Queue", {"communication": self.name, "is_completed": 0} | |||
) | |||
if email_flag_queue: | |||
return | |||
frappe.get_doc({ | |||
"doctype": "Email Flag Queue", | |||
"action": "Read", | |||
"communication": self.name, | |||
"uid": self.uid, | |||
"email_account": self.email_account | |||
}).insert(ignore_permissions=True) | |||
frappe.get_doc( | |||
{ | |||
"doctype": "Email Flag Queue", | |||
"action": "Read", | |||
"communication": self.name, | |||
"uid": self.uid, | |||
"email_account": self.email_account, | |||
} | |||
).insert(ignore_permissions=True) | |||
frappe.db.commit() | |||
def validate(self): | |||
@@ -74,25 +92,33 @@ class Communication(Document, CommunicationEmailMixin): | |||
def validate_reference(self): | |||
if self.reference_doctype and self.reference_name: | |||
if not self.reference_owner: | |||
self.reference_owner = frappe.db.get_value(self.reference_doctype, self.reference_name, "owner") | |||
self.reference_owner = frappe.db.get_value( | |||
self.reference_doctype, self.reference_name, "owner" | |||
) | |||
# prevent communication against a child table | |||
if frappe.get_meta(self.reference_doctype).istable: | |||
frappe.throw(_("Cannot create a {0} against a child document: {1}") | |||
.format(_(self.communication_type), _(self.reference_doctype))) | |||
frappe.throw( | |||
_("Cannot create a {0} against a child document: {1}").format( | |||
_(self.communication_type), _(self.reference_doctype) | |||
) | |||
) | |||
# Prevent circular linking of Communication DocTypes | |||
if self.reference_doctype == "Communication": | |||
circular_linking = False | |||
doc = get_parent_doc(self) | |||
while doc.reference_doctype == "Communication": | |||
if get_parent_doc(doc).name==self.name: | |||
if get_parent_doc(doc).name == self.name: | |||
circular_linking = True | |||
break | |||
doc = get_parent_doc(doc) | |||
if circular_linking: | |||
frappe.throw(_("Please make sure the Reference Communication Docs are not circularly linked."), frappe.CircularLinkingError) | |||
frappe.throw( | |||
_("Please make sure the Reference Communication Docs are not circularly linked."), | |||
frappe.CircularLinkingError, | |||
) | |||
def after_insert(self): | |||
if not (self.reference_doctype and self.reference_name): | |||
@@ -102,21 +128,21 @@ class Communication(Document, CommunicationEmailMixin): | |||
frappe.db.set_value("Communication", self.reference_name, "status", "Replied") | |||
if self.communication_type == "Communication": | |||
self.notify_change('add') | |||
self.notify_change("add") | |||
elif self.communication_type in ("Chat", "Notification"): | |||
if self.reference_name == frappe.session.user: | |||
message = self.as_dict() | |||
message['broadcast'] = True | |||
frappe.publish_realtime('new_message', message, after_commit=True) | |||
message["broadcast"] = True | |||
frappe.publish_realtime("new_message", message, after_commit=True) | |||
else: | |||
# reference_name contains the user who is addressed in the messages' page comment | |||
frappe.publish_realtime('new_message', self.as_dict(), | |||
user=self.reference_name, after_commit=True) | |||
frappe.publish_realtime( | |||
"new_message", self.as_dict(), user=self.reference_name, after_commit=True | |||
) | |||
def set_signature_in_email_content(self): | |||
"""Set sender's User.email_signature or default outgoing's EmailAccount.signature to the email | |||
""" | |||
"""Set sender's User.email_signature or default outgoing's EmailAccount.signature to the email""" | |||
if not self.content: | |||
return | |||
@@ -128,11 +154,15 @@ class Communication(Document, CommunicationEmailMixin): | |||
email_body = email_body[0] | |||
user_email_signature = frappe.db.get_value( | |||
"User", | |||
self.sender, | |||
"email_signature", | |||
) if self.sender else None | |||
user_email_signature = ( | |||
frappe.db.get_value( | |||
"User", | |||
self.sender, | |||
"email_signature", | |||
) | |||
if self.sender | |||
else None | |||
) | |||
signature = user_email_signature or frappe.db.get_value( | |||
"Email Account", | |||
@@ -157,19 +187,19 @@ class Communication(Document, CommunicationEmailMixin): | |||
# comments count for the list view | |||
update_comment_in_doc(self) | |||
if self.comment_type != 'Updated': | |||
if self.comment_type != "Updated": | |||
update_parent_document_on_communication(self) | |||
def on_trash(self): | |||
if self.communication_type == "Communication": | |||
self.notify_change('delete') | |||
self.notify_change("delete") | |||
@property | |||
def sender_mailid(self): | |||
return parse_addr(self.sender)[1] if self.sender else "" | |||
@staticmethod | |||
def _get_emails_list(emails=None, exclude_displayname = False): | |||
def _get_emails_list(emails=None, exclude_displayname=False): | |||
"""Returns list of emails from given email string. | |||
* Removes duplicate mailids | |||
@@ -180,35 +210,32 @@ class Communication(Document, CommunicationEmailMixin): | |||
return [email.lower() for email in set([parse_addr(email)[1] for email in emails]) if email] | |||
return [email.lower() for email in set(emails) if email] | |||
def to_list(self, exclude_displayname = True): | |||
"""Returns to list. | |||
""" | |||
def to_list(self, exclude_displayname=True): | |||
"""Returns to list.""" | |||
return self._get_emails_list(self.recipients, exclude_displayname=exclude_displayname) | |||
def cc_list(self, exclude_displayname = True): | |||
"""Returns cc list. | |||
""" | |||
def cc_list(self, exclude_displayname=True): | |||
"""Returns cc list.""" | |||
return self._get_emails_list(self.cc, exclude_displayname=exclude_displayname) | |||
def bcc_list(self, exclude_displayname = True): | |||
"""Returns bcc list. | |||
""" | |||
def bcc_list(self, exclude_displayname=True): | |||
"""Returns bcc list.""" | |||
return self._get_emails_list(self.bcc, exclude_displayname=exclude_displayname) | |||
def get_attachments(self): | |||
attachments = frappe.get_all( | |||
"File", | |||
fields=["name", "file_name", "file_url", "is_private"], | |||
filters = {"attached_to_name": self.name, "attached_to_doctype": self.DOCTYPE} | |||
filters={"attached_to_name": self.name, "attached_to_doctype": self.DOCTYPE}, | |||
) | |||
return attachments | |||
def notify_change(self, action): | |||
frappe.publish_realtime('update_docinfo_for_{}_{}'.format(self.reference_doctype, self.reference_name), { | |||
'doc': self.as_dict(), | |||
'key': 'communications', | |||
'action': action | |||
}, after_commit=True) | |||
frappe.publish_realtime( | |||
"update_docinfo_for_{}_{}".format(self.reference_doctype, self.reference_name), | |||
{"doc": self.as_dict(), "key": "communications", "action": action}, | |||
after_commit=True, | |||
) | |||
def set_status(self): | |||
if not self.is_new(): | |||
@@ -216,15 +243,19 @@ class Communication(Document, CommunicationEmailMixin): | |||
if self.reference_doctype and self.reference_name: | |||
self.status = "Linked" | |||
elif self.communication_type=="Communication": | |||
elif self.communication_type == "Communication": | |||
self.status = "Open" | |||
else: | |||
self.status = "Closed" | |||
# set email status to spam | |||
email_rule = frappe.db.get_value("Email Rule", { "email_id": self.sender, "is_spam":1 }) | |||
if self.communication_type == "Communication" and self.communication_medium == "Email" \ | |||
and self.sent_or_received == "Sent" and email_rule: | |||
email_rule = frappe.db.get_value("Email Rule", {"email_id": self.sender, "is_spam": 1}) | |||
if ( | |||
self.communication_type == "Communication" | |||
and self.communication_medium == "Email" | |||
and self.sent_or_received == "Sent" | |||
and email_rule | |||
): | |||
self.email_status = "Spam" | |||
@@ -254,7 +285,7 @@ class Communication(Document, CommunicationEmailMixin): | |||
self.sender_full_name = self.sender | |||
self.sender = None | |||
else: | |||
if self.sent_or_received=='Sent': | |||
if self.sent_or_received == "Sent": | |||
validate_email_address(self.sender, throw=True) | |||
sender_name, sender_email = parse_addr(self.sender) | |||
if sender_name == sender_email: | |||
@@ -264,40 +295,41 @@ class Communication(Document, CommunicationEmailMixin): | |||
self.sender_full_name = sender_name | |||
if not self.sender_full_name: | |||
self.sender_full_name = frappe.db.get_value('User', self.sender, 'full_name') | |||
self.sender_full_name = frappe.db.get_value("User", self.sender, "full_name") | |||
if not self.sender_full_name: | |||
first_name, last_name = frappe.db.get_value('Contact', | |||
filters={'email_id': sender_email}, | |||
fieldname=['first_name', 'last_name'] | |||
first_name, last_name = frappe.db.get_value( | |||
"Contact", filters={"email_id": sender_email}, fieldname=["first_name", "last_name"] | |||
) or [None, None] | |||
self.sender_full_name = (first_name or '') + (last_name or '') | |||
self.sender_full_name = (first_name or "") + (last_name or "") | |||
if not self.sender_full_name: | |||
self.sender_full_name = sender_email | |||
def set_delivery_status(self, commit=False): | |||
'''Look into the status of Email Queue linked to this Communication and set the Delivery Status of this Communication''' | |||
"""Look into the status of Email Queue linked to this Communication and set the Delivery Status of this Communication""" | |||
delivery_status = None | |||
status_counts = Counter(frappe.get_all("Email Queue", pluck="status", filters={"communication": self.name})) | |||
status_counts = Counter( | |||
frappe.get_all("Email Queue", pluck="status", filters={"communication": self.name}) | |||
) | |||
if self.sent_or_received == "Received": | |||
return | |||
if status_counts.get('Not Sent') or status_counts.get('Sending'): | |||
delivery_status = 'Sending' | |||
if status_counts.get("Not Sent") or status_counts.get("Sending"): | |||
delivery_status = "Sending" | |||
elif status_counts.get('Error'): | |||
delivery_status = 'Error' | |||
elif status_counts.get("Error"): | |||
delivery_status = "Error" | |||
elif status_counts.get('Expired'): | |||
delivery_status = 'Expired' | |||
elif status_counts.get("Expired"): | |||
delivery_status = "Expired" | |||
elif status_counts.get('Sent'): | |||
delivery_status = 'Sent' | |||
elif status_counts.get("Sent"): | |||
delivery_status = "Sent" | |||
if delivery_status: | |||
self.db_set('delivery_status', delivery_status) | |||
self.notify_change('update') | |||
self.db_set("delivery_status", delivery_status) | |||
self.notify_change("update") | |||
# for list views and forms | |||
self.notify_update() | |||
@@ -311,13 +343,17 @@ class Communication(Document, CommunicationEmailMixin): | |||
# Timeline Links | |||
def set_timeline_links(self): | |||
contacts = [] | |||
create_contact_enabled = self.email_account and frappe.db.get_value("Email Account", self.email_account, "create_contact") | |||
contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc], auto_create_contact=create_contact_enabled) | |||
create_contact_enabled = self.email_account and frappe.db.get_value( | |||
"Email Account", self.email_account, "create_contact" | |||
) | |||
contacts = get_contacts( | |||
[self.sender, self.recipients, self.cc, self.bcc], auto_create_contact=create_contact_enabled | |||
) | |||
for contact_name in contacts: | |||
self.add_link('Contact', contact_name) | |||
self.add_link("Contact", contact_name) | |||
#link contact's dynamic links to communication | |||
# link contact's dynamic links to communication | |||
add_contact_links_to_communication(self, contact_name) | |||
def deduplicate_timeline_links(self): | |||
@@ -332,17 +368,12 @@ class Communication(Document, CommunicationEmailMixin): | |||
duplicate = True | |||
if duplicate: | |||
del self.timeline_links[:] # make it python 2 compatible as list.clear() is python 3 only | |||
del self.timeline_links[:] # make it python 2 compatible as list.clear() is python 3 only | |||
for l in links: | |||
self.add_link(link_doctype=l[0], link_name=l[1]) | |||
def add_link(self, link_doctype, link_name, autosave=False): | |||
self.append("timeline_links", | |||
{ | |||
"link_doctype": link_doctype, | |||
"link_name": link_name | |||
} | |||
) | |||
self.append("timeline_links", {"link_doctype": link_doctype, "link_name": link_name}) | |||
if autosave: | |||
self.save(ignore_permissions=True) | |||
@@ -358,13 +389,15 @@ class Communication(Document, CommunicationEmailMixin): | |||
if autosave: | |||
self.save(ignore_permissions=ignore_permissions) | |||
def on_doctype_update(): | |||
"""Add indexes in `tabCommunication`""" | |||
frappe.db.add_index("Communication", ["reference_doctype", "reference_name"]) | |||
frappe.db.add_index("Communication", ["status", "communication_type"]) | |||
def has_permission(doc, ptype, user): | |||
if ptype=="read": | |||
if ptype == "read": | |||
if doc.reference_doctype == "Communication" and doc.reference_name == doc.name: | |||
return | |||
@@ -372,24 +405,28 @@ def has_permission(doc, ptype, user): | |||
if frappe.has_permission(doc.reference_doctype, ptype="read", doc=doc.reference_name): | |||
return True | |||
def get_permission_query_conditions_for_communication(user): | |||
if not user: user = frappe.session.user | |||
if not user: | |||
user = frappe.session.user | |||
roles = frappe.get_roles(user) | |||
if "Super Email User" in roles or "System Manager" in roles: | |||
return None | |||
else: | |||
accounts = frappe.get_all("User Email", filters={ "parent": user }, | |||
fields=["email_account"], | |||
distinct=True, order_by="idx") | |||
accounts = frappe.get_all( | |||
"User Email", filters={"parent": user}, fields=["email_account"], distinct=True, order_by="idx" | |||
) | |||
if not accounts: | |||
return """`tabCommunication`.communication_medium!='Email'""" | |||
email_accounts = [ '"%s"'%account.get("email_account") for account in accounts ] | |||
return """`tabCommunication`.email_account in ({email_accounts})"""\ | |||
.format(email_accounts=','.join(email_accounts)) | |||
email_accounts = ['"%s"' % account.get("email_account") for account in accounts] | |||
return """`tabCommunication`.email_account in ({email_accounts})""".format( | |||
email_accounts=",".join(email_accounts) | |||
) | |||
def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[str]: | |||
email_addrs = get_emails(email_strings) | |||
@@ -403,12 +440,12 @@ def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[st | |||
first_name = frappe.unscrub(email_parts[0]) | |||
try: | |||
contact_name = '{0}-{1}'.format(first_name, email_parts[1]) if first_name == 'Contact' else first_name | |||
contact = frappe.get_doc({ | |||
"doctype": "Contact", | |||
"first_name": contact_name, | |||
"name": contact_name | |||
}) | |||
contact_name = ( | |||
"{0}-{1}".format(first_name, email_parts[1]) if first_name == "Contact" else first_name | |||
) | |||
contact = frappe.get_doc( | |||
{"doctype": "Contact", "first_name": contact_name, "name": contact_name} | |||
) | |||
contact.add_email(email_id=email, is_primary=True) | |||
contact.insert(ignore_permissions=True) | |||
contact_name = contact.name | |||
@@ -421,6 +458,7 @@ def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[st | |||
return contacts | |||
def get_emails(email_strings: List[str]) -> List[str]: | |||
email_addrs = [] | |||
@@ -432,22 +470,25 @@ def get_emails(email_strings: List[str]) -> List[str]: | |||
return email_addrs | |||
def add_contact_links_to_communication(communication, contact_name): | |||
contact_links = frappe.get_all("Dynamic Link", filters={ | |||
"parenttype": "Contact", | |||
"parent": contact_name | |||
}, fields=["link_doctype", "link_name"]) | |||
contact_links = frappe.get_all( | |||
"Dynamic Link", | |||
filters={"parenttype": "Contact", "parent": contact_name}, | |||
fields=["link_doctype", "link_name"], | |||
) | |||
if contact_links: | |||
for contact_link in contact_links: | |||
communication.add_link(contact_link.link_doctype, contact_link.link_name) | |||
def parse_email(communication, email_strings): | |||
""" | |||
Parse email to add timeline links. | |||
When automatic email linking is enabled, an email from email_strings can contain | |||
a doctype and docname ie in the format `admin+doctype+docname@example.com`, | |||
the email is parsed and doctype and docname is extracted and timeline link is added. | |||
Parse email to add timeline links. | |||
When automatic email linking is enabled, an email from email_strings can contain | |||
a doctype and docname ie in the format `admin+doctype+docname@example.com`, | |||
the email is parsed and doctype and docname is extracted and timeline link is added. | |||
""" | |||
if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}): | |||
return | |||
@@ -469,10 +510,11 @@ def parse_email(communication, email_strings): | |||
if doctype and docname and frappe.db.exists(doctype, docname): | |||
communication.add_link(doctype, docname) | |||
def get_email_without_link(email): | |||
""" | |||
returns email address without doctype links | |||
returns admin@example.com for email admin+doctype+docname@example.com | |||
returns email address without doctype links | |||
returns admin@example.com for email admin+doctype+docname@example.com | |||
""" | |||
if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}): | |||
return email | |||
@@ -486,6 +528,7 @@ def get_email_without_link(email): | |||
return "{0}@{1}".format(email_id, email_host) | |||
def update_parent_document_on_communication(doc): | |||
"""Update mins_to_first_communication of parent document based on who is replying.""" | |||
@@ -516,6 +559,7 @@ def update_parent_document_on_communication(doc): | |||
parent.run_method("notify_communication", doc) | |||
parent.notify_update() | |||
def update_first_response_time(parent, communication): | |||
if parent.meta.has_field("first_response_time") and not parent.get("first_response_time"): | |||
if is_system_user(communication.sender): | |||
@@ -526,25 +570,29 @@ def update_first_response_time(parent, communication): | |||
first_response_time = round(time_diff_in_seconds(first_responded_on, parent.creation), 2) | |||
parent.db_set("first_response_time", first_response_time) | |||
def set_avg_response_time(parent, communication): | |||
if parent.meta.has_field("avg_response_time") and communication.sent_or_received == "Sent": | |||
# avg response time for all the responses | |||
communications = frappe.get_list("Communication", filters={ | |||
"reference_doctype": parent.doctype, | |||
"reference_name": parent.name | |||
}, | |||
communications = frappe.get_list( | |||
"Communication", | |||
filters={"reference_doctype": parent.doctype, "reference_name": parent.name}, | |||
fields=["sent_or_received", "name", "creation"], | |||
order_by="creation" | |||
order_by="creation", | |||
) | |||
if len(communications): | |||
response_times = [] | |||
for i in range(len(communications)): | |||
if communications[i].sent_or_received == "Sent" and communications[i-1].sent_or_received == "Received": | |||
response_time = round(time_diff_in_seconds(communications[i].creation, communications[i-1].creation), 2) | |||
if ( | |||
communications[i].sent_or_received == "Sent" | |||
and communications[i - 1].sent_or_received == "Received" | |||
): | |||
response_time = round( | |||
time_diff_in_seconds(communications[i].creation, communications[i - 1].creation), 2 | |||
) | |||
if response_time > 0: | |||
response_times.append(response_time) | |||
if response_times: | |||
avg_response_time = sum(response_times) / len(response_times) | |||
parent.db_set("avg_response_time", avg_response_time) | |||
@@ -8,17 +8,25 @@ import frappe | |||
import frappe.email.smtp | |||
from frappe import _ | |||
from frappe.email.email_body import get_message_id | |||
from frappe.utils import (cint, get_datetime, get_formatted_email, | |||
list_to_str, split_emails, validate_email_address) | |||
from frappe.utils import ( | |||
cint, | |||
get_datetime, | |||
get_formatted_email, | |||
list_to_str, | |||
split_emails, | |||
validate_email_address, | |||
) | |||
if TYPE_CHECKING: | |||
from frappe.core.doctype.communication.communication import Communication | |||
OUTGOING_EMAIL_ACCOUNT_MISSING = _(""" | |||
OUTGOING_EMAIL_ACCOUNT_MISSING = _( | |||
""" | |||
Unable to send mail because of a missing email account. | |||
Please setup default Email Account from Setup > Email > Email Account | |||
""") | |||
""" | |||
) | |||
@frappe.whitelist() | |||
@@ -64,16 +72,15 @@ def make( | |||
""" | |||
if kwargs: | |||
from frappe.utils.commands import warn | |||
warn( | |||
f"Options {kwargs} used in frappe.core.doctype.communication.email.make " | |||
"are deprecated or unsupported", | |||
category=DeprecationWarning | |||
category=DeprecationWarning, | |||
) | |||
if doctype and name and not frappe.has_permission(doctype=doctype, ptype="email", doc=name): | |||
raise frappe.PermissionError( | |||
f"You are not allowed to send emails related to: {doctype} {name}" | |||
) | |||
raise frappe.PermissionError(f"You are not allowed to send emails related to: {doctype} {name}") | |||
return _make( | |||
doctype=doctype, | |||
@@ -123,33 +130,34 @@ def _make( | |||
communication_type=None, | |||
add_signature=True, | |||
) -> Dict[str, str]: | |||
"""Internal method to make a new communication that ignores Permission checks. | |||
""" | |||
"""Internal method to make a new communication that ignores Permission checks.""" | |||
sender = sender or get_formatted_email(frappe.session.user) | |||
recipients = list_to_str(recipients) if isinstance(recipients, list) else recipients | |||
cc = list_to_str(cc) if isinstance(cc, list) else cc | |||
bcc = list_to_str(bcc) if isinstance(bcc, list) else bcc | |||
comm: "Communication" = frappe.get_doc({ | |||
"doctype":"Communication", | |||
"subject": subject, | |||
"content": content, | |||
"sender": sender, | |||
"sender_full_name":sender_full_name, | |||
"recipients": recipients, | |||
"cc": cc or None, | |||
"bcc": bcc or None, | |||
"communication_medium": communication_medium, | |||
"sent_or_received": sent_or_received, | |||
"reference_doctype": doctype, | |||
"reference_name": name, | |||
"email_template": email_template, | |||
"message_id":get_message_id().strip(" <>"), | |||
"read_receipt":read_receipt, | |||
"has_attachment": 1 if attachments else 0, | |||
"communication_type": communication_type, | |||
}) | |||
comm: "Communication" = frappe.get_doc( | |||
{ | |||
"doctype": "Communication", | |||
"subject": subject, | |||
"content": content, | |||
"sender": sender, | |||
"sender_full_name": sender_full_name, | |||
"recipients": recipients, | |||
"cc": cc or None, | |||
"bcc": bcc or None, | |||
"communication_medium": communication_medium, | |||
"sent_or_received": sent_or_received, | |||
"reference_doctype": doctype, | |||
"reference_name": name, | |||
"email_template": email_template, | |||
"message_id": get_message_id().strip(" <>"), | |||
"read_receipt": read_receipt, | |||
"has_attachment": 1 if attachments else 0, | |||
"communication_type": communication_type, | |||
} | |||
) | |||
comm.flags.skip_add_signature = not add_signature | |||
comm.insert(ignore_permissions=True) | |||
@@ -161,9 +169,7 @@ def _make( | |||
if cint(send_email): | |||
if not comm.get_outgoing_email_account(): | |||
frappe.throw( | |||
msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError | |||
) | |||
frappe.throw(msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError) | |||
comm.send_email( | |||
print_html=print_html, | |||
@@ -179,7 +185,10 @@ def _make( | |||
def validate_email(doc: "Communication") -> None: | |||
"""Validate Email Addresses of Recipients and CC""" | |||
if not (doc.communication_type=="Communication" and doc.communication_medium == "Email") or doc.flags.in_receive: | |||
if ( | |||
not (doc.communication_type == "Communication" and doc.communication_medium == "Email") | |||
or doc.flags.in_receive | |||
): | |||
return | |||
# validate recipients | |||
@@ -193,36 +202,45 @@ def validate_email(doc: "Communication") -> None: | |||
for email in split_emails(doc.bcc): | |||
validate_email_address(email, throw=True) | |||
def set_incoming_outgoing_accounts(doc): | |||
from frappe.email.doctype.email_account.email_account import EmailAccount | |||
incoming_email_account = EmailAccount.find_incoming( | |||
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype) | |||
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype | |||
) | |||
doc.incoming_email_account = incoming_email_account.email_id if incoming_email_account else None | |||
doc.outgoing_email_account = EmailAccount.find_outgoing( | |||
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype) | |||
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype | |||
) | |||
if doc.sent_or_received == "Sent": | |||
doc.db_set("email_account", doc.outgoing_email_account.name) | |||
def add_attachments(name, attachments): | |||
'''Add attachments to the given Communication''' | |||
"""Add attachments to the given Communication""" | |||
# loop through attachments | |||
for a in attachments: | |||
if isinstance(a, str): | |||
attach = frappe.db.get_value("File", {"name":a}, | |||
["file_name", "file_url", "is_private"], as_dict=1) | |||
attach = frappe.db.get_value( | |||
"File", {"name": a}, ["file_name", "file_url", "is_private"], as_dict=1 | |||
) | |||
# save attachments to new doc | |||
_file = frappe.get_doc({ | |||
"doctype": "File", | |||
"file_url": attach.file_url, | |||
"attached_to_doctype": "Communication", | |||
"attached_to_name": name, | |||
"folder": "Home/Attachments", | |||
"is_private": attach.is_private | |||
}) | |||
_file = frappe.get_doc( | |||
{ | |||
"doctype": "File", | |||
"file_url": attach.file_url, | |||
"attached_to_doctype": "Communication", | |||
"attached_to_name": name, | |||
"folder": "Home/Attachments", | |||
"is_private": attach.is_private, | |||
} | |||
) | |||
_file.save(ignore_permissions=True) | |||
@frappe.whitelist(allow_guest=True, methods=("GET",)) | |||
def mark_email_as_seen(name: str = None): | |||
try: | |||
@@ -233,33 +251,31 @@ def mark_email_as_seen(name: str = None): | |||
frappe.log_error(frappe.get_traceback()) | |||
finally: | |||
frappe.response.update({ | |||
"type": "binary", | |||
"filename": "imaginary_pixel.png", | |||
"filecontent": ( | |||
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00" | |||
b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\r" | |||
b"IDATx\x9cc\xf8\xff\xff?\x03\x00\x08\xfc\x02\xfe\xa7\x9a\xa0" | |||
b"\xa0\x00\x00\x00\x00IEND\xaeB`\x82" | |||
) | |||
}) | |||
frappe.response.update( | |||
{ | |||
"type": "binary", | |||
"filename": "imaginary_pixel.png", | |||
"filecontent": ( | |||
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00" | |||
b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\r" | |||
b"IDATx\x9cc\xf8\xff\xff?\x03\x00\x08\xfc\x02\xfe\xa7\x9a\xa0" | |||
b"\xa0\x00\x00\x00\x00IEND\xaeB`\x82" | |||
), | |||
} | |||
) | |||
def update_communication_as_read(name): | |||
if not name or not isinstance(name, str): | |||
return | |||
communication = frappe.db.get_value( | |||
"Communication", | |||
name, | |||
"read_by_recipient", | |||
as_dict=True | |||
) | |||
communication = frappe.db.get_value("Communication", name, "read_by_recipient", as_dict=True) | |||
if not communication or communication.read_by_recipient: | |||
return | |||
frappe.db.set_value("Communication", name, { | |||
"read_by_recipient": 1, | |||
"delivery_status": "Read", | |||
"read_by_recipient_on": get_datetime() | |||
}) | |||
frappe.db.set_value( | |||
"Communication", | |||
name, | |||
{"read_by_recipient": 1, "delivery_status": "Read", "read_by_recipient_on": get_datetime()}, | |||
) |
@@ -1,33 +1,34 @@ | |||
from typing import List | |||
import frappe | |||
from frappe import _ | |||
from frappe.core.utils import get_parent_doc | |||
from frappe.utils import parse_addr, get_formatted_email, get_url | |||
from frappe.email.doctype.email_account.email_account import EmailAccount | |||
from frappe.desk.doctype.todo.todo import ToDo | |||
from frappe.email.doctype.email_account.email_account import EmailAccount | |||
from frappe.utils import get_formatted_email, get_url, parse_addr | |||
class CommunicationEmailMixin: | |||
"""Mixin class to handle communication mails. | |||
""" | |||
"""Mixin class to handle communication mails.""" | |||
def is_email_communication(self): | |||
return self.communication_type=="Communication" and self.communication_medium == "Email" | |||
return self.communication_type == "Communication" and self.communication_medium == "Email" | |||
def get_owner(self): | |||
"""Get owner of the communication docs parent. | |||
""" | |||
"""Get owner of the communication docs parent.""" | |||
parent_doc = get_parent_doc(self) | |||
return parent_doc.owner if parent_doc else None | |||
def get_all_email_addresses(self, exclude_displayname=False): | |||
"""Get all Email addresses mentioned in the doc along with display name. | |||
""" | |||
return self.to_list(exclude_displayname=exclude_displayname) + \ | |||
self.cc_list(exclude_displayname=exclude_displayname) + \ | |||
self.bcc_list(exclude_displayname=exclude_displayname) | |||
"""Get all Email addresses mentioned in the doc along with display name.""" | |||
return ( | |||
self.to_list(exclude_displayname=exclude_displayname) | |||
+ self.cc_list(exclude_displayname=exclude_displayname) | |||
+ self.bcc_list(exclude_displayname=exclude_displayname) | |||
) | |||
def get_email_with_displayname(self, email_address): | |||
"""Returns email address after adding displayname. | |||
""" | |||
"""Returns email address after adding displayname.""" | |||
display_name, email = parse_addr(email_address) | |||
if display_name and display_name != email: | |||
return email_address | |||
@@ -37,26 +38,24 @@ class CommunicationEmailMixin: | |||
return email_map.get(email, email) | |||
def mail_recipients(self, is_inbound_mail_communcation=False): | |||
"""Build to(recipient) list to send an email. | |||
""" | |||
"""Build to(recipient) list to send an email.""" | |||
# Incase of inbound mail, recipients already received the mail, no need to send again. | |||
if is_inbound_mail_communcation: | |||
return [] | |||
if hasattr(self, '_final_recipients'): | |||
if hasattr(self, "_final_recipients"): | |||
return self._final_recipients | |||
to = self.to_list() | |||
self._final_recipients = list(filter(lambda id: id != 'Administrator', to)) | |||
self._final_recipients = list(filter(lambda id: id != "Administrator", to)) | |||
return self._final_recipients | |||
def get_mail_recipients_with_displayname(self, is_inbound_mail_communcation=False): | |||
"""Build to(recipient) list to send an email including displayname in email. | |||
""" | |||
"""Build to(recipient) list to send an email including displayname in email.""" | |||
to_list = self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation) | |||
return [self.get_email_with_displayname(email) for email in to_list] | |||
def mail_cc(self, is_inbound_mail_communcation=False, include_sender = False): | |||
def mail_cc(self, is_inbound_mail_communcation=False, include_sender=False): | |||
"""Build cc list to send an email. | |||
* if email copy is requested by sender, then add sender to CC. | |||
@@ -67,7 +66,7 @@ class CommunicationEmailMixin: | |||
* FixMe: Removed adding TODO owners to cc list. Check if that is needed. | |||
""" | |||
if hasattr(self, '_final_cc'): | |||
if hasattr(self, "_final_cc"): | |||
return self._final_cc | |||
cc = self.cc_list() | |||
@@ -88,11 +87,13 @@ class CommunicationEmailMixin: | |||
if is_inbound_mail_communcation: | |||
cc = cc - set(self.cc_list() + self.to_list()) | |||
self._final_cc = list(filter(lambda id: id != 'Administrator', cc)) | |||
self._final_cc = list(filter(lambda id: id != "Administrator", cc)) | |||
return self._final_cc | |||
def get_mail_cc_with_displayname(self, is_inbound_mail_communcation=False, include_sender = False): | |||
cc_list = self.mail_cc(is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender = include_sender) | |||
def get_mail_cc_with_displayname(self, is_inbound_mail_communcation=False, include_sender=False): | |||
cc_list = self.mail_cc( | |||
is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=include_sender | |||
) | |||
return [self.get_email_with_displayname(email) for email in cc_list] | |||
def mail_bcc(self, is_inbound_mail_communcation=False): | |||
@@ -102,7 +103,7 @@ class CommunicationEmailMixin: | |||
* User must be enabled in the system | |||
* remove_administrator_from_email_list | |||
""" | |||
if hasattr(self, '_final_bcc'): | |||
if hasattr(self, "_final_bcc"): | |||
return self._final_bcc | |||
bcc = set(self.bcc_list()) | |||
@@ -116,7 +117,7 @@ class CommunicationEmailMixin: | |||
if is_inbound_mail_communcation: | |||
bcc = bcc - set(self.bcc_list() + self.to_list()) | |||
self._final_bcc = list(filter(lambda id: id != 'Administrator', bcc)) | |||
self._final_bcc = list(filter(lambda id: id != "Administrator", bcc)) | |||
return self._final_bcc | |||
def get_mail_bcc_with_displayname(self, is_inbound_mail_communcation=False): | |||
@@ -145,22 +146,23 @@ class CommunicationEmailMixin: | |||
def get_attach_link(self, print_format): | |||
"""Returns public link for the attachment via `templates/emails/print_link.html`.""" | |||
return frappe.get_template("templates/emails/print_link.html").render({ | |||
"url": get_url(), | |||
"doctype": self.reference_doctype, | |||
"name": self.reference_name, | |||
"print_format": print_format, | |||
"key": get_parent_doc(self).get_signature() | |||
}) | |||
return frappe.get_template("templates/emails/print_link.html").render( | |||
{ | |||
"url": get_url(), | |||
"doctype": self.reference_doctype, | |||
"name": self.reference_name, | |||
"print_format": print_format, | |||
"key": get_parent_doc(self).get_signature(), | |||
} | |||
) | |||
def get_outgoing_email_account(self): | |||
if not hasattr(self, '_outgoing_email_account'): | |||
if not hasattr(self, "_outgoing_email_account"): | |||
if self.email_account: | |||
self._outgoing_email_account = EmailAccount.find(self.email_account) | |||
else: | |||
self._outgoing_email_account = EmailAccount.find_outgoing( | |||
match_by_email=self.sender_mailid, | |||
match_by_doctype=self.reference_doctype | |||
match_by_email=self.sender_mailid, match_by_doctype=self.reference_doctype | |||
) | |||
if self.sent_or_received == "Sent" and self._outgoing_email_account: | |||
@@ -169,10 +171,9 @@ class CommunicationEmailMixin: | |||
return self._outgoing_email_account | |||
def get_incoming_email_account(self): | |||
if not hasattr(self, '_incoming_email_account'): | |||
if not hasattr(self, "_incoming_email_account"): | |||
self._incoming_email_account = EmailAccount.find_incoming( | |||
match_by_email=self.sender_mailid, | |||
match_by_doctype=self.reference_doctype | |||
match_by_email=self.sender_mailid, match_by_doctype=self.reference_doctype | |||
) | |||
return self._incoming_email_account | |||
@@ -180,12 +181,17 @@ class CommunicationEmailMixin: | |||
final_attachments = [] | |||
if print_format or print_html: | |||
d = {'print_format': print_format, 'html': print_html, 'print_format_attachment': 1, | |||
'doctype': self.reference_doctype, 'name': self.reference_name} | |||
d = { | |||
"print_format": print_format, | |||
"html": print_html, | |||
"print_format_attachment": 1, | |||
"doctype": self.reference_doctype, | |||
"name": self.reference_name, | |||
} | |||
final_attachments.append(d) | |||
for a in self.get_attachments() or []: | |||
final_attachments.append({"fid": a['name']}) | |||
final_attachments.append({"fid": a["name"]}) | |||
return final_attachments | |||
@@ -193,48 +199,57 @@ class CommunicationEmailMixin: | |||
email_account = self.get_outgoing_email_account() | |||
if email_account and email_account.send_unsubscribe_message: | |||
return _("Leave this conversation") | |||
return '' | |||
return "" | |||
def exclude_emails_list(self, is_inbound_mail_communcation=False, include_sender=False) -> List: | |||
"""List of mail id's excluded while sending mail. | |||
""" | |||
"""List of mail id's excluded while sending mail.""" | |||
all_ids = self.get_all_email_addresses(exclude_displayname=True) | |||
final_ids = ( | |||
self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation) | |||
+ self.mail_bcc(is_inbound_mail_communcation=is_inbound_mail_communcation) | |||
+ self.mail_cc(is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=include_sender) | |||
+ self.mail_cc( | |||
is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=include_sender | |||
) | |||
) | |||
return list(set(all_ids) - set(final_ids)) | |||
def get_assignees(self): | |||
"""Get owners of the reference document. | |||
""" | |||
filters = {'status': 'Open', 'reference_name': self.reference_name, | |||
'reference_type': self.reference_doctype} | |||
"""Get owners of the reference document.""" | |||
filters = { | |||
"status": "Open", | |||
"reference_name": self.reference_name, | |||
"reference_type": self.reference_doctype, | |||
} | |||
return ToDo.get_owners(filters) | |||
@staticmethod | |||
def filter_thread_notification_disbled_users(emails): | |||
"""Filter users based on notifications for email threads setting is disabled. | |||
""" | |||
"""Filter users based on notifications for email threads setting is disabled.""" | |||
if not emails: | |||
return [] | |||
return frappe.get_all("User", pluck="email", filters={"email": ["in", emails], "thread_notify": 0}) | |||
return frappe.get_all( | |||
"User", pluck="email", filters={"email": ["in", emails], "thread_notify": 0} | |||
) | |||
@staticmethod | |||
def filter_disabled_users(emails): | |||
""" | |||
""" | |||
""" """ | |||
if not emails: | |||
return [] | |||
return frappe.get_all("User", pluck="email", filters={"email": ["in", emails], "enabled": 0}) | |||
def sendmail_input_dict(self, print_html=None, print_format=None, | |||
send_me_a_copy=None, print_letterhead=None, is_inbound_mail_communcation=None): | |||
def sendmail_input_dict( | |||
self, | |||
print_html=None, | |||
print_format=None, | |||
send_me_a_copy=None, | |||
print_letterhead=None, | |||
is_inbound_mail_communcation=None, | |||
): | |||
outgoing_email_account = self.get_outgoing_email_account() | |||
if not outgoing_email_account: | |||
@@ -244,8 +259,7 @@ class CommunicationEmailMixin: | |||
is_inbound_mail_communcation=is_inbound_mail_communcation | |||
) | |||
cc = self.get_mail_cc_with_displayname( | |||
is_inbound_mail_communcation=is_inbound_mail_communcation, | |||
include_sender = send_me_a_copy | |||
is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=send_me_a_copy | |||
) | |||
bcc = self.get_mail_bcc_with_displayname( | |||
is_inbound_mail_communcation=is_inbound_mail_communcation | |||
@@ -273,18 +287,24 @@ class CommunicationEmailMixin: | |||
"delayed": True, | |||
"communication": self.name, | |||
"read_receipt": self.read_receipt, | |||
"is_notification": (self.sent_or_received =="Received" and True) or False, | |||
"print_letterhead": print_letterhead | |||
"is_notification": (self.sent_or_received == "Received" and True) or False, | |||
"print_letterhead": print_letterhead, | |||
} | |||
def send_email(self, print_html=None, print_format=None, | |||
send_me_a_copy=None, print_letterhead=None, is_inbound_mail_communcation=None): | |||
def send_email( | |||
self, | |||
print_html=None, | |||
print_format=None, | |||
send_me_a_copy=None, | |||
print_letterhead=None, | |||
is_inbound_mail_communcation=None, | |||
): | |||
input_dict = self.sendmail_input_dict( | |||
print_html=print_html, | |||
print_format=print_format, | |||
send_me_a_copy=send_me_a_copy, | |||
print_letterhead=print_letterhead, | |||
is_inbound_mail_communcation=is_inbound_mail_communcation | |||
is_inbound_mail_communcation=is_inbound_mail_communcation, | |||
) | |||
if input_dict: | |||
@@ -7,20 +7,30 @@ import frappe | |||
from frappe.core.doctype.communication.communication import get_emails | |||
from frappe.email.doctype.email_queue.email_queue import EmailQueue | |||
test_records = frappe.get_test_records('Communication') | |||
test_records = frappe.get_test_records("Communication") | |||
class TestCommunication(unittest.TestCase): | |||
class TestCommunication(unittest.TestCase): | |||
def test_email(self): | |||
valid_email_list = ["Full Name <full@example.com>", | |||
'"Full Name with quotes and <weird@chars.com>" <weird@example.com>', | |||
"Surname, Name <name.surname@domain.com>", | |||
"Purchase@ABC <purchase@abc.com>", "xyz@abc2.com <xyz@abc.com>", | |||
"Name [something else] <name@domain.com>"] | |||
invalid_email_list = ["[invalid!email]", "invalid-email", | |||
"tes2", "e", "rrrrrrrr", "manas","[[[sample]]]", | |||
"[invalid!email].com"] | |||
valid_email_list = [ | |||
"Full Name <full@example.com>", | |||
'"Full Name with quotes and <weird@chars.com>" <weird@example.com>', | |||
"Surname, Name <name.surname@domain.com>", | |||
"Purchase@ABC <purchase@abc.com>", | |||
"xyz@abc2.com <xyz@abc.com>", | |||
"Name [something else] <name@domain.com>", | |||
] | |||
invalid_email_list = [ | |||
"[invalid!email]", | |||
"invalid-email", | |||
"tes2", | |||
"e", | |||
"rrrrrrrr", | |||
"manas", | |||
"[[[sample]]]", | |||
"[invalid!email].com", | |||
] | |||
for x in valid_email_list: | |||
self.assertTrue(frappe.utils.parse_addr(x)[1]) | |||
@@ -29,15 +39,25 @@ class TestCommunication(unittest.TestCase): | |||
self.assertFalse(frappe.utils.parse_addr(x)[0]) | |||
def test_name(self): | |||
valid_email_list = ["Full Name <full@example.com>", | |||
'"Full Name with quotes and <weird@chars.com>" <weird@example.com>', | |||
"Surname, Name <name.surname@domain.com>", | |||
"Purchase@ABC <purchase@abc.com>", "xyz@abc2.com <xyz@abc.com>", | |||
"Name [something else] <name@domain.com>"] | |||
invalid_email_list = ["[invalid!email]", "invalid-email", | |||
"tes2", "e", "rrrrrrrr", "manas","[[[sample]]]", | |||
"[invalid!email].com"] | |||
valid_email_list = [ | |||
"Full Name <full@example.com>", | |||
'"Full Name with quotes and <weird@chars.com>" <weird@example.com>', | |||
"Surname, Name <name.surname@domain.com>", | |||
"Purchase@ABC <purchase@abc.com>", | |||
"xyz@abc2.com <xyz@abc.com>", | |||
"Name [something else] <name@domain.com>", | |||
] | |||
invalid_email_list = [ | |||
"[invalid!email]", | |||
"invalid-email", | |||
"tes2", | |||
"e", | |||
"rrrrrrrr", | |||
"manas", | |||
"[[[sample]]]", | |||
"[invalid!email].com", | |||
] | |||
for x in valid_email_list: | |||
self.assertTrue(frappe.utils.parse_addr(x)[0]) | |||
@@ -46,27 +66,33 @@ class TestCommunication(unittest.TestCase): | |||
self.assertFalse(frappe.utils.parse_addr(x)[0]) | |||
def test_circular_linking(self): | |||
a = frappe.get_doc({ | |||
"doctype": "Communication", | |||
"communication_type": "Communication", | |||
"content": "This was created to test circular linking: Communication A", | |||
}).insert(ignore_permissions=True) | |||
b = frappe.get_doc({ | |||
"doctype": "Communication", | |||
"communication_type": "Communication", | |||
"content": "This was created to test circular linking: Communication B", | |||
"reference_doctype": "Communication", | |||
"reference_name": a.name | |||
}).insert(ignore_permissions=True) | |||
c = frappe.get_doc({ | |||
"doctype": "Communication", | |||
"communication_type": "Communication", | |||
"content": "This was created to test circular linking: Communication C", | |||
"reference_doctype": "Communication", | |||
"reference_name": b.name | |||
}).insert(ignore_permissions=True) | |||
a = frappe.get_doc( | |||
{ | |||
"doctype": "Communication", | |||
"communication_type": "Communication", | |||
"content": "This was created to test circular linking: Communication A", | |||
} | |||
).insert(ignore_permissions=True) | |||
b = frappe.get_doc( | |||
{ | |||
"doctype": "Communication", | |||
"communication_type": "Communication", | |||
"content": "This was created to test circular linking: Communication B", | |||
"reference_doctype": "Communication", | |||
"reference_name": a.name, | |||
} | |||
).insert(ignore_permissions=True) | |||
c = frappe.get_doc( | |||
{ | |||
"doctype": "Communication", | |||
"communication_type": "Communication", | |||
"content": "This was created to test circular linking: Communication C", | |||
"reference_doctype": "Communication", | |||
"reference_name": b.name, | |||
} | |||
).insert(ignore_permissions=True) | |||
a = frappe.get_doc("Communication", a.name) | |||
a.reference_doctype = "Communication" | |||
@@ -77,20 +103,24 @@ class TestCommunication(unittest.TestCase): | |||
def test_deduplication_timeline_links(self): | |||
frappe.delete_doc_if_exists("Note", "deduplication timeline links") | |||
note = frappe.get_doc({ | |||
"doctype": "Note", | |||
"title": "deduplication timeline links", | |||
"content": "deduplication timeline links" | |||
}).insert(ignore_permissions=True) | |||
comm = frappe.get_doc({ | |||
"doctype": "Communication", | |||
"communication_type": "Communication", | |||
"content": "Deduplication of Links", | |||
"communication_medium": "Email" | |||
}).insert(ignore_permissions=True) | |||
#adding same link twice | |||
note = frappe.get_doc( | |||
{ | |||
"doctype": "Note", | |||
"title": "deduplication timeline links", | |||
"content": "deduplication timeline links", | |||
} | |||
).insert(ignore_permissions=True) | |||
comm = frappe.get_doc( | |||
{ | |||
"doctype": "Communication", | |||
"communication_type": "Communication", | |||
"content": "Deduplication of Links", | |||
"communication_medium": "Email", | |||
} | |||
).insert(ignore_permissions=True) | |||
# adding same link twice | |||
comm.add_link(link_doctype="Note", link_name=note.name, autosave=True) | |||
comm.add_link(link_doctype="Note", link_name=note.name, autosave=True) | |||
@@ -99,35 +129,43 @@ class TestCommunication(unittest.TestCase): | |||
self.assertNotEqual(2, len(comm.timeline_links)) | |||
def test_contacts_attached(self): | |||
contact_sender = frappe.get_doc({ | |||
"doctype": "Contact", | |||
"first_name": "contact_sender", | |||
}) | |||
contact_sender = frappe.get_doc( | |||
{ | |||
"doctype": "Contact", | |||
"first_name": "contact_sender", | |||
} | |||
) | |||
contact_sender.add_email("comm_sender@example.com") | |||
contact_sender.insert(ignore_permissions=True) | |||
contact_recipient = frappe.get_doc({ | |||
"doctype": "Contact", | |||
"first_name": "contact_recipient", | |||
}) | |||
contact_recipient = frappe.get_doc( | |||
{ | |||
"doctype": "Contact", | |||
"first_name": "contact_recipient", | |||
} | |||
) | |||
contact_recipient.add_email("comm_recipient@example.com") | |||
contact_recipient.insert(ignore_permissions=True) | |||
contact_cc = frappe.get_doc({ | |||
"doctype": "Contact", | |||
"first_name": "contact_cc", | |||
}) | |||
contact_cc = frappe.get_doc( | |||
{ | |||
"doctype": "Contact", | |||
"first_name": "contact_cc", | |||
} | |||
) | |||
contact_cc.add_email("comm_cc@example.com") | |||
contact_cc.insert(ignore_permissions=True) | |||
comm = frappe.get_doc({ | |||
"doctype": "Communication", | |||
"communication_medium": "Email", | |||
"subject": "Contacts Attached Test", | |||
"sender": "comm_sender@example.com", | |||
"recipients": "comm_recipient@example.com", | |||
"cc": "comm_cc@example.com" | |||
}).insert(ignore_permissions=True) | |||
comm = frappe.get_doc( | |||
{ | |||
"doctype": "Communication", | |||
"communication_medium": "Email", | |||
"subject": "Contacts Attached Test", | |||
"sender": "comm_sender@example.com", | |||
"recipients": "comm_recipient@example.com", | |||
"cc": "comm_cc@example.com", | |||
} | |||
).insert(ignore_permissions=True) | |||
comm = frappe.get_doc("Communication", comm.name) | |||
@@ -144,27 +182,29 @@ class TestCommunication(unittest.TestCase): | |||
frappe.delete_doc_if_exists("Note", "get communication data") | |||
note = frappe.get_doc({ | |||
"doctype": "Note", | |||
"title": "get communication data", | |||
"content": "get communication data" | |||
}).insert(ignore_permissions=True) | |||
note = frappe.get_doc( | |||
{"doctype": "Note", "title": "get communication data", "content": "get communication data"} | |||
).insert(ignore_permissions=True) | |||
comm_note_1 = frappe.get_doc({ | |||
"doctype": "Communication", | |||
"communication_type": "Communication", | |||
"content": "Test Get Communication Data 1", | |||
"communication_medium": "Email" | |||
}).insert(ignore_permissions=True) | |||
comm_note_1 = frappe.get_doc( | |||
{ | |||
"doctype": "Communication", | |||
"communication_type": "Communication", | |||
"content": "Test Get Communication Data 1", | |||
"communication_medium": "Email", | |||
} | |||
).insert(ignore_permissions=True) | |||
comm_note_1.add_link(link_doctype="Note", link_name=note.name, autosave=True) | |||
comm_note_2 = frappe.get_doc({ | |||
"doctype": "Communication", | |||
"communication_type": "Communication", | |||
"content": "Test Get Communication Data 2", | |||
"communication_medium": "Email" | |||
}).insert(ignore_permissions=True) | |||
comm_note_2 = frappe.get_doc( | |||
{ | |||
"doctype": "Communication", | |||
"communication_type": "Communication", | |||
"content": "Test Get Communication Data 2", | |||
"communication_medium": "Email", | |||
} | |||
).insert(ignore_permissions=True) | |||
comm_note_2.add_link(link_doctype="Note", link_name=note.name, autosave=True) | |||
@@ -182,19 +222,23 @@ class TestCommunication(unittest.TestCase): | |||
create_email_account() | |||
note = frappe.get_doc({ | |||
"doctype": "Note", | |||
"title": "test document link in email", | |||
"content": "test document link in email" | |||
}).insert(ignore_permissions=True) | |||
comm = frappe.get_doc({ | |||
"doctype": "Communication", | |||
"communication_medium": "Email", | |||
"subject": "Document Link in Email", | |||
"sender": "comm_sender@example.com", | |||
"recipients": "comm_recipient+{0}+{1}@example.com".format(quote("Note"), quote(note.name)), | |||
}).insert(ignore_permissions=True) | |||
note = frappe.get_doc( | |||
{ | |||
"doctype": "Note", | |||
"title": "test document link in email", | |||
"content": "test document link in email", | |||
} | |||
).insert(ignore_permissions=True) | |||
comm = frappe.get_doc( | |||
{ | |||
"doctype": "Communication", | |||
"communication_medium": "Email", | |||
"subject": "Document Link in Email", | |||
"sender": "comm_sender@example.com", | |||
"recipients": "comm_recipient+{0}+{1}@example.com".format(quote("Note"), quote(note.name)), | |||
} | |||
).insert(ignore_permissions=True) | |||
doc_links = [] | |||
for timeline_link in comm.timeline_links: | |||
@@ -205,9 +249,9 @@ class TestCommunication(unittest.TestCase): | |||
def test_parse_emails(self): | |||
emails = get_emails( | |||
[ | |||
'comm_recipient+DocType+DocName@example.com', | |||
"comm_recipient+DocType+DocName@example.com", | |||
'"First, LastName" <first.lastname@email.com>', | |||
'test@user.com' | |||
"test@user.com", | |||
] | |||
) | |||
@@ -215,99 +259,108 @@ class TestCommunication(unittest.TestCase): | |||
self.assertEqual(emails[1], "first.lastname@email.com") | |||
self.assertEqual(emails[2], "test@user.com") | |||
class TestCommunicationEmailMixin(unittest.TestCase): | |||
def new_communication(self, recipients=None, cc=None, bcc=None): | |||
recipients = ', '.join(recipients or []) | |||
cc = ', '.join(cc or []) | |||
bcc = ', '.join(bcc or []) | |||
comm = frappe.get_doc({ | |||
"doctype": "Communication", | |||
"communication_type": "Communication", | |||
"communication_medium": "Email", | |||
"content": "Test content", | |||
"recipients": recipients, | |||
"cc": cc, | |||
"bcc": bcc | |||
}).insert(ignore_permissions=True) | |||
recipients = ", ".join(recipients or []) | |||
cc = ", ".join(cc or []) | |||
bcc = ", ".join(bcc or []) | |||
comm = frappe.get_doc( | |||
{ | |||
"doctype": "Communication", | |||
"communication_type": "Communication", | |||
"communication_medium": "Email", | |||
"content": "Test content", | |||
"recipients": recipients, | |||
"cc": cc, | |||
"bcc": bcc, | |||
} | |||
).insert(ignore_permissions=True) | |||
return comm | |||
def new_user(self, email, **user_data): | |||
user_data.setdefault('first_name', 'first_name') | |||
user = frappe.new_doc('User') | |||
user_data.setdefault("first_name", "first_name") | |||
user = frappe.new_doc("User") | |||
user.email = email | |||
user.update(user_data) | |||
user.insert(ignore_permissions=True, ignore_if_duplicate=True) | |||
return user | |||
def test_recipients(self): | |||
to_list = ['to@test.com', 'receiver <to+1@test.com>', 'to@test.com'] | |||
comm = self.new_communication(recipients = to_list) | |||
to_list = ["to@test.com", "receiver <to+1@test.com>", "to@test.com"] | |||
comm = self.new_communication(recipients=to_list) | |||
res = comm.get_mail_recipients_with_displayname() | |||
self.assertCountEqual(res, ['to@test.com', 'receiver <to+1@test.com>']) | |||
self.assertCountEqual(res, ["to@test.com", "receiver <to+1@test.com>"]) | |||
comm.delete() | |||
def test_cc(self): | |||
to_list = ['to@test.com'] | |||
cc_list = ['cc+1@test.com', 'cc <cc+2@test.com>', 'to@test.com'] | |||
user = self.new_user(email='cc+1@test.com', thread_notify=0) | |||
to_list = ["to@test.com"] | |||
cc_list = ["cc+1@test.com", "cc <cc+2@test.com>", "to@test.com"] | |||
user = self.new_user(email="cc+1@test.com", thread_notify=0) | |||
comm = self.new_communication(recipients=to_list, cc=cc_list) | |||
res = comm.get_mail_cc_with_displayname() | |||
self.assertCountEqual(res, ['cc <cc+2@test.com>']) | |||
self.assertCountEqual(res, ["cc <cc+2@test.com>"]) | |||
user.delete() | |||
comm.delete() | |||
def test_bcc(self): | |||
bcc_list = ['bcc+1@test.com', 'cc <bcc+2@test.com>', ] | |||
user = self.new_user(email='bcc+2@test.com', enabled=0) | |||
bcc_list = [ | |||
"bcc+1@test.com", | |||
"cc <bcc+2@test.com>", | |||
] | |||
user = self.new_user(email="bcc+2@test.com", enabled=0) | |||
comm = self.new_communication(bcc=bcc_list) | |||
res = comm.get_mail_bcc_with_displayname() | |||
self.assertCountEqual(res, ['bcc+1@test.com']) | |||
self.assertCountEqual(res, ["bcc+1@test.com"]) | |||
user.delete() | |||
comm.delete() | |||
def test_sendmail(self): | |||
to_list = ['to <to@test.com>'] | |||
cc_list = ['cc <cc+1@test.com>', 'cc <cc+2@test.com>'] | |||
to_list = ["to <to@test.com>"] | |||
cc_list = ["cc <cc+1@test.com>", "cc <cc+2@test.com>"] | |||
comm = self.new_communication(recipients=to_list, cc=cc_list) | |||
comm.send_email() | |||
doc = EmailQueue.find_one_by_filters(communication=comm.name) | |||
mail_receivers = [each.recipient for each in doc.recipients] | |||
self.assertIsNotNone(doc) | |||
self.assertCountEqual(to_list+cc_list, mail_receivers) | |||
self.assertCountEqual(to_list + cc_list, mail_receivers) | |||
doc.delete() | |||
comm.delete() | |||
def create_email_account(): | |||
frappe.delete_doc_if_exists("Email Account", "_Test Comm Account 1") | |||
frappe.flags.mute_emails = False | |||
frappe.flags.sent_mail = None | |||
email_account = frappe.get_doc({ | |||
"is_default": 1, | |||
"is_global": 1, | |||
"doctype": "Email Account", | |||
"domain":"example.com", | |||
"append_to": "ToDo", | |||
"email_account_name": "_Test Comm Account 1", | |||
"enable_outgoing": 1, | |||
"smtp_server": "test.example.com", | |||
"email_id": "test_comm@example.com", | |||
"password": "password", | |||
"add_signature": 1, | |||
"signature": "\nBest Wishes\nTest Signature", | |||
"enable_auto_reply": 1, | |||
"auto_reply_message": "", | |||
"enable_incoming": 1, | |||
"notify_if_unreplied": 1, | |||
"unreplied_for_mins": 20, | |||
"send_notification_to": "test_comm@example.com", | |||
"pop3_server": "pop.test.example.com", | |||
"imap_folder": [{"folder_name": "INBOX", "append_to": "ToDo"}], | |||
"no_remaining":"0", | |||
"enable_automatic_linking": 1 | |||
}).insert(ignore_permissions=True) | |||
email_account = frappe.get_doc( | |||
{ | |||
"is_default": 1, | |||
"is_global": 1, | |||
"doctype": "Email Account", | |||
"domain": "example.com", | |||
"append_to": "ToDo", | |||
"email_account_name": "_Test Comm Account 1", | |||
"enable_outgoing": 1, | |||
"smtp_server": "test.example.com", | |||
"email_id": "test_comm@example.com", | |||
"password": "password", | |||
"add_signature": 1, | |||
"signature": "\nBest Wishes\nTest Signature", | |||
"enable_auto_reply": 1, | |||
"auto_reply_message": "", | |||
"enable_incoming": 1, | |||
"notify_if_unreplied": 1, | |||
"unreplied_for_mins": 20, | |||
"send_notification_to": "test_comm@example.com", | |||
"pop3_server": "pop.test.example.com", | |||
"imap_folder": [{"folder_name": "INBOX", "append_to": "ToDo"}], | |||
"no_remaining": "0", | |||
"enable_automatic_linking": 1, | |||
} | |||
).insert(ignore_permissions=True) | |||
return email_account |
@@ -5,8 +5,10 @@ | |||
import frappe | |||
from frappe.model.document import Document | |||
class CommunicationLink(Document): | |||
pass | |||
def on_doctype_update(): | |||
frappe.db.add_index("Communication Link", ["link_doctype", "link_name"]) | |||
frappe.db.add_index("Communication Link", ["link_doctype", "link_name"]) |
@@ -5,6 +5,7 @@ | |||
import frappe | |||
from frappe.model.document import Document | |||
class CustomDocPerm(Document): | |||
def on_update(self): | |||
frappe.clear_cache(doctype = self.parent) | |||
frappe.clear_cache(doctype=self.parent) |
@@ -1,10 +1,12 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright (c) 2015, Frappe Technologies and Contributors | |||
# License: MIT. See LICENSE | |||
import frappe | |||
import unittest | |||
import frappe | |||
# test_records = frappe.get_test_records('Custom DocPerm') | |||
class TestCustomDocPerm(unittest.TestCase): | |||
pass |
@@ -5,16 +5,18 @@ | |||
import frappe | |||
from frappe.model.document import Document | |||
class CustomRole(Document): | |||
def validate(self): | |||
if self.report and not self.ref_doctype: | |||
self.ref_doctype = frappe.db.get_value('Report', self.report, 'ref_doctype') | |||
self.ref_doctype = frappe.db.get_value("Report", self.report, "ref_doctype") | |||
def get_custom_allowed_roles(field, name): | |||
allowed_roles = [] | |||
custom_role = frappe.db.get_value('Custom Role', {field: name}, 'name') | |||
custom_role = frappe.db.get_value("Custom Role", {field: name}, "name") | |||
if custom_role: | |||
custom_role_doc = frappe.get_doc('Custom Role', custom_role) | |||
custom_role_doc = frappe.get_doc("Custom Role", custom_role) | |||
allowed_roles = [d.role for d in custom_role_doc.roles] | |||
return allowed_roles | |||
return allowed_roles |
@@ -1,10 +1,12 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright (c) 2015, Frappe Technologies and Contributors | |||
# License: MIT. See LICENSE | |||
import frappe | |||
import unittest | |||
import frappe | |||
# test_records = frappe.get_test_records('Custom Role') | |||
class TestCustomRole(unittest.TestCase): | |||
pass |
@@ -4,5 +4,6 @@ | |||
from frappe.model.document import Document | |||
class DataExport(Document): | |||
pass |
@@ -1,47 +1,78 @@ | |||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
import csv | |||
import os | |||
import re | |||
import frappe | |||
from frappe import _ | |||
import frappe.permissions | |||
import re, csv, os | |||
from frappe.utils.csvutils import UnicodeWriter | |||
from frappe.utils import cstr, formatdate, format_datetime, parse_json, cint, format_duration | |||
from frappe import _ | |||
from frappe.core.doctype.access_log.access_log import make_access_log | |||
from frappe.utils import cint, cstr, format_datetime, format_duration, formatdate, parse_json | |||
from frappe.utils.csvutils import UnicodeWriter | |||
reflags = {"I": re.I, "L": re.L, "M": re.M, "U": re.U, "S": re.S, "X": re.X, "D": re.DEBUG} | |||
reflags = { | |||
"I":re.I, | |||
"L":re.L, | |||
"M":re.M, | |||
"U":re.U, | |||
"S":re.S, | |||
"X":re.X, | |||
"D": re.DEBUG | |||
} | |||
def get_data_keys(): | |||
return frappe._dict({ | |||
"data_separator": _('Start entering data below this line'), | |||
"main_table": _("Table") + ":", | |||
"parent_table": _("Parent Table") + ":", | |||
"columns": _("Column Name") + ":", | |||
"doctype": _("DocType") + ":" | |||
}) | |||
return frappe._dict( | |||
{ | |||
"data_separator": _("Start entering data below this line"), | |||
"main_table": _("Table") + ":", | |||
"parent_table": _("Parent Table") + ":", | |||
"columns": _("Column Name") + ":", | |||
"doctype": _("DocType") + ":", | |||
} | |||
) | |||
@frappe.whitelist() | |||
def export_data(doctype=None, parent_doctype=None, all_doctypes=True, with_data=False, | |||
select_columns=None, file_type='CSV', template=False, filters=None): | |||
def export_data( | |||
doctype=None, | |||
parent_doctype=None, | |||
all_doctypes=True, | |||
with_data=False, | |||
select_columns=None, | |||
file_type="CSV", | |||
template=False, | |||
filters=None, | |||
): | |||
_doctype = doctype | |||
if isinstance(_doctype, list): | |||
_doctype = _doctype[0] | |||
make_access_log(doctype=_doctype, file_type=file_type, columns=select_columns, filters=filters, method=parent_doctype) | |||
exporter = DataExporter(doctype=doctype, parent_doctype=parent_doctype, all_doctypes=all_doctypes, with_data=with_data, | |||
select_columns=select_columns, file_type=file_type, template=template, filters=filters) | |||
make_access_log( | |||
doctype=_doctype, | |||
file_type=file_type, | |||
columns=select_columns, | |||
filters=filters, | |||
method=parent_doctype, | |||
) | |||
exporter = DataExporter( | |||
doctype=doctype, | |||
parent_doctype=parent_doctype, | |||
all_doctypes=all_doctypes, | |||
with_data=with_data, | |||
select_columns=select_columns, | |||
file_type=file_type, | |||
template=template, | |||
filters=filters, | |||
) | |||
exporter.build_response() | |||
class DataExporter: | |||
def __init__(self, doctype=None, parent_doctype=None, all_doctypes=True, with_data=False, | |||
select_columns=None, file_type='CSV', template=False, filters=None): | |||
def __init__( | |||
self, | |||
doctype=None, | |||
parent_doctype=None, | |||
all_doctypes=True, | |||
with_data=False, | |||
select_columns=None, | |||
file_type="CSV", | |||
template=False, | |||
filters=None, | |||
): | |||
self.doctype = doctype | |||
self.parent_doctype = parent_doctype | |||
self.all_doctypes = all_doctypes | |||
@@ -81,18 +112,18 @@ class DataExporter: | |||
def build_response(self): | |||
self.writer = UnicodeWriter() | |||
self.name_field = 'parent' if self.parent_doctype != self.doctype else 'name' | |||
self.name_field = "parent" if self.parent_doctype != self.doctype else "name" | |||
if self.template: | |||
self.add_main_header() | |||
self.writer.writerow(['']) | |||
self.writer.writerow([""]) | |||
self.tablerow = [self.data_keys.doctype] | |||
self.labelrow = [_("Column Labels:")] | |||
self.fieldrow = [self.data_keys.columns] | |||
self.mandatoryrow = [_("Mandatory:")] | |||
self.typerow = [_('Type:')] | |||
self.inforow = [_('Info:')] | |||
self.typerow = [_("Type:")] | |||
self.inforow = [_("Info:")] | |||
self.columns = [] | |||
self.build_field_columns(self.doctype) | |||
@@ -100,74 +131,99 @@ class DataExporter: | |||
if self.all_doctypes: | |||
for d in self.child_doctypes: | |||
self.append_empty_field_column() | |||
if (self.select_columns and self.select_columns.get(d['doctype'], None)) or not self.select_columns: | |||
if ( | |||
self.select_columns and self.select_columns.get(d["doctype"], None) | |||
) or not self.select_columns: | |||
# if atleast one column is selected for this doctype | |||
self.build_field_columns(d['doctype'], d['parentfield']) | |||
self.build_field_columns(d["doctype"], d["parentfield"]) | |||
self.add_field_headings() | |||
self.add_data() | |||
if self.with_data and not self.data: | |||
frappe.respond_as_web_page(_('No Data'), _('There is no data to be exported'), indicator_color='orange') | |||
frappe.respond_as_web_page( | |||
_("No Data"), _("There is no data to be exported"), indicator_color="orange" | |||
) | |||
if self.file_type == 'Excel': | |||
if self.file_type == "Excel": | |||
self.build_response_as_excel() | |||
else: | |||
# write out response as a type csv | |||
frappe.response['result'] = cstr(self.writer.getvalue()) | |||
frappe.response['type'] = 'csv' | |||
frappe.response['doctype'] = self.doctype | |||
frappe.response["result"] = cstr(self.writer.getvalue()) | |||
frappe.response["type"] = "csv" | |||
frappe.response["doctype"] = self.doctype | |||
def add_main_header(self): | |||
self.writer.writerow([_('Data Import Template')]) | |||
self.writer.writerow([_("Data Import Template")]) | |||
self.writer.writerow([self.data_keys.main_table, self.doctype]) | |||
if self.parent_doctype != self.doctype: | |||
self.writer.writerow([self.data_keys.parent_table, self.parent_doctype]) | |||
else: | |||
self.writer.writerow(['']) | |||
self.writer.writerow(['']) | |||
self.writer.writerow([_('Notes:')]) | |||
self.writer.writerow([_('Please do not change the template headings.')]) | |||
self.writer.writerow([_('First data column must be blank.')]) | |||
self.writer.writerow([_('If you are uploading new records, leave the "name" (ID) column blank.')]) | |||
self.writer.writerow([_('If you are uploading new records, "Naming Series" becomes mandatory, if present.')]) | |||
self.writer.writerow([_('Only mandatory fields are necessary for new records. You can delete non-mandatory columns if you wish.')]) | |||
self.writer.writerow([_('For updating, you can update only selective columns.')]) | |||
self.writer.writerow([_('You can only upload upto 5000 records in one go. (may be less in some cases)')]) | |||
self.writer.writerow([""]) | |||
self.writer.writerow([""]) | |||
self.writer.writerow([_("Notes:")]) | |||
self.writer.writerow([_("Please do not change the template headings.")]) | |||
self.writer.writerow([_("First data column must be blank.")]) | |||
self.writer.writerow( | |||
[_('If you are uploading new records, leave the "name" (ID) column blank.')] | |||
) | |||
self.writer.writerow( | |||
[_('If you are uploading new records, "Naming Series" becomes mandatory, if present.')] | |||
) | |||
self.writer.writerow( | |||
[ | |||
_( | |||
"Only mandatory fields are necessary for new records. You can delete non-mandatory columns if you wish." | |||
) | |||
] | |||
) | |||
self.writer.writerow([_("For updating, you can update only selective columns.")]) | |||
self.writer.writerow( | |||
[_("You can only upload upto 5000 records in one go. (may be less in some cases)")] | |||
) | |||
if self.name_field == "parent": | |||
self.writer.writerow([_('"Parent" signifies the parent table in which this row must be added')]) | |||
self.writer.writerow([_('If you are updating, please select "Overwrite" else existing rows will not be deleted.')]) | |||
self.writer.writerow( | |||
[_('If you are updating, please select "Overwrite" else existing rows will not be deleted.')] | |||
) | |||
def build_field_columns(self, dt, parentfield=None): | |||
meta = frappe.get_meta(dt) | |||
# build list of valid docfields | |||
tablecolumns = [] | |||
table_name = 'tab' + dt | |||
table_name = "tab" + dt | |||
for f in frappe.db.get_table_columns_description(table_name): | |||
field = meta.get_field(f.name) | |||
if field and ((self.select_columns and f.name in self.select_columns[dt]) or not self.select_columns): | |||
if field and ( | |||
(self.select_columns and f.name in self.select_columns[dt]) or not self.select_columns | |||
): | |||
tablecolumns.append(field) | |||
tablecolumns.sort(key = lambda a: int(a.idx)) | |||
tablecolumns.sort(key=lambda a: int(a.idx)) | |||
_column_start_end = frappe._dict(start=0) | |||
if dt==self.doctype: | |||
if (meta.get('autoname') and meta.get('autoname').lower()=='prompt') or (self.with_data): | |||
if dt == self.doctype: | |||
if (meta.get("autoname") and meta.get("autoname").lower() == "prompt") or (self.with_data): | |||
self._append_name_column() | |||
# if importing only child table for new record, add parent field | |||
if meta.get('istable') and not self.with_data: | |||
self.append_field_column(frappe._dict({ | |||
"fieldname": "parent", | |||
"parent": "", | |||
"label": "Parent", | |||
"fieldtype": "Data", | |||
"reqd": 1, | |||
"info": _("Parent is the name of the document to which the data will get added to.") | |||
}), True) | |||
if meta.get("istable") and not self.with_data: | |||
self.append_field_column( | |||
frappe._dict( | |||
{ | |||
"fieldname": "parent", | |||
"parent": "", | |||
"label": "Parent", | |||
"fieldtype": "Data", | |||
"reqd": 1, | |||
"info": _("Parent is the name of the document to which the data will get added to."), | |||
} | |||
), | |||
True, | |||
) | |||
_column_start_end = frappe._dict(start=0) | |||
else: | |||
@@ -184,7 +240,7 @@ class DataExporter: | |||
self.append_field_column(docfield, False) | |||
# if there is one column, add a blank column (?) | |||
if len(self.columns)-_column_start_end.start == 1: | |||
if len(self.columns) - _column_start_end.start == 1: | |||
self.append_empty_field_column() | |||
# append DocType name | |||
@@ -204,18 +260,21 @@ class DataExporter: | |||
return | |||
if not for_mandatory and docfield.reqd: | |||
return | |||
if docfield.fieldname in ('parenttype', 'trash_reason'): | |||
if docfield.fieldname in ("parenttype", "trash_reason"): | |||
return | |||
if docfield.hidden: | |||
return | |||
if self.select_columns and docfield.fieldname not in self.select_columns.get(docfield.parent, []) \ | |||
and docfield.fieldname!="name": | |||
if ( | |||
self.select_columns | |||
and docfield.fieldname not in self.select_columns.get(docfield.parent, []) | |||
and docfield.fieldname != "name" | |||
): | |||
return | |||
self.tablerow.append("") | |||
self.fieldrow.append(docfield.fieldname) | |||
self.labelrow.append(_(docfield.label)) | |||
self.mandatoryrow.append(docfield.reqd and 'Yes' or 'No') | |||
self.mandatoryrow.append(docfield.reqd and "Yes" or "No") | |||
self.typerow.append(docfield.fieldtype) | |||
self.inforow.append(self.getinforow(docfield)) | |||
self.columns.append(docfield.fieldname) | |||
@@ -232,15 +291,15 @@ class DataExporter: | |||
@staticmethod | |||
def getinforow(docfield): | |||
"""make info comment for options, links etc.""" | |||
if docfield.fieldtype == 'Select': | |||
if docfield.fieldtype == "Select": | |||
if not docfield.options: | |||
return '' | |||
return "" | |||
else: | |||
return _("One of") + ': %s' % ', '.join(filter(None, docfield.options.split('\n'))) | |||
elif docfield.fieldtype == 'Link': | |||
return 'Valid %s' % docfield.options | |||
elif docfield.fieldtype == 'Int': | |||
return 'Integer' | |||
return _("One of") + ": %s" % ", ".join(filter(None, docfield.options.split("\n"))) | |||
elif docfield.fieldtype == "Link": | |||
return "Valid %s" % docfield.options | |||
elif docfield.fieldtype == "Int": | |||
return "Integer" | |||
elif docfield.fieldtype == "Check": | |||
return "0 or 1" | |||
elif docfield.fieldtype in ["Date", "Datetime"]: | |||
@@ -248,7 +307,7 @@ class DataExporter: | |||
elif hasattr(docfield, "info"): | |||
return docfield.info | |||
else: | |||
return '' | |||
return "" | |||
def add_field_headings(self): | |||
self.writer.writerow(self.tablerow) | |||
@@ -262,6 +321,7 @@ class DataExporter: | |||
def add_data(self): | |||
from frappe.query_builder import DocType | |||
if self.template and not self.with_data: | |||
return | |||
@@ -270,26 +330,28 @@ class DataExporter: | |||
# sort nested set doctypes by `lft asc` | |||
order_by = None | |||
table_columns = frappe.db.get_table_columns(self.parent_doctype) | |||
if 'lft' in table_columns and 'rgt' in table_columns: | |||
order_by = '`tab{doctype}`.`lft` asc'.format(doctype=self.parent_doctype) | |||
if "lft" in table_columns and "rgt" in table_columns: | |||
order_by = "`tab{doctype}`.`lft` asc".format(doctype=self.parent_doctype) | |||
# get permitted data only | |||
self.data = frappe.get_list(self.doctype, fields=["*"], filters=self.filters, limit_page_length=None, order_by=order_by) | |||
self.data = frappe.get_list( | |||
self.doctype, fields=["*"], filters=self.filters, limit_page_length=None, order_by=order_by | |||
) | |||
for doc in self.data: | |||
op = self.docs_to_export.get("op") | |||
names = self.docs_to_export.get("name") | |||
if names and op: | |||
if op == '=' and doc.name not in names: | |||
if op == "=" and doc.name not in names: | |||
continue | |||
elif op == '!=' and doc.name in names: | |||
elif op == "!=" and doc.name in names: | |||
continue | |||
elif names: | |||
try: | |||
sflags = self.docs_to_export.get("flags", "I,U").upper() | |||
flags = 0 | |||
for a in re.split(r'\W+', sflags): | |||
flags = flags | reflags.get(a,0) | |||
for a in re.split(r"\W+", sflags): | |||
flags = flags | reflags.get(a, 0) | |||
c = re.compile(names, flags) | |||
m = c.match(doc.name) | |||
@@ -315,7 +377,7 @@ class DataExporter: | |||
.orderby(child_doctype_table.idx) | |||
) | |||
for ci, child in enumerate(data_row.run(as_dict=True)): | |||
self.add_data_row(rows, c['doctype'], c['parentfield'], child, ci) | |||
self.add_data_row(rows, c["doctype"], c["parentfield"], child, ci) | |||
for row in rows: | |||
self.writer.writerow(row) | |||
@@ -333,7 +395,7 @@ class DataExporter: | |||
_column_start_end = self.column_start_end.get((dt, parentfield)) | |||
if _column_start_end: | |||
for i, c in enumerate(self.columns[_column_start_end.start:_column_start_end.end]): | |||
for i, c in enumerate(self.columns[_column_start_end.start : _column_start_end.end]): | |||
df = meta.get_field(c) | |||
fieldtype = df.fieldtype if df else "Data" | |||
value = d.get(c, "") | |||
@@ -349,27 +411,33 @@ class DataExporter: | |||
def build_response_as_excel(self): | |||
filename = frappe.generate_hash("", 10) | |||
with open(filename, 'wb') as f: | |||
f.write(cstr(self.writer.getvalue()).encode('utf-8')) | |||
with open(filename, "wb") as f: | |||
f.write(cstr(self.writer.getvalue()).encode("utf-8")) | |||
f = open(filename) | |||
reader = csv.reader(f) | |||
from frappe.utils.xlsxutils import make_xlsx | |||
xlsx_file = make_xlsx(reader, "Data Import Template" if self.template else 'Data Export') | |||
xlsx_file = make_xlsx(reader, "Data Import Template" if self.template else "Data Export") | |||
f.close() | |||
os.remove(filename) | |||
# write out response as a xlsx type | |||
frappe.response['filename'] = self.doctype + '.xlsx' | |||
frappe.response['filecontent'] = xlsx_file.getvalue() | |||
frappe.response['type'] = 'binary' | |||
frappe.response["filename"] = self.doctype + ".xlsx" | |||
frappe.response["filecontent"] = xlsx_file.getvalue() | |||
frappe.response["type"] = "binary" | |||
def _append_name_column(self, dt=None): | |||
self.append_field_column(frappe._dict({ | |||
"fieldname": "name" if dt else self.name_field, | |||
"parent": dt or "", | |||
"label": "ID", | |||
"fieldtype": "Data", | |||
"reqd": 1, | |||
}), True) | |||
self.append_field_column( | |||
frappe._dict( | |||
{ | |||
"fieldname": "name" if dt else self.name_field, | |||
"parent": dt or "", | |||
"label": "ID", | |||
"fieldtype": "Data", | |||
"reqd": 1, | |||
} | |||
), | |||
True, | |||
) |
@@ -2,13 +2,15 @@ | |||
# Copyright (c) 2019, Frappe Technologies and Contributors | |||
# License: MIT. See LICENSE | |||
import unittest | |||
import frappe | |||
from frappe.core.doctype.data_export.exporter import DataExporter | |||
class TestDataExporter(unittest.TestCase): | |||
def setUp(self): | |||
self.doctype_name = 'Test DocType for Export Tool' | |||
self.doc_name = 'Test Data for Export Tool' | |||
self.doctype_name = "Test DocType for Export Tool" | |||
self.doc_name = "Test Data for Export Tool" | |||
self.create_doctype_if_not_exists(doctype_name=self.doctype_name) | |||
self.create_test_data() | |||
@@ -17,42 +19,49 @@ class TestDataExporter(unittest.TestCase): | |||
Helper Function for setting up doctypes | |||
""" | |||
if force: | |||
frappe.delete_doc_if_exists('DocType', doctype_name) | |||
frappe.delete_doc_if_exists('DocType', 'Child 1 of ' + doctype_name) | |||
frappe.delete_doc_if_exists("DocType", doctype_name) | |||
frappe.delete_doc_if_exists("DocType", "Child 1 of " + doctype_name) | |||
if frappe.db.exists('DocType', doctype_name): | |||
if frappe.db.exists("DocType", doctype_name): | |||
return | |||
# Child Table 1 | |||
table_1_name = 'Child 1 of ' + doctype_name | |||
frappe.get_doc({ | |||
'doctype': 'DocType', | |||
'name': table_1_name, | |||
'module': 'Custom', | |||
'custom': 1, | |||
'istable': 1, | |||
'fields': [ | |||
{'label': 'Child Title', 'fieldname': 'child_title', 'reqd': 1, 'fieldtype': 'Data'}, | |||
{'label': 'Child Number', 'fieldname': 'child_number', 'fieldtype': 'Int'}, | |||
] | |||
}).insert() | |||
table_1_name = "Child 1 of " + doctype_name | |||
frappe.get_doc( | |||
{ | |||
"doctype": "DocType", | |||
"name": table_1_name, | |||
"module": "Custom", | |||
"custom": 1, | |||
"istable": 1, | |||
"fields": [ | |||
{"label": "Child Title", "fieldname": "child_title", "reqd": 1, "fieldtype": "Data"}, | |||
{"label": "Child Number", "fieldname": "child_number", "fieldtype": "Int"}, | |||
], | |||
} | |||
).insert() | |||
# Main Table | |||
frappe.get_doc({ | |||
'doctype': 'DocType', | |||
'name': doctype_name, | |||
'module': 'Custom', | |||
'custom': 1, | |||
'autoname': 'field:title', | |||
'fields': [ | |||
{'label': 'Title', 'fieldname': 'title', 'reqd': 1, 'fieldtype': 'Data'}, | |||
{'label': 'Number', 'fieldname': 'number', 'fieldtype': 'Int'}, | |||
{'label': 'Table Field 1', 'fieldname': 'table_field_1', 'fieldtype': 'Table', 'options': table_1_name}, | |||
], | |||
'permissions': [ | |||
{'role': 'System Manager'} | |||
] | |||
}).insert() | |||
frappe.get_doc( | |||
{ | |||
"doctype": "DocType", | |||
"name": doctype_name, | |||
"module": "Custom", | |||
"custom": 1, | |||
"autoname": "field:title", | |||
"fields": [ | |||
{"label": "Title", "fieldname": "title", "reqd": 1, "fieldtype": "Data"}, | |||
{"label": "Number", "fieldname": "number", "fieldtype": "Int"}, | |||
{ | |||
"label": "Table Field 1", | |||
"fieldname": "table_field_1", | |||
"fieldtype": "Table", | |||
"options": table_1_name, | |||
}, | |||
], | |||
"permissions": [{"role": "System Manager"}], | |||
} | |||
).insert() | |||
def create_test_data(self, force=False): | |||
""" | |||
@@ -69,37 +78,38 @@ class TestDataExporter(unittest.TestCase): | |||
table_field_1=[ | |||
{"child_title": "Child Title 1", "child_number": "50"}, | |||
{"child_title": "Child Title 2", "child_number": "51"}, | |||
] | |||
], | |||
).insert() | |||
else: | |||
self.doc = frappe.get_doc(self.doctype_name, self.doc_name) | |||
def test_export_content(self): | |||
exp = DataExporter(doctype=self.doctype_name, file_type='CSV') | |||
exp = DataExporter(doctype=self.doctype_name, file_type="CSV") | |||
exp.build_response() | |||
self.assertEqual(frappe.response['type'],'csv') | |||
self.assertEqual(frappe.response['doctype'], self.doctype_name) | |||
self.assertTrue(frappe.response['result']) | |||
self.assertIn('Child Title 1\",50',frappe.response['result']) | |||
self.assertIn('Child Title 2\",51',frappe.response['result']) | |||
self.assertEqual(frappe.response["type"], "csv") | |||
self.assertEqual(frappe.response["doctype"], self.doctype_name) | |||
self.assertTrue(frappe.response["result"]) | |||
self.assertIn('Child Title 1",50', frappe.response["result"]) | |||
self.assertIn('Child Title 2",51', frappe.response["result"]) | |||
def test_export_type(self): | |||
for type in ['csv', 'Excel']: | |||
for type in ["csv", "Excel"]: | |||
with self.subTest(type=type): | |||
exp = DataExporter(doctype=self.doctype_name, file_type=type) | |||
exp.build_response() | |||
self.assertEqual(frappe.response['doctype'], self.doctype_name) | |||
self.assertTrue(frappe.response['result']) | |||
self.assertEqual(frappe.response["doctype"], self.doctype_name) | |||
self.assertTrue(frappe.response["result"]) | |||
if type == 'csv': | |||
self.assertEqual(frappe.response['type'],'csv') | |||
elif type == 'Excel': | |||
self.assertEqual(frappe.response['type'],'binary') | |||
self.assertEqual(frappe.response['filename'], self.doctype_name+'.xlsx') # 'Test DocType for Export Tool.xlsx') | |||
self.assertTrue(frappe.response['filecontent']) | |||
if type == "csv": | |||
self.assertEqual(frappe.response["type"], "csv") | |||
elif type == "Excel": | |||
self.assertEqual(frappe.response["type"], "binary") | |||
self.assertEqual( | |||
frappe.response["filename"], self.doctype_name + ".xlsx" | |||
) # 'Test DocType for Export Tool.xlsx') | |||
self.assertTrue(frappe.response["filecontent"]) | |||
def tearDown(self): | |||
pass | |||
@@ -64,9 +64,7 @@ class DataImport(Document): | |||
from frappe.utils.scheduler import is_scheduler_inactive | |||
if is_scheduler_inactive() and not frappe.flags.in_test: | |||
frappe.throw( | |||
_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive") | |||
) | |||
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive")) | |||
enqueued_jobs = [d.get("job_name") for d in get_info()] | |||
@@ -100,6 +98,7 @@ def get_preview_from_template(data_import, import_file=None, google_sheets_url=N | |||
import_file, google_sheets_url | |||
) | |||
@frappe.whitelist() | |||
def form_start_import(data_import): | |||
return frappe.get_doc("Data Import", data_import).start_import() | |||
@@ -127,11 +126,11 @@ def download_template( | |||
): | |||
""" | |||
Download template from Exporter | |||
:param doctype: Document Type | |||
:param export_fields=None: Fields to export as dict {'Sales Invoice': ['name', 'customer'], 'Sales Invoice Item': ['item_code']} | |||
:param export_records=None: One of 'all', 'by_filter', 'blank_template' | |||
:param export_filters: Filter dict | |||
:param file_type: File type to export into | |||
:param doctype: Document Type | |||
:param export_fields=None: Fields to export as dict {'Sales Invoice': ['name', 'customer'], 'Sales Invoice Item': ['item_code']} | |||
:param export_records=None: One of 'all', 'by_filter', 'blank_template' | |||
:param export_filters: Filter dict | |||
:param file_type: File type to export into | |||
""" | |||
export_fields = frappe.parse_json(export_fields) | |||
@@ -154,34 +153,38 @@ def download_errored_template(data_import_name): | |||
data_import = frappe.get_doc("Data Import", data_import_name) | |||
data_import.export_errored_rows() | |||
@frappe.whitelist() | |||
def download_import_log(data_import_name): | |||
data_import = frappe.get_doc("Data Import", data_import_name) | |||
data_import.download_import_log() | |||
@frappe.whitelist() | |||
def get_import_status(data_import_name): | |||
import_status = {} | |||
logs = frappe.get_all('Data Import Log', fields=['count(*) as count', 'success'], | |||
filters={'data_import': data_import_name}, | |||
group_by='success') | |||
logs = frappe.get_all( | |||
"Data Import Log", | |||
fields=["count(*) as count", "success"], | |||
filters={"data_import": data_import_name}, | |||
group_by="success", | |||
) | |||
total_payload_count = frappe.db.get_value('Data Import', data_import_name, 'payload_count') | |||
total_payload_count = frappe.db.get_value("Data Import", data_import_name, "payload_count") | |||
for log in logs: | |||
if log.get('success'): | |||
import_status['success'] = log.get('count') | |||
if log.get("success"): | |||
import_status["success"] = log.get("count") | |||
else: | |||
import_status['failed'] = log.get('count') | |||
import_status["failed"] = log.get("count") | |||
import_status['total_records'] = total_payload_count | |||
import_status["total_records"] = total_payload_count | |||
return import_status | |||
def import_file( | |||
doctype, file_path, import_type, submit_after_import=False, console=False | |||
): | |||
def import_file(doctype, file_path, import_type, submit_after_import=False, console=False): | |||
""" | |||
Import documents in from CSV or XLSX using data import. | |||
@@ -198,9 +201,7 @@ def import_file( | |||
"Insert New Records" if import_type.lower() == "insert" else "Update Existing Records" | |||
) | |||
i = Importer( | |||
doctype=doctype, file_path=file_path, data_import=data_import, console=console | |||
) | |||
i = Importer(doctype=doctype, file_path=file_path, data_import=data_import, console=console) | |||
i.import_data() | |||
@@ -214,11 +215,7 @@ def import_doc(path, pre_process=None): | |||
if f.endswith(".json"): | |||
frappe.flags.mute_emails = True | |||
import_file_by_path( | |||
f, | |||
data_import=True, | |||
force=True, | |||
pre_process=pre_process, | |||
reset_permissions=True | |||
f, data_import=True, force=True, pre_process=pre_process, reset_permissions=True | |||
) | |||
frappe.flags.mute_emails = False | |||
frappe.db.commit() | |||
@@ -226,9 +223,7 @@ def import_doc(path, pre_process=None): | |||
raise NotImplementedError("Only .json files can be imported") | |||
def export_json( | |||
doctype, path, filters=None, or_filters=None, name=None, order_by="creation asc" | |||
): | |||
def export_json(doctype, path, filters=None, or_filters=None, name=None, order_by="creation asc"): | |||
def post_process(out): | |||
# Note on Tree DocTypes: | |||
# The tree structure is maintained in the database via the fields "lft" | |||
@@ -6,11 +6,8 @@ import typing | |||
import frappe | |||
from frappe import _ | |||
from frappe.model import ( | |||
display_fieldtypes, | |||
no_value_fields, | |||
table_fields as table_fieldtypes, | |||
) | |||
from frappe.model import display_fieldtypes, no_value_fields | |||
from frappe.model import table_fields as table_fieldtypes | |||
from frappe.utils import flt, format_duration, groupby_metric | |||
from frappe.utils.csvutils import build_csv_response | |||
from frappe.utils.xlsxutils import build_xlsx_response | |||
@@ -28,11 +25,11 @@ class Exporter: | |||
): | |||
""" | |||
Exports records of a DocType for use with Importer | |||
:param doctype: Document Type to export | |||
:param export_fields=None: One of 'All', 'Mandatory' or {'DocType': ['field1', 'field2'], 'Child DocType': ['childfield1']} | |||
:param export_data=False: Whether to export data as well | |||
:param export_filters=None: The filters (dict or list) which is used to query the records | |||
:param file_type: One of 'Excel' or 'CSV' | |||
:param doctype: Document Type to export | |||
:param export_fields=None: One of 'All', 'Mandatory' or {'DocType': ['field1', 'field2'], 'Child DocType': ['childfield1']} | |||
:param export_data=False: Whether to export data as well | |||
:param export_filters=None: The filters (dict or list) which is used to query the records | |||
:param file_type: One of 'Excel' or 'CSV' | |||
""" | |||
self.doctype = doctype | |||
self.meta = frappe.get_meta(doctype) | |||
@@ -168,9 +165,7 @@ class Exporter: | |||
else: | |||
order_by = "`tab{0}`.`creation` DESC".format(self.doctype) | |||
parent_fields = [ | |||
format_column_name(df) for df in self.fields if df.parent == self.doctype | |||
] | |||
parent_fields = [format_column_name(df) for df in self.fields if df.parent == self.doctype] | |||
parent_data = frappe.db.get_list( | |||
self.doctype, | |||
filters=filters, | |||
@@ -188,9 +183,7 @@ class Exporter: | |||
child_table_df = self.meta.get_field(key) | |||
child_table_doctype = child_table_df.options | |||
child_fields = ["name", "idx", "parent", "parentfield"] + list( | |||
set( | |||
[format_column_name(df) for df in self.fields if df.parent == child_table_doctype] | |||
) | |||
set([format_column_name(df) for df in self.fields if df.parent == child_table_doctype]) | |||
) | |||
data = frappe.db.get_all( | |||
child_table_doctype, | |||
@@ -261,4 +254,4 @@ class Exporter: | |||
build_xlsx_response(self.get_csv_array_for_export(), _(self.doctype)) | |||
def group_children_data_by_parent(self, children_data: typing.Dict[str, list]): | |||
return groupby_metric(children_data, key='parent') | |||
return groupby_metric(children_data, key="parent") |
@@ -1,21 +1,23 @@ | |||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
import os | |||
import io | |||
import frappe | |||
import timeit | |||
import json | |||
from datetime import datetime, date | |||
import os | |||
import timeit | |||
from datetime import date, datetime | |||
import frappe | |||
from frappe import _ | |||
from frappe.utils import cint, flt, update_progress_bar, cstr, duration_to_seconds | |||
from frappe.utils.csvutils import read_csv_content, get_csv_content_from_google_sheets | |||
from frappe.core.doctype.version.version import get_diff | |||
from frappe.model import no_value_fields | |||
from frappe.model import table_fields as table_fieldtypes | |||
from frappe.utils import cint, cstr, duration_to_seconds, flt, update_progress_bar | |||
from frappe.utils.csvutils import get_csv_content_from_google_sheets, read_csv_content | |||
from frappe.utils.xlsxutils import ( | |||
read_xlsx_file_from_attached_file, | |||
read_xls_file_from_attached_file, | |||
read_xlsx_file_from_attached_file, | |||
) | |||
from frappe.model import no_value_fields, table_fields as table_fieldtypes | |||
from frappe.core.doctype.version.version import get_diff | |||
INVALID_VALUES = ("", None) | |||
MAX_ROWS_IN_PREVIEW = 10 | |||
@@ -24,9 +26,7 @@ UPDATE = "Update Existing Records" | |||
class Importer: | |||
def __init__( | |||
self, doctype, data_import=None, file_path=None, import_type=None, console=False | |||
): | |||
def __init__(self, doctype, data_import=None, file_path=None, import_type=None, console=False): | |||
self.doctype = doctype | |||
self.console = console | |||
@@ -49,9 +49,13 @@ class Importer: | |||
def get_data_for_import_preview(self): | |||
out = self.import_file.get_data_for_import_preview() | |||
out.import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success"], | |||
out.import_log = frappe.db.get_all( | |||
"Data Import Log", | |||
fields=["row_indexes", "success"], | |||
filters={"data_import": self.data_import.name}, | |||
order_by="log_index", limit=10) | |||
order_by="log_index", | |||
limit=10, | |||
) | |||
return out | |||
@@ -84,14 +88,23 @@ class Importer: | |||
return | |||
# setup import log | |||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "log_index"], | |||
filters={"data_import": self.data_import.name}, | |||
order_by="log_index") or [] | |||
import_log = ( | |||
frappe.db.get_all( | |||
"Data Import Log", | |||
fields=["row_indexes", "success", "log_index"], | |||
filters={"data_import": self.data_import.name}, | |||
order_by="log_index", | |||
) | |||
or [] | |||
) | |||
log_index = 0 | |||
# Do not remove rows in case of retry after an error or pending data import | |||
if self.data_import.status == "Partial Success" and len(import_log) >= self.data_import.payload_count: | |||
if ( | |||
self.data_import.status == "Partial Success" | |||
and len(import_log) >= self.data_import.payload_count | |||
): | |||
# remove previous failures from import log only in case of retry after partial success | |||
import_log = [log for log in import_log if log.get("success")] | |||
@@ -108,9 +121,7 @@ class Importer: | |||
total_payload_count = len(payloads) | |||
batch_size = frappe.conf.data_import_batch_size or 1000 | |||
for batch_index, batched_payloads in enumerate( | |||
frappe.utils.create_batch(payloads, batch_size) | |||
): | |||
for batch_index, batched_payloads in enumerate(frappe.utils.create_batch(payloads, batch_size)): | |||
for i, payload in enumerate(batched_payloads): | |||
doc = payload.doc | |||
row_indexes = [row.row_number for row in payload.rows] | |||
@@ -156,11 +167,11 @@ class Importer: | |||
}, | |||
) | |||
create_import_log(self.data_import.name, log_index, { | |||
'success': True, | |||
'docname': doc.name, | |||
'row_indexes': row_indexes | |||
}) | |||
create_import_log( | |||
self.data_import.name, | |||
log_index, | |||
{"success": True, "docname": doc.name, "row_indexes": row_indexes}, | |||
) | |||
log_index += 1 | |||
@@ -177,19 +188,29 @@ class Importer: | |||
# rollback if exception | |||
frappe.db.rollback() | |||
create_import_log(self.data_import.name, log_index, { | |||
'success': False, | |||
'exception': frappe.get_traceback(), | |||
'messages': messages, | |||
'row_indexes': row_indexes | |||
}) | |||
create_import_log( | |||
self.data_import.name, | |||
log_index, | |||
{ | |||
"success": False, | |||
"exception": frappe.get_traceback(), | |||
"messages": messages, | |||
"row_indexes": row_indexes, | |||
}, | |||
) | |||
log_index += 1 | |||
# Logs are db inserted directly so will have to be fetched again | |||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "log_index"], | |||
filters={"data_import": self.data_import.name}, | |||
order_by="log_index") or [] | |||
import_log = ( | |||
frappe.db.get_all( | |||
"Data Import Log", | |||
fields=["row_indexes", "success", "log_index"], | |||
filters={"data_import": self.data_import.name}, | |||
order_by="log_index", | |||
) | |||
or [] | |||
) | |||
# set status | |||
failures = [log for log in import_log if not log.get("success")] | |||
@@ -274,9 +295,15 @@ class Importer: | |||
if not self.data_import: | |||
return | |||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success"], | |||
filters={"data_import": self.data_import.name}, | |||
order_by="log_index") or [] | |||
import_log = ( | |||
frappe.db.get_all( | |||
"Data Import Log", | |||
fields=["row_indexes", "success"], | |||
filters={"data_import": self.data_import.name}, | |||
order_by="log_index", | |||
) | |||
or [] | |||
) | |||
failures = [log for log in import_log if not log.get("success")] | |||
row_indexes = [] | |||
@@ -299,9 +326,12 @@ class Importer: | |||
if not self.data_import: | |||
return | |||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "messages", "exception", "docname"], | |||
import_log = frappe.db.get_all( | |||
"Data Import Log", | |||
fields=["row_indexes", "success", "messages", "exception", "docname"], | |||
filters={"data_import": self.data_import.name}, | |||
order_by="log_index") | |||
order_by="log_index", | |||
) | |||
header_row = ["Row Numbers", "Status", "Message", "Exception"] | |||
@@ -309,10 +339,13 @@ class Importer: | |||
for log in import_log: | |||
row_number = json.loads(log.get("row_indexes"))[0] | |||
status = "Success" if log.get('success') else "Failure" | |||
message = "Successfully Imported {0}".format(log.get('docname')) if log.get('success') else \ | |||
log.get("messages") | |||
exception = frappe.utils.cstr(log.get("exception", '')) | |||
status = "Success" if log.get("success") else "Failure" | |||
message = ( | |||
"Successfully Imported {0}".format(log.get("docname")) | |||
if log.get("success") | |||
else log.get("messages") | |||
) | |||
exception = frappe.utils.cstr(log.get("exception", "")) | |||
rows += [[row_number, status, message, exception]] | |||
build_csv_response(rows, self.doctype) | |||
@@ -324,9 +357,7 @@ class Importer: | |||
if successful_records: | |||
print() | |||
print( | |||
"Successfully imported {0} records out of {1}".format( | |||
len(successful_records), len(import_log) | |||
) | |||
"Successfully imported {0} records out of {1}".format(len(successful_records), len(import_log)) | |||
) | |||
if failed_records: | |||
@@ -363,9 +394,7 @@ class Importer: | |||
class ImportFile: | |||
def __init__(self, doctype, file, template_options=None, import_type=None): | |||
self.doctype = doctype | |||
self.template_options = template_options or frappe._dict( | |||
column_to_field_map=frappe._dict() | |||
) | |||
self.template_options = template_options or frappe._dict(column_to_field_map=frappe._dict()) | |||
self.column_to_field_map = self.template_options.column_to_field_map | |||
self.import_type = import_type | |||
self.warnings = [] | |||
@@ -556,9 +585,7 @@ class ImportFile: | |||
def read_content(self, content, extension): | |||
error_title = _("Template Error") | |||
if extension not in ("csv", "xlsx", "xls"): | |||
frappe.throw( | |||
_("Import template should be of type .csv, .xlsx or .xls"), title=error_title | |||
) | |||
frappe.throw(_("Import template should be of type .csv, .xlsx or .xls"), title=error_title) | |||
if extension == "csv": | |||
data = read_csv_content(content) | |||
@@ -587,12 +614,13 @@ class Row: | |||
if len_row != len_columns: | |||
less_than_columns = len_row < len_columns | |||
message = ( | |||
"Row has less values than columns" | |||
if less_than_columns | |||
else "Row has more values than columns" | |||
"Row has less values than columns" if less_than_columns else "Row has more values than columns" | |||
) | |||
self.warnings.append( | |||
{"row": self.row_number, "message": message,} | |||
{ | |||
"row": self.row_number, | |||
"message": message, | |||
} | |||
) | |||
def parse_doc(self, doctype, parent_doc=None, table_df=None): | |||
@@ -662,18 +690,24 @@ class Row: | |||
options_string = ", ".join(frappe.bold(d) for d in select_options) | |||
msg = _("Value must be one of {0}").format(options_string) | |||
self.warnings.append( | |||
{"row": self.row_number, "field": df_as_json(df), "message": msg,} | |||
{ | |||
"row": self.row_number, | |||
"field": df_as_json(df), | |||
"message": msg, | |||
} | |||
) | |||
return | |||
elif df.fieldtype == "Link": | |||
exists = self.link_exists(value, df) | |||
if not exists: | |||
msg = _("Value {0} missing for {1}").format( | |||
frappe.bold(value), frappe.bold(df.options) | |||
) | |||
msg = _("Value {0} missing for {1}").format(frappe.bold(value), frappe.bold(df.options)) | |||
self.warnings.append( | |||
{"row": self.row_number, "field": df_as_json(df), "message": msg,} | |||
{ | |||
"row": self.row_number, | |||
"field": df_as_json(df), | |||
"message": msg, | |||
} | |||
) | |||
return | |||
elif df.fieldtype in ["Date", "Datetime"]: | |||
@@ -693,6 +727,7 @@ class Row: | |||
return | |||
elif df.fieldtype == "Duration": | |||
import re | |||
is_valid_duration = re.match(r"^(?:(\d+d)?((^|\s)\d+h)?((^|\s)\d+m)?((^|\s)\d+s)?)$", value) | |||
if not is_valid_duration: | |||
self.warnings.append( | |||
@@ -702,7 +737,7 @@ class Row: | |||
"field": df_as_json(df), | |||
"message": _("Value {0} must be in the valid duration format: d h m s").format( | |||
frappe.bold(value) | |||
) | |||
), | |||
} | |||
) | |||
@@ -789,9 +824,7 @@ class Header(Row): | |||
else: | |||
doctypes.append((col.df.parent, col.df.child_table_df)) | |||
self.doctypes = sorted( | |||
list(set(doctypes)), key=lambda x: -1 if x[0] == self.doctype else 1 | |||
) | |||
self.doctypes = sorted(list(set(doctypes)), key=lambda x: -1 if x[0] == self.doctype else 1) | |||
def get_column_indexes(self, doctype, tablefield=None): | |||
def is_table_field(df): | |||
@@ -802,10 +835,7 @@ class Header(Row): | |||
return [ | |||
col.index | |||
for col in self.columns | |||
if not col.skip_import | |||
and col.df | |||
and col.df.parent == doctype | |||
and is_table_field(col.df) | |||
if not col.skip_import and col.df and col.df.parent == doctype and is_table_field(col.df) | |||
] | |||
def get_columns(self, indexes): | |||
@@ -893,9 +923,7 @@ class Column: | |||
self.warnings.append( | |||
{ | |||
"col": column_number, | |||
"message": _("Cannot match column {0} with any field").format( | |||
frappe.bold(header_title) | |||
), | |||
"message": _("Cannot match column {0} with any field").format(frappe.bold(header_title)), | |||
"type": "info", | |||
} | |||
) | |||
@@ -958,9 +986,7 @@ class Column: | |||
if self.df.fieldtype == "Link": | |||
# find all values that dont exist | |||
values = list({cstr(v) for v in self.column_values[1:] if v}) | |||
exists = [ | |||
d.name for d in frappe.db.get_all(self.df.options, filters={"name": ("in", values)}) | |||
] | |||
exists = [d.name for d in frappe.db.get_all(self.df.options, filters={"name": ("in", values)})] | |||
not_exists = list(set(values) - set(exists)) | |||
if not_exists: | |||
missing_values = ", ".join(not_exists) | |||
@@ -968,9 +994,7 @@ class Column: | |||
{ | |||
"col": self.column_number, | |||
"message": ( | |||
"The following values do not exist for {}: {}".format( | |||
self.df.options, missing_values | |||
) | |||
"The following values do not exist for {}: {}".format(self.df.options, missing_values) | |||
), | |||
"type": "warning", | |||
} | |||
@@ -983,7 +1007,9 @@ class Column: | |||
self.warnings.append( | |||
{ | |||
"col": self.column_number, | |||
"message": _("Date format could not be determined from the values in this column. Defaulting to yyyy-mm-dd."), | |||
"message": _( | |||
"Date format could not be determined from the values in this column. Defaulting to yyyy-mm-dd." | |||
), | |||
"type": "info", | |||
} | |||
) | |||
@@ -1027,12 +1053,12 @@ def build_fields_dict_for_column_matching(parent_doctype): | |||
Build a dict with various keys to match with column headers and value as docfield | |||
The keys can be label or fieldname | |||
{ | |||
'Customer': df1, | |||
'customer': df1, | |||
'Due Date': df2, | |||
'due_date': df2, | |||
'Item Code (Sales Invoice Item)': df3, | |||
'Sales Invoice Item:item_code': df3, | |||
'Customer': df1, | |||
'customer': df1, | |||
'Due Date': df2, | |||
'due_date': df2, | |||
'Item Code (Sales Invoice Item)': df3, | |||
'Sales Invoice Item:item_code': df3, | |||
} | |||
""" | |||
@@ -1062,9 +1088,7 @@ def build_fields_dict_for_column_matching(parent_doctype): | |||
out = {} | |||
# doctypes and fieldname if it is a child doctype | |||
doctypes = [(parent_doctype, None)] + [ | |||
(df.options, df) for df in parent_meta.get_table_fields() | |||
] | |||
doctypes = [(parent_doctype, None)] + [(df.options, df) for df in parent_meta.get_table_fields()] | |||
for doctype, table_df in doctypes: | |||
translated_table_label = _(table_df.label) if table_df else None | |||
@@ -1082,15 +1106,15 @@ def build_fields_dict_for_column_matching(parent_doctype): | |||
if doctype == parent_doctype: | |||
name_headers = ( | |||
"name", # fieldname | |||
"ID", # label | |||
_("ID"), # translated label | |||
"name", # fieldname | |||
"ID", # label | |||
_("ID"), # translated label | |||
) | |||
else: | |||
name_headers = ( | |||
"{0}.name".format(table_df.fieldname), # fieldname | |||
"ID ({0})".format(table_df.label), # label | |||
"{0} ({1})".format(_("ID"), translated_table_label), # translated label | |||
"{0}.name".format(table_df.fieldname), # fieldname | |||
"ID ({0})".format(table_df.label), # label | |||
"{0} ({1})".format(_("ID"), translated_table_label), # translated label | |||
) | |||
name_df.is_child_table_field = True | |||
@@ -1122,7 +1146,7 @@ def build_fields_dict_for_column_matching(parent_doctype): | |||
for header in ( | |||
df.fieldname, | |||
f"{label} ({df.fieldname})", | |||
f"{translated_label} ({df.fieldname})" | |||
f"{translated_label} ({df.fieldname})", | |||
): | |||
out[header] = df | |||
@@ -1155,9 +1179,8 @@ def build_fields_dict_for_column_matching(parent_doctype): | |||
autoname_field = get_autoname_field(parent_doctype) | |||
if autoname_field: | |||
for header in ( | |||
"ID ({})".format(autoname_field.label), # label | |||
"{0} ({1})".format(_("ID"), _(autoname_field.label)), # translated label | |||
"ID ({})".format(autoname_field.label), # label | |||
"{0} ({1})".format(_("ID"), _(autoname_field.label)), # translated label | |||
# ID field should also map to the autoname field | |||
"ID", | |||
_("ID"), | |||
@@ -1205,10 +1228,7 @@ def get_item_at_index(_list, i, default=None): | |||
def get_user_format(date_format): | |||
return ( | |||
date_format.replace("%Y", "yyyy") | |||
.replace("%y", "yy") | |||
.replace("%m", "mm") | |||
.replace("%d", "dd") | |||
date_format.replace("%Y", "yyyy").replace("%y", "yy").replace("%m", "mm").replace("%d", "dd") | |||
) | |||
@@ -1226,16 +1246,17 @@ def df_as_json(df): | |||
def get_select_options(df): | |||
return [d for d in (df.options or "").split("\n") if d] | |||
def create_import_log(data_import, log_index, log_details): | |||
frappe.get_doc({ | |||
'doctype': 'Data Import Log', | |||
'log_index': log_index, | |||
'success': log_details.get('success'), | |||
'data_import': data_import, | |||
'row_indexes': json.dumps(log_details.get('row_indexes')), | |||
'docname': log_details.get('docname'), | |||
'messages': json.dumps(log_details.get('messages', '[]')), | |||
'exception': log_details.get('exception') | |||
}).db_insert() | |||
def create_import_log(data_import, log_index, log_details): | |||
frappe.get_doc( | |||
{ | |||
"doctype": "Data Import Log", | |||
"log_index": log_index, | |||
"success": log_details.get("success"), | |||
"data_import": data_import, | |||
"row_indexes": json.dumps(log_details.get("row_indexes")), | |||
"docname": log_details.get("docname"), | |||
"messages": json.dumps(log_details.get("messages", "[]")), | |||
"exception": log_details.get("exception"), | |||
} | |||
).db_insert() |
@@ -4,5 +4,6 @@ | |||
# import frappe | |||
import unittest | |||
class TestDataImport(unittest.TestCase): | |||
pass |
@@ -2,13 +2,13 @@ | |||
# Copyright (c) 2019, Frappe Technologies and Contributors | |||
# License: MIT. See LICENSE | |||
import unittest | |||
import frappe | |||
from frappe.core.doctype.data_import.exporter import Exporter | |||
from frappe.core.doctype.data_import.test_importer import ( | |||
create_doctype_if_not_exists, | |||
) | |||
from frappe.core.doctype.data_import.test_importer import create_doctype_if_not_exists | |||
doctype_name = "DocType for Export" | |||
doctype_name = 'DocType for Export' | |||
class TestExporter(unittest.TestCase): | |||
def setUp(self): | |||
@@ -93,10 +93,10 @@ class TestExporter(unittest.TestCase): | |||
doctype_name, | |||
export_fields={doctype_name: ["title", "description"]}, | |||
export_data=True, | |||
file_type="CSV" | |||
file_type="CSV", | |||
) | |||
e.build_response() | |||
self.assertTrue(frappe.response['result']) | |||
self.assertEqual(frappe.response['doctype'], doctype_name) | |||
self.assertEqual(frappe.response['type'], "csv") | |||
self.assertTrue(frappe.response["result"]) | |||
self.assertEqual(frappe.response["doctype"], doctype_name) | |||
self.assertEqual(frappe.response["type"], "csv") |
@@ -2,53 +2,57 @@ | |||
# Copyright (c) 2019, Frappe Technologies and Contributors | |||
# License: MIT. See LICENSE | |||
import unittest | |||
import frappe | |||
from frappe.core.doctype.data_import.importer import Importer | |||
from frappe.tests.test_query_builder import db_type_is, run_only_if | |||
from frappe.utils import getdate, format_duration | |||
from frappe.utils import format_duration, getdate | |||
doctype_name = "DocType for Import" | |||
doctype_name = 'DocType for Import' | |||
class TestImporter(unittest.TestCase): | |||
@classmethod | |||
def setUpClass(cls): | |||
create_doctype_if_not_exists(doctype_name,) | |||
create_doctype_if_not_exists( | |||
doctype_name, | |||
) | |||
def test_data_import_from_file(self): | |||
import_file = get_import_file('sample_import_file') | |||
import_file = get_import_file("sample_import_file") | |||
data_import = self.get_importer(doctype_name, import_file) | |||
data_import.start_import() | |||
doc1 = frappe.get_doc(doctype_name, 'Test') | |||
doc2 = frappe.get_doc(doctype_name, 'Test 2') | |||
doc3 = frappe.get_doc(doctype_name, 'Test 3') | |||
doc1 = frappe.get_doc(doctype_name, "Test") | |||
doc2 = frappe.get_doc(doctype_name, "Test 2") | |||
doc3 = frappe.get_doc(doctype_name, "Test 3") | |||
self.assertEqual(doc1.description, 'test description') | |||
self.assertEqual(doc1.description, "test description") | |||
self.assertEqual(doc1.number, 1) | |||
self.assertEqual(format_duration(doc1.duration), '3h') | |||
self.assertEqual(format_duration(doc1.duration), "3h") | |||
self.assertEqual(doc1.table_field_1[0].child_title, 'child title') | |||
self.assertEqual(doc1.table_field_1[0].child_description, 'child description') | |||
self.assertEqual(doc1.table_field_1[0].child_title, "child title") | |||
self.assertEqual(doc1.table_field_1[0].child_description, "child description") | |||
self.assertEqual(doc1.table_field_1[1].child_title, 'child title 2') | |||
self.assertEqual(doc1.table_field_1[1].child_description, 'child description 2') | |||
self.assertEqual(doc1.table_field_1[1].child_title, "child title 2") | |||
self.assertEqual(doc1.table_field_1[1].child_description, "child description 2") | |||
self.assertEqual(doc1.table_field_2[1].child_2_title, 'title child') | |||
self.assertEqual(doc1.table_field_2[1].child_2_date, getdate('2019-10-30')) | |||
self.assertEqual(doc1.table_field_2[1].child_2_title, "title child") | |||
self.assertEqual(doc1.table_field_2[1].child_2_date, getdate("2019-10-30")) | |||
self.assertEqual(doc1.table_field_2[1].child_2_another_number, 5) | |||
self.assertEqual(doc1.table_field_1_again[0].child_title, 'child title again') | |||
self.assertEqual(doc1.table_field_1_again[1].child_title, 'child title again 2') | |||
self.assertEqual(doc1.table_field_1_again[1].child_date, getdate('2021-09-22')) | |||
self.assertEqual(doc1.table_field_1_again[0].child_title, "child title again") | |||
self.assertEqual(doc1.table_field_1_again[1].child_title, "child title again 2") | |||
self.assertEqual(doc1.table_field_1_again[1].child_date, getdate("2021-09-22")) | |||
self.assertEqual(doc2.description, 'test description 2') | |||
self.assertEqual(format_duration(doc2.duration), '4d 3h') | |||
self.assertEqual(doc2.description, "test description 2") | |||
self.assertEqual(format_duration(doc2.duration), "4d 3h") | |||
self.assertEqual(doc3.another_number, 5) | |||
self.assertEqual(format_duration(doc3.duration), '5d 5h 45m') | |||
self.assertEqual(format_duration(doc3.duration), "5d 5h 45m") | |||
def test_data_import_preview(self): | |||
import_file = get_import_file('sample_import_file') | |||
import_file = get_import_file("sample_import_file") | |||
data_import = self.get_importer(doctype_name, import_file) | |||
preview = data_import.get_preview_from_template() | |||
@@ -58,35 +62,49 @@ class TestImporter(unittest.TestCase): | |||
# ignored on postgres because myisam doesn't exist on pg | |||
@run_only_if(db_type_is.MARIADB) | |||
def test_data_import_without_mandatory_values(self): | |||
import_file = get_import_file('sample_import_file_without_mandatory') | |||
import_file = get_import_file("sample_import_file_without_mandatory") | |||
data_import = self.get_importer(doctype_name, import_file) | |||
frappe.local.message_log = [] | |||
data_import.start_import() | |||
data_import.reload() | |||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "messages", "exception", "docname"], | |||
import_log = frappe.db.get_all( | |||
"Data Import Log", | |||
fields=["row_indexes", "success", "messages", "exception", "docname"], | |||
filters={"data_import": data_import.name}, | |||
order_by="log_index") | |||
order_by="log_index", | |||
) | |||
self.assertEqual(frappe.parse_json(import_log[0]['row_indexes']), [2,3]) | |||
expected_error = "Error: <strong>Child 1 of DocType for Import</strong> Row #1: Value missing for: Child Title" | |||
self.assertEqual(frappe.parse_json(frappe.parse_json(import_log[0]['messages'])[0])['message'], expected_error) | |||
expected_error = "Error: <strong>Child 1 of DocType for Import</strong> Row #2: Value missing for: Child Title" | |||
self.assertEqual(frappe.parse_json(frappe.parse_json(import_log[0]['messages'])[1])['message'], expected_error) | |||
self.assertEqual(frappe.parse_json(import_log[0]["row_indexes"]), [2, 3]) | |||
expected_error = ( | |||
"Error: <strong>Child 1 of DocType for Import</strong> Row #1: Value missing for: Child Title" | |||
) | |||
self.assertEqual( | |||
frappe.parse_json(frappe.parse_json(import_log[0]["messages"])[0])["message"], expected_error | |||
) | |||
expected_error = ( | |||
"Error: <strong>Child 1 of DocType for Import</strong> Row #2: Value missing for: Child Title" | |||
) | |||
self.assertEqual( | |||
frappe.parse_json(frappe.parse_json(import_log[0]["messages"])[1])["message"], expected_error | |||
) | |||
self.assertEqual(frappe.parse_json(import_log[1]['row_indexes']), [4]) | |||
self.assertEqual(frappe.parse_json(frappe.parse_json(import_log[1]['messages'])[0])['message'], "Title is required") | |||
self.assertEqual(frappe.parse_json(import_log[1]["row_indexes"]), [4]) | |||
self.assertEqual( | |||
frappe.parse_json(frappe.parse_json(import_log[1]["messages"])[0])["message"], | |||
"Title is required", | |||
) | |||
def test_data_import_update(self): | |||
existing_doc = frappe.get_doc( | |||
doctype=doctype_name, | |||
title=frappe.generate_hash(doctype_name, 8), | |||
table_field_1=[{'child_title': 'child title to update'}] | |||
table_field_1=[{"child_title": "child title to update"}], | |||
) | |||
existing_doc.save() | |||
frappe.db.commit() | |||
import_file = get_import_file('sample_import_file_for_update') | |||
import_file = get_import_file("sample_import_file_for_update") | |||
data_import = self.get_importer(doctype_name, import_file, update=True) | |||
i = Importer(data_import.reference_doctype, data_import=data_import) | |||
@@ -104,15 +122,15 @@ class TestImporter(unittest.TestCase): | |||
updated_doc = frappe.get_doc(doctype_name, existing_doc.name) | |||
self.assertEqual(existing_doc.title, updated_doc.title) | |||
self.assertEqual(updated_doc.description, 'test description') | |||
self.assertEqual(updated_doc.table_field_1[0].child_title, 'child title') | |||
self.assertEqual(updated_doc.description, "test description") | |||
self.assertEqual(updated_doc.table_field_1[0].child_title, "child title") | |||
self.assertEqual(updated_doc.table_field_1[0].name, existing_doc.table_field_1[0].name) | |||
self.assertEqual(updated_doc.table_field_1[0].child_description, 'child description') | |||
self.assertEqual(updated_doc.table_field_1_again[0].child_title, 'child title again') | |||
self.assertEqual(updated_doc.table_field_1[0].child_description, "child description") | |||
self.assertEqual(updated_doc.table_field_1_again[0].child_title, "child title again") | |||
def get_importer(self, doctype, import_file, update=False): | |||
data_import = frappe.new_doc('Data Import') | |||
data_import.import_type = 'Insert New Records' if not update else 'Update Existing Records' | |||
data_import = frappe.new_doc("Data Import") | |||
data_import.import_type = "Insert New Records" if not update else "Update Existing Records" | |||
data_import.reference_doctype = doctype | |||
data_import.import_file = import_file.file_url | |||
data_import.insert() | |||
@@ -121,88 +139,109 @@ class TestImporter(unittest.TestCase): | |||
return data_import | |||
def create_doctype_if_not_exists(doctype_name, force=False): | |||
if force: | |||
frappe.delete_doc_if_exists('DocType', doctype_name) | |||
frappe.delete_doc_if_exists('DocType', 'Child 1 of ' + doctype_name) | |||
frappe.delete_doc_if_exists('DocType', 'Child 2 of ' + doctype_name) | |||
frappe.delete_doc_if_exists("DocType", doctype_name) | |||
frappe.delete_doc_if_exists("DocType", "Child 1 of " + doctype_name) | |||
frappe.delete_doc_if_exists("DocType", "Child 2 of " + doctype_name) | |||
if frappe.db.exists('DocType', doctype_name): | |||
if frappe.db.exists("DocType", doctype_name): | |||
return | |||
# Child Table 1 | |||
table_1_name = 'Child 1 of ' + doctype_name | |||
frappe.get_doc({ | |||
'doctype': 'DocType', | |||
'name': table_1_name, | |||
'module': 'Custom', | |||
'custom': 1, | |||
'istable': 1, | |||
'fields': [ | |||
{'label': 'Child Title', 'fieldname': 'child_title', 'reqd': 1, 'fieldtype': 'Data'}, | |||
{'label': 'Child Description', 'fieldname': 'child_description', 'fieldtype': 'Small Text'}, | |||
{'label': 'Child Date', 'fieldname': 'child_date', 'fieldtype': 'Date'}, | |||
{'label': 'Child Number', 'fieldname': 'child_number', 'fieldtype': 'Int'}, | |||
{'label': 'Child Number', 'fieldname': 'child_another_number', 'fieldtype': 'Int'}, | |||
] | |||
}).insert() | |||
table_1_name = "Child 1 of " + doctype_name | |||
frappe.get_doc( | |||
{ | |||
"doctype": "DocType", | |||
"name": table_1_name, | |||
"module": "Custom", | |||
"custom": 1, | |||
"istable": 1, | |||
"fields": [ | |||
{"label": "Child Title", "fieldname": "child_title", "reqd": 1, "fieldtype": "Data"}, | |||
{"label": "Child Description", "fieldname": "child_description", "fieldtype": "Small Text"}, | |||
{"label": "Child Date", "fieldname": "child_date", "fieldtype": "Date"}, | |||
{"label": "Child Number", "fieldname": "child_number", "fieldtype": "Int"}, | |||
{"label": "Child Number", "fieldname": "child_another_number", "fieldtype": "Int"}, | |||
], | |||
} | |||
).insert() | |||
# Child Table 2 | |||
table_2_name = 'Child 2 of ' + doctype_name | |||
frappe.get_doc({ | |||
'doctype': 'DocType', | |||
'name': table_2_name, | |||
'module': 'Custom', | |||
'custom': 1, | |||
'istable': 1, | |||
'fields': [ | |||
{'label': 'Child 2 Title', 'fieldname': 'child_2_title', 'reqd': 1, 'fieldtype': 'Data'}, | |||
{'label': 'Child 2 Description', 'fieldname': 'child_2_description', 'fieldtype': 'Small Text'}, | |||
{'label': 'Child 2 Date', 'fieldname': 'child_2_date', 'fieldtype': 'Date'}, | |||
{'label': 'Child 2 Number', 'fieldname': 'child_2_number', 'fieldtype': 'Int'}, | |||
{'label': 'Child 2 Number', 'fieldname': 'child_2_another_number', 'fieldtype': 'Int'}, | |||
] | |||
}).insert() | |||
table_2_name = "Child 2 of " + doctype_name | |||
frappe.get_doc( | |||
{ | |||
"doctype": "DocType", | |||
"name": table_2_name, | |||
"module": "Custom", | |||
"custom": 1, | |||
"istable": 1, | |||
"fields": [ | |||
{"label": "Child 2 Title", "fieldname": "child_2_title", "reqd": 1, "fieldtype": "Data"}, | |||
{ | |||
"label": "Child 2 Description", | |||
"fieldname": "child_2_description", | |||
"fieldtype": "Small Text", | |||
}, | |||
{"label": "Child 2 Date", "fieldname": "child_2_date", "fieldtype": "Date"}, | |||
{"label": "Child 2 Number", "fieldname": "child_2_number", "fieldtype": "Int"}, | |||
{"label": "Child 2 Number", "fieldname": "child_2_another_number", "fieldtype": "Int"}, | |||
], | |||
} | |||
).insert() | |||
# Main Table | |||
frappe.get_doc({ | |||
'doctype': 'DocType', | |||
'name': doctype_name, | |||
'module': 'Custom', | |||
'custom': 1, | |||
'autoname': 'field:title', | |||
'fields': [ | |||
{'label': 'Title', 'fieldname': 'title', 'reqd': 1, 'fieldtype': 'Data'}, | |||
{'label': 'Description', 'fieldname': 'description', 'fieldtype': 'Small Text'}, | |||
{'label': 'Date', 'fieldname': 'date', 'fieldtype': 'Date'}, | |||
{'label': 'Duration', 'fieldname': 'duration', 'fieldtype': 'Duration'}, | |||
{'label': 'Number', 'fieldname': 'number', 'fieldtype': 'Int'}, | |||
{'label': 'Number', 'fieldname': 'another_number', 'fieldtype': 'Int'}, | |||
{'label': 'Table Field 1', 'fieldname': 'table_field_1', 'fieldtype': 'Table', 'options': table_1_name}, | |||
{'label': 'Table Field 2', 'fieldname': 'table_field_2', 'fieldtype': 'Table', 'options': table_2_name}, | |||
{'label': 'Table Field 1 Again', 'fieldname': 'table_field_1_again', 'fieldtype': 'Table', 'options': table_1_name}, | |||
], | |||
'permissions': [ | |||
{'role': 'System Manager'} | |||
] | |||
}).insert() | |||
frappe.get_doc( | |||
{ | |||
"doctype": "DocType", | |||
"name": doctype_name, | |||
"module": "Custom", | |||
"custom": 1, | |||
"autoname": "field:title", | |||
"fields": [ | |||
{"label": "Title", "fieldname": "title", "reqd": 1, "fieldtype": "Data"}, | |||
{"label": "Description", "fieldname": "description", "fieldtype": "Small Text"}, | |||
{"label": "Date", "fieldname": "date", "fieldtype": "Date"}, | |||
{"label": "Duration", "fieldname": "duration", "fieldtype": "Duration"}, | |||
{"label": "Number", "fieldname": "number", "fieldtype": "Int"}, | |||
{"label": "Number", "fieldname": "another_number", "fieldtype": "Int"}, | |||
{ | |||
"label": "Table Field 1", | |||
"fieldname": "table_field_1", | |||
"fieldtype": "Table", | |||
"options": table_1_name, | |||
}, | |||
{ | |||
"label": "Table Field 2", | |||
"fieldname": "table_field_2", | |||
"fieldtype": "Table", | |||
"options": table_2_name, | |||
}, | |||
{ | |||
"label": "Table Field 1 Again", | |||
"fieldname": "table_field_1_again", | |||
"fieldtype": "Table", | |||
"options": table_1_name, | |||
}, | |||
], | |||
"permissions": [{"role": "System Manager"}], | |||
} | |||
).insert() | |||
def get_import_file(csv_file_name, force=False): | |||
file_name = csv_file_name + '.csv' | |||
_file = frappe.db.exists('File', {'file_name': file_name}) | |||
file_name = csv_file_name + ".csv" | |||
_file = frappe.db.exists("File", {"file_name": file_name}) | |||
if force and _file: | |||
frappe.delete_doc_if_exists('File', _file) | |||
frappe.delete_doc_if_exists("File", _file) | |||
if frappe.db.exists('File', {'file_name': file_name}): | |||
f = frappe.get_doc('File', {'file_name': file_name}) | |||
if frappe.db.exists("File", {"file_name": file_name}): | |||
f = frappe.get_doc("File", {"file_name": file_name}) | |||
else: | |||
full_path = get_csv_file_path(file_name) | |||
f = frappe.get_doc( | |||
doctype='File', | |||
content=frappe.read_file(full_path), | |||
file_name=file_name, | |||
is_private=1 | |||
doctype="File", content=frappe.read_file(full_path), file_name=file_name, is_private=1 | |||
) | |||
f.save(ignore_permissions=True) | |||
@@ -210,4 +249,4 @@ def get_import_file(csv_file_name, force=False): | |||
def get_csv_file_path(file_name): | |||
return frappe.get_app_path('frappe', 'core', 'doctype', 'data_import', 'fixtures', file_name) | |||
return frappe.get_app_path("frappe", "core", "doctype", "data_import", "fixtures", file_name) |
@@ -4,5 +4,6 @@ | |||
# import frappe | |||
from frappe.model.document import Document | |||
class DataImportLog(Document): | |||
pass |
@@ -4,5 +4,6 @@ | |||
# import frappe | |||
import unittest | |||
class TestDataImportLog(unittest.TestCase): | |||
pass |
@@ -1,3 +1,2 @@ | |||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
@@ -2,19 +2,24 @@ | |||
# License: MIT. See LICENSE | |||
import frappe | |||
from frappe.model.document import Document | |||
class DefaultValue(Document): | |||
pass | |||
def on_doctype_update(): | |||
"""Create indexes for `tabDefaultValue` on `(parent, defkey)`""" | |||
frappe.db.commit() | |||
frappe.db.add_index(doctype='DefaultValue', | |||
fields=['parent', 'defkey'], | |||
index_name='defaultvalue_parent_defkey_index') | |||
frappe.db.add_index( | |||
doctype="DefaultValue", | |||
fields=["parent", "defkey"], | |||
index_name="defaultvalue_parent_defkey_index", | |||
) | |||
frappe.db.add_index(doctype='DefaultValue', | |||
fields=['parent', 'parenttype'], | |||
index_name='defaultvalue_parent_parenttype_index') | |||
frappe.db.add_index( | |||
doctype="DefaultValue", | |||
fields=["parent", "parenttype"], | |||
index_name="defaultvalue_parent_parenttype_index", | |||
) |
@@ -2,11 +2,12 @@ | |||
# Copyright (c) 2015, Frappe Technologies and contributors | |||
# License: MIT. See LICENSE | |||
import frappe | |||
import json | |||
import frappe | |||
from frappe import _ | |||
from frappe.desk.doctype.bulk_update.bulk_update import show_progress | |||
from frappe.model.document import Document | |||
from frappe import _ | |||
class DeletedDocument(Document): | |||
@@ -15,7 +16,7 @@ class DeletedDocument(Document): | |||
@frappe.whitelist() | |||
def restore(name, alert=True): | |||
deleted = frappe.get_doc('Deleted Document', name) | |||
deleted = frappe.get_doc("Deleted Document", name) | |||
if deleted.restored: | |||
frappe.throw(_("Document {0} Already Restored").format(name), exc=frappe.DocumentAlreadyRestored) | |||
@@ -29,20 +30,20 @@ def restore(name, alert=True): | |||
doc.docstatus = 0 | |||
doc.insert() | |||
doc.add_comment('Edit', _('restored {0} as {1}').format(deleted.deleted_name, doc.name)) | |||
doc.add_comment("Edit", _("restored {0} as {1}").format(deleted.deleted_name, doc.name)) | |||
deleted.new_name = doc.name | |||
deleted.restored = 1 | |||
deleted.db_update() | |||
if alert: | |||
frappe.msgprint(_('Document Restored')) | |||
frappe.msgprint(_("Document Restored")) | |||
@frappe.whitelist() | |||
def bulk_restore(docnames): | |||
docnames = frappe.parse_json(docnames) | |||
message = _('Restoring Deleted Document') | |||
message = _("Restoring Deleted Document") | |||
restored, invalid, failed = [], [], [] | |||
for i, d in enumerate(docnames): | |||
@@ -61,8 +62,4 @@ def bulk_restore(docnames): | |||
failed.append(d) | |||
frappe.db.rollback() | |||
return { | |||
"restored": restored, | |||
"invalid": invalid, | |||
"failed": failed | |||
} | |||
return {"restored": restored, "invalid": invalid, "failed": failed} |
@@ -1,10 +1,12 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright (c) 2015, Frappe Technologies and Contributors | |||
# License: MIT. See LICENSE | |||
import frappe | |||
import unittest | |||
import frappe | |||
# test_records = frappe.get_test_records('Deleted Document') | |||
class TestDeletedDocument(unittest.TestCase): | |||
pass |
@@ -1,3 +1,2 @@ | |||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
@@ -4,28 +4,28 @@ | |||
import frappe | |||
from frappe.model.document import Document | |||
class DocField(Document): | |||
def get_link_doctype(self): | |||
'''Returns the Link doctype for the docfield (if applicable) | |||
"""Returns the Link doctype for the docfield (if applicable) | |||
if fieldtype is Link: Returns "options" | |||
if fieldtype is Table MultiSelect: Returns "options" of the Link field in the Child Table | |||
''' | |||
if self.fieldtype == 'Link': | |||
""" | |||
if self.fieldtype == "Link": | |||
return self.options | |||
if self.fieldtype == 'Table MultiSelect': | |||
if self.fieldtype == "Table MultiSelect": | |||
table_doctype = self.options | |||
link_doctype = frappe.db.get_value('DocField', { | |||
'fieldtype': 'Link', | |||
'parenttype': 'DocType', | |||
'parent': table_doctype, | |||
'in_list_view': 1 | |||
}, 'options') | |||
link_doctype = frappe.db.get_value( | |||
"DocField", | |||
{"fieldtype": "Link", "parenttype": "DocType", "parent": table_doctype, "in_list_view": 1}, | |||
"options", | |||
) | |||
return link_doctype | |||
def get_select_options(self): | |||
if self.fieldtype == 'Select': | |||
options = self.options or '' | |||
return [d for d in options.split('\n') if d] | |||
if self.fieldtype == "Select": | |||
options = self.options or "" | |||
return [d for d in options.split("\n") if d] |
@@ -1,3 +1,2 @@ | |||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
@@ -2,8 +2,8 @@ | |||
# License: MIT. See LICENSE | |||
import frappe | |||
from frappe.model.document import Document | |||
class DocPerm(Document): | |||
pass |
@@ -2,12 +2,13 @@ | |||
# License: MIT. See LICENSE | |||
import frappe | |||
from frappe.model.document import Document | |||
from frappe import _ | |||
from frappe.utils import get_fullname, cint | |||
from frappe.model.document import Document | |||
from frappe.utils import cint, get_fullname | |||
exclude_from_linked_with = True | |||
class DocShare(Document): | |||
no_feed_on_delete = True | |||
@@ -36,15 +37,21 @@ class DocShare(Document): | |||
frappe.throw(_("User is mandatory for Share"), frappe.MandatoryError) | |||
def check_share_permission(self): | |||
if (not self.flags.ignore_share_permission and | |||
not frappe.has_permission(self.share_doctype, "share", self.get_doc())): | |||
if not self.flags.ignore_share_permission and not frappe.has_permission( | |||
self.share_doctype, "share", self.get_doc() | |||
): | |||
frappe.throw(_('You need to have "Share" permission'), frappe.PermissionError) | |||
def check_is_submittable(self): | |||
if self.submit and not cint(frappe.db.get_value("DocType", self.share_doctype, "is_submittable")): | |||
frappe.throw(_("Cannot share {0} with submit permission as the doctype {1} is not submittable").format( | |||
frappe.bold(self.share_name), frappe.bold(self.share_doctype))) | |||
if self.submit and not cint( | |||
frappe.db.get_value("DocType", self.share_doctype, "is_submittable") | |||
): | |||
frappe.throw( | |||
_("Cannot share {0} with submit permission as the doctype {1} is not submittable").format( | |||
frappe.bold(self.share_name), frappe.bold(self.share_doctype) | |||
) | |||
) | |||
def after_insert(self): | |||
doc = self.get_doc() | |||
@@ -53,14 +60,21 @@ class DocShare(Document): | |||
if self.everyone: | |||
doc.add_comment("Shared", _("{0} shared this document with everyone").format(owner)) | |||
else: | |||
doc.add_comment("Shared", _("{0} shared this document with {1}").format(owner, get_fullname(self.user))) | |||
doc.add_comment( | |||
"Shared", _("{0} shared this document with {1}").format(owner, get_fullname(self.user)) | |||
) | |||
def on_trash(self): | |||
if not self.flags.ignore_share_permission: | |||
self.check_share_permission() | |||
self.get_doc().add_comment("Unshared", | |||
_("{0} un-shared this document with {1}").format(get_fullname(self.owner), get_fullname(self.user))) | |||
self.get_doc().add_comment( | |||
"Unshared", | |||
_("{0} un-shared this document with {1}").format( | |||
get_fullname(self.owner), get_fullname(self.user) | |||
), | |||
) | |||
def on_doctype_update(): | |||
"""Add index in `tabDocShare` for `(user, share_doctype)`""" | |||
@@ -1,20 +1,26 @@ | |||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
import unittest | |||
import frappe | |||
import frappe.share | |||
import unittest | |||
from frappe.automation.doctype.auto_repeat.test_auto_repeat import create_submittable_doctype | |||
test_dependencies = ['User'] | |||
test_dependencies = ["User"] | |||
class TestDocShare(unittest.TestCase): | |||
def setUp(self): | |||
self.user = "test@example.com" | |||
self.event = frappe.get_doc({"doctype": "Event", | |||
"subject": "test share event", | |||
"starts_on": "2015-01-01 10:00:00", | |||
"event_type": "Private"}).insert() | |||
self.event = frappe.get_doc( | |||
{ | |||
"doctype": "Event", | |||
"subject": "test share event", | |||
"starts_on": "2015-01-01 10:00:00", | |||
"event_type": "Private", | |||
} | |||
).insert() | |||
def tearDown(self): | |||
frappe.set_user("Administrator") | |||
@@ -98,7 +104,9 @@ class TestDocShare(unittest.TestCase): | |||
doctype = "Test DocShare with Submit" | |||
create_submittable_doctype(doctype, submit_perms=0) | |||
submittable_doc = frappe.get_doc(dict(doctype=doctype, test="test docshare with submit")).insert() | |||
submittable_doc = frappe.get_doc( | |||
dict(doctype=doctype, test="test docshare with submit") | |||
).insert() | |||
frappe.set_user(self.user) | |||
self.assertFalse(frappe.has_permission(doctype, "submit", user=self.user)) | |||
@@ -107,10 +115,14 @@ class TestDocShare(unittest.TestCase): | |||
frappe.share.add(doctype, submittable_doc.name, self.user, submit=1) | |||
frappe.set_user(self.user) | |||
self.assertTrue(frappe.has_permission(doctype, "submit", doc=submittable_doc.name, user=self.user)) | |||
self.assertTrue( | |||
frappe.has_permission(doctype, "submit", doc=submittable_doc.name, user=self.user) | |||
) | |||
# test cascade | |||
self.assertTrue(frappe.has_permission(doctype, "read", doc=submittable_doc.name, user=self.user)) | |||
self.assertTrue(frappe.has_permission(doctype, "write", doc=submittable_doc.name, user=self.user)) | |||
self.assertTrue( | |||
frappe.has_permission(doctype, "write", doc=submittable_doc.name, user=self.user) | |||
) | |||
frappe.share.remove(doctype, submittable_doc.name, self.user) |
@@ -1,3 +1,2 @@ | |||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
@@ -1,7 +1,8 @@ | |||
import frappe | |||
from frappe.desk.utils import slug | |||
def execute(): | |||
for doctype in frappe.get_all('DocType', ['name', 'route'], dict(istable=0)): | |||
if not doctype.route: | |||
frappe.db.set_value('DocType', doctype.name, 'route', slug(doctype.name), update_modified = False) | |||
for doctype in frappe.get_all("DocType", ["name", "route"], dict(istable=0)): | |||
if not doctype.route: | |||
frappe.db.set_value("DocType", doctype.name, "route", slug(doctype.name), update_modified=False) |
@@ -1,21 +1,24 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
import frappe | |||
import unittest | |||
from frappe.core.doctype.doctype.doctype import (UniqueFieldnameError, | |||
IllegalMandatoryError, | |||
import frappe | |||
from frappe.core.doctype.doctype.doctype import ( | |||
CannotIndexedError, | |||
DoctypeLinkError, | |||
WrongOptionsDoctypeLinkError, | |||
HiddenAndMandatoryWithoutDefaultError, | |||
CannotIndexedError, | |||
IllegalMandatoryError, | |||
InvalidFieldNameError, | |||
validate_links_table_fieldnames) | |||
UniqueFieldnameError, | |||
WrongOptionsDoctypeLinkError, | |||
validate_links_table_fieldnames, | |||
) | |||
# test_records = frappe.get_test_records('DocType') | |||
class TestDocType(unittest.TestCase): | |||
class TestDocType(unittest.TestCase): | |||
def tearDown(self): | |||
frappe.db.rollback() | |||
@@ -23,7 +26,10 @@ class TestDocType(unittest.TestCase): | |||
self.assertRaises(frappe.NameError, new_doctype("_Some DocType").insert) | |||
self.assertRaises(frappe.NameError, new_doctype("8Some DocType").insert) | |||
self.assertRaises(frappe.NameError, new_doctype("Some (DocType)").insert) | |||
self.assertRaises(frappe.NameError, new_doctype("Some Doctype with a name whose length is more than 61 characters").insert) | |||
self.assertRaises( | |||
frappe.NameError, | |||
new_doctype("Some Doctype with a name whose length is more than 61 characters").insert, | |||
) | |||
for name in ("Some DocType", "Some_DocType", "Some-DocType"): | |||
if frappe.db.exists("DocType", name): | |||
frappe.delete_doc("DocType", name) | |||
@@ -86,19 +92,33 @@ class TestDocType(unittest.TestCase): | |||
def test_all_depends_on_fields_conditions(self): | |||
import re | |||
docfields = frappe.get_all("DocField", | |||
or_filters={ | |||
"ifnull(depends_on, '')": ("!=", ''), | |||
"ifnull(collapsible_depends_on, '')": ("!=", ''), | |||
"ifnull(mandatory_depends_on, '')": ("!=", ''), | |||
"ifnull(read_only_depends_on, '')": ("!=", '') | |||
docfields = frappe.get_all( | |||
"DocField", | |||
or_filters={ | |||
"ifnull(depends_on, '')": ("!=", ""), | |||
"ifnull(collapsible_depends_on, '')": ("!=", ""), | |||
"ifnull(mandatory_depends_on, '')": ("!=", ""), | |||
"ifnull(read_only_depends_on, '')": ("!=", ""), | |||
}, | |||
fields=["parent", "depends_on", "collapsible_depends_on", "mandatory_depends_on",\ | |||
"read_only_depends_on", "fieldname", "fieldtype"]) | |||
fields=[ | |||
"parent", | |||
"depends_on", | |||
"collapsible_depends_on", | |||
"mandatory_depends_on", | |||
"read_only_depends_on", | |||
"fieldname", | |||
"fieldtype", | |||
], | |||
) | |||
pattern = r'[\w\.:_]+\s*={1}\s*[\w\.@\'"]+' | |||
for field in docfields: | |||
for depends_on in ["depends_on", "collapsible_depends_on", "mandatory_depends_on", "read_only_depends_on"]: | |||
for depends_on in [ | |||
"depends_on", | |||
"collapsible_depends_on", | |||
"mandatory_depends_on", | |||
"read_only_depends_on", | |||
]: | |||
condition = field.get(depends_on) | |||
if condition: | |||
self.assertFalse(re.match(pattern, condition)) | |||
@@ -108,18 +128,18 @@ class TestDocType(unittest.TestCase): | |||
valid_data_field_options = frappe.model.data_field_options + ("",) | |||
invalid_data_field_options = ("Invalid Option 1", frappe.utils.random_string(5)) | |||
for field_option in (valid_data_field_options + invalid_data_field_options): | |||
test_doctype = frappe.get_doc({ | |||
"doctype": "DocType", | |||
"name": doctype_name, | |||
"module": "Core", | |||
"custom": 1, | |||
"fields": [{ | |||
"fieldname": "{0}_field".format(field_option), | |||
"fieldtype": "Data", | |||
"options": field_option | |||
}] | |||
}) | |||
for field_option in valid_data_field_options + invalid_data_field_options: | |||
test_doctype = frappe.get_doc( | |||
{ | |||
"doctype": "DocType", | |||
"name": doctype_name, | |||
"module": "Core", | |||
"custom": 1, | |||
"fields": [ | |||
{"fieldname": "{0}_field".format(field_option), "fieldtype": "Data", "options": field_option} | |||
], | |||
} | |||
) | |||
if field_option in invalid_data_field_options: | |||
# assert that only data options in frappe.model.data_field_options are valid | |||
@@ -130,45 +150,29 @@ class TestDocType(unittest.TestCase): | |||
test_doctype.delete() | |||
def test_sync_field_order(self): | |||
from frappe.modules.import_file import get_file_path | |||
import os | |||
from frappe.modules.import_file import get_file_path | |||
# create test doctype | |||
test_doctype = frappe.get_doc({ | |||
"doctype": "DocType", | |||
"module": "Core", | |||
"fields": [ | |||
{ | |||
"label": "Field 1", | |||
"fieldname": "field_1", | |||
"fieldtype": "Data" | |||
}, | |||
{ | |||
"label": "Field 2", | |||
"fieldname": "field_2", | |||
"fieldtype": "Data" | |||
}, | |||
{ | |||
"label": "Field 3", | |||
"fieldname": "field_3", | |||
"fieldtype": "Data" | |||
}, | |||
{ | |||
"label": "Field 4", | |||
"fieldname": "field_4", | |||
"fieldtype": "Data" | |||
} | |||
], | |||
"permissions": [{ | |||
"role": "System Manager", | |||
"read": 1 | |||
}], | |||
"name": "Test Field Order DocType", | |||
"__islocal": 1 | |||
}) | |||
test_doctype = frappe.get_doc( | |||
{ | |||
"doctype": "DocType", | |||
"module": "Core", | |||
"fields": [ | |||
{"label": "Field 1", "fieldname": "field_1", "fieldtype": "Data"}, | |||
{"label": "Field 2", "fieldname": "field_2", "fieldtype": "Data"}, | |||
{"label": "Field 3", "fieldname": "field_3", "fieldtype": "Data"}, | |||
{"label": "Field 4", "fieldname": "field_4", "fieldtype": "Data"}, | |||
], | |||
"permissions": [{"role": "System Manager", "read": 1}], | |||
"name": "Test Field Order DocType", | |||
"__islocal": 1, | |||
} | |||
) | |||
path = get_file_path(test_doctype.module, test_doctype.doctype, test_doctype.name) | |||
initial_fields_order = ['field_1', 'field_2', 'field_3', 'field_4'] | |||
initial_fields_order = ["field_1", "field_2", "field_3", "field_4"] | |||
frappe.delete_doc_if_exists("DocType", "Test Field Order DocType") | |||
if os.path.isfile(path): | |||
@@ -181,14 +185,18 @@ class TestDocType(unittest.TestCase): | |||
# assert that field_order list is being created with the default order | |||
test_doctype_json = frappe.get_file_json(path) | |||
self.assertTrue(test_doctype_json.get("field_order")) | |||
self.assertEqual(len(test_doctype_json['fields']), len(test_doctype_json['field_order'])) | |||
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], test_doctype_json['field_order']) | |||
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], initial_fields_order) | |||
self.assertListEqual(test_doctype_json['field_order'], initial_fields_order) | |||
self.assertEqual(len(test_doctype_json["fields"]), len(test_doctype_json["field_order"])) | |||
self.assertListEqual( | |||
[f["fieldname"] for f in test_doctype_json["fields"]], test_doctype_json["field_order"] | |||
) | |||
self.assertListEqual( | |||
[f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order | |||
) | |||
self.assertListEqual(test_doctype_json["field_order"], initial_fields_order) | |||
# remove field_order to test reload_doc/sync/migrate is backwards compatible without field_order | |||
del test_doctype_json['field_order'] | |||
with open(path, 'w+') as txtfile: | |||
del test_doctype_json["field_order"] | |||
with open(path, "w+") as txtfile: | |||
txtfile.write(frappe.as_json(test_doctype_json)) | |||
# assert that field_order is actually removed from the json file | |||
@@ -203,10 +211,14 @@ class TestDocType(unittest.TestCase): | |||
test_doctype.save() | |||
test_doctype_json = frappe.get_file_json(path) | |||
self.assertTrue(test_doctype_json.get("field_order")) | |||
self.assertEqual(len(test_doctype_json['fields']), len(test_doctype_json['field_order'])) | |||
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], test_doctype_json['field_order']) | |||
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], initial_fields_order) | |||
self.assertListEqual(test_doctype_json['field_order'], initial_fields_order) | |||
self.assertEqual(len(test_doctype_json["fields"]), len(test_doctype_json["field_order"])) | |||
self.assertListEqual( | |||
[f["fieldname"] for f in test_doctype_json["fields"]], test_doctype_json["field_order"] | |||
) | |||
self.assertListEqual( | |||
[f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order | |||
) | |||
self.assertListEqual(test_doctype_json["field_order"], initial_fields_order) | |||
# reorder fields: swap row 1 and 3 | |||
test_doctype.fields[0], test_doctype.fields[2] = test_doctype.fields[2], test_doctype.fields[0] | |||
@@ -216,25 +228,30 @@ class TestDocType(unittest.TestCase): | |||
# assert that reordering fields only affects `field_order` rather than `fields` attr | |||
test_doctype.save() | |||
test_doctype_json = frappe.get_file_json(path) | |||
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], initial_fields_order) | |||
self.assertListEqual(test_doctype_json['field_order'], ['field_3', 'field_2', 'field_1', 'field_4']) | |||
self.assertListEqual( | |||
[f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order | |||
) | |||
self.assertListEqual( | |||
test_doctype_json["field_order"], ["field_3", "field_2", "field_1", "field_4"] | |||
) | |||
# reorder `field_order` in the json file: swap row 2 and 4 | |||
test_doctype_json['field_order'][1], test_doctype_json['field_order'][3] = test_doctype_json['field_order'][3], test_doctype_json['field_order'][1] | |||
with open(path, 'w+') as txtfile: | |||
test_doctype_json["field_order"][1], test_doctype_json["field_order"][3] = ( | |||
test_doctype_json["field_order"][3], | |||
test_doctype_json["field_order"][1], | |||
) | |||
with open(path, "w+") as txtfile: | |||
txtfile.write(frappe.as_json(test_doctype_json)) | |||
# assert that reordering `field_order` from json file is reflected in DocType upon migrate/sync | |||
frappe.reload_doctype(test_doctype.name, force=True) | |||
test_doctype.reload() | |||
self.assertListEqual([f.fieldname for f in test_doctype.fields], ['field_3', 'field_4', 'field_1', 'field_2']) | |||
self.assertListEqual( | |||
[f.fieldname for f in test_doctype.fields], ["field_3", "field_4", "field_1", "field_2"] | |||
) | |||
# insert row in the middle and remove first row (field 3) | |||
test_doctype.append("fields", { | |||
"label": "Field 5", | |||
"fieldname": "field_5", | |||
"fieldtype": "Data" | |||
}) | |||
test_doctype.append("fields", {"label": "Field 5", "fieldname": "field_5", "fieldtype": "Data"}) | |||
test_doctype.fields[4], test_doctype.fields[3] = test_doctype.fields[3], test_doctype.fields[4] | |||
test_doctype.fields[3], test_doctype.fields[2] = test_doctype.fields[2], test_doctype.fields[3] | |||
test_doctype.remove(test_doctype.fields[0]) | |||
@@ -243,115 +260,121 @@ class TestDocType(unittest.TestCase): | |||
test_doctype.save() | |||
test_doctype_json = frappe.get_file_json(path) | |||
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], ['field_1', 'field_2', 'field_4', 'field_5']) | |||
self.assertListEqual(test_doctype_json['field_order'], ['field_4', 'field_5', 'field_1', 'field_2']) | |||
self.assertListEqual( | |||
[f["fieldname"] for f in test_doctype_json["fields"]], | |||
["field_1", "field_2", "field_4", "field_5"], | |||
) | |||
self.assertListEqual( | |||
test_doctype_json["field_order"], ["field_4", "field_5", "field_1", "field_2"] | |||
) | |||
except: | |||
raise | |||
finally: | |||
frappe.flags.allow_doctype_export = 0 | |||
def test_unique_field_name_for_two_fields(self): | |||
doc = new_doctype('Test Unique Field') | |||
field_1 = doc.append('fields', {}) | |||
field_1.fieldname = 'some_fieldname_1' | |||
field_1.fieldtype = 'Data' | |||
doc = new_doctype("Test Unique Field") | |||
field_1 = doc.append("fields", {}) | |||
field_1.fieldname = "some_fieldname_1" | |||
field_1.fieldtype = "Data" | |||
field_2 = doc.append('fields', {}) | |||
field_2.fieldname = 'some_fieldname_1' | |||
field_2.fieldtype = 'Data' | |||
field_2 = doc.append("fields", {}) | |||
field_2.fieldname = "some_fieldname_1" | |||
field_2.fieldtype = "Data" | |||
self.assertRaises(UniqueFieldnameError, doc.insert) | |||
def test_fieldname_is_not_name(self): | |||
doc = new_doctype('Test Name Field') | |||
field_1 = doc.append('fields', {}) | |||
field_1.label = 'Name' | |||
field_1.fieldtype = 'Data' | |||
doc = new_doctype("Test Name Field") | |||
field_1 = doc.append("fields", {}) | |||
field_1.label = "Name" | |||
field_1.fieldtype = "Data" | |||
doc.insert() | |||
self.assertEqual(doc.fields[1].fieldname, "name1") | |||
doc.fields[1].fieldname = 'name' | |||
doc.fields[1].fieldname = "name" | |||
self.assertRaises(InvalidFieldNameError, doc.save) | |||
def test_illegal_mandatory_validation(self): | |||
doc = new_doctype('Test Illegal mandatory') | |||
field_1 = doc.append('fields', {}) | |||
field_1.fieldname = 'some_fieldname_1' | |||
field_1.fieldtype = 'Section Break' | |||
doc = new_doctype("Test Illegal mandatory") | |||
field_1 = doc.append("fields", {}) | |||
field_1.fieldname = "some_fieldname_1" | |||
field_1.fieldtype = "Section Break" | |||
field_1.reqd = 1 | |||
self.assertRaises(IllegalMandatoryError, doc.insert) | |||
def test_link_with_wrong_and_no_options(self): | |||
doc = new_doctype('Test link') | |||
field_1 = doc.append('fields', {}) | |||
field_1.fieldname = 'some_fieldname_1' | |||
field_1.fieldtype = 'Link' | |||
doc = new_doctype("Test link") | |||
field_1 = doc.append("fields", {}) | |||
field_1.fieldname = "some_fieldname_1" | |||
field_1.fieldtype = "Link" | |||
self.assertRaises(DoctypeLinkError, doc.insert) | |||
field_1.options = 'wrongdoctype' | |||
field_1.options = "wrongdoctype" | |||
self.assertRaises(WrongOptionsDoctypeLinkError, doc.insert) | |||
def test_hidden_and_mandatory_without_default(self): | |||
doc = new_doctype('Test hidden and mandatory') | |||
field_1 = doc.append('fields', {}) | |||
field_1.fieldname = 'some_fieldname_1' | |||
field_1.fieldtype = 'Data' | |||
doc = new_doctype("Test hidden and mandatory") | |||
field_1 = doc.append("fields", {}) | |||
field_1.fieldname = "some_fieldname_1" | |||
field_1.fieldtype = "Data" | |||
field_1.reqd = 1 | |||
field_1.hidden = 1 | |||
self.assertRaises(HiddenAndMandatoryWithoutDefaultError, doc.insert) | |||
def test_field_can_not_be_indexed_validation(self): | |||
doc = new_doctype('Test index') | |||
field_1 = doc.append('fields', {}) | |||
field_1.fieldname = 'some_fieldname_1' | |||
field_1.fieldtype = 'Long Text' | |||
doc = new_doctype("Test index") | |||
field_1 = doc.append("fields", {}) | |||
field_1.fieldname = "some_fieldname_1" | |||
field_1.fieldtype = "Long Text" | |||
field_1.search_index = 1 | |||
self.assertRaises(CannotIndexedError, doc.insert) | |||
def test_cancel_link_doctype(self): | |||
import json | |||
from frappe.desk.form.linked_with import get_submitted_linked_docs, cancel_all_linked_docs | |||
#create doctype | |||
link_doc = new_doctype('Test Linked Doctype') | |||
from frappe.desk.form.linked_with import cancel_all_linked_docs, get_submitted_linked_docs | |||
# create doctype | |||
link_doc = new_doctype("Test Linked Doctype") | |||
link_doc.is_submittable = 1 | |||
for data in link_doc.get('permissions'): | |||
for data in link_doc.get("permissions"): | |||
data.submit = 1 | |||
data.cancel = 1 | |||
link_doc.insert() | |||
doc = new_doctype('Test Doctype') | |||
doc = new_doctype("Test Doctype") | |||
doc.is_submittable = 1 | |||
field_2 = doc.append('fields', {}) | |||
field_2.label = 'Test Linked Doctype' | |||
field_2.fieldname = 'test_linked_doctype' | |||
field_2.fieldtype = 'Link' | |||
field_2.options = 'Test Linked Doctype' | |||
for data in link_doc.get('permissions'): | |||
field_2 = doc.append("fields", {}) | |||
field_2.label = "Test Linked Doctype" | |||
field_2.fieldname = "test_linked_doctype" | |||
field_2.fieldtype = "Link" | |||
field_2.options = "Test Linked Doctype" | |||
for data in link_doc.get("permissions"): | |||
data.submit = 1 | |||
data.cancel = 1 | |||
doc.insert() | |||
# create doctype data | |||
data_link_doc = frappe.new_doc('Test Linked Doctype') | |||
data_link_doc.some_fieldname = 'Data1' | |||
data_link_doc = frappe.new_doc("Test Linked Doctype") | |||
data_link_doc.some_fieldname = "Data1" | |||
data_link_doc.insert() | |||
data_link_doc.save() | |||
data_link_doc.submit() | |||
data_doc = frappe.new_doc('Test Doctype') | |||
data_doc.some_fieldname = 'Data1' | |||
data_doc = frappe.new_doc("Test Doctype") | |||
data_doc.some_fieldname = "Data1" | |||
data_doc.test_linked_doctype = data_link_doc.name | |||
data_doc.insert() | |||
data_doc.save() | |||
data_doc.submit() | |||
docs = get_submitted_linked_docs(link_doc.name, data_link_doc.name) | |||
dump_docs = json.dumps(docs.get('docs')) | |||
dump_docs = json.dumps(docs.get("docs")) | |||
cancel_all_linked_docs(dump_docs) | |||
data_link_doc.cancel() | |||
data_doc.load_from_db() | |||
@@ -369,69 +392,70 @@ class TestDocType(unittest.TestCase): | |||
def test_ignore_cancelation_of_linked_doctype_during_cancel(self): | |||
import json | |||
from frappe.desk.form.linked_with import get_submitted_linked_docs, cancel_all_linked_docs | |||
#create linked doctype | |||
link_doc = new_doctype('Test Linked Doctype 1') | |||
from frappe.desk.form.linked_with import cancel_all_linked_docs, get_submitted_linked_docs | |||
# create linked doctype | |||
link_doc = new_doctype("Test Linked Doctype 1") | |||
link_doc.is_submittable = 1 | |||
for data in link_doc.get('permissions'): | |||
for data in link_doc.get("permissions"): | |||
data.submit = 1 | |||
data.cancel = 1 | |||
link_doc.insert() | |||
#create first parent doctype | |||
test_doc_1 = new_doctype('Test Doctype 1') | |||
# create first parent doctype | |||
test_doc_1 = new_doctype("Test Doctype 1") | |||
test_doc_1.is_submittable = 1 | |||
field_2 = test_doc_1.append('fields', {}) | |||
field_2.label = 'Test Linked Doctype 1' | |||
field_2.fieldname = 'test_linked_doctype_a' | |||
field_2.fieldtype = 'Link' | |||
field_2.options = 'Test Linked Doctype 1' | |||
field_2 = test_doc_1.append("fields", {}) | |||
field_2.label = "Test Linked Doctype 1" | |||
field_2.fieldname = "test_linked_doctype_a" | |||
field_2.fieldtype = "Link" | |||
field_2.options = "Test Linked Doctype 1" | |||
for data in test_doc_1.get('permissions'): | |||
for data in test_doc_1.get("permissions"): | |||
data.submit = 1 | |||
data.cancel = 1 | |||
test_doc_1.insert() | |||
#crete second parent doctype | |||
doc = new_doctype('Test Doctype 2') | |||
# crete second parent doctype | |||
doc = new_doctype("Test Doctype 2") | |||
doc.is_submittable = 1 | |||
field_2 = doc.append('fields', {}) | |||
field_2.label = 'Test Linked Doctype 1' | |||
field_2.fieldname = 'test_linked_doctype_a' | |||
field_2.fieldtype = 'Link' | |||
field_2.options = 'Test Linked Doctype 1' | |||
field_2 = doc.append("fields", {}) | |||
field_2.label = "Test Linked Doctype 1" | |||
field_2.fieldname = "test_linked_doctype_a" | |||
field_2.fieldtype = "Link" | |||
field_2.options = "Test Linked Doctype 1" | |||
for data in link_doc.get('permissions'): | |||
for data in link_doc.get("permissions"): | |||
data.submit = 1 | |||
data.cancel = 1 | |||
doc.insert() | |||
# create doctype data | |||
data_link_doc_1 = frappe.new_doc('Test Linked Doctype 1') | |||
data_link_doc_1.some_fieldname = 'Data1' | |||
data_link_doc_1 = frappe.new_doc("Test Linked Doctype 1") | |||
data_link_doc_1.some_fieldname = "Data1" | |||
data_link_doc_1.insert() | |||
data_link_doc_1.save() | |||
data_link_doc_1.submit() | |||
data_doc_2 = frappe.new_doc('Test Doctype 1') | |||
data_doc_2.some_fieldname = 'Data1' | |||
data_doc_2 = frappe.new_doc("Test Doctype 1") | |||
data_doc_2.some_fieldname = "Data1" | |||
data_doc_2.test_linked_doctype_a = data_link_doc_1.name | |||
data_doc_2.insert() | |||
data_doc_2.save() | |||
data_doc_2.submit() | |||
data_doc = frappe.new_doc('Test Doctype 2') | |||
data_doc.some_fieldname = 'Data1' | |||
data_doc = frappe.new_doc("Test Doctype 2") | |||
data_doc.some_fieldname = "Data1" | |||
data_doc.test_linked_doctype_a = data_link_doc_1.name | |||
data_doc.insert() | |||
data_doc.save() | |||
data_doc.submit() | |||
docs = get_submitted_linked_docs(link_doc.name, data_link_doc_1.name) | |||
dump_docs = json.dumps(docs.get('docs')) | |||
dump_docs = json.dumps(docs.get("docs")) | |||
cancel_all_linked_docs(dump_docs, ignore_doctypes_on_cancel_all=["Test Doctype 2"]) | |||
@@ -442,10 +466,10 @@ class TestDocType(unittest.TestCase): | |||
data_doc_2.load_from_db() | |||
self.assertEqual(data_link_doc_1.docstatus, 2) | |||
#linked doc is canceled | |||
# linked doc is canceled | |||
self.assertEqual(data_doc_2.docstatus, 2) | |||
#ignored doctype 2 during cancel | |||
# ignored doctype 2 during cancel | |||
self.assertEqual(data_doc.docstatus, 1) | |||
# delete doctype record | |||
@@ -464,42 +488,35 @@ class TestDocType(unittest.TestCase): | |||
doc = new_doctype("Test Links Table Validation") | |||
# check valid data | |||
doc.append("links", { | |||
'link_doctype': "User", | |||
'link_fieldname': "first_name" | |||
}) | |||
validate_links_table_fieldnames(doc) # no error | |||
doc.links = [] # reset links table | |||
doc.append("links", {"link_doctype": "User", "link_fieldname": "first_name"}) | |||
validate_links_table_fieldnames(doc) # no error | |||
doc.links = [] # reset links table | |||
# check invalid doctype | |||
doc.append("links", { | |||
'link_doctype': "User2", | |||
'link_fieldname': "first_name" | |||
}) | |||
doc.append("links", {"link_doctype": "User2", "link_fieldname": "first_name"}) | |||
self.assertRaises(frappe.DoesNotExistError, validate_links_table_fieldnames, doc) | |||
doc.links = [] # reset links table | |||
doc.links = [] # reset links table | |||
# check invalid fieldname | |||
doc.append("links", { | |||
'link_doctype': "User", | |||
'link_fieldname': "a_field_that_does_not_exists" | |||
}) | |||
doc.append("links", {"link_doctype": "User", "link_fieldname": "a_field_that_does_not_exists"}) | |||
self.assertRaises(InvalidFieldNameError, validate_links_table_fieldnames, doc) | |||
def test_create_virtual_doctype(self): | |||
"""Test virtual DOcTYpe.""" | |||
virtual_doc = new_doctype('Test Virtual Doctype') | |||
virtual_doc = new_doctype("Test Virtual Doctype") | |||
virtual_doc.is_virtual = 1 | |||
virtual_doc.insert() | |||
virtual_doc.save() | |||
doc = frappe.get_doc("DocType", "Test Virtual Doctype") | |||
self.assertEqual(doc.is_virtual, 1) | |||
self.assertFalse(frappe.db.table_exists('Test Virtual Doctype')) | |||
self.assertFalse(frappe.db.table_exists("Test Virtual Doctype")) | |||
def test_default_fieldname(self): | |||
fields = [{"label": "title", "fieldname": "title", "fieldtype": "Data", "default": "{some_fieldname}"}] | |||
fields = [ | |||
{"label": "title", "fieldname": "title", "fieldtype": "Data", "default": "{some_fieldname}"} | |||
] | |||
dt = new_doctype("DT with default field", fields=fields) | |||
dt.insert() | |||
@@ -521,28 +538,34 @@ class TestDocType(unittest.TestCase): | |||
dt.delete(ignore_permissions=True) | |||
def new_doctype(name, unique=0, depends_on='', fields=None, autoincremented=False): | |||
doc = frappe.get_doc({ | |||
"doctype": "DocType", | |||
"module": "Core", | |||
"custom": 1, | |||
"fields": [{ | |||
"label": "Some Field", | |||
"fieldname": "some_fieldname", | |||
"fieldtype": "Data", | |||
"unique": unique, | |||
"depends_on": depends_on, | |||
}], | |||
"permissions": [{ | |||
"role": "System Manager", | |||
"read": 1, | |||
}], | |||
"name": name, | |||
"autoname": "autoincrement" if autoincremented else "" | |||
}) | |||
def new_doctype(name, unique=0, depends_on="", fields=None, autoincremented=False): | |||
doc = frappe.get_doc( | |||
{ | |||
"doctype": "DocType", | |||
"module": "Core", | |||
"custom": 1, | |||
"fields": [ | |||
{ | |||
"label": "Some Field", | |||
"fieldname": "some_fieldname", | |||
"fieldtype": "Data", | |||
"unique": unique, | |||
"depends_on": depends_on, | |||
} | |||
], | |||
"permissions": [ | |||
{ | |||
"role": "System Manager", | |||
"read": 1, | |||
} | |||
], | |||
"name": name, | |||
"autoname": "autoincrement" if autoincremented else "", | |||
} | |||
) | |||
if fields: | |||
for f in fields: | |||
doc.append('fields', f) | |||
doc.append("fields", f) | |||
return doc |
@@ -5,5 +5,6 @@ | |||
# import frappe | |||
from frappe.model.document import Document | |||
class DocTypeAction(Document): | |||
pass |
@@ -5,5 +5,6 @@ | |||
# import frappe | |||
from frappe.model.document import Document | |||
class DocTypeLink(Document): | |||
pass |
@@ -4,5 +4,6 @@ | |||
# import frappe | |||
from frappe.model.document import Document | |||
class DocTypeState(Document): | |||
pass |
@@ -3,10 +3,11 @@ | |||
# License: MIT. See LICENSE | |||
import frappe | |||
from frappe import _ | |||
from frappe.model.document import Document | |||
from frappe.utils.data import evaluate_filters | |||
from frappe.model.naming import parse_naming_series | |||
from frappe import _ | |||
from frappe.utils.data import evaluate_filters | |||
class DocumentNamingRule(Document): | |||
def validate(self): | |||
@@ -17,23 +18,30 @@ class DocumentNamingRule(Document): | |||
docfields = [x.fieldname for x in frappe.get_meta(self.document_type).fields] | |||
for condition in self.conditions: | |||
if condition.field not in docfields: | |||
frappe.throw(_("{0} is not a field of doctype {1}").format(frappe.bold(condition.field), frappe.bold(self.document_type))) | |||
frappe.throw( | |||
_("{0} is not a field of doctype {1}").format( | |||
frappe.bold(condition.field), frappe.bold(self.document_type) | |||
) | |||
) | |||
def apply(self, doc): | |||
''' | |||
""" | |||
Apply naming rules for the given document. Will set `name` if the rule is matched. | |||
''' | |||
""" | |||
if self.conditions: | |||
if not evaluate_filters(doc, [(self.document_type, d.field, d.condition, d.value) for d in self.conditions]): | |||
if not evaluate_filters( | |||
doc, [(self.document_type, d.field, d.condition, d.value) for d in self.conditions] | |||
): | |||
return | |||
counter = frappe.db.get_value(self.doctype, self.name, 'counter', for_update=True) or 0 | |||
counter = frappe.db.get_value(self.doctype, self.name, "counter", for_update=True) or 0 | |||
naming_series = parse_naming_series(self.prefix, doc=doc) | |||
doc.name = naming_series + ('%0'+str(self.prefix_digits)+'d') % (counter + 1) | |||
frappe.db.set_value(self.doctype, self.name, 'counter', counter + 1) | |||
doc.name = naming_series + ("%0" + str(self.prefix_digits) + "d") % (counter + 1) | |||
frappe.db.set_value(self.doctype, self.name, "counter", counter + 1) | |||
@frappe.whitelist() | |||
def update_current(name, new_counter): | |||
frappe.only_for('System Manager') | |||
frappe.db.set_value('Document Naming Rule', name, 'counter', new_counter) | |||
frappe.only_for("System Manager") | |||
frappe.db.set_value("Document Naming Rule", name, "counter", new_counter) |
@@ -1,79 +1,68 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright (c) 2020, Frappe Technologies and Contributors | |||
# License: MIT. See LICENSE | |||
import frappe | |||
import unittest | |||
import frappe | |||
class TestDocumentNamingRule(unittest.TestCase): | |||
def test_naming_rule_by_series(self): | |||
naming_rule = frappe.get_doc(dict( | |||
doctype = 'Document Naming Rule', | |||
document_type = 'ToDo', | |||
prefix = 'test-todo-', | |||
prefix_digits = 5 | |||
)).insert() | |||
naming_rule = frappe.get_doc( | |||
dict(doctype="Document Naming Rule", document_type="ToDo", prefix="test-todo-", prefix_digits=5) | |||
).insert() | |||
todo = frappe.get_doc(dict( | |||
doctype = 'ToDo', | |||
description = 'Is this my name ' + frappe.generate_hash() | |||
)).insert() | |||
todo = frappe.get_doc( | |||
dict(doctype="ToDo", description="Is this my name " + frappe.generate_hash()) | |||
).insert() | |||
self.assertEqual(todo.name, 'test-todo-00001') | |||
self.assertEqual(todo.name, "test-todo-00001") | |||
naming_rule.delete() | |||
todo.delete() | |||
def test_naming_rule_by_condition(self): | |||
naming_rule = frappe.get_doc(dict( | |||
doctype = 'Document Naming Rule', | |||
document_type = 'ToDo', | |||
prefix = 'test-high-', | |||
prefix_digits = 5, | |||
priority = 10, | |||
conditions = [dict( | |||
field = 'priority', | |||
condition = '=', | |||
value = 'High' | |||
)] | |||
)).insert() | |||
naming_rule = frappe.get_doc( | |||
dict( | |||
doctype="Document Naming Rule", | |||
document_type="ToDo", | |||
prefix="test-high-", | |||
prefix_digits=5, | |||
priority=10, | |||
conditions=[dict(field="priority", condition="=", value="High")], | |||
) | |||
).insert() | |||
# another rule | |||
naming_rule_1 = frappe.copy_doc(naming_rule) | |||
naming_rule_1.prefix = 'test-medium-' | |||
naming_rule_1.conditions[0].value = 'Medium' | |||
naming_rule_1.prefix = "test-medium-" | |||
naming_rule_1.conditions[0].value = "Medium" | |||
naming_rule_1.insert() | |||
# default rule with low priority - should not get applied for rules | |||
# with higher priority | |||
naming_rule_2 = frappe.copy_doc(naming_rule) | |||
naming_rule_2.prefix = 'test-low-' | |||
naming_rule_2.prefix = "test-low-" | |||
naming_rule_2.priority = 0 | |||
naming_rule_2.conditions = [] | |||
naming_rule_2.insert() | |||
todo = frappe.get_doc( | |||
dict(doctype="ToDo", priority="High", description="Is this my name " + frappe.generate_hash()) | |||
).insert() | |||
todo = frappe.get_doc(dict( | |||
doctype = 'ToDo', | |||
priority = 'High', | |||
description = 'Is this my name ' + frappe.generate_hash() | |||
)).insert() | |||
todo_1 = frappe.get_doc(dict( | |||
doctype = 'ToDo', | |||
priority = 'Medium', | |||
description = 'Is this my name ' + frappe.generate_hash() | |||
)).insert() | |||
todo_1 = frappe.get_doc( | |||
dict(doctype="ToDo", priority="Medium", description="Is this my name " + frappe.generate_hash()) | |||
).insert() | |||
todo_2 = frappe.get_doc(dict( | |||
doctype = 'ToDo', | |||
priority = 'Low', | |||
description = 'Is this my name ' + frappe.generate_hash() | |||
)).insert() | |||
todo_2 = frappe.get_doc( | |||
dict(doctype="ToDo", priority="Low", description="Is this my name " + frappe.generate_hash()) | |||
).insert() | |||
try: | |||
self.assertEqual(todo.name, 'test-high-00001') | |||
self.assertEqual(todo_1.name, 'test-medium-00001') | |||
self.assertEqual(todo_2.name, 'test-low-00001') | |||
self.assertEqual(todo.name, "test-high-00001") | |||
self.assertEqual(todo_1.name, "test-medium-00001") | |||
self.assertEqual(todo_2.name, "test-low-00001") | |||
finally: | |||
naming_rule.delete() | |||
naming_rule_1.delete() | |||
@@ -5,5 +5,6 @@ | |||
# import frappe | |||
from frappe.model.document import Document | |||
class DocumentNamingRuleCondition(Document): | |||
pass |
@@ -4,5 +4,6 @@ | |||
# import frappe | |||
import unittest | |||
class TestDocumentNamingRuleCondition(unittest.TestCase): | |||
pass |
@@ -3,16 +3,17 @@ | |||
# License: MIT. See LICENSE | |||
import frappe | |||
from frappe.model.document import Document | |||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields | |||
from frappe.model.document import Document | |||
class Domain(Document): | |||
'''Domain documents are created automatically when DocTypes | |||
"""Domain documents are created automatically when DocTypes | |||
with "Restricted" domains are imported during | |||
installation or migration''' | |||
installation or migration""" | |||
def setup_domain(self): | |||
'''Setup domain icons, permissions, custom fields etc.''' | |||
"""Setup domain icons, permissions, custom fields etc.""" | |||
self.setup_data() | |||
self.setup_roles() | |||
self.setup_properties() | |||
@@ -31,20 +32,20 @@ class Domain(Document): | |||
frappe.get_attr(self.data.on_setup)() | |||
def remove_domain(self): | |||
'''Unset domain settings''' | |||
"""Unset domain settings""" | |||
self.setup_data() | |||
if self.data.restricted_roles: | |||
for role_name in self.data.restricted_roles: | |||
if frappe.db.exists('Role', role_name): | |||
role = frappe.get_doc('Role', role_name) | |||
if frappe.db.exists("Role", role_name): | |||
role = frappe.get_doc("Role", role_name) | |||
role.disabled = 1 | |||
role.save() | |||
self.remove_custom_field() | |||
def remove_custom_field(self): | |||
'''Remove custom_fields when disabling domain''' | |||
"""Remove custom_fields when disabling domain""" | |||
if self.data.custom_fields: | |||
for doctype in self.data.custom_fields: | |||
custom_fields = self.data.custom_fields[doctype] | |||
@@ -54,47 +55,48 @@ class Domain(Document): | |||
custom_fields = [custom_fields] | |||
for custom_field_detail in custom_fields: | |||
custom_field_name = frappe.db.get_value('Custom Field', | |||
dict(dt=doctype, fieldname=custom_field_detail.get('fieldname'))) | |||
custom_field_name = frappe.db.get_value( | |||
"Custom Field", dict(dt=doctype, fieldname=custom_field_detail.get("fieldname")) | |||
) | |||
if custom_field_name: | |||
frappe.delete_doc('Custom Field', custom_field_name) | |||
frappe.delete_doc("Custom Field", custom_field_name) | |||
def setup_roles(self): | |||
'''Enable roles that are restricted to this domain''' | |||
"""Enable roles that are restricted to this domain""" | |||
if self.data.restricted_roles: | |||
user = frappe.get_doc("User", frappe.session.user) | |||
for role_name in self.data.restricted_roles: | |||
user.append("roles", {"role": role_name}) | |||
if not frappe.db.get_value('Role', role_name): | |||
frappe.get_doc(dict(doctype='Role', role_name=role_name)).insert() | |||
if not frappe.db.get_value("Role", role_name): | |||
frappe.get_doc(dict(doctype="Role", role_name=role_name)).insert() | |||
continue | |||
role = frappe.get_doc('Role', role_name) | |||
role = frappe.get_doc("Role", role_name) | |||
role.disabled = 0 | |||
role.save() | |||
user.save() | |||
def setup_data(self, domain=None): | |||
'''Load domain info via hooks''' | |||
"""Load domain info via hooks""" | |||
self.data = frappe.get_domain_data(self.name) | |||
def get_domain_data(self, module): | |||
return frappe.get_attr(frappe.get_hooks('domains')[self.name] + '.data') | |||
return frappe.get_attr(frappe.get_hooks("domains")[self.name] + ".data") | |||
def set_default_portal_role(self): | |||
'''Set default portal role based on domain''' | |||
if self.data.get('default_portal_role'): | |||
frappe.db.set_value('Portal Settings', None, 'default_role', | |||
self.data.get('default_portal_role')) | |||
"""Set default portal role based on domain""" | |||
if self.data.get("default_portal_role"): | |||
frappe.db.set_value( | |||
"Portal Settings", None, "default_role", self.data.get("default_portal_role") | |||
) | |||
def setup_properties(self): | |||
if self.data.properties: | |||
for args in self.data.properties: | |||
frappe.make_property_setter(args) | |||
def set_values(self): | |||
'''set values based on `data.set_value`''' | |||
"""set values based on `data.set_value`""" | |||
if self.data.set_value: | |||
for args in self.data.set_value: | |||
frappe.reload_doctype(args[0]) | |||
@@ -103,19 +105,27 @@ class Domain(Document): | |||
doc.save() | |||
def setup_sidebar_items(self): | |||
'''Enable / disable sidebar items''' | |||
"""Enable / disable sidebar items""" | |||
if self.data.allow_sidebar_items: | |||
# disable all | |||
frappe.db.sql('update `tabPortal Menu Item` set enabled=0') | |||
frappe.db.sql("update `tabPortal Menu Item` set enabled=0") | |||
# enable | |||
frappe.db.sql('''update `tabPortal Menu Item` set enabled=1 | |||
where route in ({0})'''.format(', '.join('"{0}"'.format(d) for d in self.data.allow_sidebar_items))) | |||
frappe.db.sql( | |||
"""update `tabPortal Menu Item` set enabled=1 | |||
where route in ({0})""".format( | |||
", ".join('"{0}"'.format(d) for d in self.data.allow_sidebar_items) | |||
) | |||
) | |||
if self.data.remove_sidebar_items: | |||
# disable all | |||
frappe.db.sql('update `tabPortal Menu Item` set enabled=1') | |||
frappe.db.sql("update `tabPortal Menu Item` set enabled=1") | |||
# enable | |||
frappe.db.sql('''update `tabPortal Menu Item` set enabled=0 | |||
where route in ({0})'''.format(', '.join('"{0}"'.format(d) for d in self.data.remove_sidebar_items))) | |||
frappe.db.sql( | |||
"""update `tabPortal Menu Item` set enabled=0 | |||
where route in ({0})""".format( | |||
", ".join('"{0}"'.format(d) for d in self.data.remove_sidebar_items) | |||
) | |||
) |
@@ -1,8 +1,10 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright (c) 2017, Frappe Technologies and Contributors | |||
# License: MIT. See LICENSE | |||
import frappe | |||
import unittest | |||
import frappe | |||
class TestDomain(unittest.TestCase): | |||
pass |
@@ -5,13 +5,14 @@ | |||
import frappe | |||
from frappe.model.document import Document | |||
class DomainSettings(Document): | |||
def set_active_domains(self, domains): | |||
active_domains = [d.domain for d in self.active_domains] | |||
added = False | |||
for d in domains: | |||
if not d in active_domains: | |||
self.append('active_domains', dict(domain=d)) | |||
self.append("active_domains", dict(domain=d)) | |||
added = True | |||
if added: | |||
@@ -22,49 +23,52 @@ class DomainSettings(Document): | |||
# set the flag to update the the desktop icons of all domains | |||
if i >= 1: | |||
frappe.flags.keep_desktop_icons = True | |||
domain = frappe.get_doc('Domain', d.domain) | |||
domain = frappe.get_doc("Domain", d.domain) | |||
domain.setup_domain() | |||
self.restrict_roles_and_modules() | |||
frappe.clear_cache() | |||
def restrict_roles_and_modules(self): | |||
'''Disable all restricted roles and set `restrict_to_domain` property in Module Def''' | |||
"""Disable all restricted roles and set `restrict_to_domain` property in Module Def""" | |||
active_domains = frappe.get_active_domains() | |||
all_domains = list((frappe.get_hooks('domains') or {})) | |||
all_domains = list((frappe.get_hooks("domains") or {})) | |||
def remove_role(role): | |||
frappe.db.delete("Has Role", {"role": role}) | |||
frappe.set_value('Role', role, 'disabled', 1) | |||
frappe.set_value("Role", role, "disabled", 1) | |||
for domain in all_domains: | |||
data = frappe.get_domain_data(domain) | |||
if not frappe.db.get_value('Domain', domain): | |||
frappe.get_doc(dict(doctype='Domain', domain=domain)).insert() | |||
if 'modules' in data: | |||
for module in data.get('modules'): | |||
frappe.db.set_value('Module Def', module, 'restrict_to_domain', domain) | |||
if 'restricted_roles' in data: | |||
for role in data['restricted_roles']: | |||
if not frappe.db.get_value('Role', role): | |||
frappe.get_doc(dict(doctype='Role', role_name=role)).insert() | |||
frappe.db.set_value('Role', role, 'restrict_to_domain', domain) | |||
if not frappe.db.get_value("Domain", domain): | |||
frappe.get_doc(dict(doctype="Domain", domain=domain)).insert() | |||
if "modules" in data: | |||
for module in data.get("modules"): | |||
frappe.db.set_value("Module Def", module, "restrict_to_domain", domain) | |||
if "restricted_roles" in data: | |||
for role in data["restricted_roles"]: | |||
if not frappe.db.get_value("Role", role): | |||
frappe.get_doc(dict(doctype="Role", role_name=role)).insert() | |||
frappe.db.set_value("Role", role, "restrict_to_domain", domain) | |||
if domain not in active_domains: | |||
remove_role(role) | |||
if 'custom_fields' in data: | |||
if "custom_fields" in data: | |||
if domain not in active_domains: | |||
inactive_domain = frappe.get_doc("Domain", domain) | |||
inactive_domain.setup_data() | |||
inactive_domain.remove_custom_field() | |||
def get_active_domains(): | |||
""" get the domains set in the Domain Settings as active domain """ | |||
"""get the domains set in the Domain Settings as active domain""" | |||
def _get_active_domains(): | |||
domains = frappe.get_all("Has Domain", filters={ "parent": "Domain Settings" }, | |||
fields=["domain"], distinct=True) | |||
domains = frappe.get_all( | |||
"Has Domain", filters={"parent": "Domain Settings"}, fields=["domain"], distinct=True | |||
) | |||
active_domains = [row.get("domain") for row in domains] | |||
active_domains.append("") | |||
@@ -72,14 +76,16 @@ def get_active_domains(): | |||
return frappe.cache().get_value("active_domains", _get_active_domains) | |||
def get_active_modules(): | |||
""" get the active modules from Module Def""" | |||
"""get the active modules from Module Def""" | |||
def _get_active_modules(): | |||
active_modules = [] | |||
active_domains = get_active_domains() | |||
for m in frappe.get_all("Module Def", fields=['name', 'restrict_to_domain']): | |||
for m in frappe.get_all("Module Def", fields=["name", "restrict_to_domain"]): | |||
if (not m.restrict_to_domain) or (m.restrict_to_domain in active_domains): | |||
active_modules.append(m.name) | |||
return active_modules | |||
return frappe.cache().get_value('active_modules', _get_active_modules) | |||
return frappe.cache().get_value("active_modules", _get_active_modules) |
@@ -5,12 +5,15 @@ | |||
import frappe | |||
from frappe.model.document import Document | |||
class DynamicLink(Document): | |||
pass | |||
def on_doctype_update(): | |||
frappe.db.add_index("Dynamic Link", ["link_doctype", "link_name"]) | |||
def deduplicate_dynamic_links(doc): | |||
links, duplicate = [], False | |||
for l in doc.links or []: | |||
@@ -23,4 +26,4 @@ def deduplicate_dynamic_links(doc): | |||
if duplicate: | |||
doc.links = [] | |||
for l in links: | |||
doc.append('links', dict(link_doctype=l[0], link_name=l[1])) | |||
doc.append("links", dict(link_doctype=l[0], link_name=l[1])) |
@@ -5,19 +5,24 @@ | |||
import frappe | |||
from frappe.model.document import Document | |||
class ErrorLog(Document): | |||
def onload(self): | |||
if not self.seen: | |||
self.db_set('seen', 1, update_modified=0) | |||
self.db_set("seen", 1, update_modified=0) | |||
frappe.db.commit() | |||
def set_old_logs_as_seen(): | |||
# set logs as seen | |||
frappe.db.sql("""UPDATE `tabError Log` SET `seen`=1 | |||
WHERE `seen`=0 AND `creation` < (NOW() - INTERVAL '7' DAY)""") | |||
frappe.db.sql( | |||
"""UPDATE `tabError Log` SET `seen`=1 | |||
WHERE `seen`=0 AND `creation` < (NOW() - INTERVAL '7' DAY)""" | |||
) | |||
@frappe.whitelist() | |||
def clear_error_logs(): | |||
'''Flush all Error Logs''' | |||
frappe.only_for('System Manager') | |||
"""Flush all Error Logs""" | |||
frappe.only_for("System Manager") | |||
frappe.db.truncate("Error Log") |
@@ -1,10 +1,12 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright (c) 2015, Frappe Technologies and Contributors | |||
# License: MIT. See LICENSE | |||
import frappe | |||
import unittest | |||
import frappe | |||
# test_records = frappe.get_test_records('Error Log') | |||
class TestErrorLog(unittest.TestCase): | |||
pass |