Co-authored-by: Frappe Bot <developers@frappe.io>version-14
@@ -19,3 +19,6 @@ fe20515c23a3ac41f1092bf0eaf0a0a452ec2e85 | |||||
# Clean up whitespace | # Clean up whitespace | ||||
b2fc959307c7c79f5584625569d5aed04133ba13 | b2fc959307c7c79f5584625569d5aed04133ba13 | ||||
# Format codebase and sort imports | |||||
cb6f68e8c106ee2d037dd4b39dbb6d7c68caf1c8 |
@@ -16,6 +16,17 @@ repos: | |||||
- id: check-merge-conflict | - id: check-merge-conflict | ||||
- id: check-ast | - id: check-ast | ||||
- repo: https://github.com/adityahase/black | |||||
rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119 | |||||
hooks: | |||||
- id: black | |||||
additional_dependencies: ['click==8.0.4'] | |||||
- repo: https://github.com/timothycrosley/isort | |||||
rev: 5.9.1 | |||||
hooks: | |||||
- id: isort | |||||
exclude: ".*setup.py$" | |||||
ci: | ci: | ||||
autoupdate_schedule: weekly | autoupdate_schedule: weekly | ||||
@@ -9,8 +9,8 @@ import frappe | |||||
import frappe.client | import frappe.client | ||||
import frappe.handler | import frappe.handler | ||||
from frappe import _ | from frappe import _ | ||||
from frappe.utils.response import build_response | |||||
from frappe.utils.data import sbool | from frappe.utils.data import sbool | ||||
from frappe.utils.response import build_response | |||||
def handle(): | def handle(): | ||||
@@ -22,22 +22,22 @@ def handle(): | |||||
`/api/method/{methodname}` will call a whitelisted method | `/api/method/{methodname}` will call a whitelisted method | ||||
`/api/resource/{doctype}` will query a table | `/api/resource/{doctype}` will query a table | ||||
examples: | |||||
- `?fields=["name", "owner"]` | |||||
- `?filters=[["Task", "name", "like", "%005"]]` | |||||
- `?limit_start=0` | |||||
- `?limit_page_length=20` | |||||
examples: | |||||
- `?fields=["name", "owner"]` | |||||
- `?filters=[["Task", "name", "like", "%005"]]` | |||||
- `?limit_start=0` | |||||
- `?limit_page_length=20` | |||||
`/api/resource/{doctype}/{name}` will point to a resource | `/api/resource/{doctype}/{name}` will point to a resource | ||||
`GET` will return doclist | |||||
`POST` will insert | |||||
`PUT` will update | |||||
`DELETE` will delete | |||||
`GET` will return doclist | |||||
`POST` will insert | |||||
`PUT` will update | |||||
`DELETE` will delete | |||||
`/api/resource/{doctype}/{name}?run_method={method}` will run a whitelisted controller method | `/api/resource/{doctype}/{name}?run_method={method}` will run a whitelisted controller method | ||||
""" | """ | ||||
parts = frappe.request.path[1:].split("/",3) | |||||
parts = frappe.request.path[1:].split("/", 3) | |||||
call = doctype = name = None | call = doctype = name = None | ||||
if len(parts) > 1: | if len(parts) > 1: | ||||
@@ -49,22 +49,22 @@ def handle(): | |||||
if len(parts) > 3: | if len(parts) > 3: | ||||
name = parts[3] | name = parts[3] | ||||
if call=="method": | |||||
if call == "method": | |||||
frappe.local.form_dict.cmd = doctype | frappe.local.form_dict.cmd = doctype | ||||
return frappe.handler.handle() | return frappe.handler.handle() | ||||
elif call=="resource": | |||||
elif call == "resource": | |||||
if "run_method" in frappe.local.form_dict: | if "run_method" in frappe.local.form_dict: | ||||
method = frappe.local.form_dict.pop("run_method") | method = frappe.local.form_dict.pop("run_method") | ||||
doc = frappe.get_doc(doctype, name) | doc = frappe.get_doc(doctype, name) | ||||
doc.is_whitelisted(method) | doc.is_whitelisted(method) | ||||
if frappe.local.request.method=="GET": | |||||
if frappe.local.request.method == "GET": | |||||
if not doc.has_permission("read"): | if not doc.has_permission("read"): | ||||
frappe.throw(_("Not permitted"), frappe.PermissionError) | frappe.throw(_("Not permitted"), frappe.PermissionError) | ||||
frappe.local.response.update({"data": doc.run_method(method, **frappe.local.form_dict)}) | frappe.local.response.update({"data": doc.run_method(method, **frappe.local.form_dict)}) | ||||
if frappe.local.request.method=="POST": | |||||
if frappe.local.request.method == "POST": | |||||
if not doc.has_permission("write"): | if not doc.has_permission("write"): | ||||
frappe.throw(_("Not permitted"), frappe.PermissionError) | frappe.throw(_("Not permitted"), frappe.PermissionError) | ||||
@@ -73,13 +73,13 @@ def handle(): | |||||
else: | else: | ||||
if name: | if name: | ||||
if frappe.local.request.method=="GET": | |||||
if frappe.local.request.method == "GET": | |||||
doc = frappe.get_doc(doctype, name) | doc = frappe.get_doc(doctype, name) | ||||
if not doc.has_permission("read"): | if not doc.has_permission("read"): | ||||
raise frappe.PermissionError | raise frappe.PermissionError | ||||
frappe.local.response.update({"data": doc}) | frappe.local.response.update({"data": doc}) | ||||
if frappe.local.request.method=="PUT": | |||||
if frappe.local.request.method == "PUT": | |||||
data = get_request_form_data() | data = get_request_form_data() | ||||
doc = frappe.get_doc(doctype, name, for_update=True) | doc = frappe.get_doc(doctype, name, for_update=True) | ||||
@@ -90,9 +90,7 @@ def handle(): | |||||
# Not checking permissions here because it's checked in doc.save | # Not checking permissions here because it's checked in doc.save | ||||
doc.update(data) | doc.update(data) | ||||
frappe.local.response.update({ | |||||
"data": doc.save().as_dict() | |||||
}) | |||||
frappe.local.response.update({"data": doc.save().as_dict()}) | |||||
# check for child table doctype | # check for child table doctype | ||||
if doc.get("parenttype"): | if doc.get("parenttype"): | ||||
@@ -183,7 +181,7 @@ def validate_oauth(authorization_header): | |||||
Authenticate request using OAuth and set session user | Authenticate request using OAuth and set session user | ||||
Args: | Args: | ||||
authorization_header (list of str): The 'Authorization' header containing the prefix and token | |||||
authorization_header (list of str): The 'Authorization' header containing the prefix and token | |||||
""" | """ | ||||
from frappe.integrations.oauth2 import get_oauth_server | from frappe.integrations.oauth2 import get_oauth_server | ||||
@@ -194,7 +192,9 @@ def validate_oauth(authorization_header): | |||||
req = frappe.request | req = frappe.request | ||||
parsed_url = urlparse(req.url) | parsed_url = urlparse(req.url) | ||||
access_token = {"access_token": token} | access_token = {"access_token": token} | ||||
uri = parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + "?" + urlencode(access_token) | |||||
uri = ( | |||||
parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + "?" + urlencode(access_token) | |||||
) | |||||
http_method = req.method | http_method = req.method | ||||
headers = req.headers | headers = req.headers | ||||
body = req.get_data() | body = req.get_data() | ||||
@@ -202,8 +202,12 @@ def validate_oauth(authorization_header): | |||||
body = None | body = None | ||||
try: | try: | ||||
required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(get_url_delimiter()) | |||||
valid, oauthlib_request = get_oauth_server().verify_request(uri, http_method, body, headers, required_scopes) | |||||
required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split( | |||||
get_url_delimiter() | |||||
) | |||||
valid, oauthlib_request = get_oauth_server().verify_request( | |||||
uri, http_method, body, headers, required_scopes | |||||
) | |||||
if valid: | if valid: | ||||
frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user")) | frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user")) | ||||
frappe.local.form_dict = form_dict | frappe.local.form_dict = form_dict | ||||
@@ -216,48 +220,43 @@ def validate_auth_via_api_keys(authorization_header): | |||||
Authenticate request using API keys and set session user | Authenticate request using API keys and set session user | ||||
Args: | Args: | ||||
authorization_header (list of str): The 'Authorization' header containing the prefix and token | |||||
authorization_header (list of str): The 'Authorization' header containing the prefix and token | |||||
""" | """ | ||||
try: | try: | ||||
auth_type, auth_token = authorization_header | auth_type, auth_token = authorization_header | ||||
authorization_source = frappe.get_request_header("Frappe-Authorization-Source") | authorization_source = frappe.get_request_header("Frappe-Authorization-Source") | ||||
if auth_type.lower() == 'basic': | |||||
if auth_type.lower() == "basic": | |||||
api_key, api_secret = frappe.safe_decode(base64.b64decode(auth_token)).split(":") | api_key, api_secret = frappe.safe_decode(base64.b64decode(auth_token)).split(":") | ||||
validate_api_key_secret(api_key, api_secret, authorization_source) | validate_api_key_secret(api_key, api_secret, authorization_source) | ||||
elif auth_type.lower() == 'token': | |||||
elif auth_type.lower() == "token": | |||||
api_key, api_secret = auth_token.split(":") | api_key, api_secret = auth_token.split(":") | ||||
validate_api_key_secret(api_key, api_secret, authorization_source) | validate_api_key_secret(api_key, api_secret, authorization_source) | ||||
except binascii.Error: | except binascii.Error: | ||||
frappe.throw(_("Failed to decode token, please provide a valid base64-encoded token."), frappe.InvalidAuthorizationToken) | |||||
frappe.throw( | |||||
_("Failed to decode token, please provide a valid base64-encoded token."), | |||||
frappe.InvalidAuthorizationToken, | |||||
) | |||||
except (AttributeError, TypeError, ValueError): | except (AttributeError, TypeError, ValueError): | ||||
pass | pass | ||||
def validate_api_key_secret(api_key, api_secret, frappe_authorization_source=None): | def validate_api_key_secret(api_key, api_secret, frappe_authorization_source=None): | ||||
"""frappe_authorization_source to provide api key and secret for a doctype apart from User""" | """frappe_authorization_source to provide api key and secret for a doctype apart from User""" | ||||
doctype = frappe_authorization_source or 'User' | |||||
doc = frappe.db.get_value( | |||||
doctype=doctype, | |||||
filters={"api_key": api_key}, | |||||
fieldname=["name"] | |||||
) | |||||
doctype = frappe_authorization_source or "User" | |||||
doc = frappe.db.get_value(doctype=doctype, filters={"api_key": api_key}, fieldname=["name"]) | |||||
form_dict = frappe.local.form_dict | form_dict = frappe.local.form_dict | ||||
doc_secret = frappe.utils.password.get_decrypted_password(doctype, doc, fieldname='api_secret') | |||||
doc_secret = frappe.utils.password.get_decrypted_password(doctype, doc, fieldname="api_secret") | |||||
if api_secret == doc_secret: | if api_secret == doc_secret: | ||||
if doctype == 'User': | |||||
user = frappe.db.get_value( | |||||
doctype="User", | |||||
filters={"api_key": api_key}, | |||||
fieldname=["name"] | |||||
) | |||||
if doctype == "User": | |||||
user = frappe.db.get_value(doctype="User", filters={"api_key": api_key}, fieldname=["name"]) | |||||
else: | else: | ||||
user = frappe.db.get_value(doctype, doc, 'user') | |||||
if frappe.local.login_manager.user in ('', 'Guest'): | |||||
user = frappe.db.get_value(doctype, doc, "user") | |||||
if frappe.local.login_manager.user in ("", "Guest"): | |||||
frappe.set_user(user) | frappe.set_user(user) | ||||
frappe.local.form_dict = form_dict | frappe.local.form_dict = form_dict | ||||
def validate_auth_via_hooks(): | def validate_auth_via_hooks(): | ||||
for auth_hook in frappe.get_hooks('auth_hooks', []): | |||||
for auth_hook in frappe.get_hooks("auth_hooks", []): | |||||
frappe.get_attr(auth_hook)() | frappe.get_attr(auth_hook)() |
@@ -2,37 +2,37 @@ | |||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import os | |||||
import logging | import logging | ||||
import os | |||||
from werkzeug.local import LocalManager | |||||
from werkzeug.wrappers import Request, Response | |||||
from werkzeug.exceptions import HTTPException, NotFound | from werkzeug.exceptions import HTTPException, NotFound | ||||
from werkzeug.local import LocalManager | |||||
from werkzeug.middleware.profiler import ProfilerMiddleware | from werkzeug.middleware.profiler import ProfilerMiddleware | ||||
from werkzeug.middleware.shared_data import SharedDataMiddleware | from werkzeug.middleware.shared_data import SharedDataMiddleware | ||||
from werkzeug.wrappers import Request, Response | |||||
import frappe | import frappe | ||||
import frappe.handler | |||||
import frappe.auth | |||||
import frappe.api | import frappe.api | ||||
import frappe.auth | |||||
import frappe.handler | |||||
import frappe.monitor | |||||
import frappe.rate_limiter | |||||
import frappe.recorder | |||||
import frappe.utils.response | import frappe.utils.response | ||||
from frappe.utils import get_site_name, sanitize_html | |||||
from frappe import _ | |||||
from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request | |||||
from frappe.middlewares import StaticDataMiddleware | from frappe.middlewares import StaticDataMiddleware | ||||
from frappe.website.serve import get_response | |||||
from frappe.utils import get_site_name, sanitize_html | |||||
from frappe.utils.error import make_error_snapshot | from frappe.utils.error import make_error_snapshot | ||||
from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request | |||||
from frappe import _ | |||||
import frappe.recorder | |||||
import frappe.monitor | |||||
import frappe.rate_limiter | |||||
from frappe.website.serve import get_response | |||||
local_manager = LocalManager([frappe.local]) | local_manager = LocalManager([frappe.local]) | ||||
_site = None | _site = None | ||||
_sites_path = os.environ.get("SITES_PATH", ".") | _sites_path = os.environ.get("SITES_PATH", ".") | ||||
class RequestContext(object): | |||||
class RequestContext(object): | |||||
def __init__(self, environ): | def __init__(self, environ): | ||||
self.request = Request(environ) | self.request = Request(environ) | ||||
@@ -42,6 +42,7 @@ class RequestContext(object): | |||||
def __exit__(self, type, value, traceback): | def __exit__(self, type, value, traceback): | ||||
frappe.destroy() | frappe.destroy() | ||||
@Request.application | @Request.application | ||||
def application(request): | def application(request): | ||||
response = None | response = None | ||||
@@ -65,13 +66,13 @@ def application(request): | |||||
elif request.path.startswith("/api/"): | elif request.path.startswith("/api/"): | ||||
response = frappe.api.handle() | response = frappe.api.handle() | ||||
elif request.path.startswith('/backups'): | |||||
elif request.path.startswith("/backups"): | |||||
response = frappe.utils.response.download_backup(request.path) | response = frappe.utils.response.download_backup(request.path) | ||||
elif request.path.startswith('/private/files/'): | |||||
elif request.path.startswith("/private/files/"): | |||||
response = frappe.utils.response.download_private_file(request.path) | response = frappe.utils.response.download_private_file(request.path) | ||||
elif request.method in ('GET', 'HEAD', 'POST'): | |||||
elif request.method in ("GET", "HEAD", "POST"): | |||||
response = get_response() | response = get_response() | ||||
else: | else: | ||||
@@ -103,41 +104,45 @@ def application(request): | |||||
return response | return response | ||||
def init_request(request): | def init_request(request): | ||||
frappe.local.request = request | frappe.local.request = request | ||||
frappe.local.is_ajax = frappe.get_request_header("X-Requested-With")=="XMLHttpRequest" | |||||
frappe.local.is_ajax = frappe.get_request_header("X-Requested-With") == "XMLHttpRequest" | |||||
site = _site or request.headers.get('X-Frappe-Site-Name') or get_site_name(request.host) | |||||
site = _site or request.headers.get("X-Frappe-Site-Name") or get_site_name(request.host) | |||||
frappe.init(site=site, sites_path=_sites_path) | frappe.init(site=site, sites_path=_sites_path) | ||||
if not (frappe.local.conf and frappe.local.conf.db_name): | if not (frappe.local.conf and frappe.local.conf.db_name): | ||||
# site does not exist | # site does not exist | ||||
raise NotFound | raise NotFound | ||||
if frappe.local.conf.get('maintenance_mode'): | |||||
if frappe.local.conf.get("maintenance_mode"): | |||||
frappe.connect() | frappe.connect() | ||||
raise frappe.SessionStopped('Session Stopped') | |||||
raise frappe.SessionStopped("Session Stopped") | |||||
else: | else: | ||||
frappe.connect(set_admin_as_user=False) | frappe.connect(set_admin_as_user=False) | ||||
request.max_content_length = frappe.local.conf.get('max_file_size') or 10 * 1024 * 1024 | |||||
request.max_content_length = frappe.local.conf.get("max_file_size") or 10 * 1024 * 1024 | |||||
make_form_dict(request) | make_form_dict(request) | ||||
if request.method != "OPTIONS": | if request.method != "OPTIONS": | ||||
frappe.local.http_request = frappe.auth.HTTPRequest() | frappe.local.http_request = frappe.auth.HTTPRequest() | ||||
def log_request(request, response): | def log_request(request, response): | ||||
if hasattr(frappe.local, 'conf') and frappe.local.conf.enable_frappe_logger: | |||||
frappe.logger("frappe.web", allow_site=frappe.local.site).info({ | |||||
"site": get_site_name(request.host), | |||||
"remote_addr": getattr(request, "remote_addr", "NOTFOUND"), | |||||
"base_url": getattr(request, "base_url", "NOTFOUND"), | |||||
"full_path": getattr(request, "full_path", "NOTFOUND"), | |||||
"method": getattr(request, "method", "NOTFOUND"), | |||||
"scheme": getattr(request, "scheme", "NOTFOUND"), | |||||
"http_status_code": getattr(response, "status_code", "NOTFOUND") | |||||
}) | |||||
if hasattr(frappe.local, "conf") and frappe.local.conf.enable_frappe_logger: | |||||
frappe.logger("frappe.web", allow_site=frappe.local.site).info( | |||||
{ | |||||
"site": get_site_name(request.host), | |||||
"remote_addr": getattr(request, "remote_addr", "NOTFOUND"), | |||||
"base_url": getattr(request, "base_url", "NOTFOUND"), | |||||
"full_path": getattr(request, "full_path", "NOTFOUND"), | |||||
"method": getattr(request, "method", "NOTFOUND"), | |||||
"scheme": getattr(request, "scheme", "NOTFOUND"), | |||||
"http_status_code": getattr(response, "status_code", "NOTFOUND"), | |||||
} | |||||
) | |||||
def process_response(response): | def process_response(response): | ||||
@@ -145,19 +150,20 @@ def process_response(response): | |||||
return | return | ||||
# set cookies | # set cookies | ||||
if hasattr(frappe.local, 'cookie_manager'): | |||||
if hasattr(frappe.local, "cookie_manager"): | |||||
frappe.local.cookie_manager.flush_cookies(response=response) | frappe.local.cookie_manager.flush_cookies(response=response) | ||||
# rate limiter headers | # rate limiter headers | ||||
if hasattr(frappe.local, 'rate_limiter'): | |||||
if hasattr(frappe.local, "rate_limiter"): | |||||
response.headers.extend(frappe.local.rate_limiter.headers()) | response.headers.extend(frappe.local.rate_limiter.headers()) | ||||
# CORS headers | # CORS headers | ||||
if hasattr(frappe.local, 'conf') and frappe.conf.allow_cors: | |||||
if hasattr(frappe.local, "conf") and frappe.conf.allow_cors: | |||||
set_cors_headers(response) | set_cors_headers(response) | ||||
def set_cors_headers(response): | def set_cors_headers(response): | ||||
origin = frappe.request.headers.get('Origin') | |||||
origin = frappe.request.headers.get("Origin") | |||||
allow_cors = frappe.conf.allow_cors | allow_cors = frappe.conf.allow_cors | ||||
if not (origin and allow_cors): | if not (origin and allow_cors): | ||||
return | return | ||||
@@ -169,20 +175,25 @@ def set_cors_headers(response): | |||||
if origin not in allow_cors: | if origin not in allow_cors: | ||||
return | return | ||||
response.headers.extend({ | |||||
'Access-Control-Allow-Origin': origin, | |||||
'Access-Control-Allow-Credentials': 'true', | |||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', | |||||
'Access-Control-Allow-Headers': ('Authorization,DNT,X-Mx-ReqToken,' | |||||
'Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,' | |||||
'Cache-Control,Content-Type') | |||||
}) | |||||
response.headers.extend( | |||||
{ | |||||
"Access-Control-Allow-Origin": origin, | |||||
"Access-Control-Allow-Credentials": "true", | |||||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", | |||||
"Access-Control-Allow-Headers": ( | |||||
"Authorization,DNT,X-Mx-ReqToken," | |||||
"Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since," | |||||
"Cache-Control,Content-Type" | |||||
), | |||||
} | |||||
) | |||||
def make_form_dict(request): | def make_form_dict(request): | ||||
import json | import json | ||||
request_data = request.get_data(as_text=True) | request_data = request.get_data(as_text=True) | ||||
if 'application/json' in (request.content_type or '') and request_data: | |||||
if "application/json" in (request.content_type or "") and request_data: | |||||
args = json.loads(request_data) | args = json.loads(request_data) | ||||
else: | else: | ||||
args = {} | args = {} | ||||
@@ -198,20 +209,19 @@ def make_form_dict(request): | |||||
# _ is passed by $.ajax so that the request is not cached by the browser. So, remove _ from form_dict | # _ is passed by $.ajax so that the request is not cached by the browser. So, remove _ from form_dict | ||||
frappe.local.form_dict.pop("_") | frappe.local.form_dict.pop("_") | ||||
def handle_exception(e): | def handle_exception(e): | ||||
response = None | response = None | ||||
http_status_code = getattr(e, "http_status_code", 500) | http_status_code = getattr(e, "http_status_code", 500) | ||||
return_as_message = False | return_as_message = False | ||||
accept_header = frappe.get_request_header("Accept") or "" | accept_header = frappe.get_request_header("Accept") or "" | ||||
respond_as_json = ( | respond_as_json = ( | ||||
frappe.get_request_header('Accept') | |||||
and (frappe.local.is_ajax or 'application/json' in accept_header) | |||||
or ( | |||||
frappe.local.request.path.startswith("/api/") and not accept_header.startswith("text") | |||||
) | |||||
frappe.get_request_header("Accept") | |||||
and (frappe.local.is_ajax or "application/json" in accept_header) | |||||
or (frappe.local.request.path.startswith("/api/") and not accept_header.startswith("text")) | |||||
) | ) | ||||
if frappe.conf.get('developer_mode'): | |||||
if frappe.conf.get("developer_mode"): | |||||
# don't fail silently | # don't fail silently | ||||
print(frappe.get_traceback()) | print(frappe.get_traceback()) | ||||
@@ -220,27 +230,38 @@ def handle_exception(e): | |||||
# if the request is ajax, send back the trace or error message | # if the request is ajax, send back the trace or error message | ||||
response = frappe.utils.response.report_error(http_status_code) | response = frappe.utils.response.report_error(http_status_code) | ||||
elif (http_status_code==500 | |||||
elif ( | |||||
http_status_code == 500 | |||||
and (frappe.db and isinstance(e, frappe.db.InternalError)) | and (frappe.db and isinstance(e, frappe.db.InternalError)) | ||||
and (frappe.db and (frappe.db.is_deadlocked(e) or frappe.db.is_timedout(e)))): | |||||
http_status_code = 508 | |||||
and (frappe.db and (frappe.db.is_deadlocked(e) or frappe.db.is_timedout(e))) | |||||
): | |||||
http_status_code = 508 | |||||
elif http_status_code==401: | |||||
frappe.respond_as_web_page(_("Session Expired"), | |||||
elif http_status_code == 401: | |||||
frappe.respond_as_web_page( | |||||
_("Session Expired"), | |||||
_("Your session has expired, please login again to continue."), | _("Your session has expired, please login again to continue."), | ||||
http_status_code=http_status_code, indicator_color='red') | |||||
http_status_code=http_status_code, | |||||
indicator_color="red", | |||||
) | |||||
return_as_message = True | return_as_message = True | ||||
elif http_status_code==403: | |||||
frappe.respond_as_web_page(_("Not Permitted"), | |||||
elif http_status_code == 403: | |||||
frappe.respond_as_web_page( | |||||
_("Not Permitted"), | |||||
_("You do not have enough permissions to complete the action"), | _("You do not have enough permissions to complete the action"), | ||||
http_status_code=http_status_code, indicator_color='red') | |||||
http_status_code=http_status_code, | |||||
indicator_color="red", | |||||
) | |||||
return_as_message = True | return_as_message = True | ||||
elif http_status_code==404: | |||||
frappe.respond_as_web_page(_("Not Found"), | |||||
elif http_status_code == 404: | |||||
frappe.respond_as_web_page( | |||||
_("Not Found"), | |||||
_("The resource you are looking for is not available"), | _("The resource you are looking for is not available"), | ||||
http_status_code=http_status_code, indicator_color='red') | |||||
http_status_code=http_status_code, | |||||
indicator_color="red", | |||||
) | |||||
return_as_message = True | return_as_message = True | ||||
elif http_status_code == 429: | elif http_status_code == 429: | ||||
@@ -252,9 +273,9 @@ def handle_exception(e): | |||||
if frappe.local.flags.disable_traceback and not frappe.local.dev_server: | if frappe.local.flags.disable_traceback and not frappe.local.dev_server: | ||||
traceback = "" | traceback = "" | ||||
frappe.respond_as_web_page("Server Error", | |||||
traceback, http_status_code=http_status_code, | |||||
indicator_color='red', width=640) | |||||
frappe.respond_as_web_page( | |||||
"Server Error", traceback, http_status_code=http_status_code, indicator_color="red", width=640 | |||||
) | |||||
return_as_message = True | return_as_message = True | ||||
if e.__class__ == frappe.AuthenticationError: | if e.__class__ == frappe.AuthenticationError: | ||||
@@ -269,6 +290,7 @@ def handle_exception(e): | |||||
return response | return response | ||||
def after_request(rollback): | def after_request(rollback): | ||||
if (frappe.local.request.method in ("POST", "PUT") or frappe.local.flags.commit) and frappe.db: | if (frappe.local.request.method in ("POST", "PUT") or frappe.local.flags.commit) and frappe.db: | ||||
if frappe.db.transaction_writes: | if frappe.db.transaction_writes: | ||||
@@ -286,41 +308,47 @@ def after_request(rollback): | |||||
return rollback | return rollback | ||||
application = local_manager.make_middleware(application) | application = local_manager.make_middleware(application) | ||||
def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=None, sites_path='.'): | |||||
def serve( | |||||
port=8000, profile=False, no_reload=False, no_threading=False, site=None, sites_path="." | |||||
): | |||||
global application, _site, _sites_path | global application, _site, _sites_path | ||||
_site = site | _site = site | ||||
_sites_path = sites_path | _sites_path = sites_path | ||||
from werkzeug.serving import run_simple | from werkzeug.serving import run_simple | ||||
if profile or os.environ.get('USE_PROFILER'): | |||||
application = ProfilerMiddleware(application, sort_by=('cumtime', 'calls')) | |||||
if profile or os.environ.get("USE_PROFILER"): | |||||
application = ProfilerMiddleware(application, sort_by=("cumtime", "calls")) | |||||
if not os.environ.get('NO_STATICS'): | |||||
application = SharedDataMiddleware(application, { | |||||
str('/assets'): str(os.path.join(sites_path, 'assets')) | |||||
}) | |||||
if not os.environ.get("NO_STATICS"): | |||||
application = SharedDataMiddleware( | |||||
application, {str("/assets"): str(os.path.join(sites_path, "assets"))} | |||||
) | |||||
application = StaticDataMiddleware(application, { | |||||
str('/files'): str(os.path.abspath(sites_path)) | |||||
}) | |||||
application = StaticDataMiddleware( | |||||
application, {str("/files"): str(os.path.abspath(sites_path))} | |||||
) | |||||
application.debug = True | application.debug = True | ||||
application.config = { | |||||
'SERVER_NAME': 'localhost:8000' | |||||
} | |||||
application.config = {"SERVER_NAME": "localhost:8000"} | |||||
log = logging.getLogger('werkzeug') | |||||
log = logging.getLogger("werkzeug") | |||||
log.propagate = False | log.propagate = False | ||||
in_test_env = os.environ.get('CI') | |||||
in_test_env = os.environ.get("CI") | |||||
if in_test_env: | if in_test_env: | ||||
log.setLevel(logging.ERROR) | log.setLevel(logging.ERROR) | ||||
run_simple('0.0.0.0', int(port), application, | |||||
run_simple( | |||||
"0.0.0.0", | |||||
int(port), | |||||
application, | |||||
use_reloader=False if in_test_env else not no_reload, | use_reloader=False if in_test_env else not no_reload, | ||||
use_debugger=not in_test_env, | use_debugger=not in_test_env, | ||||
use_evalex=not in_test_env, | use_evalex=not in_test_env, | ||||
threaded=not no_threading) | |||||
threaded=not no_threading, | |||||
) |
@@ -11,7 +11,12 @@ from frappe.core.doctype.activity_log.activity_log import add_authentication_log | |||||
from frappe.modules.patch_handler import check_session_stopped | from frappe.modules.patch_handler import check_session_stopped | ||||
from frappe.sessions import Session, clear_sessions, delete_session | from frappe.sessions import Session, clear_sessions, delete_session | ||||
from frappe.translate import get_language | from frappe.translate import get_language | ||||
from frappe.twofactor import authenticate_for_2factor, confirm_otp_token, get_cached_user_pass, should_run_2fa | |||||
from frappe.twofactor import ( | |||||
authenticate_for_2factor, | |||||
confirm_otp_token, | |||||
get_cached_user_pass, | |||||
should_run_2fa, | |||||
) | |||||
from frappe.utils import cint, date_diff, datetime, get_datetime, today | from frappe.utils import cint, date_diff, datetime, get_datetime, today | ||||
from frappe.utils.password import check_password | from frappe.utils.password import check_password | ||||
from frappe.website.utils import get_home_page | from frappe.website.utils import get_home_page | ||||
@@ -47,20 +52,20 @@ class HTTPRequest: | |||||
def domain(self): | def domain(self): | ||||
if not getattr(self, "_domain", None): | if not getattr(self, "_domain", None): | ||||
self._domain = frappe.request.host | self._domain = frappe.request.host | ||||
if self._domain and self._domain.startswith('www.'): | |||||
if self._domain and self._domain.startswith("www."): | |||||
self._domain = self._domain[4:] | self._domain = self._domain[4:] | ||||
return self._domain | return self._domain | ||||
def set_request_ip(self): | def set_request_ip(self): | ||||
if frappe.get_request_header('X-Forwarded-For'): | |||||
frappe.local.request_ip = (frappe.get_request_header('X-Forwarded-For').split(",")[0]).strip() | |||||
if frappe.get_request_header("X-Forwarded-For"): | |||||
frappe.local.request_ip = (frappe.get_request_header("X-Forwarded-For").split(",")[0]).strip() | |||||
elif frappe.get_request_header('REMOTE_ADDR'): | |||||
frappe.local.request_ip = frappe.get_request_header('REMOTE_ADDR') | |||||
elif frappe.get_request_header("REMOTE_ADDR"): | |||||
frappe.local.request_ip = frappe.get_request_header("REMOTE_ADDR") | |||||
else: | else: | ||||
frappe.local.request_ip = '127.0.0.1' | |||||
frappe.local.request_ip = "127.0.0.1" | |||||
def set_cookies(self): | def set_cookies(self): | ||||
frappe.local.cookie_manager = CookieManager() | frappe.local.cookie_manager = CookieManager() | ||||
@@ -75,7 +80,7 @@ class HTTPRequest: | |||||
if ( | if ( | ||||
not frappe.local.session.data.csrf_token | not frappe.local.session.data.csrf_token | ||||
or frappe.local.session.data.device == "mobile" | or frappe.local.session.data.device == "mobile" | ||||
or frappe.conf.get('ignore_csrf', None) | |||||
or frappe.conf.get("ignore_csrf", None) | |||||
): | ): | ||||
# not via boot | # not via boot | ||||
return | return | ||||
@@ -99,10 +104,10 @@ class HTTPRequest: | |||||
def connect(self): | def connect(self): | ||||
"""connect to db, from ac_name or db_name""" | """connect to db, from ac_name or db_name""" | ||||
frappe.local.db = frappe.database.get_db( | frappe.local.db = frappe.database.get_db( | ||||
user=self.get_db_name(), | |||||
password=getattr(conf, 'db_password', '') | |||||
user=self.get_db_name(), password=getattr(conf, "db_password", "") | |||||
) | ) | ||||
class LoginManager: | class LoginManager: | ||||
def __init__(self): | def __init__(self): | ||||
self.user = None | self.user = None | ||||
@@ -110,13 +115,15 @@ class LoginManager: | |||||
self.full_name = None | self.full_name = None | ||||
self.user_type = None | self.user_type = None | ||||
if frappe.local.form_dict.get('cmd')=='login' or frappe.local.request.path=="/api/method/login": | |||||
if ( | |||||
frappe.local.form_dict.get("cmd") == "login" or frappe.local.request.path == "/api/method/login" | |||||
): | |||||
if self.login() is False: | if self.login() is False: | ||||
return | return | ||||
self.resume = False | self.resume = False | ||||
# run login triggers | # run login triggers | ||||
self.run_trigger('on_session_creation') | |||||
self.run_trigger("on_session_creation") | |||||
else: | else: | ||||
try: | try: | ||||
self.resume = True | self.resume = True | ||||
@@ -131,12 +138,14 @@ class LoginManager: | |||||
def login(self): | def login(self): | ||||
# clear cache | # clear cache | ||||
frappe.clear_cache(user = frappe.form_dict.get('usr')) | |||||
frappe.clear_cache(user=frappe.form_dict.get("usr")) | |||||
user, pwd = get_cached_user_pass() | user, pwd = get_cached_user_pass() | ||||
self.authenticate(user=user, pwd=pwd) | self.authenticate(user=user, pwd=pwd) | ||||
if self.force_user_to_reset_password(): | if self.force_user_to_reset_password(): | ||||
doc = frappe.get_doc("User", self.user) | doc = frappe.get_doc("User", self.user) | ||||
frappe.local.response["redirect_to"] = doc.reset_password(send_email=False, password_expired=True) | |||||
frappe.local.response["redirect_to"] = doc.reset_password( | |||||
send_email=False, password_expired=True | |||||
) | |||||
frappe.local.response["message"] = "Password Reset" | frappe.local.response["message"] = "Password Reset" | ||||
return False | return False | ||||
@@ -147,7 +156,7 @@ class LoginManager: | |||||
self.post_login() | self.post_login() | ||||
def post_login(self): | def post_login(self): | ||||
self.run_trigger('on_login') | |||||
self.run_trigger("on_login") | |||||
validate_ip_address(self.user) | validate_ip_address(self.user) | ||||
self.validate_hour() | self.validate_hour() | ||||
self.get_user_info() | self.get_user_info() | ||||
@@ -156,8 +165,9 @@ class LoginManager: | |||||
self.set_user_info() | self.set_user_info() | ||||
def get_user_info(self): | def get_user_info(self): | ||||
self.info = frappe.db.get_value("User", self.user, | |||||
["user_type", "first_name", "last_name", "user_image"], as_dict=1) | |||||
self.info = frappe.db.get_value( | |||||
"User", self.user, ["user_type", "first_name", "last_name", "user_image"], as_dict=1 | |||||
) | |||||
self.user_type = self.info.user_type | self.user_type = self.info.user_type | ||||
@@ -170,28 +180,27 @@ class LoginManager: | |||||
# set sid again | # set sid again | ||||
frappe.local.cookie_manager.init_cookies() | frappe.local.cookie_manager.init_cookies() | ||||
self.full_name = " ".join(filter(None, [self.info.first_name, | |||||
self.info.last_name])) | |||||
self.full_name = " ".join(filter(None, [self.info.first_name, self.info.last_name])) | |||||
if self.info.user_type=="Website User": | |||||
if self.info.user_type == "Website User": | |||||
frappe.local.cookie_manager.set_cookie("system_user", "no") | frappe.local.cookie_manager.set_cookie("system_user", "no") | ||||
if not resume: | if not resume: | ||||
frappe.local.response["message"] = "No App" | frappe.local.response["message"] = "No App" | ||||
frappe.local.response["home_page"] = '/' + get_home_page() | |||||
frappe.local.response["home_page"] = "/" + get_home_page() | |||||
else: | else: | ||||
frappe.local.cookie_manager.set_cookie("system_user", "yes") | frappe.local.cookie_manager.set_cookie("system_user", "yes") | ||||
if not resume: | if not resume: | ||||
frappe.local.response['message'] = 'Logged In' | |||||
frappe.local.response["message"] = "Logged In" | |||||
frappe.local.response["home_page"] = "/app" | frappe.local.response["home_page"] = "/app" | ||||
if not resume: | if not resume: | ||||
frappe.response["full_name"] = self.full_name | frappe.response["full_name"] = self.full_name | ||||
# redirect information | # redirect information | ||||
redirect_to = frappe.cache().hget('redirect_after_login', self.user) | |||||
redirect_to = frappe.cache().hget("redirect_after_login", self.user) | |||||
if redirect_to: | if redirect_to: | ||||
frappe.local.response["redirect_to"] = redirect_to | frappe.local.response["redirect_to"] = redirect_to | ||||
frappe.cache().hdel('redirect_after_login', self.user) | |||||
frappe.cache().hdel("redirect_after_login", self.user) | |||||
frappe.local.cookie_manager.set_cookie("full_name", self.full_name) | frappe.local.cookie_manager.set_cookie("full_name", self.full_name) | ||||
frappe.local.cookie_manager.set_cookie("user_id", self.user) | frappe.local.cookie_manager.set_cookie("user_id", self.user) | ||||
@@ -202,8 +211,9 @@ class LoginManager: | |||||
def make_session(self, resume=False): | def make_session(self, resume=False): | ||||
# start session | # start session | ||||
frappe.local.session_obj = Session(user=self.user, resume=resume, | |||||
full_name=self.full_name, user_type=self.user_type) | |||||
frappe.local.session_obj = Session( | |||||
user=self.user, resume=resume, full_name=self.full_name, user_type=self.user_type | |||||
) | |||||
# reset user if changed to Guest | # reset user if changed to Guest | ||||
self.user = frappe.local.session_obj.user | self.user = frappe.local.session_obj.user | ||||
@@ -212,7 +222,10 @@ class LoginManager: | |||||
def clear_active_sessions(self): | def clear_active_sessions(self): | ||||
"""Clear other sessions of the current user if `deny_multiple_sessions` is not set""" | """Clear other sessions of the current user if `deny_multiple_sessions` is not set""" | ||||
if not (cint(frappe.conf.get("deny_multiple_sessions")) or cint(frappe.db.get_system_setting('deny_multiple_sessions'))): | |||||
if not ( | |||||
cint(frappe.conf.get("deny_multiple_sessions")) | |||||
or cint(frappe.db.get_system_setting("deny_multiple_sessions")) | |||||
): | |||||
return | return | ||||
if frappe.session.user != "Guest": | if frappe.session.user != "Guest": | ||||
@@ -222,27 +235,27 @@ class LoginManager: | |||||
from frappe.core.doctype.user.user import User | from frappe.core.doctype.user.user import User | ||||
if not (user and pwd): | if not (user and pwd): | ||||
user, pwd = frappe.form_dict.get('usr'), frappe.form_dict.get('pwd') | |||||
user, pwd = frappe.form_dict.get("usr"), frappe.form_dict.get("pwd") | |||||
if not (user and pwd): | if not (user and pwd): | ||||
self.fail(_('Incomplete login details'), user=user) | |||||
self.fail(_("Incomplete login details"), user=user) | |||||
user = User.find_by_credentials(user, pwd) | user = User.find_by_credentials(user, pwd) | ||||
if not user: | if not user: | ||||
self.fail('Invalid login credentials') | |||||
self.fail("Invalid login credentials") | |||||
# Current login flow uses cached credentials for authentication while checking OTP. | # Current login flow uses cached credentials for authentication while checking OTP. | ||||
# Incase of OTP check, tracker for auth needs to be disabled(If not, it can remove tracker history as it is going to succeed anyway) | # Incase of OTP check, tracker for auth needs to be disabled(If not, it can remove tracker history as it is going to succeed anyway) | ||||
# Tracker is activated for 2FA incase of OTP. | # Tracker is activated for 2FA incase of OTP. | ||||
ignore_tracker = should_run_2fa(user.name) and ('otp' in frappe.form_dict) | |||||
ignore_tracker = should_run_2fa(user.name) and ("otp" in frappe.form_dict) | |||||
tracker = None if ignore_tracker else get_login_attempt_tracker(user.name) | tracker = None if ignore_tracker else get_login_attempt_tracker(user.name) | ||||
if not user.is_authenticated: | if not user.is_authenticated: | ||||
tracker and tracker.add_failure_attempt() | tracker and tracker.add_failure_attempt() | ||||
self.fail('Invalid login credentials', user=user.name) | |||||
elif not (user.name == 'Administrator' or user.enabled): | |||||
self.fail("Invalid login credentials", user=user.name) | |||||
elif not (user.name == "Administrator" or user.enabled): | |||||
tracker and tracker.add_failure_attempt() | tracker and tracker.add_failure_attempt() | ||||
self.fail('User disabled or missing', user=user.name) | |||||
self.fail("User disabled or missing", user=user.name) | |||||
else: | else: | ||||
tracker and tracker.add_success_attempt() | tracker and tracker.add_success_attempt() | ||||
self.user = user.name | self.user = user.name | ||||
@@ -254,12 +267,14 @@ class LoginManager: | |||||
if self.user in frappe.STANDARD_USERS: | if self.user in frappe.STANDARD_USERS: | ||||
return False | return False | ||||
reset_pwd_after_days = cint(frappe.db.get_single_value("System Settings", | |||||
"force_user_to_reset_password")) | |||||
reset_pwd_after_days = cint( | |||||
frappe.db.get_single_value("System Settings", "force_user_to_reset_password") | |||||
) | |||||
if reset_pwd_after_days: | if reset_pwd_after_days: | ||||
last_password_reset_date = frappe.db.get_value("User", | |||||
self.user, "last_password_reset_date") or today() | |||||
last_password_reset_date = ( | |||||
frappe.db.get_value("User", self.user, "last_password_reset_date") or today() | |||||
) | |||||
last_pwd_reset_days = date_diff(today(), last_password_reset_date) | last_pwd_reset_days = date_diff(today(), last_password_reset_date) | ||||
@@ -272,30 +287,31 @@ class LoginManager: | |||||
# returns user in correct case | # returns user in correct case | ||||
return check_password(user, pwd) | return check_password(user, pwd) | ||||
except frappe.AuthenticationError: | except frappe.AuthenticationError: | ||||
self.fail('Incorrect password', user=user) | |||||
self.fail("Incorrect password", user=user) | |||||
def fail(self, message, user=None): | def fail(self, message, user=None): | ||||
if not user: | if not user: | ||||
user = _('Unknown User') | |||||
frappe.local.response['message'] = message | |||||
user = _("Unknown User") | |||||
frappe.local.response["message"] = message | |||||
add_authentication_log(message, user, status="Failed") | add_authentication_log(message, user, status="Failed") | ||||
frappe.db.commit() | frappe.db.commit() | ||||
raise frappe.AuthenticationError | raise frappe.AuthenticationError | ||||
def run_trigger(self, event='on_login'): | |||||
def run_trigger(self, event="on_login"): | |||||
for method in frappe.get_hooks().get(event, []): | for method in frappe.get_hooks().get(event, []): | ||||
frappe.call(frappe.get_attr(method), login_manager=self) | frappe.call(frappe.get_attr(method), login_manager=self) | ||||
def validate_hour(self): | def validate_hour(self): | ||||
"""check if user is logging in during restricted hours""" | """check if user is logging in during restricted hours""" | ||||
login_before = int(frappe.db.get_value('User', self.user, 'login_before', ignore=True) or 0) | |||||
login_after = int(frappe.db.get_value('User', self.user, 'login_after', ignore=True) or 0) | |||||
login_before = int(frappe.db.get_value("User", self.user, "login_before", ignore=True) or 0) | |||||
login_after = int(frappe.db.get_value("User", self.user, "login_after", ignore=True) or 0) | |||||
if not (login_before or login_after): | if not (login_before or login_after): | ||||
return | return | ||||
from frappe.utils import now_datetime | from frappe.utils import now_datetime | ||||
current_hour = int(now_datetime().strftime('%H')) | |||||
current_hour = int(now_datetime().strftime("%H")) | |||||
if login_before and current_hour > login_before: | if login_before and current_hour > login_before: | ||||
frappe.throw(_("Login not allowed at this time"), frappe.AuthenticationError) | frappe.throw(_("Login not allowed at this time"), frappe.AuthenticationError) | ||||
@@ -311,9 +327,10 @@ class LoginManager: | |||||
self.user = user | self.user = user | ||||
self.post_login() | self.post_login() | ||||
def logout(self, arg='', user=None): | |||||
if not user: user = frappe.session.user | |||||
self.run_trigger('on_logout') | |||||
def logout(self, arg="", user=None): | |||||
if not user: | |||||
user = frappe.session.user | |||||
self.run_trigger("on_logout") | |||||
if user == frappe.session.user: | if user == frappe.session.user: | ||||
delete_session(frappe.session.sid, user=user, reason="User Manually Logged Out") | delete_session(frappe.session.sid, user=user, reason="User Manually Logged Out") | ||||
@@ -324,13 +341,15 @@ class LoginManager: | |||||
def clear_cookies(self): | def clear_cookies(self): | ||||
clear_cookies() | clear_cookies() | ||||
class CookieManager: | class CookieManager: | ||||
def __init__(self): | def __init__(self): | ||||
self.cookies = {} | self.cookies = {} | ||||
self.to_delete = [] | self.to_delete = [] | ||||
def init_cookies(self): | def init_cookies(self): | ||||
if not frappe.local.session.get('sid'): return | |||||
if not frappe.local.session.get("sid"): | |||||
return | |||||
# sid expires in 3 days | # sid expires in 3 days | ||||
expires = datetime.datetime.now() + datetime.timedelta(days=3) | expires = datetime.datetime.now() + datetime.timedelta(days=3) | ||||
@@ -340,7 +359,7 @@ class CookieManager: | |||||
self.set_cookie("country", frappe.session.session_country) | self.set_cookie("country", frappe.session.session_country) | ||||
def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Lax"): | def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Lax"): | ||||
if not secure and hasattr(frappe.local, 'request'): | |||||
if not secure and hasattr(frappe.local, "request"): | |||||
secure = frappe.local.request.scheme == "https" | secure = frappe.local.request.scheme == "https" | ||||
# Cordova does not work with Lax | # Cordova does not work with Lax | ||||
@@ -352,7 +371,7 @@ class CookieManager: | |||||
"expires": expires, | "expires": expires, | ||||
"secure": secure, | "secure": secure, | ||||
"httponly": httponly, | "httponly": httponly, | ||||
"samesite": samesite | |||||
"samesite": samesite, | |||||
} | } | ||||
def delete_cookie(self, to_delete): | def delete_cookie(self, to_delete): | ||||
@@ -363,11 +382,14 @@ class CookieManager: | |||||
def flush_cookies(self, response): | def flush_cookies(self, response): | ||||
for key, opts in self.cookies.items(): | for key, opts in self.cookies.items(): | ||||
response.set_cookie(key, quote((opts.get("value") or "").encode('utf-8')), | |||||
response.set_cookie( | |||||
key, | |||||
quote((opts.get("value") or "").encode("utf-8")), | |||||
expires=opts.get("expires"), | expires=opts.get("expires"), | ||||
secure=opts.get("secure"), | secure=opts.get("secure"), | ||||
httponly=opts.get("httponly"), | httponly=opts.get("httponly"), | ||||
samesite=opts.get("samesite")) | |||||
samesite=opts.get("samesite"), | |||||
) | |||||
# expires yesterday! | # expires yesterday! | ||||
expires = datetime.datetime.now() + datetime.timedelta(days=-1) | expires = datetime.datetime.now() + datetime.timedelta(days=-1) | ||||
@@ -379,19 +401,29 @@ class CookieManager: | |||||
def get_logged_user(): | def get_logged_user(): | ||||
return frappe.session.user | return frappe.session.user | ||||
def clear_cookies(): | def clear_cookies(): | ||||
if hasattr(frappe.local, "session"): | if hasattr(frappe.local, "session"): | ||||
frappe.session.sid = "" | frappe.session.sid = "" | ||||
frappe.local.cookie_manager.delete_cookie(["full_name", "user_id", "sid", "user_image", "system_user"]) | |||||
frappe.local.cookie_manager.delete_cookie( | |||||
["full_name", "user_id", "sid", "user_image", "system_user"] | |||||
) | |||||
def validate_ip_address(user): | def validate_ip_address(user): | ||||
"""check if IP Address is valid""" | """check if IP Address is valid""" | ||||
user = frappe.get_cached_doc("User", user) if not frappe.flags.in_test else frappe.get_doc("User", user) | |||||
user = ( | |||||
frappe.get_cached_doc("User", user) if not frappe.flags.in_test else frappe.get_doc("User", user) | |||||
) | |||||
ip_list = user.get_restricted_ip_list() | ip_list = user.get_restricted_ip_list() | ||||
if not ip_list: | if not ip_list: | ||||
return | return | ||||
system_settings = frappe.get_cached_doc("System Settings") if not frappe.flags.in_test else frappe.get_single("System Settings") | |||||
system_settings = ( | |||||
frappe.get_cached_doc("System Settings") | |||||
if not frappe.flags.in_test | |||||
else frappe.get_single("System Settings") | |||||
) | |||||
# check if bypass restrict ip is enabled for all users | # check if bypass restrict ip is enabled for all users | ||||
bypass_restrict_ip_check = system_settings.bypass_restrict_ip_check_if_2fa_enabled | bypass_restrict_ip_check = system_settings.bypass_restrict_ip_check_if_2fa_enabled | ||||
@@ -406,6 +438,7 @@ def validate_ip_address(user): | |||||
frappe.throw(_("Access not allowed from this IP Address"), frappe.AuthenticationError) | frappe.throw(_("Access not allowed from this IP Address"), frappe.AuthenticationError) | ||||
def get_login_attempt_tracker(user_name: str, raise_locked_exception: bool = True): | def get_login_attempt_tracker(user_name: str, raise_locked_exception: bool = True): | ||||
"""Get login attempt tracker instance. | """Get login attempt tracker instance. | ||||
@@ -413,18 +446,22 @@ def get_login_attempt_tracker(user_name: str, raise_locked_exception: bool = Tru | |||||
:param raise_locked_exception: If set, raises an exception incase of user not allowed to login | :param raise_locked_exception: If set, raises an exception incase of user not allowed to login | ||||
""" | """ | ||||
sys_settings = frappe.get_doc("System Settings") | sys_settings = frappe.get_doc("System Settings") | ||||
track_login_attempts = (sys_settings.allow_consecutive_login_attempts >0) | |||||
track_login_attempts = sys_settings.allow_consecutive_login_attempts > 0 | |||||
tracker_kwargs = {} | tracker_kwargs = {} | ||||
if track_login_attempts: | if track_login_attempts: | ||||
tracker_kwargs['lock_interval'] = sys_settings.allow_login_after_fail | |||||
tracker_kwargs['max_consecutive_login_attempts'] = sys_settings.allow_consecutive_login_attempts | |||||
tracker_kwargs["lock_interval"] = sys_settings.allow_login_after_fail | |||||
tracker_kwargs["max_consecutive_login_attempts"] = sys_settings.allow_consecutive_login_attempts | |||||
tracker = LoginAttemptTracker(user_name, **tracker_kwargs) | tracker = LoginAttemptTracker(user_name, **tracker_kwargs) | ||||
if raise_locked_exception and track_login_attempts and not tracker.is_user_allowed(): | if raise_locked_exception and track_login_attempts and not tracker.is_user_allowed(): | ||||
frappe.throw(_("Your account has been locked and will resume after {0} seconds") | |||||
.format(sys_settings.allow_login_after_fail), frappe.SecurityException) | |||||
frappe.throw( | |||||
_("Your account has been locked and will resume after {0} seconds").format( | |||||
sys_settings.allow_login_after_fail | |||||
), | |||||
frappe.SecurityException, | |||||
) | |||||
return tracker | return tracker | ||||
@@ -433,8 +470,11 @@ class LoginAttemptTracker(object): | |||||
Lock the account for s number of seconds if there have been n consecutive unsuccessful attempts to log in. | Lock the account for s number of seconds if there have been n consecutive unsuccessful attempts to log in. | ||||
""" | """ | ||||
def __init__(self, user_name: str, max_consecutive_login_attempts: int=3, lock_interval:int = 5*60): | |||||
""" Initialize the tracker. | |||||
def __init__( | |||||
self, user_name: str, max_consecutive_login_attempts: int = 3, lock_interval: int = 5 * 60 | |||||
): | |||||
"""Initialize the tracker. | |||||
:param user_name: Name of the loggedin user | :param user_name: Name of the loggedin user | ||||
:param max_consecutive_login_attempts: Maximum allowed consecutive failed login attempts | :param max_consecutive_login_attempts: Maximum allowed consecutive failed login attempts | ||||
@@ -446,15 +486,15 @@ class LoginAttemptTracker(object): | |||||
@property | @property | ||||
def login_failed_count(self): | def login_failed_count(self): | ||||
return frappe.cache().hget('login_failed_count', self.user_name) | |||||
return frappe.cache().hget("login_failed_count", self.user_name) | |||||
@login_failed_count.setter | @login_failed_count.setter | ||||
def login_failed_count(self, count): | def login_failed_count(self, count): | ||||
frappe.cache().hset('login_failed_count', self.user_name, count) | |||||
frappe.cache().hset("login_failed_count", self.user_name, count) | |||||
@login_failed_count.deleter | @login_failed_count.deleter | ||||
def login_failed_count(self): | def login_failed_count(self): | ||||
frappe.cache().hdel('login_failed_count', self.user_name) | |||||
frappe.cache().hdel("login_failed_count", self.user_name) | |||||
@property | @property | ||||
def login_failed_time(self): | def login_failed_time(self): | ||||
@@ -462,23 +502,23 @@ class LoginAttemptTracker(object): | |||||
For every user we track only First failed login attempt time within lock interval of time. | For every user we track only First failed login attempt time within lock interval of time. | ||||
""" | """ | ||||
return frappe.cache().hget('login_failed_time', self.user_name) | |||||
return frappe.cache().hget("login_failed_time", self.user_name) | |||||
@login_failed_time.setter | @login_failed_time.setter | ||||
def login_failed_time(self, timestamp): | def login_failed_time(self, timestamp): | ||||
frappe.cache().hset('login_failed_time', self.user_name, timestamp) | |||||
frappe.cache().hset("login_failed_time", self.user_name, timestamp) | |||||
@login_failed_time.deleter | @login_failed_time.deleter | ||||
def login_failed_time(self): | def login_failed_time(self): | ||||
frappe.cache().hdel('login_failed_time', self.user_name) | |||||
frappe.cache().hdel("login_failed_time", self.user_name) | |||||
def add_failure_attempt(self): | def add_failure_attempt(self): | ||||
""" Log user failure attempts into the system. | |||||
"""Log user failure attempts into the system. | |||||
Increase the failure count if new failure is with in current lock interval time period, if not reset the login failure count. | Increase the failure count if new failure is with in current lock interval time period, if not reset the login failure count. | ||||
""" | """ | ||||
login_failed_time = self.login_failed_time | login_failed_time = self.login_failed_time | ||||
login_failed_count = self.login_failed_count # Consecutive login failure count | |||||
login_failed_count = self.login_failed_count # Consecutive login failure count | |||||
current_time = get_datetime() | current_time = get_datetime() | ||||
if not (login_failed_time and login_failed_count): | if not (login_failed_time and login_failed_count): | ||||
@@ -493,8 +533,7 @@ class LoginAttemptTracker(object): | |||||
self.login_failed_count = login_failed_count | self.login_failed_count = login_failed_count | ||||
def add_success_attempt(self): | def add_success_attempt(self): | ||||
"""Reset login failures. | |||||
""" | |||||
"""Reset login failures.""" | |||||
del self.login_failed_count | del self.login_failed_count | ||||
del self.login_failed_time | del self.login_failed_time | ||||
@@ -507,6 +546,10 @@ class LoginAttemptTracker(object): | |||||
login_failed_count = self.login_failed_count or 0 | login_failed_count = self.login_failed_count or 0 | ||||
current_time = get_datetime() | current_time = get_datetime() | ||||
if login_failed_time and login_failed_time + self.lock_interval > current_time and login_failed_count > self.max_failed_logins: | |||||
if ( | |||||
login_failed_time | |||||
and login_failed_time + self.lock_interval > current_time | |||||
and login_failed_count > self.max_failed_logins | |||||
): | |||||
return False | return False | ||||
return True | return True |
@@ -24,9 +24,7 @@ class AssignmentRule(Document): | |||||
def validate_document_types(self): | def validate_document_types(self): | ||||
if self.document_type == "ToDo": | if self.document_type == "ToDo": | ||||
frappe.throw( | frappe.throw( | ||||
_('Assignment Rule is not allowed on {0} document type').format( | |||||
frappe.bold("ToDo") | |||||
) | |||||
_("Assignment Rule is not allowed on {0} document type").format(frappe.bold("ToDo")) | |||||
) | ) | ||||
def validate_assignment_days(self): | def validate_assignment_days(self): | ||||
@@ -38,70 +36,70 @@ class AssignmentRule(Document): | |||||
frappe.throw( | frappe.throw( | ||||
_("Assignment Day{0} {1} has been repeated.").format( | _("Assignment Day{0} {1} has been repeated.").format( | ||||
plural, | |||||
frappe.bold(", ".join(repeated_days)) | |||||
plural, frappe.bold(", ".join(repeated_days)) | |||||
) | ) | ||||
) | ) | ||||
def apply_unassign(self, doc, assignments): | def apply_unassign(self, doc, assignments): | ||||
if (self.unassign_condition and | |||||
self.name in [d.assignment_rule for d in assignments]): | |||||
if self.unassign_condition and self.name in [d.assignment_rule for d in assignments]: | |||||
return self.clear_assignment(doc) | return self.clear_assignment(doc) | ||||
return False | return False | ||||
def apply_assign(self, doc): | def apply_assign(self, doc): | ||||
if self.safe_eval('assign_condition', doc): | |||||
if self.safe_eval("assign_condition", doc): | |||||
return self.do_assignment(doc) | return self.do_assignment(doc) | ||||
def do_assignment(self, doc): | def do_assignment(self, doc): | ||||
# clear existing assignment, to reassign | # clear existing assignment, to reassign | ||||
assign_to.clear(doc.get('doctype'), doc.get('name')) | |||||
assign_to.clear(doc.get("doctype"), doc.get("name")) | |||||
user = self.get_user(doc) | user = self.get_user(doc) | ||||
if user: | if user: | ||||
assign_to.add(dict( | |||||
assign_to = [user], | |||||
doctype = doc.get('doctype'), | |||||
name = doc.get('name'), | |||||
description = frappe.render_template(self.description, doc), | |||||
assignment_rule = self.name, | |||||
notify = True, | |||||
date = doc.get(self.due_date_based_on) if self.due_date_based_on else None | |||||
)) | |||||
assign_to.add( | |||||
dict( | |||||
assign_to=[user], | |||||
doctype=doc.get("doctype"), | |||||
name=doc.get("name"), | |||||
description=frappe.render_template(self.description, doc), | |||||
assignment_rule=self.name, | |||||
notify=True, | |||||
date=doc.get(self.due_date_based_on) if self.due_date_based_on else None, | |||||
) | |||||
) | |||||
# set for reference in round robin | # set for reference in round robin | ||||
self.db_set('last_user', user) | |||||
self.db_set("last_user", user) | |||||
return True | return True | ||||
return False | return False | ||||
def clear_assignment(self, doc): | def clear_assignment(self, doc): | ||||
'''Clear assignments''' | |||||
if self.safe_eval('unassign_condition', doc): | |||||
return assign_to.clear(doc.get('doctype'), doc.get('name')) | |||||
"""Clear assignments""" | |||||
if self.safe_eval("unassign_condition", doc): | |||||
return assign_to.clear(doc.get("doctype"), doc.get("name")) | |||||
def close_assignments(self, doc): | def close_assignments(self, doc): | ||||
'''Close assignments''' | |||||
if self.safe_eval('close_condition', doc): | |||||
return assign_to.close_all_assignments(doc.get('doctype'), doc.get('name')) | |||||
"""Close assignments""" | |||||
if self.safe_eval("close_condition", doc): | |||||
return assign_to.close_all_assignments(doc.get("doctype"), doc.get("name")) | |||||
def get_user(self, doc): | def get_user(self, doc): | ||||
''' | |||||
""" | |||||
Get the next user for assignment | Get the next user for assignment | ||||
''' | |||||
if self.rule == 'Round Robin': | |||||
""" | |||||
if self.rule == "Round Robin": | |||||
return self.get_user_round_robin() | return self.get_user_round_robin() | ||||
elif self.rule == 'Load Balancing': | |||||
elif self.rule == "Load Balancing": | |||||
return self.get_user_load_balancing() | return self.get_user_load_balancing() | ||||
elif self.rule == 'Based on Field': | |||||
elif self.rule == "Based on Field": | |||||
return self.get_user_based_on_field(doc) | return self.get_user_based_on_field(doc) | ||||
def get_user_round_robin(self): | def get_user_round_robin(self): | ||||
''' | |||||
""" | |||||
Get next user based on round robin | Get next user based on round robin | ||||
''' | |||||
""" | |||||
# first time, or last in list, pick the first | # first time, or last in list, pick the first | ||||
if not self.last_user or self.last_user == self.users[-1].user: | if not self.last_user or self.last_user == self.users[-1].user: | ||||
@@ -110,32 +108,33 @@ class AssignmentRule(Document): | |||||
# find out the next user in the list | # find out the next user in the list | ||||
for i, d in enumerate(self.users): | for i, d in enumerate(self.users): | ||||
if self.last_user == d.user: | if self.last_user == d.user: | ||||
return self.users[i+1].user | |||||
return self.users[i + 1].user | |||||
# bad last user, assign to the first one | # bad last user, assign to the first one | ||||
return self.users[0].user | return self.users[0].user | ||||
def get_user_load_balancing(self): | def get_user_load_balancing(self): | ||||
'''Assign to the user with least number of open assignments''' | |||||
"""Assign to the user with least number of open assignments""" | |||||
counts = [] | counts = [] | ||||
for d in self.users: | for d in self.users: | ||||
counts.append(dict( | |||||
user = d.user, | |||||
count = frappe.db.count('ToDo', dict( | |||||
reference_type = self.document_type, | |||||
allocated_to = d.user, | |||||
status = "Open")) | |||||
)) | |||||
counts.append( | |||||
dict( | |||||
user=d.user, | |||||
count=frappe.db.count( | |||||
"ToDo", dict(reference_type=self.document_type, allocated_to=d.user, status="Open") | |||||
), | |||||
) | |||||
) | |||||
# sort by dict value | # sort by dict value | ||||
sorted_counts = sorted(counts, key = lambda k: k['count']) | |||||
sorted_counts = sorted(counts, key=lambda k: k["count"]) | |||||
# pick the first user | # pick the first user | ||||
return sorted_counts[0].get('user') | |||||
return sorted_counts[0].get("user") | |||||
def get_user_based_on_field(self, doc): | def get_user_based_on_field(self, doc): | ||||
val = doc.get(self.field) | val = doc.get(self.field) | ||||
if frappe.db.exists('User', val): | |||||
if frappe.db.exists("User", val): | |||||
return val | return val | ||||
def safe_eval(self, fieldname, doc): | def safe_eval(self, fieldname, doc): | ||||
@@ -145,12 +144,12 @@ class AssignmentRule(Document): | |||||
except Exception as e: | except Exception as e: | ||||
# when assignment fails, don't block the document as it may be | # when assignment fails, don't block the document as it may be | ||||
# a part of the email pulling | # a part of the email pulling | ||||
frappe.msgprint(frappe._('Auto assignment failed: {0}').format(str(e)), indicator = 'orange') | |||||
frappe.msgprint(frappe._("Auto assignment failed: {0}").format(str(e)), indicator="orange") | |||||
return False | return False | ||||
def get_assignment_days(self): | def get_assignment_days(self): | ||||
return [d.day for d in self.get('assignment_days', [])] | |||||
return [d.day for d in self.get("assignment_days", [])] | |||||
def is_rule_not_applicable_today(self): | def is_rule_not_applicable_today(self): | ||||
today = frappe.flags.assignment_day or frappe.utils.get_weekday() | today = frappe.flags.assignment_day or frappe.utils.get_weekday() | ||||
@@ -159,11 +158,14 @@ class AssignmentRule(Document): | |||||
def get_assignments(doc) -> List[Dict]: | def get_assignments(doc) -> List[Dict]: | ||||
return frappe.get_all('ToDo', fields = ['name', 'assignment_rule'], filters = dict( | |||||
reference_type = doc.get('doctype'), | |||||
reference_name = doc.get('name'), | |||||
status = ('!=', 'Cancelled') | |||||
), limit=5) | |||||
return frappe.get_all( | |||||
"ToDo", | |||||
fields=["name", "assignment_rule"], | |||||
filters=dict( | |||||
reference_type=doc.get("doctype"), reference_name=doc.get("name"), status=("!=", "Cancelled") | |||||
), | |||||
limit=5, | |||||
) | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
@@ -173,21 +175,30 @@ def bulk_apply(doctype, docnames): | |||||
for name in docnames: | for name in docnames: | ||||
if background: | if background: | ||||
frappe.enqueue('frappe.automation.doctype.assignment_rule.assignment_rule.apply', doc=None, doctype=doctype, name=name) | |||||
frappe.enqueue( | |||||
"frappe.automation.doctype.assignment_rule.assignment_rule.apply", | |||||
doc=None, | |||||
doctype=doctype, | |||||
name=name, | |||||
) | |||||
else: | else: | ||||
apply(doctype=doctype, name=name) | apply(doctype=doctype, name=name) | ||||
def reopen_closed_assignment(doc): | def reopen_closed_assignment(doc): | ||||
todo_list = frappe.get_all("ToDo", filters={ | |||||
"reference_type": doc.doctype, | |||||
"reference_name": doc.name, | |||||
"status": "Closed", | |||||
}, pluck="name") | |||||
todo_list = frappe.get_all( | |||||
"ToDo", | |||||
filters={ | |||||
"reference_type": doc.doctype, | |||||
"reference_name": doc.name, | |||||
"status": "Closed", | |||||
}, | |||||
pluck="name", | |||||
) | |||||
for todo in todo_list: | for todo in todo_list: | ||||
todo_doc = frappe.get_doc('ToDo', todo) | |||||
todo_doc.status = 'Open' | |||||
todo_doc = frappe.get_doc("ToDo", todo) | |||||
todo_doc.status = "Open" | |||||
todo_doc.save(ignore_permissions=True) | todo_doc.save(ignore_permissions=True) | ||||
return bool(todo_list) | return bool(todo_list) | ||||
@@ -209,13 +220,16 @@ def apply(doc=None, method=None, doctype=None, name=None): | |||||
if not doc and doctype and name: | if not doc and doctype and name: | ||||
doc = frappe.get_doc(doctype, name) | doc = frappe.get_doc(doctype, name) | ||||
assignment_rules = get_doctype_map("Assignment Rule", doc.doctype, filters={ | |||||
"document_type": doc.doctype, "disabled": 0 | |||||
}, order_by="priority desc") | |||||
assignment_rules = get_doctype_map( | |||||
"Assignment Rule", | |||||
doc.doctype, | |||||
filters={"document_type": doc.doctype, "disabled": 0}, | |||||
order_by="priority desc", | |||||
) | |||||
# multiple auto assigns | # multiple auto assigns | ||||
assignment_rule_docs: List[AssignmentRule] = [ | assignment_rule_docs: List[AssignmentRule] = [ | ||||
frappe.get_cached_doc("Assignment Rule", d.get('name')) for d in assignment_rules | |||||
frappe.get_cached_doc("Assignment Rule", d.get("name")) for d in assignment_rules | |||||
] | ] | ||||
if not assignment_rule_docs: | if not assignment_rule_docs: | ||||
@@ -224,8 +238,8 @@ def apply(doc=None, method=None, doctype=None, name=None): | |||||
doc = doc.as_dict() | doc = doc.as_dict() | ||||
assignments = get_assignments(doc) | assignments = get_assignments(doc) | ||||
clear = True # are all assignments cleared | |||||
new_apply = False # are new assignments applied | |||||
clear = True # are all assignments cleared | |||||
new_apply = False # are new assignments applied | |||||
if assignments: | if assignments: | ||||
# first unassign | # first unassign | ||||
@@ -260,14 +274,18 @@ def apply(doc=None, method=None, doctype=None, name=None): | |||||
if not new_apply: | if not new_apply: | ||||
# only reopen if close condition is not satisfied | # only reopen if close condition is not satisfied | ||||
to_close_todos = assignment_rule.safe_eval('close_condition', doc) | |||||
to_close_todos = assignment_rule.safe_eval("close_condition", doc) | |||||
if to_close_todos: | if to_close_todos: | ||||
# close todo status | # close todo status | ||||
todos_to_close = frappe.get_all("ToDo", filters={ | |||||
"reference_type": doc.doctype, | |||||
"reference_name": doc.name, | |||||
}, pluck="name") | |||||
todos_to_close = frappe.get_all( | |||||
"ToDo", | |||||
filters={ | |||||
"reference_type": doc.doctype, | |||||
"reference_name": doc.name, | |||||
}, | |||||
pluck="name", | |||||
) | |||||
for todo in todos_to_close: | for todo in todos_to_close: | ||||
_todo = frappe.get_doc("ToDo", todo) | _todo = frappe.get_doc("ToDo", todo) | ||||
@@ -286,8 +304,7 @@ def apply(doc=None, method=None, doctype=None, name=None): | |||||
def update_due_date(doc, state=None): | def update_due_date(doc, state=None): | ||||
"""Run on_update on every Document (via hooks.py) | |||||
""" | |||||
"""Run on_update on every Document (via hooks.py)""" | |||||
skip_document_update = ( | skip_document_update = ( | ||||
frappe.flags.in_migrate | frappe.flags.in_migrate | ||||
or frappe.flags.in_patch | or frappe.flags.in_patch | ||||
@@ -306,7 +323,7 @@ def update_due_date(doc, state=None): | |||||
"due_date_based_on": ["is", "set"], | "due_date_based_on": ["is", "set"], | ||||
"document_type": doc.doctype, | "document_type": doc.doctype, | ||||
"disabled": 0, | "disabled": 0, | ||||
} | |||||
}, | |||||
) | ) | ||||
for rule in assignment_rules: | for rule in assignment_rules: | ||||
@@ -319,20 +336,24 @@ def update_due_date(doc, state=None): | |||||
) | ) | ||||
if field_updated: | if field_updated: | ||||
assignment_todos = frappe.get_all("ToDo", filters={ | |||||
"assignment_rule": rule.get("name"), | |||||
"reference_type": doc.doctype, | |||||
"reference_name": doc.name, | |||||
"status": "Open", | |||||
}, pluck="name") | |||||
assignment_todos = frappe.get_all( | |||||
"ToDo", | |||||
filters={ | |||||
"assignment_rule": rule.get("name"), | |||||
"reference_type": doc.doctype, | |||||
"reference_name": doc.name, | |||||
"status": "Open", | |||||
}, | |||||
pluck="name", | |||||
) | |||||
for todo in assignment_todos: | for todo in assignment_todos: | ||||
todo_doc = frappe.get_doc('ToDo', todo) | |||||
todo_doc = frappe.get_doc("ToDo", todo) | |||||
todo_doc.date = doc.get(due_date_field) | todo_doc.date = doc.get(due_date_field) | ||||
todo_doc.flags.updater_reference = { | todo_doc.flags.updater_reference = { | ||||
'doctype': 'Assignment Rule', | |||||
'docname': rule.get('name'), | |||||
'label': _('via Assignment Rule') | |||||
"doctype": "Assignment Rule", | |||||
"docname": rule.get("name"), | |||||
"label": _("via Assignment Rule"), | |||||
} | } | ||||
todo_doc.save(ignore_permissions=True) | todo_doc.save(ignore_permissions=True) | ||||
@@ -20,13 +20,13 @@ class TestAutoAssign(unittest.TestCase): | |||||
def setUp(self): | def setUp(self): | ||||
make_test_records("User") | make_test_records("User") | ||||
days = [ | days = [ | ||||
dict(day = 'Sunday'), | |||||
dict(day = 'Monday'), | |||||
dict(day = 'Tuesday'), | |||||
dict(day = 'Wednesday'), | |||||
dict(day = 'Thursday'), | |||||
dict(day = 'Friday'), | |||||
dict(day = 'Saturday'), | |||||
dict(day="Sunday"), | |||||
dict(day="Monday"), | |||||
dict(day="Tuesday"), | |||||
dict(day="Wednesday"), | |||||
dict(day="Thursday"), | |||||
dict(day="Friday"), | |||||
dict(day="Saturday"), | |||||
] | ] | ||||
self.days = days | self.days = days | ||||
self.assignment_rule = get_assignment_rule([days, days]) | self.assignment_rule = get_assignment_rule([days, days]) | ||||
@@ -36,20 +36,22 @@ class TestAutoAssign(unittest.TestCase): | |||||
note = make_note(dict(public=1)) | note = make_note(dict(public=1)) | ||||
# check if auto assigned to first user | # check if auto assigned to first user | ||||
self.assertEqual(frappe.db.get_value('ToDo', dict( | |||||
reference_type = 'Note', | |||||
reference_name = note.name, | |||||
status = 'Open' | |||||
), 'allocated_to'), 'test@example.com') | |||||
self.assertEqual( | |||||
frappe.db.get_value( | |||||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" | |||||
), | |||||
"test@example.com", | |||||
) | |||||
note = make_note(dict(public=1)) | note = make_note(dict(public=1)) | ||||
# check if auto assigned to second user | # check if auto assigned to second user | ||||
self.assertEqual(frappe.db.get_value('ToDo', dict( | |||||
reference_type = 'Note', | |||||
reference_name = note.name, | |||||
status = 'Open' | |||||
), 'allocated_to'), 'test1@example.com') | |||||
self.assertEqual( | |||||
frappe.db.get_value( | |||||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" | |||||
), | |||||
"test1@example.com", | |||||
) | |||||
clear_assignments() | clear_assignments() | ||||
@@ -57,35 +59,41 @@ class TestAutoAssign(unittest.TestCase): | |||||
# check if auto assigned to third user, even if | # check if auto assigned to third user, even if | ||||
# previous assignments where closed | # previous assignments where closed | ||||
self.assertEqual(frappe.db.get_value('ToDo', dict( | |||||
reference_type = 'Note', | |||||
reference_name = note.name, | |||||
status = 'Open' | |||||
), 'allocated_to'), 'test2@example.com') | |||||
self.assertEqual( | |||||
frappe.db.get_value( | |||||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" | |||||
), | |||||
"test2@example.com", | |||||
) | |||||
# check loop back to first user | # check loop back to first user | ||||
note = make_note(dict(public=1)) | note = make_note(dict(public=1)) | ||||
self.assertEqual(frappe.db.get_value('ToDo', dict( | |||||
reference_type = 'Note', | |||||
reference_name = note.name, | |||||
status = 'Open' | |||||
), 'allocated_to'), 'test@example.com') | |||||
self.assertEqual( | |||||
frappe.db.get_value( | |||||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" | |||||
), | |||||
"test@example.com", | |||||
) | |||||
def test_load_balancing(self): | def test_load_balancing(self): | ||||
self.assignment_rule.rule = 'Load Balancing' | |||||
self.assignment_rule.rule = "Load Balancing" | |||||
self.assignment_rule.save() | self.assignment_rule.save() | ||||
for _ in range(30): | for _ in range(30): | ||||
note = make_note(dict(public=1)) | note = make_note(dict(public=1)) | ||||
# check if each user has 10 assignments (?) | # check if each user has 10 assignments (?) | ||||
for user in ('test@example.com', 'test1@example.com', 'test2@example.com'): | |||||
self.assertEqual(len(frappe.get_all('ToDo', dict(allocated_to = user, reference_type = 'Note'))), 10) | |||||
for user in ("test@example.com", "test1@example.com", "test2@example.com"): | |||||
self.assertEqual( | |||||
len(frappe.get_all("ToDo", dict(allocated_to=user, reference_type="Note"))), 10 | |||||
) | |||||
# clear 5 assignments for first user | # clear 5 assignments for first user | ||||
# can't do a limit in "delete" since postgres does not support it | # can't do a limit in "delete" since postgres does not support it | ||||
for d in frappe.get_all('ToDo', dict(reference_type = 'Note', allocated_to = 'test@example.com'), limit=5): | |||||
for d in frappe.get_all( | |||||
"ToDo", dict(reference_type="Note", allocated_to="test@example.com"), limit=5 | |||||
): | |||||
frappe.db.delete("ToDo", {"name": d.name}) | frappe.db.delete("ToDo", {"name": d.name}) | ||||
# add 5 more assignments | # add 5 more assignments | ||||
@@ -93,56 +101,59 @@ class TestAutoAssign(unittest.TestCase): | |||||
make_note(dict(public=1)) | make_note(dict(public=1)) | ||||
# check if each user still has 10 assignments | # check if each user still has 10 assignments | ||||
for user in ('test@example.com', 'test1@example.com', 'test2@example.com'): | |||||
self.assertEqual(len(frappe.get_all('ToDo', dict(allocated_to = user, reference_type = 'Note'))), 10) | |||||
for user in ("test@example.com", "test1@example.com", "test2@example.com"): | |||||
self.assertEqual( | |||||
len(frappe.get_all("ToDo", dict(allocated_to=user, reference_type="Note"))), 10 | |||||
) | |||||
def test_based_on_field(self): | def test_based_on_field(self): | ||||
self.assignment_rule.rule = 'Based on Field' | |||||
self.assignment_rule.field = 'owner' | |||||
self.assignment_rule.rule = "Based on Field" | |||||
self.assignment_rule.field = "owner" | |||||
self.assignment_rule.save() | self.assignment_rule.save() | ||||
frappe.set_user('test1@example.com') | |||||
frappe.set_user("test1@example.com") | |||||
note = make_note(dict(public=1)) | note = make_note(dict(public=1)) | ||||
# check if auto assigned to doc owner, test1@example.com | # check if auto assigned to doc owner, test1@example.com | ||||
self.assertEqual(frappe.db.get_value('ToDo', dict( | |||||
reference_type = 'Note', | |||||
reference_name = note.name, | |||||
status = 'Open' | |||||
), 'owner'), 'test1@example.com') | |||||
frappe.set_user('test2@example.com') | |||||
self.assertEqual( | |||||
frappe.db.get_value( | |||||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "owner" | |||||
), | |||||
"test1@example.com", | |||||
) | |||||
frappe.set_user("test2@example.com") | |||||
note = make_note(dict(public=1)) | note = make_note(dict(public=1)) | ||||
# check if auto assigned to doc owner, test2@example.com | # check if auto assigned to doc owner, test2@example.com | ||||
self.assertEqual(frappe.db.get_value('ToDo', dict( | |||||
reference_type = 'Note', | |||||
reference_name = note.name, | |||||
status = 'Open' | |||||
), 'owner'), 'test2@example.com') | |||||
self.assertEqual( | |||||
frappe.db.get_value( | |||||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "owner" | |||||
), | |||||
"test2@example.com", | |||||
) | |||||
frappe.set_user('Administrator') | |||||
frappe.set_user("Administrator") | |||||
def test_assign_condition(self): | def test_assign_condition(self): | ||||
# check condition | # check condition | ||||
note = make_note(dict(public=0)) | note = make_note(dict(public=0)) | ||||
self.assertEqual(frappe.db.get_value('ToDo', dict( | |||||
reference_type = 'Note', | |||||
reference_name = note.name, | |||||
status = 'Open' | |||||
), 'allocated_to'), None) | |||||
self.assertEqual( | |||||
frappe.db.get_value( | |||||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" | |||||
), | |||||
None, | |||||
) | |||||
def test_clear_assignment(self): | def test_clear_assignment(self): | ||||
note = make_note(dict(public=1)) | note = make_note(dict(public=1)) | ||||
# check if auto assigned to first user | # check if auto assigned to first user | ||||
todo = frappe.get_list('ToDo', dict( | |||||
reference_type = 'Note', | |||||
reference_name = note.name, | |||||
status = 'Open' | |||||
), limit=1)[0] | |||||
todo = frappe.get_list( | |||||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), limit=1 | |||||
)[0] | |||||
todo = frappe.get_doc('ToDo', todo['name']) | |||||
self.assertEqual(todo.allocated_to, 'test@example.com') | |||||
todo = frappe.get_doc("ToDo", todo["name"]) | |||||
self.assertEqual(todo.allocated_to, "test@example.com") | |||||
# test auto unassign | # test auto unassign | ||||
note.public = 0 | note.public = 0 | ||||
@@ -151,99 +162,101 @@ class TestAutoAssign(unittest.TestCase): | |||||
todo.load_from_db() | todo.load_from_db() | ||||
# check if todo is cancelled | # check if todo is cancelled | ||||
self.assertEqual(todo.status, 'Cancelled') | |||||
self.assertEqual(todo.status, "Cancelled") | |||||
def test_close_assignment(self): | def test_close_assignment(self): | ||||
note = make_note(dict(public=1, content="valid")) | note = make_note(dict(public=1, content="valid")) | ||||
# check if auto assigned | # check if auto assigned | ||||
todo = frappe.get_list('ToDo', dict( | |||||
reference_type = 'Note', | |||||
reference_name = note.name, | |||||
status = 'Open' | |||||
), limit=1)[0] | |||||
todo = frappe.get_list( | |||||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), limit=1 | |||||
)[0] | |||||
todo = frappe.get_doc('ToDo', todo['name']) | |||||
self.assertEqual(todo.allocated_to, 'test@example.com') | |||||
todo = frappe.get_doc("ToDo", todo["name"]) | |||||
self.assertEqual(todo.allocated_to, "test@example.com") | |||||
note.content="Closed" | |||||
note.content = "Closed" | |||||
note.save() | note.save() | ||||
todo.load_from_db() | todo.load_from_db() | ||||
# check if todo is closed | # check if todo is closed | ||||
self.assertEqual(todo.status, 'Closed') | |||||
self.assertEqual(todo.status, "Closed") | |||||
# check if closed todo retained assignment | # check if closed todo retained assignment | ||||
self.assertEqual(todo.allocated_to, 'test@example.com') | |||||
self.assertEqual(todo.allocated_to, "test@example.com") | |||||
def check_multiple_rules(self): | def check_multiple_rules(self): | ||||
note = make_note(dict(public=1, notify_on_login=1)) | note = make_note(dict(public=1, notify_on_login=1)) | ||||
# check if auto assigned to test3 (2nd rule is applied, as it has higher priority) | # check if auto assigned to test3 (2nd rule is applied, as it has higher priority) | ||||
self.assertEqual(frappe.db.get_value('ToDo', dict( | |||||
reference_type = 'Note', | |||||
reference_name = note.name, | |||||
status = 'Open' | |||||
), 'allocated_to'), 'test@example.com') | |||||
self.assertEqual( | |||||
frappe.db.get_value( | |||||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" | |||||
), | |||||
"test@example.com", | |||||
) | |||||
def check_assignment_rule_scheduling(self): | def check_assignment_rule_scheduling(self): | ||||
frappe.db.delete("Assignment Rule") | frappe.db.delete("Assignment Rule") | ||||
days_1 = [dict(day = 'Sunday'), dict(day = 'Monday'), dict(day = 'Tuesday')] | |||||
days_1 = [dict(day="Sunday"), dict(day="Monday"), dict(day="Tuesday")] | |||||
days_2 = [dict(day = 'Wednesday'), dict(day = 'Thursday'), dict(day = 'Friday'), dict(day = 'Saturday')] | |||||
days_2 = [dict(day="Wednesday"), dict(day="Thursday"), dict(day="Friday"), dict(day="Saturday")] | |||||
get_assignment_rule([days_1, days_2], ['public == 1', 'public == 1']) | |||||
get_assignment_rule([days_1, days_2], ["public == 1", "public == 1"]) | |||||
frappe.flags.assignment_day = "Monday" | frappe.flags.assignment_day = "Monday" | ||||
note = make_note(dict(public=1)) | note = make_note(dict(public=1)) | ||||
self.assertIn(frappe.db.get_value('ToDo', dict( | |||||
reference_type = 'Note', | |||||
reference_name = note.name, | |||||
status = 'Open' | |||||
), 'allocated_to'), ['test@example.com', 'test1@example.com', 'test2@example.com']) | |||||
self.assertIn( | |||||
frappe.db.get_value( | |||||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" | |||||
), | |||||
["test@example.com", "test1@example.com", "test2@example.com"], | |||||
) | |||||
frappe.flags.assignment_day = "Friday" | frappe.flags.assignment_day = "Friday" | ||||
note = make_note(dict(public=1)) | note = make_note(dict(public=1)) | ||||
self.assertIn(frappe.db.get_value('ToDo', dict( | |||||
reference_type = 'Note', | |||||
reference_name = note.name, | |||||
status = 'Open' | |||||
), 'allocated_to'), ['test3@example.com']) | |||||
self.assertIn( | |||||
frappe.db.get_value( | |||||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to" | |||||
), | |||||
["test3@example.com"], | |||||
) | |||||
def test_assignment_rule_condition(self): | def test_assignment_rule_condition(self): | ||||
frappe.db.delete("Assignment Rule") | frappe.db.delete("Assignment Rule") | ||||
# Add expiry_date custom field | # Add expiry_date custom field | ||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field | from frappe.custom.doctype.custom_field.custom_field import create_custom_field | ||||
df = dict(fieldname='expiry_date', label='Expiry Date', fieldtype='Date') | |||||
create_custom_field('Note', df) | |||||
assignment_rule = frappe.get_doc(dict( | |||||
name = 'Assignment with Due Date', | |||||
doctype = 'Assignment Rule', | |||||
document_type = 'Note', | |||||
assign_condition = 'public == 0', | |||||
due_date_based_on = 'expiry_date', | |||||
assignment_days = self.days, | |||||
users = [ | |||||
dict(user = 'test@example.com'), | |||||
] | |||||
)).insert() | |||||
df = dict(fieldname="expiry_date", label="Expiry Date", fieldtype="Date") | |||||
create_custom_field("Note", df) | |||||
assignment_rule = frappe.get_doc( | |||||
dict( | |||||
name="Assignment with Due Date", | |||||
doctype="Assignment Rule", | |||||
document_type="Note", | |||||
assign_condition="public == 0", | |||||
due_date_based_on="expiry_date", | |||||
assignment_days=self.days, | |||||
users=[ | |||||
dict(user="test@example.com"), | |||||
], | |||||
) | |||||
).insert() | |||||
expiry_date = frappe.utils.add_days(frappe.utils.nowdate(), 2) | expiry_date = frappe.utils.add_days(frappe.utils.nowdate(), 2) | ||||
note1 = make_note({'expiry_date': expiry_date}) | |||||
note2 = make_note({'expiry_date': expiry_date}) | |||||
note1 = make_note({"expiry_date": expiry_date}) | |||||
note2 = make_note({"expiry_date": expiry_date}) | |||||
note1_todo = frappe.get_all('ToDo', filters=dict( | |||||
reference_type = 'Note', | |||||
reference_name = note1.name, | |||||
status = 'Open' | |||||
))[0] | |||||
note1_todo = frappe.get_all( | |||||
"ToDo", filters=dict(reference_type="Note", reference_name=note1.name, status="Open") | |||||
)[0] | |||||
note1_todo_doc = frappe.get_doc('ToDo', note1_todo.name) | |||||
note1_todo_doc = frappe.get_doc("ToDo", note1_todo.name) | |||||
self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), expiry_date) | self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), expiry_date) | ||||
# due date should be updated if the reference doc's date is updated. | # due date should be updated if the reference doc's date is updated. | ||||
@@ -253,66 +266,67 @@ class TestAutoAssign(unittest.TestCase): | |||||
self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), note1.expiry_date) | self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), note1.expiry_date) | ||||
# saving one note's expiry should not update other note todo's due date | # saving one note's expiry should not update other note todo's due date | ||||
note2_todo = frappe.get_all('ToDo', filters=dict( | |||||
reference_type = 'Note', | |||||
reference_name = note2.name, | |||||
status = 'Open' | |||||
), fields=['name', 'date'])[0] | |||||
note2_todo = frappe.get_all( | |||||
"ToDo", | |||||
filters=dict(reference_type="Note", reference_name=note2.name, status="Open"), | |||||
fields=["name", "date"], | |||||
)[0] | |||||
self.assertNotEqual(frappe.utils.get_date_str(note2_todo.date), note1.expiry_date) | self.assertNotEqual(frappe.utils.get_date_str(note2_todo.date), note1.expiry_date) | ||||
self.assertEqual(frappe.utils.get_date_str(note2_todo.date), expiry_date) | self.assertEqual(frappe.utils.get_date_str(note2_todo.date), expiry_date) | ||||
assignment_rule.delete() | assignment_rule.delete() | ||||
def clear_assignments(): | def clear_assignments(): | ||||
frappe.db.delete("ToDo", {"reference_type": "Note"}) | frappe.db.delete("ToDo", {"reference_type": "Note"}) | ||||
def get_assignment_rule(days, assign=None): | def get_assignment_rule(days, assign=None): | ||||
frappe.delete_doc_if_exists('Assignment Rule', 'For Note 1') | |||||
frappe.delete_doc_if_exists("Assignment Rule", "For Note 1") | |||||
if not assign: | if not assign: | ||||
assign = ['public == 1', 'notify_on_login == 1'] | |||||
assignment_rule = frappe.get_doc(dict( | |||||
name = 'For Note 1', | |||||
doctype = 'Assignment Rule', | |||||
priority = 0, | |||||
document_type = 'Note', | |||||
assign_condition = assign[0], | |||||
unassign_condition = 'public == 0 or notify_on_login == 1', | |||||
close_condition = '"Closed" in content', | |||||
rule = 'Round Robin', | |||||
assignment_days = days[0], | |||||
users = [ | |||||
dict(user = 'test@example.com'), | |||||
dict(user = 'test1@example.com'), | |||||
dict(user = 'test2@example.com'), | |||||
] | |||||
)).insert() | |||||
frappe.delete_doc_if_exists('Assignment Rule', 'For Note 2') | |||||
assign = ["public == 1", "notify_on_login == 1"] | |||||
assignment_rule = frappe.get_doc( | |||||
dict( | |||||
name="For Note 1", | |||||
doctype="Assignment Rule", | |||||
priority=0, | |||||
document_type="Note", | |||||
assign_condition=assign[0], | |||||
unassign_condition="public == 0 or notify_on_login == 1", | |||||
close_condition='"Closed" in content', | |||||
rule="Round Robin", | |||||
assignment_days=days[0], | |||||
users=[ | |||||
dict(user="test@example.com"), | |||||
dict(user="test1@example.com"), | |||||
dict(user="test2@example.com"), | |||||
], | |||||
) | |||||
).insert() | |||||
frappe.delete_doc_if_exists("Assignment Rule", "For Note 2") | |||||
# 2nd rule | # 2nd rule | ||||
frappe.get_doc(dict( | |||||
name = 'For Note 2', | |||||
doctype = 'Assignment Rule', | |||||
priority = 1, | |||||
document_type = 'Note', | |||||
assign_condition = assign[1], | |||||
unassign_condition = 'notify_on_login == 0', | |||||
rule = 'Round Robin', | |||||
assignment_days = days[1], | |||||
users = [ | |||||
dict(user = 'test3@example.com') | |||||
] | |||||
)).insert() | |||||
frappe.get_doc( | |||||
dict( | |||||
name="For Note 2", | |||||
doctype="Assignment Rule", | |||||
priority=1, | |||||
document_type="Note", | |||||
assign_condition=assign[1], | |||||
unassign_condition="notify_on_login == 0", | |||||
rule="Round Robin", | |||||
assignment_days=days[1], | |||||
users=[dict(user="test3@example.com")], | |||||
) | |||||
).insert() | |||||
return assignment_rule | return assignment_rule | ||||
def make_note(values=None): | def make_note(values=None): | ||||
note = frappe.get_doc(dict( | |||||
doctype = 'Note', | |||||
title = random_string(10), | |||||
content = random_string(20) | |||||
)) | |||||
note = frappe.get_doc(dict(doctype="Note", title=random_string(10), content=random_string(20))) | |||||
if values: | if values: | ||||
note.update(values) | note.update(values) | ||||
@@ -5,5 +5,6 @@ | |||||
# import frappe | # import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class AssignmentRuleDay(Document): | class AssignmentRuleDay(Document): | ||||
pass | pass |
@@ -5,5 +5,6 @@ | |||||
# import frappe | # import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class AssignmentRuleUser(Document): | class AssignmentRuleUser(Document): | ||||
pass | pass |
@@ -2,23 +2,45 @@ | |||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors | # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
from datetime import timedelta | |||||
from dateutil.relativedelta import relativedelta | |||||
import frappe | import frappe | ||||
from frappe import _ | from frappe import _ | ||||
from datetime import timedelta | |||||
from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeated | |||||
from frappe.contacts.doctype.contact.contact import ( | |||||
get_contacts_linked_from, | |||||
get_contacts_linking_to, | |||||
) | |||||
from frappe.core.doctype.communication.email import make | |||||
from frappe.desk.form import assign_to | from frappe.desk.form import assign_to | ||||
from frappe.utils.jinja import validate_template | |||||
from dateutil.relativedelta import relativedelta | |||||
from frappe.utils.user import get_system_managers | |||||
from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_day, get_first_day, month_diff | |||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe.core.doctype.communication.email import make | |||||
from frappe.utils import ( | |||||
add_days, | |||||
cstr, | |||||
get_first_day, | |||||
get_last_day, | |||||
getdate, | |||||
month_diff, | |||||
split_emails, | |||||
today, | |||||
) | |||||
from frappe.utils.background_jobs import get_jobs | from frappe.utils.background_jobs import get_jobs | ||||
from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeated | |||||
from frappe.contacts.doctype.contact.contact import get_contacts_linked_from | |||||
from frappe.contacts.doctype.contact.contact import get_contacts_linking_to | |||||
from frappe.utils.jinja import validate_template | |||||
from frappe.utils.user import get_system_managers | |||||
month_map = {"Monthly": 1, "Quarterly": 3, "Half-yearly": 6, "Yearly": 12} | |||||
week_map = { | |||||
"Monday": 0, | |||||
"Tuesday": 1, | |||||
"Wednesday": 2, | |||||
"Thursday": 3, | |||||
"Friday": 4, | |||||
"Saturday": 5, | |||||
"Sunday": 6, | |||||
} | |||||
month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12} | |||||
week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6} | |||||
class AutoRepeat(Document): | class AutoRepeat(Document): | ||||
def validate(self): | def validate(self): | ||||
@@ -46,7 +68,7 @@ class AutoRepeat(Document): | |||||
frappe.get_doc(self.reference_doctype, self.reference_document).notify_update() | frappe.get_doc(self.reference_doctype, self.reference_document).notify_update() | ||||
def on_trash(self): | def on_trash(self): | ||||
frappe.db.set_value(self.reference_doctype, self.reference_document, 'auto_repeat', '') | |||||
frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", "") | |||||
frappe.get_doc(self.reference_doctype, self.reference_document).notify_update() | frappe.get_doc(self.reference_doctype, self.reference_document).notify_update() | ||||
def set_dates(self): | def set_dates(self): | ||||
@@ -56,29 +78,36 @@ class AutoRepeat(Document): | |||||
self.next_schedule_date = self.get_next_schedule_date(schedule_date=self.start_date) | self.next_schedule_date = self.get_next_schedule_date(schedule_date=self.start_date) | ||||
def unlink_if_applicable(self): | def unlink_if_applicable(self): | ||||
if self.status == 'Completed' or self.disabled: | |||||
frappe.db.set_value(self.reference_doctype, self.reference_document, 'auto_repeat', '') | |||||
if self.status == "Completed" or self.disabled: | |||||
frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", "") | |||||
def validate_reference_doctype(self): | def validate_reference_doctype(self): | ||||
if frappe.flags.in_test or frappe.flags.in_patch: | if frappe.flags.in_test or frappe.flags.in_patch: | ||||
return | return | ||||
if not frappe.get_meta(self.reference_doctype).allow_auto_repeat: | if not frappe.get_meta(self.reference_doctype).allow_auto_repeat: | ||||
frappe.throw(_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(self.reference_doctype)) | |||||
frappe.throw( | |||||
_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format( | |||||
self.reference_doctype | |||||
) | |||||
) | |||||
def validate_submit_on_creation(self): | def validate_submit_on_creation(self): | ||||
if self.submit_on_creation and not frappe.get_meta(self.reference_doctype).is_submittable: | if self.submit_on_creation and not frappe.get_meta(self.reference_doctype).is_submittable: | ||||
frappe.throw(_('Cannot enable {0} for a non-submittable doctype').format( | |||||
frappe.bold('Submit on Creation'))) | |||||
frappe.throw( | |||||
_("Cannot enable {0} for a non-submittable doctype").format(frappe.bold("Submit on Creation")) | |||||
) | |||||
def validate_dates(self): | def validate_dates(self): | ||||
if frappe.flags.in_patch: | if frappe.flags.in_patch: | ||||
return | return | ||||
if self.end_date: | if self.end_date: | ||||
self.validate_from_to_dates('start_date', 'end_date') | |||||
self.validate_from_to_dates("start_date", "end_date") | |||||
if self.end_date == self.start_date: | if self.end_date == self.start_date: | ||||
frappe.throw(_('{0} should not be same as {1}').format(frappe.bold('End Date'), frappe.bold('Start Date'))) | |||||
frappe.throw( | |||||
_("{0} should not be same as {1}").format(frappe.bold("End Date"), frappe.bold("Start Date")) | |||||
) | |||||
def validate_email_id(self): | def validate_email_id(self): | ||||
if self.notify_by_email: | if self.notify_by_email: | ||||
@@ -100,17 +129,17 @@ class AutoRepeat(Document): | |||||
frappe.throw( | frappe.throw( | ||||
_("Auto Repeat Day{0} {1} has been repeated.").format( | _("Auto Repeat Day{0} {1} has been repeated.").format( | ||||
plural, | |||||
frappe.bold(", ".join(repeated_days)) | |||||
plural, frappe.bold(", ".join(repeated_days)) | |||||
) | ) | ||||
) | ) | ||||
def update_auto_repeat_id(self): | def update_auto_repeat_id(self): | ||||
#check if document is already on auto repeat | |||||
# check if document is already on auto repeat | |||||
auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat") | auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat") | ||||
if auto_repeat and auto_repeat != self.name and not frappe.flags.in_patch: | if auto_repeat and auto_repeat != self.name and not frappe.flags.in_patch: | ||||
frappe.throw(_("The {0} is already on auto repeat {1}").format(self.reference_document, auto_repeat)) | |||||
frappe.throw( | |||||
_("The {0} is already on auto repeat {1}").format(self.reference_document, auto_repeat) | |||||
) | |||||
else: | else: | ||||
frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", self.name) | frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", self.name) | ||||
@@ -136,18 +165,18 @@ class AutoRepeat(Document): | |||||
row = { | row = { | ||||
"reference_document": self.reference_document, | "reference_document": self.reference_document, | ||||
"frequency": self.frequency, | "frequency": self.frequency, | ||||
"next_scheduled_date": next_date | |||||
"next_scheduled_date": next_date, | |||||
} | } | ||||
schedule_details.append(row) | schedule_details.append(row) | ||||
if self.end_date: | if self.end_date: | ||||
next_date = self.get_next_schedule_date(schedule_date=start_date, for_full_schedule=True) | next_date = self.get_next_schedule_date(schedule_date=start_date, for_full_schedule=True) | ||||
while (getdate(next_date) < getdate(end_date)): | |||||
while getdate(next_date) < getdate(end_date): | |||||
row = { | row = { | ||||
"reference_document" : self.reference_document, | |||||
"frequency" : self.frequency, | |||||
"next_scheduled_date" : next_date | |||||
"reference_document": self.reference_document, | |||||
"frequency": self.frequency, | |||||
"next_scheduled_date": next_date, | |||||
} | } | ||||
schedule_details.append(row) | schedule_details.append(row) | ||||
next_date = self.get_next_schedule_date(schedule_date=next_date, for_full_schedule=True) | next_date = self.get_next_schedule_date(schedule_date=next_date, for_full_schedule=True) | ||||
@@ -169,9 +198,9 @@ class AutoRepeat(Document): | |||||
def make_new_document(self): | def make_new_document(self): | ||||
reference_doc = frappe.get_doc(self.reference_doctype, self.reference_document) | reference_doc = frappe.get_doc(self.reference_doctype, self.reference_document) | ||||
new_doc = frappe.copy_doc(reference_doc, ignore_no_copy = False) | |||||
new_doc = frappe.copy_doc(reference_doc, ignore_no_copy=False) | |||||
self.update_doc(new_doc, reference_doc) | self.update_doc(new_doc, reference_doc) | ||||
new_doc.insert(ignore_permissions = True) | |||||
new_doc.insert(ignore_permissions=True) | |||||
if self.submit_on_creation: | if self.submit_on_creation: | ||||
new_doc.submit() | new_doc.submit() | ||||
@@ -180,61 +209,72 @@ class AutoRepeat(Document): | |||||
def update_doc(self, new_doc, reference_doc): | def update_doc(self, new_doc, reference_doc): | ||||
new_doc.docstatus = 0 | new_doc.docstatus = 0 | ||||
if new_doc.meta.get_field('set_posting_time'): | |||||
new_doc.set('set_posting_time', 1) | |||||
if new_doc.meta.get_field('auto_repeat'): | |||||
new_doc.set('auto_repeat', self.name) | |||||
for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'user_remark', 'remarks', 'owner']: | |||||
if new_doc.meta.get_field("set_posting_time"): | |||||
new_doc.set("set_posting_time", 1) | |||||
if new_doc.meta.get_field("auto_repeat"): | |||||
new_doc.set("auto_repeat", self.name) | |||||
for fieldname in [ | |||||
"naming_series", | |||||
"ignore_pricing_rule", | |||||
"posting_time", | |||||
"select_print_heading", | |||||
"user_remark", | |||||
"remarks", | |||||
"owner", | |||||
]: | |||||
if new_doc.meta.get_field(fieldname): | if new_doc.meta.get_field(fieldname): | ||||
new_doc.set(fieldname, reference_doc.get(fieldname)) | new_doc.set(fieldname, reference_doc.get(fieldname)) | ||||
for data in new_doc.meta.fields: | for data in new_doc.meta.fields: | ||||
if data.fieldtype == 'Date' and data.reqd: | |||||
if data.fieldtype == "Date" and data.reqd: | |||||
new_doc.set(data.fieldname, self.next_schedule_date) | new_doc.set(data.fieldname, self.next_schedule_date) | ||||
self.set_auto_repeat_period(new_doc) | self.set_auto_repeat_period(new_doc) | ||||
auto_repeat_doc = frappe.get_doc('Auto Repeat', self.name) | |||||
auto_repeat_doc = frappe.get_doc("Auto Repeat", self.name) | |||||
#for any action that needs to take place after the recurring document creation | |||||
#on recurring method of that doctype is triggered | |||||
new_doc.run_method('on_recurring', reference_doc = reference_doc, auto_repeat_doc = auto_repeat_doc) | |||||
# for any action that needs to take place after the recurring document creation | |||||
# on recurring method of that doctype is triggered | |||||
new_doc.run_method("on_recurring", reference_doc=reference_doc, auto_repeat_doc=auto_repeat_doc) | |||||
def set_auto_repeat_period(self, new_doc): | def set_auto_repeat_period(self, new_doc): | ||||
mcount = month_map.get(self.frequency) | mcount = month_map.get(self.frequency) | ||||
if mcount and new_doc.meta.get_field('from_date') and new_doc.meta.get_field('to_date'): | |||||
last_ref_doc = frappe.db.get_all(doctype = self.reference_doctype, | |||||
fields = ['name', 'from_date', 'to_date'], | |||||
filters = [ | |||||
['auto_repeat', '=', self.name], | |||||
['docstatus', '<', 2], | |||||
if mcount and new_doc.meta.get_field("from_date") and new_doc.meta.get_field("to_date"): | |||||
last_ref_doc = frappe.db.get_all( | |||||
doctype=self.reference_doctype, | |||||
fields=["name", "from_date", "to_date"], | |||||
filters=[ | |||||
["auto_repeat", "=", self.name], | |||||
["docstatus", "<", 2], | |||||
], | ], | ||||
order_by = 'creation desc', | |||||
limit = 1) | |||||
order_by="creation desc", | |||||
limit=1, | |||||
) | |||||
if not last_ref_doc: | if not last_ref_doc: | ||||
return | return | ||||
from_date = get_next_date(last_ref_doc[0].from_date, mcount) | from_date = get_next_date(last_ref_doc[0].from_date, mcount) | ||||
if (cstr(get_first_day(last_ref_doc[0].from_date)) == cstr(last_ref_doc[0].from_date)) and \ | |||||
(cstr(get_last_day(last_ref_doc[0].to_date)) == cstr(last_ref_doc[0].to_date)): | |||||
if (cstr(get_first_day(last_ref_doc[0].from_date)) == cstr(last_ref_doc[0].from_date)) and ( | |||||
cstr(get_last_day(last_ref_doc[0].to_date)) == cstr(last_ref_doc[0].to_date) | |||||
): | |||||
to_date = get_last_day(get_next_date(last_ref_doc[0].to_date, mcount)) | to_date = get_last_day(get_next_date(last_ref_doc[0].to_date, mcount)) | ||||
else: | else: | ||||
to_date = get_next_date(last_ref_doc[0].to_date, mcount) | to_date = get_next_date(last_ref_doc[0].to_date, mcount) | ||||
new_doc.set('from_date', from_date) | |||||
new_doc.set('to_date', to_date) | |||||
new_doc.set("from_date", from_date) | |||||
new_doc.set("to_date", to_date) | |||||
def get_next_schedule_date(self, schedule_date, for_full_schedule=False): | def get_next_schedule_date(self, schedule_date, for_full_schedule=False): | ||||
""" | """ | ||||
Returns the next schedule date for auto repeat after a recurring document has been created. | |||||
Adds required offset to the schedule_date param and returns the next schedule date. | |||||
Returns the next schedule date for auto repeat after a recurring document has been created. | |||||
Adds required offset to the schedule_date param and returns the next schedule date. | |||||
:param schedule_date: The date when the last recurring document was created. | |||||
:param for_full_schedule: If True, returns the immediate next schedule date, else the full schedule. | |||||
:param schedule_date: The date when the last recurring document was created. | |||||
:param for_full_schedule: If True, returns the immediate next schedule date, else the full schedule. | |||||
""" | """ | ||||
if month_map.get(self.frequency): | if month_map.get(self.frequency): | ||||
month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1 | month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1 | ||||
@@ -295,60 +335,75 @@ class AutoRepeat(Document): | |||||
return 7 | return 7 | ||||
def get_auto_repeat_days(self): | def get_auto_repeat_days(self): | ||||
return [d.day for d in self.get('repeat_on_days', [])] | |||||
return [d.day for d in self.get("repeat_on_days", [])] | |||||
def send_notification(self, new_doc): | def send_notification(self, new_doc): | ||||
"""Notify concerned people about recurring document generation""" | """Notify concerned people about recurring document generation""" | ||||
subject = self.subject or '' | |||||
message = self.message or '' | |||||
subject = self.subject or "" | |||||
message = self.message or "" | |||||
if not self.subject: | if not self.subject: | ||||
subject = _("New {0}: {1}").format(new_doc.doctype, new_doc.name) | subject = _("New {0}: {1}").format(new_doc.doctype, new_doc.name) | ||||
elif "{" in self.subject: | elif "{" in self.subject: | ||||
subject = frappe.render_template(self.subject, {'doc': new_doc}) | |||||
subject = frappe.render_template(self.subject, {"doc": new_doc}) | |||||
print_format = self.print_format or 'Standard' | |||||
print_format = self.print_format or "Standard" | |||||
error_string = None | error_string = None | ||||
try: | try: | ||||
attachments = [frappe.attach_print(new_doc.doctype, new_doc.name, | |||||
file_name=new_doc.name, print_format=print_format)] | |||||
attachments = [ | |||||
frappe.attach_print( | |||||
new_doc.doctype, new_doc.name, file_name=new_doc.name, print_format=print_format | |||||
) | |||||
] | |||||
except frappe.PermissionError: | except frappe.PermissionError: | ||||
error_string = _("A recurring {0} {1} has been created for you via Auto Repeat {2}.").format(new_doc.doctype, new_doc.name, self.name) | |||||
error_string = _("A recurring {0} {1} has been created for you via Auto Repeat {2}.").format( | |||||
new_doc.doctype, new_doc.name, self.name | |||||
) | |||||
error_string += "<br><br>" | error_string += "<br><br>" | ||||
error_string += _("{0}: Failed to attach new recurring document. To enable attaching document in the auto repeat notification email, enable {1} in Print Settings").format( | |||||
frappe.bold(_('Note')), | |||||
frappe.bold(_('Allow Print for Draft')) | |||||
) | |||||
attachments = '[]' | |||||
error_string += _( | |||||
"{0}: Failed to attach new recurring document. To enable attaching document in the auto repeat notification email, enable {1} in Print Settings" | |||||
).format(frappe.bold(_("Note")), frappe.bold(_("Allow Print for Draft"))) | |||||
attachments = "[]" | |||||
if error_string: | if error_string: | ||||
message = error_string | message = error_string | ||||
elif not self.message: | elif not self.message: | ||||
message = _("Please find attached {0}: {1}").format(new_doc.doctype, new_doc.name) | message = _("Please find attached {0}: {1}").format(new_doc.doctype, new_doc.name) | ||||
elif "{" in self.message: | elif "{" in self.message: | ||||
message = frappe.render_template(self.message, {'doc': new_doc}) | |||||
message = frappe.render_template(self.message, {"doc": new_doc}) | |||||
recipients = self.recipients.split('\n') | |||||
recipients = self.recipients.split("\n") | |||||
make(doctype=new_doc.doctype, name=new_doc.name, recipients=recipients, | |||||
subject=subject, content=message, attachments=attachments, send_email=1) | |||||
make( | |||||
doctype=new_doc.doctype, | |||||
name=new_doc.name, | |||||
recipients=recipients, | |||||
subject=subject, | |||||
content=message, | |||||
attachments=attachments, | |||||
send_email=1, | |||||
) | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def fetch_linked_contacts(self): | def fetch_linked_contacts(self): | ||||
if self.reference_doctype and self.reference_document: | if self.reference_doctype and self.reference_document: | ||||
res = get_contacts_linking_to(self.reference_doctype, self.reference_document, fields=['email_id']) | |||||
res += get_contacts_linked_from(self.reference_doctype, self.reference_document, fields=['email_id']) | |||||
res = get_contacts_linking_to( | |||||
self.reference_doctype, self.reference_document, fields=["email_id"] | |||||
) | |||||
res += get_contacts_linked_from( | |||||
self.reference_doctype, self.reference_document, fields=["email_id"] | |||||
) | |||||
email_ids = {d.email_id for d in res} | email_ids = {d.email_id for d in res} | ||||
if not email_ids: | if not email_ids: | ||||
frappe.msgprint(_('No contacts linked to document'), alert=True) | |||||
frappe.msgprint(_("No contacts linked to document"), alert=True) | |||||
else: | else: | ||||
self.recipients = ', '.join(email_ids) | |||||
self.recipients = ", ".join(email_ids) | |||||
def disable_auto_repeat(self): | def disable_auto_repeat(self): | ||||
frappe.db.set_value('Auto Repeat', self.name, 'disabled', 1) | |||||
frappe.db.set_value("Auto Repeat", self.name, "disabled", 1) | |||||
def notify_error_to_user(self, error_log): | def notify_error_to_user(self, error_log): | ||||
recipients = list(get_system_managers(only_name=True)) | recipients = list(get_system_managers(only_name=True)) | ||||
@@ -356,20 +411,17 @@ class AutoRepeat(Document): | |||||
subject = _("Auto Repeat Document Creation Failed") | subject = _("Auto Repeat Document Creation Failed") | ||||
form_link = frappe.utils.get_link_to_form(self.reference_doctype, self.reference_document) | form_link = frappe.utils.get_link_to_form(self.reference_doctype, self.reference_document) | ||||
auto_repeat_failed_for = _('Auto Repeat failed for {0}').format(form_link) | |||||
auto_repeat_failed_for = _("Auto Repeat failed for {0}").format(form_link) | |||||
error_log_link = frappe.utils.get_link_to_form('Error Log', error_log.name) | |||||
error_log_message = _('Check the Error Log for more information: {0}').format(error_log_link) | |||||
error_log_link = frappe.utils.get_link_to_form("Error Log", error_log.name) | |||||
error_log_message = _("Check the Error Log for more information: {0}").format(error_log_link) | |||||
frappe.sendmail( | frappe.sendmail( | ||||
recipients=recipients, | recipients=recipients, | ||||
subject=subject, | subject=subject, | ||||
template="auto_repeat_fail", | template="auto_repeat_fail", | ||||
args={ | |||||
'auto_repeat_failed_for': auto_repeat_failed_for, | |||||
'error_log_message': error_log_message | |||||
}, | |||||
header=[subject, 'red'] | |||||
args={"auto_repeat_failed_for": auto_repeat_failed_for, "error_log_message": error_log_message}, | |||||
header=[subject, "red"], | |||||
) | ) | ||||
@@ -382,18 +434,18 @@ def get_next_date(dt, mcount, day=None): | |||||
def get_next_weekday(current_schedule_day, weekdays): | def get_next_weekday(current_schedule_day, weekdays): | ||||
days = list(week_map.keys()) | days = list(week_map.keys()) | ||||
if current_schedule_day > 0: | if current_schedule_day > 0: | ||||
days = days[(current_schedule_day + 1):] + days[:current_schedule_day] | |||||
days = days[(current_schedule_day + 1) :] + days[:current_schedule_day] | |||||
else: | else: | ||||
days = days[(current_schedule_day + 1):] | |||||
days = days[(current_schedule_day + 1) :] | |||||
for entry in days: | for entry in days: | ||||
if entry in weekdays: | if entry in weekdays: | ||||
return entry | return entry | ||||
#called through hooks | |||||
# called through hooks | |||||
def make_auto_repeat_entry(): | def make_auto_repeat_entry(): | ||||
enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries' | |||||
enqueued_method = "frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries" | |||||
jobs = get_jobs() | jobs = get_jobs() | ||||
if not jobs or enqueued_method not in jobs[frappe.local.site]: | if not jobs or enqueued_method not in jobs[frappe.local.site]: | ||||
@@ -404,7 +456,7 @@ def make_auto_repeat_entry(): | |||||
def create_repeated_entries(data): | def create_repeated_entries(data): | ||||
for d in data: | for d in data: | ||||
doc = frappe.get_doc('Auto Repeat', d.name) | |||||
doc = frappe.get_doc("Auto Repeat", d.name) | |||||
current_date = getdate(today()) | current_date = getdate(today()) | ||||
schedule_date = getdate(doc.next_schedule_date) | schedule_date = getdate(doc.next_schedule_date) | ||||
@@ -413,33 +465,32 @@ def create_repeated_entries(data): | |||||
doc.create_documents() | doc.create_documents() | ||||
schedule_date = doc.get_next_schedule_date(schedule_date=schedule_date) | schedule_date = doc.get_next_schedule_date(schedule_date=schedule_date) | ||||
if schedule_date and not doc.disabled: | if schedule_date and not doc.disabled: | ||||
frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date) | |||||
frappe.db.set_value("Auto Repeat", doc.name, "next_schedule_date", schedule_date) | |||||
def get_auto_repeat_entries(date=None): | def get_auto_repeat_entries(date=None): | ||||
if not date: | if not date: | ||||
date = getdate(today()) | date = getdate(today()) | ||||
return frappe.db.get_all('Auto Repeat', filters=[ | |||||
['next_schedule_date', '<=', date], | |||||
['status', '=', 'Active'] | |||||
]) | |||||
return frappe.db.get_all( | |||||
"Auto Repeat", filters=[["next_schedule_date", "<=", date], ["status", "=", "Active"]] | |||||
) | |||||
#called through hooks | |||||
# called through hooks | |||||
def set_auto_repeat_as_completed(): | def set_auto_repeat_as_completed(): | ||||
auto_repeat = frappe.get_all("Auto Repeat", filters = {'status': ['!=', 'Disabled']}) | |||||
auto_repeat = frappe.get_all("Auto Repeat", filters={"status": ["!=", "Disabled"]}) | |||||
for entry in auto_repeat: | for entry in auto_repeat: | ||||
doc = frappe.get_doc("Auto Repeat", entry.name) | doc = frappe.get_doc("Auto Repeat", entry.name) | ||||
if doc.is_completed(): | if doc.is_completed(): | ||||
doc.status = 'Completed' | |||||
doc.status = "Completed" | |||||
doc.save() | doc.save() | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, end_date = None): | |||||
def make_auto_repeat(doctype, docname, frequency="Daily", start_date=None, end_date=None): | |||||
if not start_date: | if not start_date: | ||||
start_date = getdate(today()) | start_date = getdate(today()) | ||||
doc = frappe.new_doc('Auto Repeat') | |||||
doc = frappe.new_doc("Auto Repeat") | |||||
doc.reference_doctype = doctype | doc.reference_doctype = doctype | ||||
doc.reference_document = docname | doc.reference_document = docname | ||||
doc.frequency = frequency | doc.frequency = frequency | ||||
@@ -449,24 +500,34 @@ def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, e | |||||
doc.save() | doc.save() | ||||
return doc | return doc | ||||
# method for reference_doctype filter | # method for reference_doctype filter | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
@frappe.validate_and_sanitize_search_inputs | @frappe.validate_and_sanitize_search_inputs | ||||
def get_auto_repeat_doctypes(doctype, txt, searchfield, start, page_len, filters): | def get_auto_repeat_doctypes(doctype, txt, searchfield, start, page_len, filters): | ||||
res = frappe.db.get_all('Property Setter', { | |||||
'property': 'allow_auto_repeat', | |||||
'value': '1', | |||||
}, ['doc_type']) | |||||
res = frappe.db.get_all( | |||||
"Property Setter", | |||||
{ | |||||
"property": "allow_auto_repeat", | |||||
"value": "1", | |||||
}, | |||||
["doc_type"], | |||||
) | |||||
docs = [r.doc_type for r in res] | docs = [r.doc_type for r in res] | ||||
res = frappe.db.get_all('DocType', { | |||||
'allow_auto_repeat': 1, | |||||
}, ['name']) | |||||
res = frappe.db.get_all( | |||||
"DocType", | |||||
{ | |||||
"allow_auto_repeat": 1, | |||||
}, | |||||
["name"], | |||||
) | |||||
docs += [r.name for r in res] | docs += [r.name for r in res] | ||||
docs = set(list(docs)) | docs = set(list(docs)) | ||||
return [[d] for d in docs] | return [[d] for d in docs] | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def update_reference(docname, reference): | def update_reference(docname, reference): | ||||
result = "" | result = "" | ||||
@@ -478,13 +539,14 @@ def update_reference(docname, reference): | |||||
raise e | raise e | ||||
return result | return result | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def generate_message_preview(reference_dt, reference_doc, message=None, subject=None): | def generate_message_preview(reference_dt, reference_doc, message=None, subject=None): | ||||
frappe.has_permission("Auto Repeat", "write", throw=True) | frappe.has_permission("Auto Repeat", "write", throw=True) | ||||
doc = frappe.get_doc(reference_dt, reference_doc) | doc = frappe.get_doc(reference_dt, reference_doc) | ||||
subject_preview = _("Please add a subject to your email") | subject_preview = _("Please add a subject to your email") | ||||
msg_preview = frappe.render_template(message, {'doc': doc}) | |||||
msg_preview = frappe.render_template(message, {"doc": doc}) | |||||
if subject: | if subject: | ||||
subject_preview = frappe.render_template(subject, {'doc': doc}) | |||||
subject_preview = frappe.render_template(subject, {"doc": doc}) | |||||
return {'message': msg_preview, 'subject': subject_preview} | |||||
return {"message": msg_preview, "subject": subject_preview} |
@@ -4,24 +4,40 @@ | |||||
import unittest | import unittest | ||||
import frappe | import frappe | ||||
from frappe.automation.doctype.auto_repeat.auto_repeat import ( | |||||
create_repeated_entries, | |||||
get_auto_repeat_entries, | |||||
week_map, | |||||
) | |||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field | from frappe.custom.doctype.custom_field.custom_field import create_custom_field | ||||
from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries, week_map | |||||
from frappe.utils import today, add_days, getdate, add_months | |||||
from frappe.utils import add_days, add_months, getdate, today | |||||
def add_custom_fields(): | def add_custom_fields(): | ||||
df = dict( | df = dict( | ||||
fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', insert_after='sender', | |||||
options='Auto Repeat', hidden=1, print_hide=1, read_only=1) | |||||
create_custom_field('ToDo', df) | |||||
fieldname="auto_repeat", | |||||
label="Auto Repeat", | |||||
fieldtype="Link", | |||||
insert_after="sender", | |||||
options="Auto Repeat", | |||||
hidden=1, | |||||
print_hide=1, | |||||
read_only=1, | |||||
) | |||||
create_custom_field("ToDo", df) | |||||
class TestAutoRepeat(unittest.TestCase): | class TestAutoRepeat(unittest.TestCase): | ||||
def setUp(self): | def setUp(self): | ||||
if not frappe.db.sql("SELECT `fieldname` FROM `tabCustom Field` WHERE `fieldname`='auto_repeat' and `dt`=%s", "Todo"): | |||||
if not frappe.db.sql( | |||||
"SELECT `fieldname` FROM `tabCustom Field` WHERE `fieldname`='auto_repeat' and `dt`=%s", "Todo" | |||||
): | |||||
add_custom_fields() | add_custom_fields() | ||||
def test_daily_auto_repeat(self): | def test_daily_auto_repeat(self): | ||||
todo = frappe.get_doc( | todo = frappe.get_doc( | ||||
dict(doctype='ToDo', description='test recurring todo', assigned_by='Administrator')).insert() | |||||
dict(doctype="ToDo", description="test recurring todo", assigned_by="Administrator") | |||||
).insert() | |||||
doc = make_auto_repeat(reference_document=todo.name) | doc = make_auto_repeat(reference_document=todo.name) | ||||
self.assertEqual(doc.next_schedule_date, today()) | self.assertEqual(doc.next_schedule_date, today()) | ||||
@@ -32,19 +48,25 @@ class TestAutoRepeat(unittest.TestCase): | |||||
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document) | todo = frappe.get_doc(doc.reference_doctype, doc.reference_document) | ||||
self.assertEqual(todo.auto_repeat, doc.name) | self.assertEqual(todo.auto_repeat, doc.name) | ||||
new_todo = frappe.db.get_value('ToDo', | |||||
{'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name') | |||||
new_todo = frappe.db.get_value( | |||||
"ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name" | |||||
) | |||||
new_todo = frappe.get_doc('ToDo', new_todo) | |||||
new_todo = frappe.get_doc("ToDo", new_todo) | |||||
self.assertEqual(todo.get('description'), new_todo.get('description')) | |||||
self.assertEqual(todo.get("description"), new_todo.get("description")) | |||||
def test_weekly_auto_repeat(self): | def test_weekly_auto_repeat(self): | ||||
todo = frappe.get_doc( | todo = frappe.get_doc( | ||||
dict(doctype='ToDo', description='test weekly todo', assigned_by='Administrator')).insert() | |||||
dict(doctype="ToDo", description="test weekly todo", assigned_by="Administrator") | |||||
).insert() | |||||
doc = make_auto_repeat(reference_doctype='ToDo', | |||||
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7)) | |||||
doc = make_auto_repeat( | |||||
reference_doctype="ToDo", | |||||
frequency="Weekly", | |||||
reference_document=todo.name, | |||||
start_date=add_days(today(), -7), | |||||
) | |||||
self.assertEqual(doc.next_schedule_date, today()) | self.assertEqual(doc.next_schedule_date, today()) | ||||
data = get_auto_repeat_entries(getdate(today())) | data = get_auto_repeat_entries(getdate(today())) | ||||
@@ -54,25 +76,29 @@ class TestAutoRepeat(unittest.TestCase): | |||||
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document) | todo = frappe.get_doc(doc.reference_doctype, doc.reference_document) | ||||
self.assertEqual(todo.auto_repeat, doc.name) | self.assertEqual(todo.auto_repeat, doc.name) | ||||
new_todo = frappe.db.get_value('ToDo', | |||||
{'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name') | |||||
new_todo = frappe.db.get_value( | |||||
"ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name" | |||||
) | |||||
new_todo = frappe.get_doc('ToDo', new_todo) | |||||
new_todo = frappe.get_doc("ToDo", new_todo) | |||||
self.assertEqual(todo.get('description'), new_todo.get('description')) | |||||
self.assertEqual(todo.get("description"), new_todo.get("description")) | |||||
def test_weekly_auto_repeat_with_weekdays(self): | def test_weekly_auto_repeat_with_weekdays(self): | ||||
todo = frappe.get_doc( | todo = frappe.get_doc( | ||||
dict(doctype='ToDo', description='test auto repeat with weekdays', assigned_by='Administrator')).insert() | |||||
dict(doctype="ToDo", description="test auto repeat with weekdays", assigned_by="Administrator") | |||||
).insert() | |||||
weekdays = list(week_map.keys()) | weekdays = list(week_map.keys()) | ||||
current_weekday = getdate().weekday() | current_weekday = getdate().weekday() | ||||
days = [ | |||||
{'day': weekdays[current_weekday]}, | |||||
{'day': weekdays[(current_weekday + 2) % 7]} | |||||
] | |||||
doc = make_auto_repeat(reference_doctype='ToDo', | |||||
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7), days=days) | |||||
days = [{"day": weekdays[current_weekday]}, {"day": weekdays[(current_weekday + 2) % 7]}] | |||||
doc = make_auto_repeat( | |||||
reference_doctype="ToDo", | |||||
frequency="Weekly", | |||||
reference_document=todo.name, | |||||
start_date=add_days(today(), -7), | |||||
days=days, | |||||
) | |||||
self.assertEqual(doc.next_schedule_date, today()) | self.assertEqual(doc.next_schedule_date, today()) | ||||
data = get_auto_repeat_entries(getdate(today())) | data = get_auto_repeat_entries(getdate(today())) | ||||
@@ -90,136 +116,173 @@ class TestAutoRepeat(unittest.TestCase): | |||||
end_date = add_months(start_date, 12) | end_date = add_months(start_date, 12) | ||||
todo = frappe.get_doc( | todo = frappe.get_doc( | ||||
dict(doctype='ToDo', description='test recurring todo', assigned_by='Administrator')).insert() | |||||
dict(doctype="ToDo", description="test recurring todo", assigned_by="Administrator") | |||||
).insert() | |||||
self.monthly_auto_repeat('ToDo', todo.name, start_date, end_date) | |||||
#test without end_date | |||||
todo = frappe.get_doc(dict(doctype='ToDo', description='test recurring todo without end_date', assigned_by='Administrator')).insert() | |||||
self.monthly_auto_repeat('ToDo', todo.name, start_date) | |||||
self.monthly_auto_repeat("ToDo", todo.name, start_date, end_date) | |||||
# test without end_date | |||||
todo = frappe.get_doc( | |||||
dict( | |||||
doctype="ToDo", description="test recurring todo without end_date", assigned_by="Administrator" | |||||
) | |||||
).insert() | |||||
self.monthly_auto_repeat("ToDo", todo.name, start_date) | |||||
def monthly_auto_repeat(self, doctype, docname, start_date, end_date = None): | |||||
def monthly_auto_repeat(self, doctype, docname, start_date, end_date=None): | |||||
def get_months(start, end): | def get_months(start, end): | ||||
diff = (12 * end.year + end.month) - (12 * start.year + start.month) | diff = (12 * end.year + end.month) - (12 * start.year + start.month) | ||||
return diff + 1 | return diff + 1 | ||||
doc = make_auto_repeat( | doc = make_auto_repeat( | ||||
reference_doctype=doctype, frequency='Monthly', reference_document=docname, start_date=start_date, | |||||
end_date=end_date) | |||||
reference_doctype=doctype, | |||||
frequency="Monthly", | |||||
reference_document=docname, | |||||
start_date=start_date, | |||||
end_date=end_date, | |||||
) | |||||
doc.disable_auto_repeat() | doc.disable_auto_repeat() | ||||
data = get_auto_repeat_entries(getdate(today())) | data = get_auto_repeat_entries(getdate(today())) | ||||
create_repeated_entries(data) | create_repeated_entries(data) | ||||
docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name}) | |||||
docnames = frappe.get_all(doc.reference_doctype, {"auto_repeat": doc.name}) | |||||
self.assertEqual(len(docnames), 1) | self.assertEqual(len(docnames), 1) | ||||
doc = frappe.get_doc('Auto Repeat', doc.name) | |||||
doc.db_set('disabled', 0) | |||||
doc = frappe.get_doc("Auto Repeat", doc.name) | |||||
doc.db_set("disabled", 0) | |||||
months = get_months(getdate(start_date), getdate(today())) | months = get_months(getdate(start_date), getdate(today())) | ||||
data = get_auto_repeat_entries(getdate(today())) | data = get_auto_repeat_entries(getdate(today())) | ||||
create_repeated_entries(data) | create_repeated_entries(data) | ||||
docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name}) | |||||
docnames = frappe.get_all(doc.reference_doctype, {"auto_repeat": doc.name}) | |||||
self.assertEqual(len(docnames), months) | self.assertEqual(len(docnames), months) | ||||
def test_notification_is_attached(self): | def test_notification_is_attached(self): | ||||
todo = frappe.get_doc( | todo = frappe.get_doc( | ||||
dict(doctype='ToDo', description='Test recurring notification attachment', assigned_by='Administrator')).insert() | |||||
dict( | |||||
doctype="ToDo", | |||||
description="Test recurring notification attachment", | |||||
assigned_by="Administrator", | |||||
) | |||||
).insert() | |||||
doc = make_auto_repeat(reference_document=todo.name, notify=1, recipients="test@domain.com", subject="New ToDo", | |||||
message="A new ToDo has just been created for you") | |||||
doc = make_auto_repeat( | |||||
reference_document=todo.name, | |||||
notify=1, | |||||
recipients="test@domain.com", | |||||
subject="New ToDo", | |||||
message="A new ToDo has just been created for you", | |||||
) | |||||
data = get_auto_repeat_entries(getdate(today())) | data = get_auto_repeat_entries(getdate(today())) | ||||
create_repeated_entries(data) | create_repeated_entries(data) | ||||
frappe.db.commit() | frappe.db.commit() | ||||
new_todo = frappe.db.get_value('ToDo', | |||||
{'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name') | |||||
new_todo = frappe.db.get_value( | |||||
"ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name" | |||||
) | |||||
linked_comm = frappe.db.exists("Communication", dict(reference_doctype="ToDo", reference_name=new_todo)) | |||||
linked_comm = frappe.db.exists( | |||||
"Communication", dict(reference_doctype="ToDo", reference_name=new_todo) | |||||
) | |||||
self.assertTrue(linked_comm) | self.assertTrue(linked_comm) | ||||
def test_next_schedule_date(self): | def test_next_schedule_date(self): | ||||
current_date = getdate(today()) | current_date = getdate(today()) | ||||
todo = frappe.get_doc( | todo = frappe.get_doc( | ||||
dict(doctype='ToDo', description='test next schedule date for monthly', assigned_by='Administrator')).insert() | |||||
doc = make_auto_repeat(frequency='Monthly', reference_document=todo.name, start_date=add_months(today(), -2)) | |||||
dict( | |||||
doctype="ToDo", description="test next schedule date for monthly", assigned_by="Administrator" | |||||
) | |||||
).insert() | |||||
doc = make_auto_repeat( | |||||
frequency="Monthly", reference_document=todo.name, start_date=add_months(today(), -2) | |||||
) | |||||
# next_schedule_date is set as on or after current date | # next_schedule_date is set as on or after current date | ||||
# it should not be a previous month's date | # it should not be a previous month's date | ||||
self.assertTrue((doc.next_schedule_date >= current_date)) | self.assertTrue((doc.next_schedule_date >= current_date)) | ||||
todo = frappe.get_doc( | todo = frappe.get_doc( | ||||
dict(doctype='ToDo', description='test next schedule date for daily', assigned_by='Administrator')).insert() | |||||
doc = make_auto_repeat(frequency='Daily', reference_document=todo.name, start_date=add_days(today(), -2)) | |||||
dict( | |||||
doctype="ToDo", description="test next schedule date for daily", assigned_by="Administrator" | |||||
) | |||||
).insert() | |||||
doc = make_auto_repeat( | |||||
frequency="Daily", reference_document=todo.name, start_date=add_days(today(), -2) | |||||
) | |||||
self.assertEqual(getdate(doc.next_schedule_date), current_date) | self.assertEqual(getdate(doc.next_schedule_date), current_date) | ||||
def test_submit_on_creation(self): | def test_submit_on_creation(self): | ||||
doctype = 'Test Submittable DocType' | |||||
doctype = "Test Submittable DocType" | |||||
create_submittable_doctype(doctype) | create_submittable_doctype(doctype) | ||||
current_date = getdate() | current_date = getdate() | ||||
submittable_doc = frappe.get_doc(dict(doctype=doctype, test='test submit on creation')).insert() | |||||
submittable_doc = frappe.get_doc(dict(doctype=doctype, test="test submit on creation")).insert() | |||||
submittable_doc.submit() | submittable_doc.submit() | ||||
doc = make_auto_repeat(frequency='Daily', reference_doctype=doctype, reference_document=submittable_doc.name, | |||||
start_date=add_days(current_date, -1), submit_on_creation=1) | |||||
doc = make_auto_repeat( | |||||
frequency="Daily", | |||||
reference_doctype=doctype, | |||||
reference_document=submittable_doc.name, | |||||
start_date=add_days(current_date, -1), | |||||
submit_on_creation=1, | |||||
) | |||||
data = get_auto_repeat_entries(current_date) | data = get_auto_repeat_entries(current_date) | ||||
create_repeated_entries(data) | create_repeated_entries(data) | ||||
docnames = frappe.db.get_all(doc.reference_doctype, | |||||
filters={'auto_repeat': doc.name}, | |||||
fields=['docstatus'], | |||||
limit=1 | |||||
docnames = frappe.db.get_all( | |||||
doc.reference_doctype, filters={"auto_repeat": doc.name}, fields=["docstatus"], limit=1 | |||||
) | ) | ||||
self.assertEqual(docnames[0].docstatus, 1) | self.assertEqual(docnames[0].docstatus, 1) | ||||
def make_auto_repeat(**args): | def make_auto_repeat(**args): | ||||
args = frappe._dict(args) | args = frappe._dict(args) | ||||
doc = frappe.get_doc({ | |||||
'doctype': 'Auto Repeat', | |||||
'reference_doctype': args.reference_doctype or 'ToDo', | |||||
'reference_document': args.reference_document or frappe.db.get_value('ToDo', 'name'), | |||||
'submit_on_creation': args.submit_on_creation or 0, | |||||
'frequency': args.frequency or 'Daily', | |||||
'start_date': args.start_date or add_days(today(), -1), | |||||
'end_date': args.end_date or "", | |||||
'notify_by_email': args.notify or 0, | |||||
'recipients': args.recipients or "", | |||||
'subject': args.subject or "", | |||||
'message': args.message or "", | |||||
'repeat_on_days': args.days or [] | |||||
}).insert(ignore_permissions=True) | |||||
doc = frappe.get_doc( | |||||
{ | |||||
"doctype": "Auto Repeat", | |||||
"reference_doctype": args.reference_doctype or "ToDo", | |||||
"reference_document": args.reference_document or frappe.db.get_value("ToDo", "name"), | |||||
"submit_on_creation": args.submit_on_creation or 0, | |||||
"frequency": args.frequency or "Daily", | |||||
"start_date": args.start_date or add_days(today(), -1), | |||||
"end_date": args.end_date or "", | |||||
"notify_by_email": args.notify or 0, | |||||
"recipients": args.recipients or "", | |||||
"subject": args.subject or "", | |||||
"message": args.message or "", | |||||
"repeat_on_days": args.days or [], | |||||
} | |||||
).insert(ignore_permissions=True) | |||||
return doc | return doc | ||||
def create_submittable_doctype(doctype, submit_perms=1): | def create_submittable_doctype(doctype, submit_perms=1): | ||||
if frappe.db.exists('DocType', doctype): | |||||
if frappe.db.exists("DocType", doctype): | |||||
return | return | ||||
else: | else: | ||||
doc = frappe.get_doc({ | |||||
'doctype': 'DocType', | |||||
'__newname': doctype, | |||||
'module': 'Custom', | |||||
'custom': 1, | |||||
'is_submittable': 1, | |||||
'fields': [{ | |||||
'fieldname': 'test', | |||||
'label': 'Test', | |||||
'fieldtype': 'Data' | |||||
}], | |||||
'permissions': [{ | |||||
'role': 'System Manager', | |||||
'read': 1, | |||||
'write': 1, | |||||
'create': 1, | |||||
'delete': 1, | |||||
'submit': submit_perms, | |||||
'cancel': submit_perms, | |||||
'amend': submit_perms | |||||
}] | |||||
}).insert() | |||||
doc = frappe.get_doc( | |||||
{ | |||||
"doctype": "DocType", | |||||
"__newname": doctype, | |||||
"module": "Custom", | |||||
"custom": 1, | |||||
"is_submittable": 1, | |||||
"fields": [{"fieldname": "test", "label": "Test", "fieldtype": "Data"}], | |||||
"permissions": [ | |||||
{ | |||||
"role": "System Manager", | |||||
"read": 1, | |||||
"write": 1, | |||||
"create": 1, | |||||
"delete": 1, | |||||
"submit": submit_perms, | |||||
"cancel": submit_perms, | |||||
"amend": submit_perms, | |||||
} | |||||
], | |||||
} | |||||
).insert() | |||||
doc.allow_auto_repeat = 1 | doc.allow_auto_repeat = 1 | ||||
doc.save() | |||||
doc.save() |
@@ -5,5 +5,6 @@ | |||||
# import frappe | # import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class AutoRepeatDay(Document): | class AutoRepeatDay(Document): | ||||
pass | pass |
@@ -5,8 +5,10 @@ | |||||
import frappe | import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class Milestone(Document): | class Milestone(Document): | ||||
pass | pass | ||||
def on_doctype_update(): | def on_doctype_update(): | ||||
frappe.db.add_index("Milestone", ["reference_type", "reference_name"]) | frappe.db.add_index("Milestone", ["reference_type", "reference_name"]) |
@@ -1,8 +1,9 @@ | |||||
# -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
# Copyright (c) 2019, Frappe Technologies and Contributors | # Copyright (c) 2019, Frappe Technologies and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
#import frappe | |||||
# import frappe | |||||
import unittest | import unittest | ||||
class TestMilestone(unittest.TestCase): | class TestMilestone(unittest.TestCase): | ||||
pass | pass |
@@ -3,43 +3,50 @@ | |||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | import frappe | ||||
from frappe.model.document import Document | |||||
import frappe.cache_manager | import frappe.cache_manager | ||||
from frappe.model import log_types | from frappe.model import log_types | ||||
from frappe.model.document import Document | |||||
class MilestoneTracker(Document): | class MilestoneTracker(Document): | ||||
def on_update(self): | def on_update(self): | ||||
frappe.cache_manager.clear_doctype_map('Milestone Tracker', self.document_type) | |||||
frappe.cache_manager.clear_doctype_map("Milestone Tracker", self.document_type) | |||||
def on_trash(self): | def on_trash(self): | ||||
frappe.cache_manager.clear_doctype_map('Milestone Tracker', self.document_type) | |||||
frappe.cache_manager.clear_doctype_map("Milestone Tracker", self.document_type) | |||||
def apply(self, doc): | def apply(self, doc): | ||||
before_save = doc.get_doc_before_save() | before_save = doc.get_doc_before_save() | ||||
from_value = before_save and before_save.get(self.track_field) or None | from_value = before_save and before_save.get(self.track_field) or None | ||||
if from_value != doc.get(self.track_field): | if from_value != doc.get(self.track_field): | ||||
frappe.get_doc(dict( | |||||
doctype = 'Milestone', | |||||
reference_type = doc.doctype, | |||||
reference_name = doc.name, | |||||
track_field = self.track_field, | |||||
from_value = from_value, | |||||
value = doc.get(self.track_field), | |||||
milestone_tracker = self.name, | |||||
)).insert(ignore_permissions=True) | |||||
frappe.get_doc( | |||||
dict( | |||||
doctype="Milestone", | |||||
reference_type=doc.doctype, | |||||
reference_name=doc.name, | |||||
track_field=self.track_field, | |||||
from_value=from_value, | |||||
value=doc.get(self.track_field), | |||||
milestone_tracker=self.name, | |||||
) | |||||
).insert(ignore_permissions=True) | |||||
def evaluate_milestone(doc, event): | def evaluate_milestone(doc, event): | ||||
if (frappe.flags.in_install | |||||
if ( | |||||
frappe.flags.in_install | |||||
or frappe.flags.in_migrate | or frappe.flags.in_migrate | ||||
or frappe.flags.in_setup_wizard | or frappe.flags.in_setup_wizard | ||||
or doc.doctype in log_types): | |||||
or doc.doctype in log_types | |||||
): | |||||
return | return | ||||
# track milestones related to this doctype | # track milestones related to this doctype | ||||
for d in get_milestone_trackers(doc.doctype): | for d in get_milestone_trackers(doc.doctype): | ||||
frappe.get_doc('Milestone Tracker', d.get('name')).apply(doc) | |||||
frappe.get_doc("Milestone Tracker", d.get("name")).apply(doc) | |||||
def get_milestone_trackers(doctype): | |||||
return frappe.cache_manager.get_doctype_map('Milestone Tracker', doctype, | |||||
dict(document_type = doctype, disabled=0)) | |||||
def get_milestone_trackers(doctype): | |||||
return frappe.cache_manager.get_doctype_map( | |||||
"Milestone Tracker", doctype, dict(document_type=doctype, disabled=0) | |||||
) |
@@ -1,48 +1,48 @@ | |||||
# -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
# Copyright (c) 2019, Frappe Technologies and Contributors | # Copyright (c) 2019, Frappe Technologies and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import unittest | |||||
import frappe | import frappe | ||||
import frappe.cache_manager | import frappe.cache_manager | ||||
import unittest | |||||
class TestMilestoneTracker(unittest.TestCase): | class TestMilestoneTracker(unittest.TestCase): | ||||
def test_milestone(self): | def test_milestone(self): | ||||
frappe.db.delete("Milestone Tracker") | frappe.db.delete("Milestone Tracker") | ||||
frappe.cache().delete_key('milestone_tracker_map') | |||||
frappe.cache().delete_key("milestone_tracker_map") | |||||
milestone_tracker = frappe.get_doc(dict( | |||||
doctype = 'Milestone Tracker', | |||||
document_type = 'ToDo', | |||||
track_field = 'status' | |||||
)).insert() | |||||
milestone_tracker = frappe.get_doc( | |||||
dict(doctype="Milestone Tracker", document_type="ToDo", track_field="status") | |||||
).insert() | |||||
todo = frappe.get_doc(dict( | |||||
doctype = 'ToDo', | |||||
description = 'test milestone', | |||||
status = 'Open' | |||||
)).insert() | |||||
todo = frappe.get_doc(dict(doctype="ToDo", description="test milestone", status="Open")).insert() | |||||
milestones = frappe.get_all('Milestone', | |||||
fields = ['track_field', 'value', 'milestone_tracker'], | |||||
filters = dict(reference_type = todo.doctype, reference_name=todo.name)) | |||||
milestones = frappe.get_all( | |||||
"Milestone", | |||||
fields=["track_field", "value", "milestone_tracker"], | |||||
filters=dict(reference_type=todo.doctype, reference_name=todo.name), | |||||
) | |||||
self.assertEqual(len(milestones), 1) | self.assertEqual(len(milestones), 1) | ||||
self.assertEqual(milestones[0].track_field, 'status') | |||||
self.assertEqual(milestones[0].value, 'Open') | |||||
self.assertEqual(milestones[0].track_field, "status") | |||||
self.assertEqual(milestones[0].value, "Open") | |||||
todo.status = 'Closed' | |||||
todo.status = "Closed" | |||||
todo.save() | todo.save() | ||||
milestones = frappe.get_all('Milestone', | |||||
fields = ['track_field', 'value', 'milestone_tracker'], | |||||
filters = dict(reference_type = todo.doctype, reference_name=todo.name), | |||||
order_by = 'modified desc') | |||||
milestones = frappe.get_all( | |||||
"Milestone", | |||||
fields=["track_field", "value", "milestone_tracker"], | |||||
filters=dict(reference_type=todo.doctype, reference_name=todo.name), | |||||
order_by="modified desc", | |||||
) | |||||
self.assertEqual(len(milestones), 2) | self.assertEqual(len(milestones), 2) | ||||
self.assertEqual(milestones[0].track_field, 'status') | |||||
self.assertEqual(milestones[0].value, 'Closed') | |||||
self.assertEqual(milestones[0].track_field, "status") | |||||
self.assertEqual(milestones[0].value, "Closed") | |||||
# cleanup | # cleanup | ||||
frappe.db.delete("Milestone") | frappe.db.delete("Milestone") | ||||
milestone_tracker.delete() | |||||
milestone_tracker.delete() |
@@ -7,20 +7,22 @@ bootstrap client session | |||||
import frappe | import frappe | ||||
import frappe.defaults | import frappe.defaults | ||||
import frappe.desk.desk_page | import frappe.desk.desk_page | ||||
from frappe.core.doctype.navbar_settings.navbar_settings import get_app_logo, get_navbar_settings | |||||
from frappe.desk.doctype.route_history.route_history import frequently_visited_links | from frappe.desk.doctype.route_history.route_history import frequently_visited_links | ||||
from frappe.desk.form.load import get_meta_bundle | from frappe.desk.form.load import get_meta_bundle | ||||
from frappe.utils.change_log import get_versions | |||||
from frappe.translate import get_lang_dict | |||||
from frappe.email.inbox import get_email_accounts | from frappe.email.inbox import get_email_accounts | ||||
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled | |||||
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled | |||||
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points | |||||
from frappe.model.base_document import get_controller | from frappe.model.base_document import get_controller | ||||
from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings, get_app_logo | |||||
from frappe.utils import get_time_zone, add_user_info | |||||
from frappe.query_builder import DocType | from frappe.query_builder import DocType | ||||
from frappe.query_builder.functions import Count | from frappe.query_builder.functions import Count | ||||
from frappe.query_builder.terms import subqry | from frappe.query_builder.terms import subqry | ||||
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points | |||||
from frappe.social.doctype.energy_point_settings.energy_point_settings import ( | |||||
is_energy_point_enabled, | |||||
) | |||||
from frappe.translate import get_lang_dict | |||||
from frappe.utils import add_user_info, get_time_zone | |||||
from frappe.utils.change_log import get_versions | |||||
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled | |||||
def get_bootinfo(): | def get_bootinfo(): | ||||
@@ -38,9 +40,9 @@ def get_bootinfo(): | |||||
bootinfo.sysdefaults = frappe.defaults.get_defaults() | bootinfo.sysdefaults = frappe.defaults.get_defaults() | ||||
bootinfo.server_date = frappe.utils.nowdate() | bootinfo.server_date = frappe.utils.nowdate() | ||||
if frappe.session['user'] != 'Guest': | |||||
if frappe.session["user"] != "Guest": | |||||
bootinfo.user_info = get_user_info() | bootinfo.user_info = get_user_info() | ||||
bootinfo.sid = frappe.session['sid'] | |||||
bootinfo.sid = frappe.session["sid"] | |||||
bootinfo.modules = {} | bootinfo.modules = {} | ||||
bootinfo.module_list = [] | bootinfo.module_list = [] | ||||
@@ -51,8 +53,10 @@ def get_bootinfo(): | |||||
add_layouts(bootinfo) | add_layouts(bootinfo) | ||||
bootinfo.module_app = frappe.local.module_app | bootinfo.module_app = frappe.local.module_app | ||||
bootinfo.single_types = [d.name for d in frappe.get_all('DocType', {'issingle': 1})] | |||||
bootinfo.nested_set_doctypes = [d.parent for d in frappe.get_all('DocField', {'fieldname': 'lft'}, ['parent'])] | |||||
bootinfo.single_types = [d.name for d in frappe.get_all("DocType", {"issingle": 1})] | |||||
bootinfo.nested_set_doctypes = [ | |||||
d.parent for d in frappe.get_all("DocField", {"fieldname": "lft"}, ["parent"]) | |||||
] | |||||
add_home_page(bootinfo, doclist) | add_home_page(bootinfo, doclist) | ||||
bootinfo.page_info = get_allowed_pages() | bootinfo.page_info = get_allowed_pages() | ||||
load_translations(bootinfo) | load_translations(bootinfo) | ||||
@@ -66,8 +70,8 @@ def get_bootinfo(): | |||||
set_time_zone(bootinfo) | set_time_zone(bootinfo) | ||||
# ipinfo | # ipinfo | ||||
if frappe.session.data.get('ipinfo'): | |||||
bootinfo.ipinfo = frappe.session['data']['ipinfo'] | |||||
if frappe.session.data.get("ipinfo"): | |||||
bootinfo.ipinfo = frappe.session["data"]["ipinfo"] | |||||
# add docs | # add docs | ||||
bootinfo.docs = doclist | bootinfo.docs = doclist | ||||
@@ -77,7 +81,7 @@ def get_bootinfo(): | |||||
if bootinfo.lang: | if bootinfo.lang: | ||||
bootinfo.lang = str(bootinfo.lang) | bootinfo.lang = str(bootinfo.lang) | ||||
bootinfo.versions = {k: v['version'] for k, v in get_versions().items()} | |||||
bootinfo.versions = {k: v["version"] for k, v in get_versions().items()} | |||||
bootinfo.error_report_email = frappe.conf.error_report_email | bootinfo.error_report_email = frappe.conf.error_report_email | ||||
bootinfo.calendars = sorted(frappe.get_hooks("calendars")) | bootinfo.calendars = sorted(frappe.get_hooks("calendars")) | ||||
@@ -97,37 +101,47 @@ def get_bootinfo(): | |||||
return bootinfo | return bootinfo | ||||
def get_letter_heads(): | def get_letter_heads(): | ||||
letter_heads = {} | letter_heads = {} | ||||
for letter_head in frappe.get_all("Letter Head", fields = ["name", "content", "footer"]): | |||||
letter_heads.setdefault(letter_head.name, | |||||
{'header': letter_head.content, 'footer': letter_head.footer}) | |||||
for letter_head in frappe.get_all("Letter Head", fields=["name", "content", "footer"]): | |||||
letter_heads.setdefault( | |||||
letter_head.name, {"header": letter_head.content, "footer": letter_head.footer} | |||||
) | |||||
return letter_heads | return letter_heads | ||||
def load_conf_settings(bootinfo): | def load_conf_settings(bootinfo): | ||||
from frappe import conf | from frappe import conf | ||||
bootinfo.max_file_size = conf.get('max_file_size') or 10485760 | |||||
for key in ('developer_mode', 'socketio_port', 'file_watcher_port'): | |||||
if key in conf: bootinfo[key] = conf.get(key) | |||||
bootinfo.max_file_size = conf.get("max_file_size") or 10485760 | |||||
for key in ("developer_mode", "socketio_port", "file_watcher_port"): | |||||
if key in conf: | |||||
bootinfo[key] = conf.get(key) | |||||
def load_desktop_data(bootinfo): | def load_desktop_data(bootinfo): | ||||
from frappe.desk.desktop import get_workspace_sidebar_items | from frappe.desk.desktop import get_workspace_sidebar_items | ||||
bootinfo.allowed_workspaces = get_workspace_sidebar_items().get('pages') | |||||
bootinfo.allowed_workspaces = get_workspace_sidebar_items().get("pages") | |||||
bootinfo.module_page_map = get_controller("Workspace").get_module_page_map() | bootinfo.module_page_map = get_controller("Workspace").get_module_page_map() | ||||
bootinfo.dashboards = frappe.get_all("Dashboard") | bootinfo.dashboards = frappe.get_all("Dashboard") | ||||
def get_allowed_pages(cache=False): | def get_allowed_pages(cache=False): | ||||
return get_user_pages_or_reports('Page', cache=cache) | |||||
return get_user_pages_or_reports("Page", cache=cache) | |||||
def get_allowed_reports(cache=False): | def get_allowed_reports(cache=False): | ||||
return get_user_pages_or_reports('Report', cache=cache) | |||||
return get_user_pages_or_reports("Report", cache=cache) | |||||
def get_user_pages_or_reports(parent, cache=False): | def get_user_pages_or_reports(parent, cache=False): | ||||
_cache = frappe.cache() | _cache = frappe.cache() | ||||
if cache: | if cache: | ||||
has_role = _cache.get_value('has_role:' + parent, user=frappe.session.user) | |||||
has_role = _cache.get_value("has_role:" + parent, user=frappe.session.user) | |||||
if has_role: | if has_role: | ||||
return has_role | return has_role | ||||
@@ -140,8 +154,7 @@ def get_user_pages_or_reports(parent, cache=False): | |||||
if parent == "Report": | if parent == "Report": | ||||
columns = (report.name.as_("title"), report.ref_doctype, report.report_type) | columns = (report.name.as_("title"), report.ref_doctype, report.report_type) | ||||
else: | else: | ||||
columns = (page.title.as_("title"), ) | |||||
columns = (page.title.as_("title"),) | |||||
customRole = DocType("Custom Role") | customRole = DocType("Custom Role") | ||||
hasRole = DocType("Has Role") | hasRole = DocType("Has Role") | ||||
@@ -149,31 +162,39 @@ def get_user_pages_or_reports(parent, cache=False): | |||||
# get pages or reports set on custom role | # get pages or reports set on custom role | ||||
pages_with_custom_roles = ( | pages_with_custom_roles = ( | ||||
frappe.qb.from_(customRole).from_(hasRole).from_(parentTable) | |||||
.select(customRole[parent.lower()].as_("name"), customRole.modified, customRole.ref_doctype, *columns) | |||||
frappe.qb.from_(customRole) | |||||
.from_(hasRole) | |||||
.from_(parentTable) | |||||
.select( | |||||
customRole[parent.lower()].as_("name"), customRole.modified, customRole.ref_doctype, *columns | |||||
) | |||||
.where( | .where( | ||||
(hasRole.parent == customRole.name) | (hasRole.parent == customRole.name) | ||||
& (parentTable.name == customRole[parent.lower()]) | & (parentTable.name == customRole[parent.lower()]) | ||||
& (customRole[parent.lower()].isnotnull()) | & (customRole[parent.lower()].isnotnull()) | ||||
& (hasRole.role.isin(roles))) | |||||
& (hasRole.role.isin(roles)) | |||||
) | |||||
).run(as_dict=True) | ).run(as_dict=True) | ||||
for p in pages_with_custom_roles: | for p in pages_with_custom_roles: | ||||
has_role[p.name] = {"modified":p.modified, "title": p.title, "ref_doctype": p.ref_doctype} | |||||
has_role[p.name] = {"modified": p.modified, "title": p.title, "ref_doctype": p.ref_doctype} | |||||
subq = ( | subq = ( | ||||
frappe.qb.from_(customRole).select(customRole[parent.lower()]) | |||||
frappe.qb.from_(customRole) | |||||
.select(customRole[parent.lower()]) | |||||
.where(customRole[parent.lower()].isnotnull()) | .where(customRole[parent.lower()].isnotnull()) | ||||
) | ) | ||||
pages_with_standard_roles = ( | pages_with_standard_roles = ( | ||||
frappe.qb.from_(hasRole).from_(parentTable) | |||||
frappe.qb.from_(hasRole) | |||||
.from_(parentTable) | |||||
.select(parentTable.name.as_("name"), parentTable.modified, *columns) | .select(parentTable.name.as_("name"), parentTable.modified, *columns) | ||||
.where( | .where( | ||||
(hasRole.role.isin(roles)) | (hasRole.role.isin(roles)) | ||||
& (hasRole.parent == parentTable.name) | & (hasRole.parent == parentTable.name) | ||||
& (parentTable.name.notin(subq)) | & (parentTable.name.notin(subq)) | ||||
).distinct() | |||||
) | |||||
.distinct() | |||||
) | ) | ||||
if parent == "Report": | if parent == "Report": | ||||
@@ -183,18 +204,20 @@ def get_user_pages_or_reports(parent, cache=False): | |||||
for p in pages_with_standard_roles: | for p in pages_with_standard_roles: | ||||
if p.name not in has_role: | if p.name not in has_role: | ||||
has_role[p.name] = {"modified":p.modified, "title": p.title} | |||||
has_role[p.name] = {"modified": p.modified, "title": p.title} | |||||
if parent == "Report": | if parent == "Report": | ||||
has_role[p.name].update({'ref_doctype': p.ref_doctype}) | |||||
has_role[p.name].update({"ref_doctype": p.ref_doctype}) | |||||
no_of_roles = (frappe.qb.from_(hasRole).select(Count("*")) | |||||
.where(hasRole.parent == parentTable.name) | |||||
no_of_roles = ( | |||||
frappe.qb.from_(hasRole).select(Count("*")).where(hasRole.parent == parentTable.name) | |||||
) | ) | ||||
# pages with no role are allowed | # pages with no role are allowed | ||||
if parent =="Page": | |||||
if parent == "Page": | |||||
pages_with_no_roles = (frappe.qb.from_(parentTable).select(parentTable.name, parentTable.modified, *columns) | |||||
pages_with_no_roles = ( | |||||
frappe.qb.from_(parentTable) | |||||
.select(parentTable.name, parentTable.modified, *columns) | |||||
.where(subqry(no_of_roles) == 0) | .where(subqry(no_of_roles) == 0) | ||||
).run(as_dict=True) | ).run(as_dict=True) | ||||
@@ -203,18 +226,20 @@ def get_user_pages_or_reports(parent, cache=False): | |||||
has_role[p.name] = {"modified": p.modified, "title": p.title} | has_role[p.name] = {"modified": p.modified, "title": p.title} | ||||
elif parent == "Report": | elif parent == "Report": | ||||
reports = frappe.get_all("Report", | |||||
reports = frappe.get_all( | |||||
"Report", | |||||
fields=["name", "report_type"], | fields=["name", "report_type"], | ||||
filters={"name": ("in", has_role.keys())}, | filters={"name": ("in", has_role.keys())}, | ||||
ignore_ifnull=True | |||||
ignore_ifnull=True, | |||||
) | ) | ||||
for report in reports: | for report in reports: | ||||
has_role[report.name]["report_type"] = report.report_type | has_role[report.name]["report_type"] = report.report_type | ||||
# Expire every six hours | # Expire every six hours | ||||
_cache.set_value('has_role:' + parent, has_role, frappe.session.user, 21600) | |||||
_cache.set_value("has_role:" + parent, has_role, frappe.session.user, 21600) | |||||
return has_role | return has_role | ||||
def load_translations(bootinfo): | def load_translations(bootinfo): | ||||
messages = frappe.get_lang_dict("boot") | messages = frappe.get_lang_dict("boot") | ||||
@@ -225,27 +250,30 @@ def load_translations(bootinfo): | |||||
messages[name] = frappe._(name) | messages[name] = frappe._(name) | ||||
# only untranslated | # only untranslated | ||||
messages = {k: v for k, v in messages.items() if k!=v} | |||||
messages = {k: v for k, v in messages.items() if k != v} | |||||
bootinfo["__messages"] = messages | bootinfo["__messages"] = messages | ||||
def get_user_info(): | def get_user_info(): | ||||
# get info for current user | # get info for current user | ||||
user_info = frappe._dict() | user_info = frappe._dict() | ||||
add_user_info(frappe.session.user, user_info) | add_user_info(frappe.session.user, user_info) | ||||
if frappe.session.user == 'Administrator' and user_info.Administrator.email: | |||||
if frappe.session.user == "Administrator" and user_info.Administrator.email: | |||||
user_info[user_info.Administrator.email] = user_info.Administrator | user_info[user_info.Administrator.email] = user_info.Administrator | ||||
return user_info | return user_info | ||||
def get_user(bootinfo): | def get_user(bootinfo): | ||||
"""get user info""" | """get user info""" | ||||
bootinfo.user = frappe.get_user().load_user() | bootinfo.user = frappe.get_user().load_user() | ||||
def add_home_page(bootinfo, docs): | def add_home_page(bootinfo, docs): | ||||
"""load home page""" | """load home page""" | ||||
if frappe.session.user=="Guest": | |||||
if frappe.session.user == "Guest": | |||||
return | return | ||||
home_page = frappe.db.get_default("desktop:home_page") | home_page = frappe.db.get_default("desktop:home_page") | ||||
@@ -255,50 +283,65 @@ def add_home_page(bootinfo, docs): | |||||
try: | try: | ||||
page = frappe.desk.desk_page.get(home_page) | page = frappe.desk.desk_page.get(home_page) | ||||
docs.append(page) | docs.append(page) | ||||
bootinfo['home_page'] = page.name | |||||
bootinfo["home_page"] = page.name | |||||
except (frappe.DoesNotExistError, frappe.PermissionError): | except (frappe.DoesNotExistError, frappe.PermissionError): | ||||
if frappe.message_log: | if frappe.message_log: | ||||
frappe.message_log.pop() | frappe.message_log.pop() | ||||
bootinfo['home_page'] = 'Workspaces' | |||||
bootinfo["home_page"] = "Workspaces" | |||||
def add_timezone_info(bootinfo): | def add_timezone_info(bootinfo): | ||||
system = bootinfo.sysdefaults.get("time_zone") | system = bootinfo.sysdefaults.get("time_zone") | ||||
import frappe.utils.momentjs | import frappe.utils.momentjs | ||||
bootinfo.timezone_info = {"zones":{}, "rules":{}, "links":{}} | |||||
bootinfo.timezone_info = {"zones": {}, "rules": {}, "links": {}} | |||||
frappe.utils.momentjs.update(system, bootinfo.timezone_info) | frappe.utils.momentjs.update(system, bootinfo.timezone_info) | ||||
def load_print(bootinfo, doclist): | def load_print(bootinfo, doclist): | ||||
print_settings = frappe.db.get_singles_dict("Print Settings") | print_settings = frappe.db.get_singles_dict("Print Settings") | ||||
print_settings.doctype = ":Print Settings" | print_settings.doctype = ":Print Settings" | ||||
doclist.append(print_settings) | doclist.append(print_settings) | ||||
load_print_css(bootinfo, print_settings) | load_print_css(bootinfo, print_settings) | ||||
def load_print_css(bootinfo, print_settings): | def load_print_css(bootinfo, print_settings): | ||||
import frappe.www.printview | import frappe.www.printview | ||||
bootinfo.print_css = frappe.www.printview.get_print_style(print_settings.print_style or "Redesign", for_legacy=True) | |||||
bootinfo.print_css = frappe.www.printview.get_print_style( | |||||
print_settings.print_style or "Redesign", for_legacy=True | |||||
) | |||||
def get_unseen_notes(): | def get_unseen_notes(): | ||||
note = DocType("Note") | note = DocType("Note") | ||||
nsb = DocType("Note Seen By").as_("nsb") | nsb = DocType("Note Seen By").as_("nsb") | ||||
return ( | return ( | ||||
frappe.qb.from_(note).select(note.name, note.title, note.content, note.notify_on_every_login) | |||||
frappe.qb.from_(note) | |||||
.select(note.name, note.title, note.content, note.notify_on_every_login) | |||||
.where( | .where( | ||||
(note.notify_on_every_login == 1) | (note.notify_on_every_login == 1) | ||||
& (note.expire_notification_on > frappe.utils.now()) | & (note.expire_notification_on > frappe.utils.now()) | ||||
& (subqry(frappe.qb.from_(nsb).select(nsb.user).where(nsb.parent == note.name)).notin([frappe.session.user]))) | |||||
).run(as_dict=1) | |||||
& ( | |||||
subqry(frappe.qb.from_(nsb).select(nsb.user).where(nsb.parent == note.name)).notin( | |||||
[frappe.session.user] | |||||
) | |||||
) | |||||
) | |||||
).run(as_dict=1) | |||||
def get_success_action(): | def get_success_action(): | ||||
return frappe.get_all("Success Action", fields=["*"]) | return frappe.get_all("Success Action", fields=["*"]) | ||||
def get_link_preview_doctypes(): | def get_link_preview_doctypes(): | ||||
from frappe.utils import cint | from frappe.utils import cint | ||||
link_preview_doctypes = [d.name for d in frappe.db.get_all('DocType', {'show_preview_popup': 1})] | |||||
customizations = frappe.get_all("Property Setter", | |||||
fields=['doc_type', 'value'], | |||||
filters={'property': 'show_preview_popup'} | |||||
link_preview_doctypes = [d.name for d in frappe.db.get_all("DocType", {"show_preview_popup": 1})] | |||||
customizations = frappe.get_all( | |||||
"Property Setter", fields=["doc_type", "value"], filters={"property": "show_preview_popup"} | |||||
) | ) | ||||
for custom in customizations: | for custom in customizations: | ||||
@@ -309,22 +352,23 @@ def get_link_preview_doctypes(): | |||||
return link_preview_doctypes | return link_preview_doctypes | ||||
def get_additional_filters_from_hooks(): | def get_additional_filters_from_hooks(): | ||||
filter_config = frappe._dict() | filter_config = frappe._dict() | ||||
filter_hooks = frappe.get_hooks('filters_config') | |||||
filter_hooks = frappe.get_hooks("filters_config") | |||||
for hook in filter_hooks: | for hook in filter_hooks: | ||||
filter_config.update(frappe.get_attr(hook)()) | filter_config.update(frappe.get_attr(hook)()) | ||||
return filter_config | return filter_config | ||||
def add_layouts(bootinfo): | def add_layouts(bootinfo): | ||||
# add routes for readable doctypes | # add routes for readable doctypes | ||||
bootinfo.doctype_layouts = frappe.get_all('DocType Layout', ['name', 'route', 'document_type']) | |||||
bootinfo.doctype_layouts = frappe.get_all("DocType Layout", ["name", "route", "document_type"]) | |||||
def get_desk_settings(): | def get_desk_settings(): | ||||
role_list = frappe.get_all('Role', fields=['*'], filters=dict( | |||||
name=['in', frappe.get_roles()] | |||||
)) | |||||
role_list = frappe.get_all("Role", fields=["*"], filters=dict(name=["in", frappe.get_roles()])) | |||||
desk_settings = {} | desk_settings = {} | ||||
from frappe.core.doctype.role.role import desk_properties | from frappe.core.doctype.role.role import desk_properties | ||||
@@ -335,8 +379,10 @@ def get_desk_settings(): | |||||
return desk_settings | return desk_settings | ||||
def get_notification_settings(): | def get_notification_settings(): | ||||
return frappe.get_cached_doc('Notification Settings', frappe.session.user) | |||||
return frappe.get_cached_doc("Notification Settings", frappe.session.user) | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_link_title_doctypes(): | def get_link_title_doctypes(): | ||||
@@ -348,8 +394,10 @@ def get_link_title_doctypes(): | |||||
) | ) | ||||
return [d.name for d in dts + custom_dts if d] | return [d.name for d in dts + custom_dts if d] | ||||
def set_time_zone(bootinfo): | def set_time_zone(bootinfo): | ||||
bootinfo.time_zone = { | bootinfo.time_zone = { | ||||
"system": get_time_zone(), | "system": get_time_zone(), | ||||
"user": bootinfo.get("user_info", {}).get(frappe.session.user, {}).get("time_zone", None) or get_time_zone() | |||||
"user": bootinfo.get("user_info", {}).get(frappe.session.user, {}).get("time_zone", None) | |||||
or get_time_zone(), | |||||
} | } |
@@ -1,8 +1,8 @@ | |||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import os | import os | ||||
import shutil | |||||
import re | import re | ||||
import shutil | |||||
import subprocess | import subprocess | ||||
from distutils.spawn import find_executable | from distutils.spawn import find_executable | ||||
from subprocess import getoutput | from subprocess import getoutput | ||||
@@ -25,6 +25,7 @@ sites_path = os.path.abspath(os.getcwd()) | |||||
class AssetsNotDownloadedError(Exception): | class AssetsNotDownloadedError(Exception): | ||||
pass | pass | ||||
class AssetsDontExistError(HTTPError): | class AssetsDontExistError(HTTPError): | ||||
pass | pass | ||||
@@ -43,7 +44,7 @@ def download_file(url, prefix): | |||||
def build_missing_files(): | def build_missing_files(): | ||||
'''Check which files dont exist yet from the assets.json and run build for those files''' | |||||
"""Check which files dont exist yet from the assets.json and run build for those files""" | |||||
missing_assets = [] | missing_assets = [] | ||||
current_asset_files = [] | current_asset_files = [] | ||||
@@ -60,7 +61,7 @@ def build_missing_files(): | |||||
assets_json = frappe.parse_json(assets_json) | assets_json = frappe.parse_json(assets_json) | ||||
for bundle_file, output_file in assets_json.items(): | for bundle_file, output_file in assets_json.items(): | ||||
if not output_file.startswith('/assets/frappe'): | |||||
if not output_file.startswith("/assets/frappe"): | |||||
continue | continue | ||||
if os.path.basename(output_file) not in current_asset_files: | if os.path.basename(output_file) not in current_asset_files: | ||||
@@ -78,8 +79,7 @@ def build_missing_files(): | |||||
def get_assets_link(frappe_head) -> str: | def get_assets_link(frappe_head) -> str: | ||||
tag = getoutput( | tag = getoutput( | ||||
r"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*" | r"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*" | ||||
r" refs/tags/,,' -e 's/\^{}//'" | |||||
% frappe_head | |||||
r" refs/tags/,,' -e 's/\^{}//'" % frappe_head | |||||
) | ) | ||||
if tag: | if tag: | ||||
@@ -111,6 +111,7 @@ def fetch_assets(url, frappe_head): | |||||
def setup_assets(assets_archive): | def setup_assets(assets_archive): | ||||
import tarfile | import tarfile | ||||
directories_created = set() | directories_created = set() | ||||
click.secho("\nExtracting assets...\n", fg="yellow") | click.secho("\nExtracting assets...\n", fg="yellow") | ||||
@@ -221,7 +222,16 @@ def setup(): | |||||
assets_path = os.path.join(frappe.local.sites_path, "assets") | assets_path = os.path.join(frappe.local.sites_path, "assets") | ||||
def bundle(mode, apps=None, hard_link=False, make_copy=False, restore=False, verbose=False, skip_frappe=False, files=None): | |||||
def bundle( | |||||
mode, | |||||
apps=None, | |||||
hard_link=False, | |||||
make_copy=False, | |||||
restore=False, | |||||
verbose=False, | |||||
skip_frappe=False, | |||||
files=None, | |||||
): | |||||
"""concat / minify js files""" | """concat / minify js files""" | ||||
setup() | setup() | ||||
make_asset_dirs(hard_link=hard_link) | make_asset_dirs(hard_link=hard_link) | ||||
@@ -236,7 +246,7 @@ def bundle(mode, apps=None, hard_link=False, make_copy=False, restore=False, ver | |||||
command += " --skip_frappe" | command += " --skip_frappe" | ||||
if files: | if files: | ||||
command += " --files {files}".format(files=','.join(files)) | |||||
command += " --files {files}".format(files=",".join(files)) | |||||
command += " --run-build-command" | command += " --run-build-command" | ||||
@@ -253,9 +263,7 @@ def watch(apps=None): | |||||
if apps: | if apps: | ||||
command += " --apps {apps}".format(apps=apps) | command += " --apps {apps}".format(apps=apps) | ||||
live_reload = frappe.utils.cint( | |||||
os.environ.get("LIVE_RELOAD", frappe.conf.live_reload) | |||||
) | |||||
live_reload = frappe.utils.cint(os.environ.get("LIVE_RELOAD", frappe.conf.live_reload)) | |||||
if live_reload: | if live_reload: | ||||
command += " --live-reload" | command += " --live-reload" | ||||
@@ -266,8 +274,8 @@ def watch(apps=None): | |||||
def check_node_executable(): | def check_node_executable(): | ||||
node_version = Version(subprocess.getoutput('node -v')[1:]) | |||||
warn = '⚠️ ' | |||||
node_version = Version(subprocess.getoutput("node -v")[1:]) | |||||
warn = "⚠️ " | |||||
if node_version.major < 14: | if node_version.major < 14: | ||||
click.echo(f"{warn} Please update your node version to 14") | click.echo(f"{warn} Please update your node version to 14") | ||||
if not find_executable("yarn"): | if not find_executable("yarn"): | ||||
@@ -276,9 +284,7 @@ def check_node_executable(): | |||||
def get_node_env(): | def get_node_env(): | ||||
node_env = { | |||||
"NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}" | |||||
} | |||||
node_env = {"NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}"} | |||||
return node_env | return node_env | ||||
@@ -345,8 +351,7 @@ def clear_broken_symlinks(): | |||||
def unstrip(message: str) -> str: | def unstrip(message: str) -> str: | ||||
"""Pads input string on the right side until the last available column in the terminal | |||||
""" | |||||
"""Pads input string on the right side until the last available column in the terminal""" | |||||
_len = len(message) | _len = len(message) | ||||
try: | try: | ||||
max_str = os.get_terminal_size().columns | max_str = os.get_terminal_size().columns | ||||
@@ -367,7 +372,9 @@ def make_asset_dirs(hard_link=False): | |||||
symlinks = generate_assets_map() | symlinks = generate_assets_map() | ||||
for source, target in symlinks.items(): | for source, target in symlinks.items(): | ||||
start_message = unstrip(f"{'Copying assets from' if hard_link else 'Linking'} {source} to {target}") | |||||
start_message = unstrip( | |||||
f"{'Copying assets from' if hard_link else 'Linking'} {source} to {target}" | |||||
) | |||||
fail_message = unstrip(f"Cannot {'copy' if hard_link else 'link'} {source} to {target}") | fail_message = unstrip(f"Cannot {'copy' if hard_link else 'link'} {source} to {target}") | ||||
# Used '\r' instead of '\x1b[1K\r' to print entire lines in smaller terminal sizes | # Used '\r' instead of '\x1b[1K\r' to print entire lines in smaller terminal sizes | ||||
@@ -404,10 +411,11 @@ def scrub_html_template(content): | |||||
# strip comments | # strip comments | ||||
content = re.sub(r"(<!--.*?-->)", "", content) | content = re.sub(r"(<!--.*?-->)", "", content) | ||||
return content.replace("'", "\'") | |||||
return content.replace("'", "'") | |||||
def html_to_js_template(path, content): | def html_to_js_template(path, content): | ||||
"""returns HTML template content as Javascript code, adding it to `frappe.templates`""" | """returns HTML template content as Javascript code, adding it to `frappe.templates`""" | ||||
return """frappe.templates["{key}"] = '{content}';\n""".format( | return """frappe.templates["{key}"] = '{content}';\n""".format( | ||||
key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content)) | |||||
key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content) | |||||
) |
@@ -1,33 +1,75 @@ | |||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe, json | |||||
import json | |||||
import frappe | |||||
from frappe.desk.notifications import clear_notifications, delete_notification_count_for | |||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe.desk.notifications import (delete_notification_count_for, | |||||
clear_notifications) | |||||
common_default_keys = ["__default", "__global"] | common_default_keys = ["__default", "__global"] | ||||
doctype_map_keys = ('energy_point_rule_map', 'assignment_rule_map', | |||||
'milestone_tracker_map', 'event_consumer_document_type_map') | |||||
bench_cache_keys = ('assets_json',) | |||||
global_cache_keys = ("app_hooks", "installed_apps", 'all_apps', | |||||
"app_modules", "module_app", "system_settings", | |||||
'scheduler_events', 'time_zone', 'webhooks', 'active_domains', | |||||
'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version', | |||||
'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts', | |||||
'sitemap_routes', 'db_tables', 'server_script_autocompletion_items') + doctype_map_keys | |||||
user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang", | |||||
"defaults", "user_permissions", "home_page", "linked_with", | |||||
"desktop_icons", 'portal_menu_items', 'user_perm_can_read', | |||||
"has_role:Page", "has_role:Report", "desk_sidebar_items") | |||||
doctype_map_keys = ( | |||||
"energy_point_rule_map", | |||||
"assignment_rule_map", | |||||
"milestone_tracker_map", | |||||
"event_consumer_document_type_map", | |||||
) | |||||
bench_cache_keys = ("assets_json",) | |||||
global_cache_keys = ( | |||||
"app_hooks", | |||||
"installed_apps", | |||||
"all_apps", | |||||
"app_modules", | |||||
"module_app", | |||||
"system_settings", | |||||
"scheduler_events", | |||||
"time_zone", | |||||
"webhooks", | |||||
"active_domains", | |||||
"active_modules", | |||||
"assignment_rule", | |||||
"server_script_map", | |||||
"wkhtmltopdf_version", | |||||
"domain_restricted_doctypes", | |||||
"domain_restricted_pages", | |||||
"information_schema:counts", | |||||
"sitemap_routes", | |||||
"db_tables", | |||||
"server_script_autocompletion_items", | |||||
) + doctype_map_keys | |||||
user_cache_keys = ( | |||||
"bootinfo", | |||||
"user_recent", | |||||
"roles", | |||||
"user_doc", | |||||
"lang", | |||||
"defaults", | |||||
"user_permissions", | |||||
"home_page", | |||||
"linked_with", | |||||
"desktop_icons", | |||||
"portal_menu_items", | |||||
"user_perm_can_read", | |||||
"has_role:Page", | |||||
"has_role:Report", | |||||
"desk_sidebar_items", | |||||
) | |||||
doctype_cache_keys = ( | |||||
"meta", | |||||
"form_meta", | |||||
"table_columns", | |||||
"last_modified", | |||||
"linked_doctypes", | |||||
"notifications", | |||||
"workflow", | |||||
"data_import_column_header_map", | |||||
) + doctype_map_keys | |||||
doctype_cache_keys = ("meta", "form_meta", "table_columns", "last_modified", | |||||
"linked_doctypes", 'notifications', 'workflow' , | |||||
'data_import_column_header_map') + doctype_map_keys | |||||
def clear_user_cache(user=None): | def clear_user_cache(user=None): | ||||
cache = frappe.cache() | cache = frappe.cache() | ||||
@@ -47,11 +89,13 @@ def clear_user_cache(user=None): | |||||
clear_defaults_cache() | clear_defaults_cache() | ||||
clear_global_cache() | clear_global_cache() | ||||
def clear_domain_cache(user=None): | def clear_domain_cache(user=None): | ||||
cache = frappe.cache() | cache = frappe.cache() | ||||
domain_cache_keys = ('domain_restricted_doctypes', 'domain_restricted_pages') | |||||
domain_cache_keys = ("domain_restricted_doctypes", "domain_restricted_pages") | |||||
cache.delete_value(domain_cache_keys) | cache.delete_value(domain_cache_keys) | ||||
def clear_global_cache(): | def clear_global_cache(): | ||||
from frappe.website.utils import clear_website_cache | from frappe.website.utils import clear_website_cache | ||||
@@ -61,21 +105,23 @@ def clear_global_cache(): | |||||
frappe.cache().delete_value(bench_cache_keys) | frappe.cache().delete_value(bench_cache_keys) | ||||
frappe.setup_module_map() | frappe.setup_module_map() | ||||
def clear_defaults_cache(user=None): | def clear_defaults_cache(user=None): | ||||
if user: | if user: | ||||
for p in ([user] + common_default_keys): | |||||
for p in [user] + common_default_keys: | |||||
frappe.cache().hdel("defaults", p) | frappe.cache().hdel("defaults", p) | ||||
elif frappe.flags.in_install!="frappe": | |||||
elif frappe.flags.in_install != "frappe": | |||||
frappe.cache().delete_key("defaults") | frappe.cache().delete_key("defaults") | ||||
def clear_doctype_cache(doctype=None): | def clear_doctype_cache(doctype=None): | ||||
clear_controller_cache(doctype) | clear_controller_cache(doctype) | ||||
cache = frappe.cache() | cache = frappe.cache() | ||||
if getattr(frappe.local, 'meta_cache') and (doctype in frappe.local.meta_cache): | |||||
if getattr(frappe.local, "meta_cache") and (doctype in frappe.local.meta_cache): | |||||
del frappe.local.meta_cache[doctype] | del frappe.local.meta_cache[doctype] | ||||
for key in ('is_table', 'doctype_modules', 'document_cache'): | |||||
for key in ("is_table", "doctype_modules", "document_cache"): | |||||
cache.delete_value(key) | cache.delete_value(key) | ||||
frappe.local.document_cache = {} | frappe.local.document_cache = {} | ||||
@@ -89,8 +135,9 @@ def clear_doctype_cache(doctype=None): | |||||
# clear all parent doctypes | # clear all parent doctypes | ||||
for dt in frappe.db.get_all('DocField', 'parent', | |||||
dict(fieldtype=['in', frappe.model.table_fields], options=doctype)): | |||||
for dt in frappe.db.get_all( | |||||
"DocField", "parent", dict(fieldtype=["in", frappe.model.table_fields], options=doctype) | |||||
): | |||||
clear_single(dt.parent) | clear_single(dt.parent) | ||||
# clear all notifications | # clear all notifications | ||||
@@ -101,6 +148,7 @@ def clear_doctype_cache(doctype=None): | |||||
for name in doctype_cache_keys: | for name in doctype_cache_keys: | ||||
cache.delete_value(name) | cache.delete_value(name) | ||||
def clear_controller_cache(doctype=None): | def clear_controller_cache(doctype=None): | ||||
if not doctype: | if not doctype: | ||||
del frappe.controllers | del frappe.controllers | ||||
@@ -110,9 +158,10 @@ def clear_controller_cache(doctype=None): | |||||
for site_controllers in frappe.controllers.values(): | for site_controllers in frappe.controllers.values(): | ||||
site_controllers.pop(doctype, None) | site_controllers.pop(doctype, None) | ||||
def get_doctype_map(doctype, name, filters=None, order_by=None): | def get_doctype_map(doctype, name, filters=None, order_by=None): | ||||
cache = frappe.cache() | cache = frappe.cache() | ||||
cache_key = frappe.scrub(doctype) + '_map' | |||||
cache_key = frappe.scrub(doctype) + "_map" | |||||
doctype_map = cache.hget(cache_key, name) | doctype_map = cache.hget(cache_key, name) | ||||
if doctype_map is not None: | if doctype_map is not None: | ||||
@@ -121,7 +170,7 @@ def get_doctype_map(doctype, name, filters=None, order_by=None): | |||||
else: | else: | ||||
# non cached, build cache | # non cached, build cache | ||||
try: | try: | ||||
items = frappe.get_all(doctype, filters=filters, order_by = order_by) | |||||
items = frappe.get_all(doctype, filters=filters, order_by=order_by) | |||||
cache.hset(cache_key, name, json.dumps(items)) | cache.hset(cache_key, name, json.dumps(items)) | ||||
except frappe.db.TableMissingError: | except frappe.db.TableMissingError: | ||||
# executed from inside patch, ignore | # executed from inside patch, ignore | ||||
@@ -129,15 +178,19 @@ def get_doctype_map(doctype, name, filters=None, order_by=None): | |||||
return items | return items | ||||
def clear_doctype_map(doctype, name): | def clear_doctype_map(doctype, name): | ||||
frappe.cache().hdel(frappe.scrub(doctype) + '_map', name) | |||||
frappe.cache().hdel(frappe.scrub(doctype) + "_map", name) | |||||
def build_table_count_cache(): | def build_table_count_cache(): | ||||
if (frappe.flags.in_patch | |||||
if ( | |||||
frappe.flags.in_patch | |||||
or frappe.flags.in_install | or frappe.flags.in_install | ||||
or frappe.flags.in_migrate | or frappe.flags.in_migrate | ||||
or frappe.flags.in_import | or frappe.flags.in_import | ||||
or frappe.flags.in_setup_wizard): | |||||
or frappe.flags.in_setup_wizard | |||||
): | |||||
return | return | ||||
_cache = frappe.cache() | _cache = frappe.cache() | ||||
@@ -145,39 +198,45 @@ def build_table_count_cache(): | |||||
table_rows = frappe.qb.Field("table_rows").as_("count") | table_rows = frappe.qb.Field("table_rows").as_("count") | ||||
information_schema = frappe.qb.Schema("information_schema") | information_schema = frappe.qb.Schema("information_schema") | ||||
data = ( | |||||
frappe.qb.from_(information_schema.tables).select(table_name, table_rows) | |||||
).run(as_dict=True) | |||||
counts = {d.get('name').replace('tab', '', 1): d.get('count', None) for d in data} | |||||
data = (frappe.qb.from_(information_schema.tables).select(table_name, table_rows)).run( | |||||
as_dict=True | |||||
) | |||||
counts = {d.get("name").replace("tab", "", 1): d.get("count", None) for d in data} | |||||
_cache.set_value("information_schema:counts", counts) | _cache.set_value("information_schema:counts", counts) | ||||
return counts | return counts | ||||
def build_domain_restriced_doctype_cache(*args, **kwargs): | def build_domain_restriced_doctype_cache(*args, **kwargs): | ||||
if (frappe.flags.in_patch | |||||
if ( | |||||
frappe.flags.in_patch | |||||
or frappe.flags.in_install | or frappe.flags.in_install | ||||
or frappe.flags.in_migrate | or frappe.flags.in_migrate | ||||
or frappe.flags.in_import | or frappe.flags.in_import | ||||
or frappe.flags.in_setup_wizard): | |||||
or frappe.flags.in_setup_wizard | |||||
): | |||||
return | return | ||||
_cache = frappe.cache() | _cache = frappe.cache() | ||||
active_domains = frappe.get_active_domains() | active_domains = frappe.get_active_domains() | ||||
doctypes = frappe.get_all("DocType", filters={'restrict_to_domain': ('IN', active_domains)}) | |||||
doctypes = frappe.get_all("DocType", filters={"restrict_to_domain": ("IN", active_domains)}) | |||||
doctypes = [doc.name for doc in doctypes] | doctypes = [doc.name for doc in doctypes] | ||||
_cache.set_value("domain_restricted_doctypes", doctypes) | _cache.set_value("domain_restricted_doctypes", doctypes) | ||||
return doctypes | return doctypes | ||||
def build_domain_restriced_page_cache(*args, **kwargs): | def build_domain_restriced_page_cache(*args, **kwargs): | ||||
if (frappe.flags.in_patch | |||||
if ( | |||||
frappe.flags.in_patch | |||||
or frappe.flags.in_install | or frappe.flags.in_install | ||||
or frappe.flags.in_migrate | or frappe.flags.in_migrate | ||||
or frappe.flags.in_import | or frappe.flags.in_import | ||||
or frappe.flags.in_setup_wizard): | |||||
or frappe.flags.in_setup_wizard | |||||
): | |||||
return | return | ||||
_cache = frappe.cache() | _cache = frappe.cache() | ||||
active_domains = frappe.get_active_domains() | active_domains = frappe.get_active_domains() | ||||
pages = frappe.get_all("Page", filters={'restrict_to_domain': ('IN', active_domains)}) | |||||
pages = frappe.get_all("Page", filters={"restrict_to_domain": ("IN", active_domains)}) | |||||
pages = [page.name for page in pages] | pages = [page.name for page in pages] | ||||
_cache.set_value("domain_restricted_pages", pages) | _cache.set_value("domain_restricted_pages", pages) | ||||
@@ -1,32 +1,44 @@ | |||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import json | |||||
import os | |||||
import frappe | import frappe | ||||
from frappe import _ | |||||
import frappe.model | import frappe.model | ||||
import frappe.utils | import frappe.utils | ||||
import json, os | |||||
from frappe.utils import get_safe_filters | |||||
from frappe import _ | |||||
from frappe.desk.reportview import validate_args | from frappe.desk.reportview import validate_args | ||||
from frappe.model.db_query import check_parent_permission | from frappe.model.db_query import check_parent_permission | ||||
from frappe.utils import get_safe_filters | |||||
''' | |||||
""" | |||||
Handle RESTful requests that are mapped to the `/api/resource` route. | Handle RESTful requests that are mapped to the `/api/resource` route. | ||||
Requests via FrappeClient are also handled here. | Requests via FrappeClient are also handled here. | ||||
''' | |||||
""" | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_list(doctype, fields=None, filters=None, order_by=None, | |||||
limit_start=None, limit_page_length=20, parent=None, debug=False, as_dict=True, or_filters=None): | |||||
'''Returns a list of records by filters, fields, ordering and limit | |||||
def get_list( | |||||
doctype, | |||||
fields=None, | |||||
filters=None, | |||||
order_by=None, | |||||
limit_start=None, | |||||
limit_page_length=20, | |||||
parent=None, | |||||
debug=False, | |||||
as_dict=True, | |||||
or_filters=None, | |||||
): | |||||
"""Returns a list of records by filters, fields, ordering and limit | |||||
:param doctype: DocType of the data to be queried | :param doctype: DocType of the data to be queried | ||||
:param fields: fields to be returned. Default is `name` | :param fields: fields to be returned. Default is `name` | ||||
:param filters: filter list by this dict | :param filters: filter list by this dict | ||||
:param order_by: Order by this fieldname | :param order_by: Order by this fieldname | ||||
:param limit_start: Start at this index | :param limit_start: Start at this index | ||||
:param limit_page_length: Number of records to be returned (default 20)''' | |||||
:param limit_page_length: Number of records to be returned (default 20)""" | |||||
if frappe.is_table(doctype): | if frappe.is_table(doctype): | ||||
check_parent_permission(parent, doctype) | check_parent_permission(parent, doctype) | ||||
@@ -40,23 +52,25 @@ def get_list(doctype, fields=None, filters=None, order_by=None, | |||||
limit_start=limit_start, | limit_start=limit_start, | ||||
limit_page_length=limit_page_length, | limit_page_length=limit_page_length, | ||||
debug=debug, | debug=debug, | ||||
as_list=not as_dict | |||||
as_list=not as_dict, | |||||
) | ) | ||||
validate_args(args) | validate_args(args) | ||||
return frappe.get_list(**args) | return frappe.get_list(**args) | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_count(doctype, filters=None, debug=False, cache=False): | def get_count(doctype, filters=None, debug=False, cache=False): | ||||
return frappe.db.count(doctype, get_safe_filters(filters), debug, cache) | return frappe.db.count(doctype, get_safe_filters(filters), debug, cache) | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get(doctype, name=None, filters=None, parent=None): | def get(doctype, name=None, filters=None, parent=None): | ||||
'''Returns a document by name or filters | |||||
"""Returns a document by name or filters | |||||
:param doctype: DocType of the document to be returned | :param doctype: DocType of the document to be returned | ||||
:param name: return document of this `name` | :param name: return document of this `name` | ||||
:param filters: If name is not set, filter by these values and return the first match''' | |||||
:param filters: If name is not set, filter by these values and return the first match""" | |||||
if frappe.is_table(doctype): | if frappe.is_table(doctype): | ||||
check_parent_permission(parent, doctype) | check_parent_permission(parent, doctype) | ||||
@@ -71,13 +85,14 @@ def get(doctype, name=None, filters=None, parent=None): | |||||
return frappe.get_doc(doctype, name).as_dict() | return frappe.get_doc(doctype, name).as_dict() | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, parent=None): | def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, parent=None): | ||||
'''Returns a value form a document | |||||
"""Returns a value form a document | |||||
:param doctype: DocType to be queried | :param doctype: DocType to be queried | ||||
:param fieldname: Field to be returned (default `name`) | :param fieldname: Field to be returned (default `name`) | ||||
:param filters: dict or string for identifying the record''' | |||||
:param filters: dict or string for identifying the record""" | |||||
if frappe.is_table(doctype): | if frappe.is_table(doctype): | ||||
check_parent_permission(parent, doctype) | check_parent_permission(parent, doctype) | ||||
@@ -102,7 +117,15 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, paren | |||||
if frappe.get_meta(doctype).issingle: | if frappe.get_meta(doctype).issingle: | ||||
value = frappe.db.get_values_from_single(fields, filters, doctype, as_dict=as_dict, debug=debug) | value = frappe.db.get_values_from_single(fields, filters, doctype, as_dict=as_dict, debug=debug) | ||||
else: | else: | ||||
value = get_list(doctype, filters=filters, fields=fields, debug=debug, limit_page_length=1, parent=parent, as_dict=as_dict) | |||||
value = get_list( | |||||
doctype, | |||||
filters=filters, | |||||
fields=fields, | |||||
debug=debug, | |||||
limit_page_length=1, | |||||
parent=parent, | |||||
as_dict=as_dict, | |||||
) | |||||
if as_dict: | if as_dict: | ||||
return value[0] if value else {} | return value[0] if value else {} | ||||
@@ -112,6 +135,7 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, paren | |||||
return value[0] if len(fields) > 1 else value[0][0] | return value[0] if len(fields) > 1 else value[0][0] | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_single_value(doctype, field): | def get_single_value(doctype, field): | ||||
if not frappe.has_permission(doctype): | if not frappe.has_permission(doctype): | ||||
@@ -119,14 +143,15 @@ def get_single_value(doctype, field): | |||||
value = frappe.db.get_single_value(doctype, field) | value = frappe.db.get_single_value(doctype, field) | ||||
return value | return value | ||||
@frappe.whitelist(methods=['POST', 'PUT']) | |||||
@frappe.whitelist(methods=["POST", "PUT"]) | |||||
def set_value(doctype, name, fieldname, value=None): | def set_value(doctype, name, fieldname, value=None): | ||||
'''Set a value using get_doc, group of values | |||||
"""Set a value using get_doc, group of values | |||||
:param doctype: DocType of the document | :param doctype: DocType of the document | ||||
:param name: name of the document | :param name: name of the document | ||||
:param fieldname: fieldname string or JSON / dict with key value pair | :param fieldname: fieldname string or JSON / dict with key value pair | ||||
:param value: value if fieldname is JSON / dict''' | |||||
:param value: value if fieldname is JSON / dict""" | |||||
if fieldname in (frappe.model.default_fields + frappe.model.child_table_fields): | if fieldname in (frappe.model.default_fields + frappe.model.child_table_fields): | ||||
frappe.throw(_("Cannot edit standard fields")) | frappe.throw(_("Cannot edit standard fields")) | ||||
@@ -137,7 +162,7 @@ def set_value(doctype, name, fieldname, value=None): | |||||
try: | try: | ||||
values = json.loads(fieldname) | values = json.loads(fieldname) | ||||
except ValueError: | except ValueError: | ||||
values = {fieldname: ''} | |||||
values = {fieldname: ""} | |||||
else: | else: | ||||
values = {fieldname: value} | values = {fieldname: value} | ||||
@@ -155,11 +180,12 @@ def set_value(doctype, name, fieldname, value=None): | |||||
return doc.as_dict() | return doc.as_dict() | ||||
@frappe.whitelist(methods=['POST', 'PUT']) | |||||
@frappe.whitelist(methods=["POST", "PUT"]) | |||||
def insert(doc=None): | def insert(doc=None): | ||||
'''Insert a document | |||||
"""Insert a document | |||||
:param doc: JSON or dict object to be inserted''' | |||||
:param doc: JSON or dict object to be inserted""" | |||||
if isinstance(doc, str): | if isinstance(doc, str): | ||||
doc = json.loads(doc) | doc = json.loads(doc) | ||||
@@ -173,18 +199,19 @@ def insert(doc=None): | |||||
doc = frappe.get_doc(doc).insert() | doc = frappe.get_doc(doc).insert() | ||||
return doc.as_dict() | return doc.as_dict() | ||||
@frappe.whitelist(methods=['POST', 'PUT']) | |||||
@frappe.whitelist(methods=["POST", "PUT"]) | |||||
def insert_many(docs=None): | def insert_many(docs=None): | ||||
'''Insert multiple documents | |||||
"""Insert multiple documents | |||||
:param docs: JSON or list of dict objects to be inserted in one request''' | |||||
:param docs: JSON or list of dict objects to be inserted in one request""" | |||||
if isinstance(docs, str): | if isinstance(docs, str): | ||||
docs = json.loads(docs) | docs = json.loads(docs) | ||||
out = [] | out = [] | ||||
if len(docs) > 200: | if len(docs) > 200: | ||||
frappe.throw(_('Only 200 inserts allowed in one request')) | |||||
frappe.throw(_("Only 200 inserts allowed in one request")) | |||||
for doc in docs: | for doc in docs: | ||||
if doc.get("parenttype"): | if doc.get("parenttype"): | ||||
@@ -199,11 +226,12 @@ def insert_many(docs=None): | |||||
return out | return out | ||||
@frappe.whitelist(methods=['POST', 'PUT']) | |||||
@frappe.whitelist(methods=["POST", "PUT"]) | |||||
def save(doc): | def save(doc): | ||||
'''Update (save) an existing document | |||||
"""Update (save) an existing document | |||||
:param doc: JSON or dict object with the properties of the document to be updated''' | |||||
:param doc: JSON or dict object with the properties of the document to be updated""" | |||||
if isinstance(doc, str): | if isinstance(doc, str): | ||||
doc = json.loads(doc) | doc = json.loads(doc) | ||||
@@ -212,21 +240,23 @@ def save(doc): | |||||
return doc.as_dict() | return doc.as_dict() | ||||
@frappe.whitelist(methods=['POST', 'PUT']) | |||||
@frappe.whitelist(methods=["POST", "PUT"]) | |||||
def rename_doc(doctype, old_name, new_name, merge=False): | def rename_doc(doctype, old_name, new_name, merge=False): | ||||
'''Rename document | |||||
"""Rename document | |||||
:param doctype: DocType of the document to be renamed | :param doctype: DocType of the document to be renamed | ||||
:param old_name: Current `name` of the document to be renamed | :param old_name: Current `name` of the document to be renamed | ||||
:param new_name: New `name` to be set''' | |||||
:param new_name: New `name` to be set""" | |||||
new_name = frappe.rename_doc(doctype, old_name, new_name, merge=merge) | new_name = frappe.rename_doc(doctype, old_name, new_name, merge=merge) | ||||
return new_name | return new_name | ||||
@frappe.whitelist(methods=['POST', 'PUT']) | |||||
@frappe.whitelist(methods=["POST", "PUT"]) | |||||
def submit(doc): | def submit(doc): | ||||
'''Submit a document | |||||
"""Submit a document | |||||
:param doc: JSON or dict object to be submitted remotely''' | |||||
:param doc: JSON or dict object to be submitted remotely""" | |||||
if isinstance(doc, str): | if isinstance(doc, str): | ||||
doc = json.loads(doc) | doc = json.loads(doc) | ||||
@@ -235,52 +265,57 @@ def submit(doc): | |||||
return doc.as_dict() | return doc.as_dict() | ||||
@frappe.whitelist(methods=['POST', 'PUT']) | |||||
@frappe.whitelist(methods=["POST", "PUT"]) | |||||
def cancel(doctype, name): | def cancel(doctype, name): | ||||
'''Cancel a document | |||||
"""Cancel a document | |||||
:param doctype: DocType of the document to be cancelled | :param doctype: DocType of the document to be cancelled | ||||
:param name: name of the document to be cancelled''' | |||||
:param name: name of the document to be cancelled""" | |||||
wrapper = frappe.get_doc(doctype, name) | wrapper = frappe.get_doc(doctype, name) | ||||
wrapper.cancel() | wrapper.cancel() | ||||
return wrapper.as_dict() | return wrapper.as_dict() | ||||
@frappe.whitelist(methods=['DELETE', 'POST']) | |||||
@frappe.whitelist(methods=["DELETE", "POST"]) | |||||
def delete(doctype, name): | def delete(doctype, name): | ||||
'''Delete a remote document | |||||
"""Delete a remote document | |||||
:param doctype: DocType of the document to be deleted | :param doctype: DocType of the document to be deleted | ||||
:param name: name of the document to be deleted''' | |||||
:param name: name of the document to be deleted""" | |||||
frappe.delete_doc(doctype, name, ignore_missing=False) | frappe.delete_doc(doctype, name, ignore_missing=False) | ||||
@frappe.whitelist(methods=['POST', 'PUT']) | |||||
@frappe.whitelist(methods=["POST", "PUT"]) | |||||
def set_default(key, value, parent=None): | def set_default(key, value, parent=None): | ||||
"""set a user default value""" | """set a user default value""" | ||||
frappe.db.set_default(key, value, parent or frappe.session.user) | frappe.db.set_default(key, value, parent or frappe.session.user) | ||||
frappe.clear_cache(user=frappe.session.user) | frappe.clear_cache(user=frappe.session.user) | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_default(key, parent=None): | def get_default(key, parent=None): | ||||
"""set a user default value""" | """set a user default value""" | ||||
return frappe.db.get_default(key, parent) | return frappe.db.get_default(key, parent) | ||||
@frappe.whitelist(methods=['POST', 'PUT']) | |||||
@frappe.whitelist(methods=["POST", "PUT"]) | |||||
def make_width_property_setter(doc): | def make_width_property_setter(doc): | ||||
'''Set width Property Setter | |||||
"""Set width Property Setter | |||||
:param doc: Property Setter document with `width` property''' | |||||
:param doc: Property Setter document with `width` property""" | |||||
if isinstance(doc, str): | if isinstance(doc, str): | ||||
doc = json.loads(doc) | doc = json.loads(doc) | ||||
if doc["doctype"]=="Property Setter" and doc["property"]=="width": | |||||
frappe.get_doc(doc).insert(ignore_permissions = True) | |||||
if doc["doctype"] == "Property Setter" and doc["property"] == "width": | |||||
frappe.get_doc(doc).insert(ignore_permissions=True) | |||||
@frappe.whitelist(methods=['POST', 'PUT']) | |||||
@frappe.whitelist(methods=["POST", "PUT"]) | |||||
def bulk_update(docs): | def bulk_update(docs): | ||||
'''Bulk update documents | |||||
"""Bulk update documents | |||||
:param docs: JSON list of documents to be updated remotely. Each document must have `docname` property''' | |||||
:param docs: JSON list of documents to be updated remotely. Each document must have `docname` property""" | |||||
docs = json.loads(docs) | docs = json.loads(docs) | ||||
failed_docs = [] | failed_docs = [] | ||||
for doc in docs: | for doc in docs: | ||||
@@ -290,41 +325,40 @@ def bulk_update(docs): | |||||
existing_doc.update(doc) | existing_doc.update(doc) | ||||
existing_doc.save() | existing_doc.save() | ||||
except Exception: | except Exception: | ||||
failed_docs.append({ | |||||
'doc': doc, | |||||
'exc': frappe.utils.get_traceback() | |||||
}) | |||||
failed_docs.append({"doc": doc, "exc": frappe.utils.get_traceback()}) | |||||
return {"failed_docs": failed_docs} | |||||
return {'failed_docs': failed_docs} | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def has_permission(doctype, docname, perm_type="read"): | def has_permission(doctype, docname, perm_type="read"): | ||||
'''Returns a JSON with data whether the document has the requested permission | |||||
"""Returns a JSON with data whether the document has the requested permission | |||||
:param doctype: DocType of the document to be checked | :param doctype: DocType of the document to be checked | ||||
:param docname: `name` of the document to be checked | :param docname: `name` of the document to be checked | ||||
:param perm_type: one of `read`, `write`, `create`, `submit`, `cancel`, `report`. Default is `read`''' | |||||
:param perm_type: one of `read`, `write`, `create`, `submit`, `cancel`, `report`. Default is `read`""" | |||||
# perm_type can be one of read, write, create, submit, cancel, report | # perm_type can be one of read, write, create, submit, cancel, report | ||||
return {"has_permission": frappe.has_permission(doctype, perm_type.lower(), docname)} | return {"has_permission": frappe.has_permission(doctype, perm_type.lower(), docname)} | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_password(doctype, name, fieldname): | def get_password(doctype, name, fieldname): | ||||
'''Return a password type property. Only applicable for System Managers | |||||
"""Return a password type property. Only applicable for System Managers | |||||
:param doctype: DocType of the document that holds the password | :param doctype: DocType of the document that holds the password | ||||
:param name: `name` of the document that holds the password | :param name: `name` of the document that holds the password | ||||
:param fieldname: `fieldname` of the password property | :param fieldname: `fieldname` of the password property | ||||
''' | |||||
""" | |||||
frappe.only_for("System Manager") | frappe.only_for("System Manager") | ||||
return frappe.get_doc(doctype, name).get_password(fieldname) | return frappe.get_doc(doctype, name).get_password(fieldname) | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_js(items): | def get_js(items): | ||||
'''Load JS code files. Will also append translations | |||||
"""Load JS code files. Will also append translations | |||||
and extend `frappe._messages` | and extend `frappe._messages` | ||||
:param items: JSON list of paths of the js files to be loaded.''' | |||||
:param items: JSON list of paths of the js files to be loaded.""" | |||||
items = json.loads(items) | items = json.loads(items) | ||||
out = [] | out = [] | ||||
for src in items: | for src in items: | ||||
@@ -346,14 +380,25 @@ def get_js(items): | |||||
return out | return out | ||||
@frappe.whitelist(allow_guest=True) | @frappe.whitelist(allow_guest=True) | ||||
def get_time_zone(): | def get_time_zone(): | ||||
'''Returns default time zone''' | |||||
"""Returns default time zone""" | |||||
return {"time_zone": frappe.defaults.get_defaults().get("time_zone")} | return {"time_zone": frappe.defaults.get_defaults().get("time_zone")} | ||||
@frappe.whitelist(methods=['POST', 'PUT']) | |||||
def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder=None, decode_base64=False, is_private=None, docfield=None): | |||||
'''Attach a file to Document (POST) | |||||
@frappe.whitelist(methods=["POST", "PUT"]) | |||||
def attach_file( | |||||
filename=None, | |||||
filedata=None, | |||||
doctype=None, | |||||
docname=None, | |||||
folder=None, | |||||
decode_base64=False, | |||||
is_private=None, | |||||
docfield=None, | |||||
): | |||||
"""Attach a file to Document (POST) | |||||
:param filename: filename e.g. test-file.txt | :param filename: filename e.g. test-file.txt | ||||
:param filedata: base64 encode filedata which must be urlencoded | :param filedata: base64 encode filedata which must be urlencoded | ||||
@@ -362,7 +407,7 @@ def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder | |||||
:param folder: Folder to add File into | :param folder: Folder to add File into | ||||
:param decode_base64: decode filedata from base64 encode, default is False | :param decode_base64: decode filedata from base64 encode, default is False | ||||
:param is_private: Attach file as private file (1 or 0) | :param is_private: Attach file as private file (1 or 0) | ||||
:param docfield: file to attach to (optional)''' | |||||
:param docfield: file to attach to (optional)""" | |||||
request_method = frappe.local.request.environ.get("REQUEST_METHOD") | request_method = frappe.local.request.environ.get("REQUEST_METHOD") | ||||
@@ -374,16 +419,19 @@ def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder | |||||
if not doc.has_permission(): | if not doc.has_permission(): | ||||
frappe.throw(_("Not permitted"), frappe.PermissionError) | frappe.throw(_("Not permitted"), frappe.PermissionError) | ||||
_file = frappe.get_doc({ | |||||
"doctype": "File", | |||||
"file_name": filename, | |||||
"attached_to_doctype": doctype, | |||||
"attached_to_name": docname, | |||||
"attached_to_field": docfield, | |||||
"folder": folder, | |||||
"is_private": is_private, | |||||
"content": filedata, | |||||
"decode": decode_base64}) | |||||
_file = frappe.get_doc( | |||||
{ | |||||
"doctype": "File", | |||||
"file_name": filename, | |||||
"attached_to_doctype": doctype, | |||||
"attached_to_name": docname, | |||||
"attached_to_field": docfield, | |||||
"folder": folder, | |||||
"is_private": is_private, | |||||
"content": filedata, | |||||
"decode": decode_base64, | |||||
} | |||||
) | |||||
_file.save() | _file.save() | ||||
if docfield and doctype: | if docfield and doctype: | ||||
@@ -392,22 +440,23 @@ def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder | |||||
return _file.as_dict() | return _file.as_dict() | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_hooks(hook, app_name=None): | def get_hooks(hook, app_name=None): | ||||
return frappe.get_hooks(hook, app_name) | return frappe.get_hooks(hook, app_name) | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def is_document_amended(doctype, docname): | def is_document_amended(doctype, docname): | ||||
if frappe.permissions.has_permission(doctype): | if frappe.permissions.has_permission(doctype): | ||||
try: | try: | ||||
return frappe.db.exists(doctype, { | |||||
'amended_from': docname | |||||
}) | |||||
return frappe.db.exists(doctype, {"amended_from": docname}) | |||||
except frappe.db.InternalError: | except frappe.db.InternalError: | ||||
pass | pass | ||||
return False | return False | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def validate_link(doctype: str, docname: str, fields=None): | def validate_link(doctype: str, docname: str, fields=None): | ||||
if not isinstance(doctype, str): | if not isinstance(doctype, str): | ||||
@@ -417,13 +466,11 @@ def validate_link(doctype: str, docname: str, fields=None): | |||||
frappe.throw(_("Document Name must be a string")) | frappe.throw(_("Document Name must be a string")) | ||||
if doctype != "DocType" and not ( | if doctype != "DocType" and not ( | ||||
frappe.has_permission(doctype, "select") | |||||
or frappe.has_permission(doctype, "read") | |||||
frappe.has_permission(doctype, "select") or frappe.has_permission(doctype, "read") | |||||
): | ): | ||||
frappe.throw( | frappe.throw( | ||||
_("You do not have Read or Select Permissions for {}") | |||||
.format(frappe.bold(doctype)), | |||||
frappe.PermissionError | |||||
_("You do not have Read or Select Permissions for {}").format(frappe.bold(doctype)), | |||||
frappe.PermissionError, | |||||
) | ) | ||||
values = frappe._dict() | values = frappe._dict() | ||||
@@ -438,14 +485,11 @@ def validate_link(doctype: str, docname: str, fields=None): | |||||
except frappe.PermissionError: | except frappe.PermissionError: | ||||
frappe.clear_last_message() | frappe.clear_last_message() | ||||
frappe.msgprint( | frappe.msgprint( | ||||
_("You need {0} permission to fetch values from {1} {2}") | |||||
.format( | |||||
frappe.bold(_("Read")), | |||||
frappe.bold(doctype), | |||||
frappe.bold(docname) | |||||
_("You need {0} permission to fetch values from {1} {2}").format( | |||||
frappe.bold(_("Read")), frappe.bold(doctype), frappe.bold(docname) | |||||
), | ), | ||||
title=_("Cannot Fetch Values"), | title=_("Cannot Fetch Values"), | ||||
indicator="orange" | |||||
indicator="orange", | |||||
) | ) | ||||
return values | return values |
@@ -1,23 +1,26 @@ | |||||
# Copyright (c) 2015, Web Notes Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2015, Web Notes Technologies Pvt. Ltd. and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import sys | |||||
import click | |||||
import cProfile | import cProfile | ||||
import pstats | import pstats | ||||
import frappe | |||||
import frappe.utils | |||||
import subprocess # nosec | |||||
import subprocess # nosec | |||||
import sys | |||||
from functools import wraps | from functools import wraps | ||||
from io import StringIO | from io import StringIO | ||||
from os import environ | from os import environ | ||||
import click | |||||
import frappe | |||||
import frappe.utils | |||||
click.disable_unicode_literals_warning = True | click.disable_unicode_literals_warning = True | ||||
def pass_context(f): | def pass_context(f): | ||||
@wraps(f) | @wraps(f) | ||||
def _func(ctx, *args, **kwargs): | def _func(ctx, *args, **kwargs): | ||||
profile = ctx.obj['profile'] | |||||
profile = ctx.obj["profile"] | |||||
if profile: | if profile: | ||||
pr = cProfile.Profile() | pr = cProfile.Profile() | ||||
pr.enable() | pr.enable() | ||||
@@ -25,18 +28,17 @@ def pass_context(f): | |||||
try: | try: | ||||
ret = f(frappe._dict(ctx.obj), *args, **kwargs) | ret = f(frappe._dict(ctx.obj), *args, **kwargs) | ||||
except frappe.exceptions.SiteNotSpecifiedError as e: | except frappe.exceptions.SiteNotSpecifiedError as e: | ||||
click.secho(str(e), fg='yellow') | |||||
click.secho(str(e), fg="yellow") | |||||
sys.exit(1) | sys.exit(1) | ||||
except frappe.exceptions.IncorrectSitePath: | except frappe.exceptions.IncorrectSitePath: | ||||
site = ctx.obj.get("sites", "")[0] | site = ctx.obj.get("sites", "")[0] | ||||
click.secho(f'Site {site} does not exist!', fg='yellow') | |||||
click.secho(f"Site {site} does not exist!", fg="yellow") | |||||
sys.exit(1) | sys.exit(1) | ||||
if profile: | if profile: | ||||
pr.disable() | pr.disable() | ||||
s = StringIO() | s = StringIO() | ||||
ps = pstats.Stats(pr, stream=s)\ | |||||
.sort_stats('cumtime', 'tottime', 'ncalls') | |||||
ps = pstats.Stats(pr, stream=s).sort_stats("cumtime", "tottime", "ncalls") | |||||
ps.print_stats() | ps.print_stats() | ||||
# print the top-100 | # print the top-100 | ||||
@@ -47,6 +49,7 @@ def pass_context(f): | |||||
return click.pass_context(_func) | return click.pass_context(_func) | ||||
def get_site(context, raise_err=True): | def get_site(context, raise_err=True): | ||||
try: | try: | ||||
site = context.sites[0] | site = context.sites[0] | ||||
@@ -56,17 +59,19 @@ def get_site(context, raise_err=True): | |||||
raise frappe.SiteNotSpecifiedError | raise frappe.SiteNotSpecifiedError | ||||
return None | return None | ||||
def popen(command, *args, **kwargs): | def popen(command, *args, **kwargs): | ||||
output = kwargs.get('output', True) | |||||
cwd = kwargs.get('cwd') | |||||
shell = kwargs.get('shell', True) | |||||
raise_err = kwargs.get('raise_err') | |||||
env = kwargs.get('env') | |||||
output = kwargs.get("output", True) | |||||
cwd = kwargs.get("cwd") | |||||
shell = kwargs.get("shell", True) | |||||
raise_err = kwargs.get("raise_err") | |||||
env = kwargs.get("env") | |||||
if env: | if env: | ||||
env = dict(environ, **env) | env = dict(environ, **env) | ||||
def set_low_prio(): | def set_low_prio(): | ||||
import psutil | import psutil | ||||
if psutil.LINUX: | if psutil.LINUX: | ||||
psutil.Process().nice(19) | psutil.Process().nice(19) | ||||
psutil.Process().ionice(psutil.IOPRIO_CLASS_IDLE) | psutil.Process().ionice(psutil.IOPRIO_CLASS_IDLE) | ||||
@@ -77,13 +82,14 @@ def popen(command, *args, **kwargs): | |||||
psutil.Process().nice(19) | psutil.Process().nice(19) | ||||
# ionice not supported | # ionice not supported | ||||
proc = subprocess.Popen(command, | |||||
proc = subprocess.Popen( | |||||
command, | |||||
stdout=None if output else subprocess.PIPE, | stdout=None if output else subprocess.PIPE, | ||||
stderr=None if output else subprocess.PIPE, | stderr=None if output else subprocess.PIPE, | ||||
shell=shell, | shell=shell, | ||||
cwd=cwd, | cwd=cwd, | ||||
preexec_fn=set_low_prio, | preexec_fn=set_low_prio, | ||||
env=env | |||||
env=env, | |||||
) | ) | ||||
return_ = proc.wait() | return_ = proc.wait() | ||||
@@ -93,26 +99,22 @@ def popen(command, *args, **kwargs): | |||||
return return_ | return return_ | ||||
def call_command(cmd, context): | def call_command(cmd, context): | ||||
return click.Context(cmd, obj=context).forward(cmd) | return click.Context(cmd, obj=context).forward(cmd) | ||||
def get_commands(): | def get_commands(): | ||||
# prevent circular imports | # prevent circular imports | ||||
from .redis_utils import commands as redis_commands | |||||
from .scheduler import commands as scheduler_commands | from .scheduler import commands as scheduler_commands | ||||
from .site import commands as site_commands | from .site import commands as site_commands | ||||
from .translate import commands as translate_commands | from .translate import commands as translate_commands | ||||
from .utils import commands as utils_commands | from .utils import commands as utils_commands | ||||
from .redis_utils import commands as redis_commands | |||||
clickable_link = ( | |||||
"\x1b]8;;https://frappeframework.com/docs\afrappeframework.com\x1b]8;;\a" | |||||
) | |||||
clickable_link = "\x1b]8;;https://frappeframework.com/docs\afrappeframework.com\x1b]8;;\a" | |||||
all_commands = ( | all_commands = ( | ||||
scheduler_commands | |||||
+ site_commands | |||||
+ translate_commands | |||||
+ utils_commands | |||||
+ redis_commands | |||||
scheduler_commands + site_commands + translate_commands + utils_commands + redis_commands | |||||
) | ) | ||||
for command in all_commands: | for command in all_commands: | ||||
@@ -3,51 +3,71 @@ import os | |||||
import click | import click | ||||
import frappe | import frappe | ||||
from frappe.utils.redis_queue import RedisQueue | |||||
from frappe.installer import update_site_config | from frappe.installer import update_site_config | ||||
from frappe.utils.redis_queue import RedisQueue | |||||
@click.command('create-rq-users') | |||||
@click.option('--set-admin-password', is_flag=True, default=False, help='Set new Redis admin(default user) password') | |||||
@click.option('--use-rq-auth', is_flag=True, default=False, help='Enable Redis authentication for sites') | |||||
@click.command("create-rq-users") | |||||
@click.option( | |||||
"--set-admin-password", | |||||
is_flag=True, | |||||
default=False, | |||||
help="Set new Redis admin(default user) password", | |||||
) | |||||
@click.option( | |||||
"--use-rq-auth", is_flag=True, default=False, help="Enable Redis authentication for sites" | |||||
) | |||||
def create_rq_users(set_admin_password=False, use_rq_auth=False): | def create_rq_users(set_admin_password=False, use_rq_auth=False): | ||||
"""Create Redis Queue users and add to acl and app configs. | """Create Redis Queue users and add to acl and app configs. | ||||
acl config file will be used by redis server while starting the server | acl config file will be used by redis server while starting the server | ||||
and app config is used by app while connecting to redis server. | and app config is used by app while connecting to redis server. | ||||
""" | """ | ||||
acl_file_path = os.path.abspath('../config/redis_queue.acl') | |||||
acl_file_path = os.path.abspath("../config/redis_queue.acl") | |||||
with frappe.init_site(): | with frappe.init_site(): | ||||
acl_list, user_credentials = RedisQueue.gen_acl_list( | |||||
set_admin_password=set_admin_password) | |||||
acl_list, user_credentials = RedisQueue.gen_acl_list(set_admin_password=set_admin_password) | |||||
with open(acl_file_path, 'w') as f: | |||||
f.writelines([acl+'\n' for acl in acl_list]) | |||||
with open(acl_file_path, "w") as f: | |||||
f.writelines([acl + "\n" for acl in acl_list]) | |||||
sites_path = os.getcwd() | sites_path = os.getcwd() | ||||
common_site_config_path = os.path.join(sites_path, 'common_site_config.json') | |||||
update_site_config("rq_username", user_credentials['bench'][0], validate=False, | |||||
site_config_path=common_site_config_path) | |||||
update_site_config("rq_password", user_credentials['bench'][1], validate=False, | |||||
site_config_path=common_site_config_path) | |||||
update_site_config("use_rq_auth", use_rq_auth, validate=False, | |||||
site_config_path=common_site_config_path) | |||||
click.secho('* ACL and site configs are updated with new user credentials. ' | |||||
'Please restart Redis Queue server to enable namespaces.', | |||||
fg='green') | |||||
common_site_config_path = os.path.join(sites_path, "common_site_config.json") | |||||
update_site_config( | |||||
"rq_username", | |||||
user_credentials["bench"][0], | |||||
validate=False, | |||||
site_config_path=common_site_config_path, | |||||
) | |||||
update_site_config( | |||||
"rq_password", | |||||
user_credentials["bench"][1], | |||||
validate=False, | |||||
site_config_path=common_site_config_path, | |||||
) | |||||
update_site_config( | |||||
"use_rq_auth", use_rq_auth, validate=False, site_config_path=common_site_config_path | |||||
) | |||||
click.secho( | |||||
"* ACL and site configs are updated with new user credentials. " | |||||
"Please restart Redis Queue server to enable namespaces.", | |||||
fg="green", | |||||
) | |||||
if set_admin_password: | if set_admin_password: | ||||
env_key = 'RQ_ADMIN_PASWORD' | |||||
click.secho('* Redis admin password is successfully set up. ' | |||||
'Include below line in .bashrc file for system to use', | |||||
fg='green') | |||||
env_key = "RQ_ADMIN_PASWORD" | |||||
click.secho( | |||||
"* Redis admin password is successfully set up. " | |||||
"Include below line in .bashrc file for system to use", | |||||
fg="green", | |||||
) | |||||
click.secho(f"`export {env_key}={user_credentials['default'][1]}`") | click.secho(f"`export {env_key}={user_credentials['default'][1]}`") | ||||
click.secho('NOTE: Please save the admin password as you ' | |||||
'can not access redis server without the password', | |||||
fg='yellow') | |||||
click.secho( | |||||
"NOTE: Please save the admin password as you " | |||||
"can not access redis server without the password", | |||||
fg="yellow", | |||||
) | |||||
commands = [ | |||||
create_rq_users | |||||
] | |||||
commands = [create_rq_users] |
@@ -1,15 +1,20 @@ | |||||
import click | |||||
import sys | import sys | ||||
import click | |||||
import frappe | import frappe | ||||
from frappe.utils import cint | |||||
from frappe.commands import pass_context, get_site | |||||
from frappe.commands import get_site, pass_context | |||||
from frappe.exceptions import SiteNotSpecifiedError | from frappe.exceptions import SiteNotSpecifiedError | ||||
from frappe.utils import cint | |||||
def _is_scheduler_enabled(): | def _is_scheduler_enabled(): | ||||
enable_scheduler = False | enable_scheduler = False | ||||
try: | try: | ||||
frappe.connect() | frappe.connect() | ||||
enable_scheduler = cint(frappe.db.get_single_value("System Settings", "enable_scheduler")) and True or False | |||||
enable_scheduler = ( | |||||
cint(frappe.db.get_single_value("System Settings", "enable_scheduler")) and True or False | |||||
) | |||||
except: | except: | ||||
pass | pass | ||||
finally: | finally: | ||||
@@ -44,11 +49,12 @@ def trigger_scheduler_event(context, event): | |||||
sys.exit(exit_code) | sys.exit(exit_code) | ||||
@click.command('enable-scheduler') | |||||
@click.command("enable-scheduler") | |||||
@pass_context | @pass_context | ||||
def enable_scheduler(context): | def enable_scheduler(context): | ||||
"Enable scheduler" | "Enable scheduler" | ||||
import frappe.utils.scheduler | import frappe.utils.scheduler | ||||
for site in context.sites: | for site in context.sites: | ||||
try: | try: | ||||
frappe.init(site=site) | frappe.init(site=site) | ||||
@@ -61,11 +67,13 @@ def enable_scheduler(context): | |||||
if not context.sites: | if not context.sites: | ||||
raise SiteNotSpecifiedError | raise SiteNotSpecifiedError | ||||
@click.command('disable-scheduler') | |||||
@click.command("disable-scheduler") | |||||
@pass_context | @pass_context | ||||
def disable_scheduler(context): | def disable_scheduler(context): | ||||
"Disable scheduler" | "Disable scheduler" | ||||
import frappe.utils.scheduler | import frappe.utils.scheduler | ||||
for site in context.sites: | for site in context.sites: | ||||
try: | try: | ||||
frappe.init(site=site) | frappe.init(site=site) | ||||
@@ -79,13 +87,13 @@ def disable_scheduler(context): | |||||
raise SiteNotSpecifiedError | raise SiteNotSpecifiedError | ||||
@click.command('scheduler') | |||||
@click.option('--site', help='site name') | |||||
@click.argument('state', type=click.Choice(['pause', 'resume', 'disable', 'enable'])) | |||||
@click.command("scheduler") | |||||
@click.option("--site", help="site name") | |||||
@click.argument("state", type=click.Choice(["pause", "resume", "disable", "enable"])) | |||||
@pass_context | @pass_context | ||||
def scheduler(context, state, site=None): | def scheduler(context, state, site=None): | ||||
from frappe.installer import update_site_config | |||||
import frappe.utils.scheduler | import frappe.utils.scheduler | ||||
from frappe.installer import update_site_config | |||||
if not site: | if not site: | ||||
site = get_site(context) | site = get_site(context) | ||||
@@ -93,58 +101,64 @@ def scheduler(context, state, site=None): | |||||
try: | try: | ||||
frappe.init(site=site) | frappe.init(site=site) | ||||
if state == 'pause': | |||||
update_site_config('pause_scheduler', 1) | |||||
elif state == 'resume': | |||||
update_site_config('pause_scheduler', 0) | |||||
elif state == 'disable': | |||||
if state == "pause": | |||||
update_site_config("pause_scheduler", 1) | |||||
elif state == "resume": | |||||
update_site_config("pause_scheduler", 0) | |||||
elif state == "disable": | |||||
frappe.connect() | frappe.connect() | ||||
frappe.utils.scheduler.disable_scheduler() | frappe.utils.scheduler.disable_scheduler() | ||||
frappe.db.commit() | frappe.db.commit() | ||||
elif state == 'enable': | |||||
elif state == "enable": | |||||
frappe.connect() | frappe.connect() | ||||
frappe.utils.scheduler.enable_scheduler() | frappe.utils.scheduler.enable_scheduler() | ||||
frappe.db.commit() | frappe.db.commit() | ||||
print('Scheduler {0}d for site {1}'.format(state, site)) | |||||
print("Scheduler {0}d for site {1}".format(state, site)) | |||||
finally: | finally: | ||||
frappe.destroy() | frappe.destroy() | ||||
@click.command('set-maintenance-mode') | |||||
@click.option('--site', help='site name') | |||||
@click.argument('state', type=click.Choice(['on', 'off'])) | |||||
@click.command("set-maintenance-mode") | |||||
@click.option("--site", help="site name") | |||||
@click.argument("state", type=click.Choice(["on", "off"])) | |||||
@pass_context | @pass_context | ||||
def set_maintenance_mode(context, state, site=None): | def set_maintenance_mode(context, state, site=None): | ||||
from frappe.installer import update_site_config | from frappe.installer import update_site_config | ||||
if not site: | if not site: | ||||
site = get_site(context) | site = get_site(context) | ||||
try: | try: | ||||
frappe.init(site=site) | frappe.init(site=site) | ||||
update_site_config('maintenance_mode', 1 if (state == 'on') else 0) | |||||
update_site_config("maintenance_mode", 1 if (state == "on") else 0) | |||||
finally: | finally: | ||||
frappe.destroy() | frappe.destroy() | ||||
@click.command('doctor') #Passing context always gets a site and if there is no use site it breaks | |||||
@click.option('--site', help='site name') | |||||
@click.command( | |||||
"doctor" | |||||
) # Passing context always gets a site and if there is no use site it breaks | |||||
@click.option("--site", help="site name") | |||||
@pass_context | @pass_context | ||||
def doctor(context, site=None): | def doctor(context, site=None): | ||||
"Get diagnostic info about background workers" | "Get diagnostic info about background workers" | ||||
from frappe.utils.doctor import doctor as _doctor | from frappe.utils.doctor import doctor as _doctor | ||||
if not site: | if not site: | ||||
site = get_site(context, raise_err=False) | site = get_site(context, raise_err=False) | ||||
return _doctor(site=site) | return _doctor(site=site) | ||||
@click.command('show-pending-jobs') | |||||
@click.option('--site', help='site name') | |||||
@click.command("show-pending-jobs") | |||||
@click.option("--site", help="site name") | |||||
@pass_context | @pass_context | ||||
def show_pending_jobs(context, site=None): | def show_pending_jobs(context, site=None): | ||||
"Get diagnostic info about background jobs" | "Get diagnostic info about background jobs" | ||||
from frappe.utils.doctor import pending_jobs as _pending_jobs | from frappe.utils.doctor import pending_jobs as _pending_jobs | ||||
if not site: | if not site: | ||||
site = get_site(context) | site = get_site(context) | ||||
@@ -153,35 +167,45 @@ def show_pending_jobs(context, site=None): | |||||
return pending_jobs | return pending_jobs | ||||
@click.command('purge-jobs') | |||||
@click.option('--site', help='site name') | |||||
@click.option('--queue', default=None, help='one of "low", "default", "high') | |||||
@click.option('--event', default=None, help='one of "all", "weekly", "monthly", "hourly", "daily", "weekly_long", "daily_long"') | |||||
@click.command("purge-jobs") | |||||
@click.option("--site", help="site name") | |||||
@click.option("--queue", default=None, help='one of "low", "default", "high') | |||||
@click.option( | |||||
"--event", | |||||
default=None, | |||||
help='one of "all", "weekly", "monthly", "hourly", "daily", "weekly_long", "daily_long"', | |||||
) | |||||
def purge_jobs(site=None, queue=None, event=None): | def purge_jobs(site=None, queue=None, event=None): | ||||
"Purge any pending periodic tasks, if event option is not given, it will purge everything for the site" | "Purge any pending periodic tasks, if event option is not given, it will purge everything for the site" | ||||
from frappe.utils.doctor import purge_pending_jobs | from frappe.utils.doctor import purge_pending_jobs | ||||
frappe.init(site or '') | |||||
frappe.init(site or "") | |||||
count = purge_pending_jobs(event=event, site=site, queue=queue) | count = purge_pending_jobs(event=event, site=site, queue=queue) | ||||
print("Purged {} jobs".format(count)) | print("Purged {} jobs".format(count)) | ||||
@click.command('schedule') | |||||
@click.command("schedule") | |||||
def start_scheduler(): | def start_scheduler(): | ||||
from frappe.utils.scheduler import start_scheduler | from frappe.utils.scheduler import start_scheduler | ||||
start_scheduler() | start_scheduler() | ||||
@click.command('worker') | |||||
@click.option('--queue', type=str) | |||||
@click.option('--quiet', is_flag = True, default = False, help = 'Hide Log Outputs') | |||||
@click.option('-u', '--rq-username', default=None, help='Redis ACL user') | |||||
@click.option('-p', '--rq-password', default=None, help='Redis ACL user password') | |||||
def start_worker(queue, quiet = False, rq_username=None, rq_password=None): | |||||
"""Site is used to find redis credentals. | |||||
""" | |||||
@click.command("worker") | |||||
@click.option("--queue", type=str) | |||||
@click.option("--quiet", is_flag=True, default=False, help="Hide Log Outputs") | |||||
@click.option("-u", "--rq-username", default=None, help="Redis ACL user") | |||||
@click.option("-p", "--rq-password", default=None, help="Redis ACL user password") | |||||
def start_worker(queue, quiet=False, rq_username=None, rq_password=None): | |||||
"""Site is used to find redis credentals.""" | |||||
from frappe.utils.background_jobs import start_worker | from frappe.utils.background_jobs import start_worker | ||||
start_worker(queue, quiet = quiet, rq_username=rq_username, rq_password=rq_password) | |||||
@click.command('ready-for-migration') | |||||
@click.option('--site', help='site name') | |||||
start_worker(queue, quiet=quiet, rq_username=rq_username, rq_password=rq_password) | |||||
@click.command("ready-for-migration") | |||||
@click.option("--site", help="site name") | |||||
@pass_context | @pass_context | ||||
def ready_for_migration(context, site=None): | def ready_for_migration(context, site=None): | ||||
from frappe.utils.doctor import get_pending_jobs | from frappe.utils.doctor import get_pending_jobs | ||||
@@ -194,16 +218,17 @@ def ready_for_migration(context, site=None): | |||||
pending_jobs = get_pending_jobs(site=site) | pending_jobs = get_pending_jobs(site=site) | ||||
if pending_jobs: | if pending_jobs: | ||||
print('NOT READY for migration: site {0} has pending background jobs'.format(site)) | |||||
print("NOT READY for migration: site {0} has pending background jobs".format(site)) | |||||
sys.exit(1) | sys.exit(1) | ||||
else: | else: | ||||
print('READY for migration: site {0} does not have any background jobs'.format(site)) | |||||
print("READY for migration: site {0} does not have any background jobs".format(site)) | |||||
return 0 | return 0 | ||||
finally: | finally: | ||||
frappe.destroy() | frappe.destroy() | ||||
commands = [ | commands = [ | ||||
disable_scheduler, | disable_scheduler, | ||||
doctor, | doctor, | ||||
@@ -1,13 +1,16 @@ | |||||
import click | import click | ||||
from frappe.commands import pass_context, get_site | |||||
from frappe.commands import get_site, pass_context | |||||
from frappe.exceptions import SiteNotSpecifiedError | from frappe.exceptions import SiteNotSpecifiedError | ||||
# translation | # translation | ||||
@click.command('build-message-files') | |||||
@click.command("build-message-files") | |||||
@pass_context | @pass_context | ||||
def build_message_files(context): | def build_message_files(context): | ||||
"Build message files for translation" | "Build message files for translation" | ||||
import frappe.translate | import frappe.translate | ||||
for site in context.sites: | for site in context.sites: | ||||
try: | try: | ||||
frappe.init(site=site) | frappe.init(site=site) | ||||
@@ -18,32 +21,41 @@ def build_message_files(context): | |||||
if not context.sites: | if not context.sites: | ||||
raise SiteNotSpecifiedError | raise SiteNotSpecifiedError | ||||
@click.command('new-language') #, help="Create lang-code.csv for given app") | |||||
@click.command("new-language") # , help="Create lang-code.csv for given app") | |||||
@pass_context | @pass_context | ||||
@click.argument('lang_code') #, help="Language code eg. en") | |||||
@click.argument('app') #, help="App name eg. frappe") | |||||
@click.argument("lang_code") # , help="Language code eg. en") | |||||
@click.argument("app") # , help="App name eg. frappe") | |||||
def new_language(context, lang_code, app): | def new_language(context, lang_code, app): | ||||
"""Create lang-code.csv for given app""" | """Create lang-code.csv for given app""" | ||||
import frappe.translate | import frappe.translate | ||||
if not context['sites']: | |||||
raise Exception('--site is required') | |||||
if not context["sites"]: | |||||
raise Exception("--site is required") | |||||
# init site | # init site | ||||
frappe.connect(site=context['sites'][0]) | |||||
frappe.connect(site=context["sites"][0]) | |||||
frappe.translate.write_translations_file(app, lang_code) | frappe.translate.write_translations_file(app, lang_code) | ||||
print("File created at ./apps/{app}/{app}/translations/{lang_code}.csv".format(app=app, lang_code=lang_code)) | |||||
print("You will need to add the language in frappe/geo/languages.json, if you haven't done it already.") | |||||
print( | |||||
"File created at ./apps/{app}/{app}/translations/{lang_code}.csv".format( | |||||
app=app, lang_code=lang_code | |||||
) | |||||
) | |||||
print( | |||||
"You will need to add the language in frappe/geo/languages.json, if you haven't done it already." | |||||
) | |||||
@click.command('get-untranslated') | |||||
@click.argument('lang') | |||||
@click.argument('untranslated_file') | |||||
@click.option('--all', default=False, is_flag=True, help='Get all message strings') | |||||
@click.command("get-untranslated") | |||||
@click.argument("lang") | |||||
@click.argument("untranslated_file") | |||||
@click.option("--all", default=False, is_flag=True, help="Get all message strings") | |||||
@pass_context | @pass_context | ||||
def get_untranslated(context, lang, untranslated_file, all=None): | def get_untranslated(context, lang, untranslated_file, all=None): | ||||
"Get untranslated strings for language" | "Get untranslated strings for language" | ||||
import frappe.translate | import frappe.translate | ||||
site = get_site(context) | site = get_site(context) | ||||
try: | try: | ||||
frappe.init(site=site) | frappe.init(site=site) | ||||
@@ -52,14 +64,16 @@ def get_untranslated(context, lang, untranslated_file, all=None): | |||||
finally: | finally: | ||||
frappe.destroy() | frappe.destroy() | ||||
@click.command('update-translations') | |||||
@click.argument('lang') | |||||
@click.argument('untranslated_file') | |||||
@click.argument('translated-file') | |||||
@click.command("update-translations") | |||||
@click.argument("lang") | |||||
@click.argument("untranslated_file") | |||||
@click.argument("translated-file") | |||||
@pass_context | @pass_context | ||||
def update_translations(context, lang, untranslated_file, translated_file): | def update_translations(context, lang, untranslated_file, translated_file): | ||||
"Update translated strings" | "Update translated strings" | ||||
import frappe.translate | import frappe.translate | ||||
site = get_site(context) | site = get_site(context) | ||||
try: | try: | ||||
frappe.init(site=site) | frappe.init(site=site) | ||||
@@ -68,13 +82,15 @@ def update_translations(context, lang, untranslated_file, translated_file): | |||||
finally: | finally: | ||||
frappe.destroy() | frappe.destroy() | ||||
@click.command('import-translations') | |||||
@click.argument('lang') | |||||
@click.argument('path') | |||||
@click.command("import-translations") | |||||
@click.argument("lang") | |||||
@click.argument("path") | |||||
@pass_context | @pass_context | ||||
def import_translations(context, lang, path): | def import_translations(context, lang, path): | ||||
"Update translated strings" | "Update translated strings" | ||||
import frappe.translate | import frappe.translate | ||||
site = get_site(context) | site = get_site(context) | ||||
try: | try: | ||||
frappe.init(site=site) | frappe.init(site=site) | ||||
@@ -83,6 +99,7 @@ def import_translations(context, lang, path): | |||||
finally: | finally: | ||||
frappe.destroy() | frappe.destroy() | ||||
commands = [ | commands = [ | ||||
build_message_files, | build_message_files, | ||||
get_untranslated, | get_untranslated, | ||||
@@ -1,14 +1,20 @@ | |||||
import frappe | import frappe | ||||
from frappe import _ | from frappe import _ | ||||
from frappe.desk.moduleview import (get_data, get_onboard_items, config_exists, get_module_link_items_from_list) | |||||
from frappe.desk.moduleview import ( | |||||
config_exists, | |||||
get_data, | |||||
get_module_link_items_from_list, | |||||
get_onboard_items, | |||||
) | |||||
def get_modules_from_all_apps_for_user(user=None): | def get_modules_from_all_apps_for_user(user=None): | ||||
if not user: | if not user: | ||||
user = frappe.session.user | user = frappe.session.user | ||||
all_modules = get_modules_from_all_apps() | all_modules = get_modules_from_all_apps() | ||||
global_blocked_modules = frappe.get_doc('User', 'Administrator').get_blocked_modules() | |||||
user_blocked_modules = frappe.get_doc('User', user).get_blocked_modules() | |||||
global_blocked_modules = frappe.get_doc("User", "Administrator").get_blocked_modules() | |||||
user_blocked_modules = frappe.get_doc("User", user).get_blocked_modules() | |||||
blocked_modules = global_blocked_modules + user_blocked_modules | blocked_modules = global_blocked_modules + user_blocked_modules | ||||
allowed_modules_list = [m for m in all_modules if m.get("module_name") not in blocked_modules] | allowed_modules_list = [m for m in all_modules if m.get("module_name") not in blocked_modules] | ||||
@@ -22,31 +28,31 @@ def get_modules_from_all_apps_for_user(user=None): | |||||
module["onboard_present"] = 1 | module["onboard_present"] = 1 | ||||
# Set defaults links | # Set defaults links | ||||
module["links"] = get_onboard_items(module["app"], frappe.scrub(module_name))[:5] | |||||
module["links"] = get_onboard_items(module["app"], frappe.scrub(module_name))[:5] | |||||
return allowed_modules_list | return allowed_modules_list | ||||
def get_modules_from_all_apps(): | def get_modules_from_all_apps(): | ||||
modules_list = [] | modules_list = [] | ||||
for app in frappe.get_installed_apps(): | for app in frappe.get_installed_apps(): | ||||
modules_list += get_modules_from_app(app) | modules_list += get_modules_from_app(app) | ||||
return modules_list | return modules_list | ||||
def get_modules_from_app(app): | def get_modules_from_app(app): | ||||
return frappe.get_all('Module Def', | |||||
filters={'app_name': app}, | |||||
fields=['module_name', 'app_name as app'] | |||||
return frappe.get_all( | |||||
"Module Def", filters={"app_name": app}, fields=["module_name", "app_name as app"] | |||||
) | ) | ||||
def get_all_empty_tables_by_module(): | def get_all_empty_tables_by_module(): | ||||
table_rows = frappe.qb.Field("table_rows") | table_rows = frappe.qb.Field("table_rows") | ||||
table_name = frappe.qb.Field("table_name") | table_name = frappe.qb.Field("table_name") | ||||
information_schema = frappe.qb.Schema("information_schema") | information_schema = frappe.qb.Schema("information_schema") | ||||
empty_tables = ( | empty_tables = ( | ||||
frappe.qb.from_(information_schema.tables) | |||||
.select(table_name) | |||||
.where(table_rows == 0) | |||||
frappe.qb.from_(information_schema.tables).select(table_name).where(table_rows == 0) | |||||
).run() | ).run() | ||||
empty_tables = {r[0] for r in empty_tables} | empty_tables = {r[0] for r in empty_tables} | ||||
@@ -62,8 +68,10 @@ def get_all_empty_tables_by_module(): | |||||
empty_tables_by_module[module] = [doctype] | empty_tables_by_module[module] = [doctype] | ||||
return empty_tables_by_module | return empty_tables_by_module | ||||
def is_domain(module): | def is_domain(module): | ||||
return module.get("category") == "Domains" | return module.get("category") == "Domains" | ||||
def is_module(module): | def is_module(module): | ||||
return module.get("type") == "module" | |||||
return module.get("type") == "module" |
@@ -1,12 +1,13 @@ | |||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | |||||
from frappe import _ | |||||
import functools | import functools | ||||
import re | import re | ||||
import frappe | |||||
from frappe import _ | |||||
def load_address_and_contact(doc, key=None): | def load_address_and_contact(doc, key=None): | ||||
"""Loads address list and contact list in `__onload`""" | """Loads address list and contact list in `__onload`""" | ||||
from frappe.contacts.doctype.address.address import get_address_display, get_condensed_address | from frappe.contacts.doctype.address.address import get_address_display, get_condensed_address | ||||
@@ -18,15 +19,18 @@ def load_address_and_contact(doc, key=None): | |||||
] | ] | ||||
address_list = frappe.get_list("Address", filters=filters, fields=["*"]) | address_list = frappe.get_list("Address", filters=filters, fields=["*"]) | ||||
address_list = [a.update({"display": get_address_display(a)}) | |||||
for a in address_list] | |||||
address_list = [a.update({"display": get_address_display(a)}) for a in address_list] | |||||
address_list = sorted(address_list, | |||||
key = functools.cmp_to_key(lambda a, b: | |||||
(int(a.is_primary_address - b.is_primary_address)) or | |||||
(1 if a.modified - b.modified else 0)), reverse=True) | |||||
address_list = sorted( | |||||
address_list, | |||||
key=functools.cmp_to_key( | |||||
lambda a, b: (int(a.is_primary_address - b.is_primary_address)) | |||||
or (1 if a.modified - b.modified else 0) | |||||
), | |||||
reverse=True, | |||||
) | |||||
doc.set_onload('addr_list', address_list) | |||||
doc.set_onload("addr_list", address_list) | |||||
contact_list = [] | contact_list = [] | ||||
filters = [ | filters = [ | ||||
@@ -37,29 +41,38 @@ def load_address_and_contact(doc, key=None): | |||||
contact_list = frappe.get_list("Contact", filters=filters, fields=["*"]) | contact_list = frappe.get_list("Contact", filters=filters, fields=["*"]) | ||||
for contact in contact_list: | for contact in contact_list: | ||||
contact["email_ids"] = frappe.get_all("Contact Email", filters={ | |||||
"parenttype": "Contact", | |||||
"parent": contact.name, | |||||
"is_primary": 0 | |||||
}, fields=["email_id"]) | |||||
contact["phone_nos"] = frappe.get_all("Contact Phone", filters={ | |||||
contact["email_ids"] = frappe.get_all( | |||||
"Contact Email", | |||||
filters={"parenttype": "Contact", "parent": contact.name, "is_primary": 0}, | |||||
fields=["email_id"], | |||||
) | |||||
contact["phone_nos"] = frappe.get_all( | |||||
"Contact Phone", | |||||
filters={ | |||||
"parenttype": "Contact", | "parenttype": "Contact", | ||||
"parent": contact.name, | "parent": contact.name, | ||||
"is_primary_phone": 0, | "is_primary_phone": 0, | ||||
"is_primary_mobile_no": 0 | |||||
}, fields=["phone"]) | |||||
"is_primary_mobile_no": 0, | |||||
}, | |||||
fields=["phone"], | |||||
) | |||||
if contact.address: | if contact.address: | ||||
address = frappe.get_doc("Address", contact.address) | address = frappe.get_doc("Address", contact.address) | ||||
contact["address"] = get_condensed_address(address) | contact["address"] = get_condensed_address(address) | ||||
contact_list = sorted(contact_list, | |||||
key = functools.cmp_to_key(lambda a, b: | |||||
(int(a.is_primary_contact - b.is_primary_contact)) or | |||||
(1 if a.modified - b.modified else 0)), reverse=True) | |||||
contact_list = sorted( | |||||
contact_list, | |||||
key=functools.cmp_to_key( | |||||
lambda a, b: (int(a.is_primary_contact - b.is_primary_contact)) | |||||
or (1 if a.modified - b.modified else 0) | |||||
), | |||||
reverse=True, | |||||
) | |||||
doc.set_onload("contact_list", contact_list) | |||||
doc.set_onload('contact_list', contact_list) | |||||
def has_permission(doc, ptype, user): | def has_permission(doc, ptype, user): | ||||
links = get_permitted_and_not_permitted_links(doc.doctype) | links = get_permitted_and_not_permitted_links(doc.doctype) | ||||
@@ -69,7 +82,7 @@ def has_permission(doc, ptype, user): | |||||
# True if any one is True or all are empty | # True if any one is True or all are empty | ||||
names = [] | names = [] | ||||
for df in (links.get("permitted_links") + links.get("not_permitted_links")): | |||||
for df in links.get("permitted_links") + links.get("not_permitted_links"): | |||||
doctype = df.options | doctype = df.options | ||||
name = doc.get(df.fieldname) | name = doc.get(df.fieldname) | ||||
names.append(name) | names.append(name) | ||||
@@ -81,12 +94,15 @@ def has_permission(doc, ptype, user): | |||||
return True | return True | ||||
return False | return False | ||||
def get_permission_query_conditions_for_contact(user): | def get_permission_query_conditions_for_contact(user): | ||||
return get_permission_query_conditions("Contact") | return get_permission_query_conditions("Contact") | ||||
def get_permission_query_conditions_for_address(user): | def get_permission_query_conditions_for_address(user): | ||||
return get_permission_query_conditions("Address") | return get_permission_query_conditions("Address") | ||||
def get_permission_query_conditions(doctype): | def get_permission_query_conditions(doctype): | ||||
links = get_permitted_and_not_permitted_links(doctype) | links = get_permitted_and_not_permitted_links(doctype) | ||||
@@ -100,7 +116,9 @@ def get_permission_query_conditions(doctype): | |||||
# when everything is not permitted | # when everything is not permitted | ||||
for df in links.get("not_permitted_links"): | for df in links.get("not_permitted_links"): | ||||
# like ifnull(customer, '')='' and ifnull(supplier, '')='' | # like ifnull(customer, '')='' and ifnull(supplier, '')='' | ||||
conditions.append("ifnull(`tab{doctype}`.`{fieldname}`, '')=''".format(doctype=doctype, fieldname=df.fieldname)) | |||||
conditions.append( | |||||
"ifnull(`tab{doctype}`.`{fieldname}`, '')=''".format(doctype=doctype, fieldname=df.fieldname) | |||||
) | |||||
return "( " + " and ".join(conditions) + " )" | return "( " + " and ".join(conditions) + " )" | ||||
@@ -109,10 +127,13 @@ def get_permission_query_conditions(doctype): | |||||
for df in links.get("permitted_links"): | for df in links.get("permitted_links"): | ||||
# like ifnull(customer, '')!='' or ifnull(supplier, '')!='' | # like ifnull(customer, '')!='' or ifnull(supplier, '')!='' | ||||
conditions.append("ifnull(`tab{doctype}`.`{fieldname}`, '')!=''".format(doctype=doctype, fieldname=df.fieldname)) | |||||
conditions.append( | |||||
"ifnull(`tab{doctype}`.`{fieldname}`, '')!=''".format(doctype=doctype, fieldname=df.fieldname) | |||||
) | |||||
return "( " + " or ".join(conditions) + " )" | return "( " + " or ".join(conditions) + " )" | ||||
def get_permitted_and_not_permitted_links(doctype): | def get_permitted_and_not_permitted_links(doctype): | ||||
permitted_links = [] | permitted_links = [] | ||||
not_permitted_links = [] | not_permitted_links = [] | ||||
@@ -129,40 +150,40 @@ def get_permitted_and_not_permitted_links(doctype): | |||||
else: | else: | ||||
not_permitted_links.append(df) | not_permitted_links.append(df) | ||||
return { | |||||
"permitted_links": permitted_links, | |||||
"not_permitted_links": not_permitted_links | |||||
} | |||||
return {"permitted_links": permitted_links, "not_permitted_links": not_permitted_links} | |||||
def delete_contact_and_address(doctype, docname): | def delete_contact_and_address(doctype, docname): | ||||
for parenttype in ('Contact', 'Address'): | |||||
items = frappe.db.sql_list("""select parent from `tabDynamic Link` | |||||
for parenttype in ("Contact", "Address"): | |||||
items = frappe.db.sql_list( | |||||
"""select parent from `tabDynamic Link` | |||||
where parenttype=%s and link_doctype=%s and link_name=%s""", | where parenttype=%s and link_doctype=%s and link_name=%s""", | ||||
(parenttype, doctype, docname)) | |||||
(parenttype, doctype, docname), | |||||
) | |||||
for name in items: | for name in items: | ||||
doc = frappe.get_doc(parenttype, name) | doc = frappe.get_doc(parenttype, name) | ||||
if len(doc.links)==1: | |||||
if len(doc.links) == 1: | |||||
doc.delete() | doc.delete() | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
@frappe.validate_and_sanitize_search_inputs | @frappe.validate_and_sanitize_search_inputs | ||||
def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, filters): | def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, filters): | ||||
if not txt: txt = "" | |||||
if not txt: | |||||
txt = "" | |||||
doctypes = frappe.db.get_all("DocField", filters=filters, fields=["parent"], | |||||
distinct=True, as_list=True) | |||||
doctypes = frappe.db.get_all( | |||||
"DocField", filters=filters, fields=["parent"], distinct=True, as_list=True | |||||
) | |||||
doctypes = tuple(d for d in doctypes if re.search(txt+".*", _(d[0]), re.IGNORECASE)) | |||||
doctypes = tuple(d for d in doctypes if re.search(txt + ".*", _(d[0]), re.IGNORECASE)) | |||||
filters.update({ | |||||
"dt": ("not in", [d[0] for d in doctypes]) | |||||
}) | |||||
filters.update({"dt": ("not in", [d[0] for d in doctypes])}) | |||||
_doctypes = frappe.db.get_all("Custom Field", filters=filters, fields=["dt"], | |||||
as_list=True) | |||||
_doctypes = frappe.db.get_all("Custom Field", filters=filters, fields=["dt"], as_list=True) | |||||
_doctypes = tuple([d for d in _doctypes if re.search(txt+".*", _(d[0]), re.IGNORECASE)]) | |||||
_doctypes = tuple([d for d in _doctypes if re.search(txt + ".*", _(d[0]), re.IGNORECASE)]) | |||||
all_doctypes = [d[0] for d in doctypes + _doctypes] | all_doctypes = [d[0] for d in doctypes + _doctypes] | ||||
allowed_doctypes = frappe.permissions.get_doctypes_with_read() | allowed_doctypes = frappe.permissions.get_doctypes_with_read() | ||||
@@ -172,6 +193,7 @@ def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, fil | |||||
return valid_doctypes | return valid_doctypes | ||||
def set_link_title(doc): | def set_link_title(doc): | ||||
if not doc.links: | if not doc.links: | ||||
return | return | ||||
@@ -2,16 +2,15 @@ | |||||
# Copyright (c) 2015, Frappe Technologies and contributors | # Copyright (c) 2015, Frappe Technologies and contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | |||||
from frappe import throw, _ | |||||
from frappe.utils import cstr | |||||
from jinja2 import TemplateSyntaxError | |||||
import frappe | |||||
from frappe import _, throw | |||||
from frappe.contacts.address_and_contact import set_link_title | |||||
from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links | |||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from jinja2 import TemplateSyntaxError | |||||
from frappe.model.naming import make_autoname | from frappe.model.naming import make_autoname | ||||
from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links | |||||
from frappe.contacts.address_and_contact import set_link_title | |||||
from frappe.utils import cstr | |||||
class Address(Document): | class Address(Document): | ||||
@@ -24,10 +23,11 @@ class Address(Document): | |||||
self.address_title = self.links[0].link_name | self.address_title = self.links[0].link_name | ||||
if self.address_title: | if self.address_title: | ||||
self.name = (cstr(self.address_title).strip() + "-" + cstr(_(self.address_type)).strip()) | |||||
self.name = cstr(self.address_title).strip() + "-" + cstr(_(self.address_type)).strip() | |||||
if frappe.db.exists("Address", self.name): | if frappe.db.exists("Address", self.name): | ||||
self.name = make_autoname(cstr(self.address_title).strip() + "-" + | |||||
cstr(self.address_type).strip() + "-.#") | |||||
self.name = make_autoname( | |||||
cstr(self.address_title).strip() + "-" + cstr(self.address_type).strip() + "-.#" | |||||
) | |||||
else: | else: | ||||
throw(_("Address Title is mandatory.")) | throw(_("Address Title is mandatory.")) | ||||
@@ -42,15 +42,15 @@ class Address(Document): | |||||
if not self.links: | if not self.links: | ||||
contact_name = frappe.db.get_value("Contact", {"email_id": self.owner}) | contact_name = frappe.db.get_value("Contact", {"email_id": self.owner}) | ||||
if contact_name: | if contact_name: | ||||
contact = frappe.get_cached_doc('Contact', contact_name) | |||||
contact = frappe.get_cached_doc("Contact", contact_name) | |||||
for link in contact.links: | for link in contact.links: | ||||
self.append('links', dict(link_doctype=link.link_doctype, link_name=link.link_name)) | |||||
self.append("links", dict(link_doctype=link.link_doctype, link_name=link.link_name)) | |||||
return True | return True | ||||
return False | return False | ||||
def validate_preferred_address(self): | def validate_preferred_address(self): | ||||
preferred_fields = ['is_primary_address', 'is_shipping_address'] | |||||
preferred_fields = ["is_primary_address", "is_shipping_address"] | |||||
for field in preferred_fields: | for field in preferred_fields: | ||||
if self.get(field): | if self.get(field): | ||||
@@ -76,9 +76,11 @@ class Address(Document): | |||||
return False | return False | ||||
def get_preferred_address(doctype, name, preferred_key='is_primary_address'): | |||||
if preferred_key in ['is_shipping_address', 'is_primary_address']: | |||||
address = frappe.db.sql(""" SELECT | |||||
def get_preferred_address(doctype, name, preferred_key="is_primary_address"): | |||||
if preferred_key in ["is_shipping_address", "is_primary_address"]: | |||||
address = frappe.db.sql( | |||||
""" SELECT | |||||
addr.name | addr.name | ||||
FROM | FROM | ||||
`tabAddress` addr, `tabDynamic Link` dl | `tabAddress` addr, `tabDynamic Link` dl | ||||
@@ -86,27 +88,37 @@ def get_preferred_address(doctype, name, preferred_key='is_primary_address'): | |||||
dl.parent = addr.name and dl.link_doctype = %s and | dl.parent = addr.name and dl.link_doctype = %s and | ||||
dl.link_name = %s and ifnull(addr.disabled, 0) = 0 and | dl.link_name = %s and ifnull(addr.disabled, 0) = 0 and | ||||
%s = %s | %s = %s | ||||
""" % ('%s', '%s', preferred_key, '%s'), (doctype, name, 1), as_dict=1) | |||||
""" | |||||
% ("%s", "%s", preferred_key, "%s"), | |||||
(doctype, name, 1), | |||||
as_dict=1, | |||||
) | |||||
if address: | if address: | ||||
return address[0].name | return address[0].name | ||||
return | return | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_default_address(doctype, name, sort_key='is_primary_address'): | |||||
'''Returns default Address name for the given doctype, name''' | |||||
if sort_key not in ['is_shipping_address', 'is_primary_address']: | |||||
def get_default_address(doctype, name, sort_key="is_primary_address"): | |||||
"""Returns default Address name for the given doctype, name""" | |||||
if sort_key not in ["is_shipping_address", "is_primary_address"]: | |||||
return None | return None | ||||
out = frappe.db.sql(""" SELECT | |||||
out = frappe.db.sql( | |||||
""" SELECT | |||||
addr.name, addr.%s | addr.name, addr.%s | ||||
FROM | FROM | ||||
`tabAddress` addr, `tabDynamic Link` dl | `tabAddress` addr, `tabDynamic Link` dl | ||||
WHERE | WHERE | ||||
dl.parent = addr.name and dl.link_doctype = %s and | dl.parent = addr.name and dl.link_doctype = %s and | ||||
dl.link_name = %s and ifnull(addr.disabled, 0) = 0 | dl.link_name = %s and ifnull(addr.disabled, 0) = 0 | ||||
""" %(sort_key, '%s', '%s'), (doctype, name), as_dict=True) | |||||
""" | |||||
% (sort_key, "%s", "%s"), | |||||
(doctype, name), | |||||
as_dict=True, | |||||
) | |||||
if out: | if out: | ||||
for contact in out: | for contact in out: | ||||
@@ -150,84 +162,96 @@ def get_territory_from_address(address): | |||||
return territory | return territory | ||||
def get_list_context(context=None): | def get_list_context(context=None): | ||||
return { | return { | ||||
"title": _("Addresses"), | "title": _("Addresses"), | ||||
"get_list": get_address_list, | "get_list": get_address_list, | ||||
"row_template": "templates/includes/address_row.html", | "row_template": "templates/includes/address_row.html", | ||||
'no_breadcrumbs': True, | |||||
"no_breadcrumbs": True, | |||||
} | } | ||||
def get_address_list(doctype, txt, filters, limit_start, limit_page_length = 20, order_by = None): | |||||
def get_address_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by=None): | |||||
from frappe.www.list import get_list | from frappe.www.list import get_list | ||||
user = frappe.session.user | user = frappe.session.user | ||||
ignore_permissions = True | ignore_permissions = True | ||||
if not filters: filters = [] | |||||
if not filters: | |||||
filters = [] | |||||
filters.append(("Address", "owner", "=", user)) | filters.append(("Address", "owner", "=", user)) | ||||
return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions) | |||||
return get_list( | |||||
doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions | |||||
) | |||||
def has_website_permission(doc, ptype, user, verbose=False): | def has_website_permission(doc, ptype, user, verbose=False): | ||||
"""Returns true if there is a related lead or contact related to this document""" | """Returns true if there is a related lead or contact related to this document""" | ||||
contact_name = frappe.db.get_value("Contact", {"email_id": frappe.session.user}) | contact_name = frappe.db.get_value("Contact", {"email_id": frappe.session.user}) | ||||
if contact_name: | if contact_name: | ||||
contact = frappe.get_doc('Contact', contact_name) | |||||
contact = frappe.get_doc("Contact", contact_name) | |||||
return contact.has_common_link(doc) | return contact.has_common_link(doc) | ||||
return False | return False | ||||
def get_address_templates(address): | def get_address_templates(address): | ||||
result = frappe.db.get_value("Address Template", \ | |||||
{"country": address.get("country")}, ["name", "template"]) | |||||
result = frappe.db.get_value( | |||||
"Address Template", {"country": address.get("country")}, ["name", "template"] | |||||
) | |||||
if not result: | if not result: | ||||
result = frappe.db.get_value("Address Template", \ | |||||
{"is_default": 1}, ["name", "template"]) | |||||
result = frappe.db.get_value("Address Template", {"is_default": 1}, ["name", "template"]) | |||||
if not result: | if not result: | ||||
frappe.throw(_("No default Address Template found. Please create a new one from Setup > Printing and Branding > Address Template.")) | |||||
frappe.throw( | |||||
_( | |||||
"No default Address Template found. Please create a new one from Setup > Printing and Branding > Address Template." | |||||
) | |||||
) | |||||
else: | else: | ||||
return result | return result | ||||
def get_company_address(company): | def get_company_address(company): | ||||
ret = frappe._dict() | ret = frappe._dict() | ||||
ret.company_address = get_default_address('Company', company) | |||||
ret.company_address = get_default_address("Company", company) | |||||
ret.company_address_display = get_address_display(ret.company_address) | ret.company_address_display = get_address_display(ret.company_address) | ||||
return ret | return ret | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
@frappe.validate_and_sanitize_search_inputs | @frappe.validate_and_sanitize_search_inputs | ||||
def address_query(doctype, txt, searchfield, start, page_len, filters): | def address_query(doctype, txt, searchfield, start, page_len, filters): | ||||
from frappe.desk.reportview import get_match_cond | from frappe.desk.reportview import get_match_cond | ||||
link_doctype = filters.pop('link_doctype') | |||||
link_name = filters.pop('link_name') | |||||
link_doctype = filters.pop("link_doctype") | |||||
link_name = filters.pop("link_name") | |||||
condition = "" | condition = "" | ||||
meta = frappe.get_meta("Address") | meta = frappe.get_meta("Address") | ||||
for fieldname, value in filters.items(): | for fieldname, value in filters.items(): | ||||
if meta.get_field(fieldname) or fieldname in frappe.db.DEFAULT_COLUMNS: | if meta.get_field(fieldname) or fieldname in frappe.db.DEFAULT_COLUMNS: | ||||
condition += " and {field}={value}".format( | |||||
field=fieldname, | |||||
value=frappe.db.escape(value)) | |||||
condition += " and {field}={value}".format(field=fieldname, value=frappe.db.escape(value)) | |||||
searchfields = meta.get_search_fields() | searchfields = meta.get_search_fields() | ||||
if searchfield and (meta.get_field(searchfield)\ | |||||
or searchfield in frappe.db.DEFAULT_COLUMNS): | |||||
if searchfield and (meta.get_field(searchfield) or searchfield in frappe.db.DEFAULT_COLUMNS): | |||||
searchfields.append(searchfield) | searchfields.append(searchfield) | ||||
search_condition = '' | |||||
search_condition = "" | |||||
for field in searchfields: | for field in searchfields: | ||||
if search_condition == '': | |||||
search_condition += '`tabAddress`.`{field}` like %(txt)s'.format(field=field) | |||||
if search_condition == "": | |||||
search_condition += "`tabAddress`.`{field}` like %(txt)s".format(field=field) | |||||
else: | else: | ||||
search_condition += ' or `tabAddress`.`{field}` like %(txt)s'.format(field=field) | |||||
search_condition += " or `tabAddress`.`{field}` like %(txt)s".format(field=field) | |||||
return frappe.db.sql("""select | |||||
return frappe.db.sql( | |||||
"""select | |||||
`tabAddress`.name, `tabAddress`.city, `tabAddress`.country | `tabAddress`.name, `tabAddress`.city, `tabAddress`.country | ||||
from | from | ||||
`tabAddress`, `tabDynamic Link` | `tabAddress`, `tabDynamic Link` | ||||
@@ -245,19 +269,24 @@ def address_query(doctype, txt, searchfield, start, page_len, filters): | |||||
limit %(start)s, %(page_len)s """.format( | limit %(start)s, %(page_len)s """.format( | ||||
mcond=get_match_cond(doctype), | mcond=get_match_cond(doctype), | ||||
key=searchfield, | key=searchfield, | ||||
search_condition = search_condition, | |||||
condition=condition or ""), { | |||||
'txt': '%' + txt + '%', | |||||
'_txt': txt.replace("%", ""), | |||||
'start': start, | |||||
'page_len': page_len, | |||||
'link_name': link_name, | |||||
'link_doctype': link_doctype | |||||
}) | |||||
search_condition=search_condition, | |||||
condition=condition or "", | |||||
), | |||||
{ | |||||
"txt": "%" + txt + "%", | |||||
"_txt": txt.replace("%", ""), | |||||
"start": start, | |||||
"page_len": page_len, | |||||
"link_name": link_name, | |||||
"link_doctype": link_doctype, | |||||
}, | |||||
) | |||||
def get_condensed_address(doc): | def get_condensed_address(doc): | ||||
fields = ["address_title", "address_line1", "address_line2", "city", "county", "state", "country"] | fields = ["address_title", "address_line1", "address_line2", "city", "county", "state", "country"] | ||||
return ", ".join(doc.get(d) for d in fields if doc.get(d)) | return ", ".join(doc.get(d) for d in fields if doc.get(d)) | ||||
def update_preferred_address(address, field): | def update_preferred_address(address, field): | ||||
frappe.db.set_value('Address', address, field, 0) | |||||
frappe.db.set_value("Address", address, field, 0) |
@@ -1,31 +1,32 @@ | |||||
# -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
# Copyright (c) 2015, Frappe Technologies and Contributors | # Copyright (c) 2015, Frappe Technologies and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe, unittest | |||||
import unittest | |||||
import frappe | |||||
from frappe.contacts.doctype.address.address import get_address_display | from frappe.contacts.doctype.address.address import get_address_display | ||||
class TestAddress(unittest.TestCase): | class TestAddress(unittest.TestCase): | ||||
def test_template_works(self): | def test_template_works(self): | ||||
if not frappe.db.exists('Address Template', 'India'): | |||||
frappe.get_doc({ | |||||
"doctype": "Address Template", | |||||
"country": 'India', | |||||
"is_default": 1 | |||||
}).insert() | |||||
if not frappe.db.exists("Address Template", "India"): | |||||
frappe.get_doc({"doctype": "Address Template", "country": "India", "is_default": 1}).insert() | |||||
if not frappe.db.exists('Address', '_Test Address-Office'): | |||||
frappe.get_doc({ | |||||
"address_line1": "_Test Address Line 1", | |||||
"address_title": "_Test Address", | |||||
"address_type": "Office", | |||||
"city": "_Test City", | |||||
"state": "Test State", | |||||
"country": "India", | |||||
"doctype": "Address", | |||||
"is_primary_address": 1, | |||||
"phone": "+91 0000000000" | |||||
}).insert() | |||||
if not frappe.db.exists("Address", "_Test Address-Office"): | |||||
frappe.get_doc( | |||||
{ | |||||
"address_line1": "_Test Address Line 1", | |||||
"address_title": "_Test Address", | |||||
"address_type": "Office", | |||||
"city": "_Test City", | |||||
"state": "Test State", | |||||
"country": "India", | |||||
"doctype": "Address", | |||||
"is_primary_address": 1, | |||||
"phone": "+91 0000000000", | |||||
} | |||||
).insert() | |||||
address = frappe.get_list("Address")[0].name | address = frappe.get_list("Address")[0].name | ||||
display = get_address_display(frappe.get_doc("Address", address).as_dict()) | display = get_address_display(frappe.get_doc("Address", address).as_dict()) | ||||
self.assertTrue(display) | |||||
self.assertTrue(display) |
@@ -3,21 +3,24 @@ | |||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | import frappe | ||||
from frappe import _ | |||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe.utils import cint | from frappe.utils import cint | ||||
from frappe.utils.jinja import validate_template | from frappe.utils.jinja import validate_template | ||||
from frappe import _ | |||||
class AddressTemplate(Document): | class AddressTemplate(Document): | ||||
def validate(self): | def validate(self): | ||||
if not self.template: | if not self.template: | ||||
self.template = get_default_address_template() | self.template = get_default_address_template() | ||||
self.defaults = frappe.db.get_values("Address Template", {"is_default":1, "name":("!=", self.name)}) | |||||
self.defaults = frappe.db.get_values( | |||||
"Address Template", {"is_default": 1, "name": ("!=", self.name)} | |||||
) | |||||
if not self.is_default: | if not self.is_default: | ||||
if not self.defaults: | if not self.defaults: | ||||
self.is_default = 1 | self.is_default = 1 | ||||
if cint(frappe.db.get_single_value('System Settings', 'setup_complete')): | |||||
if cint(frappe.db.get_single_value("System Settings", "setup_complete")): | |||||
frappe.msgprint(_("Setting this Address Template as default as there is no other default")) | frappe.msgprint(_("Setting this Address Template as default as there is no other default")) | ||||
validate_template(self.template) | validate_template(self.template) | ||||
@@ -31,14 +34,23 @@ class AddressTemplate(Document): | |||||
if self.is_default: | if self.is_default: | ||||
frappe.throw(_("Default Address Template cannot be deleted")) | frappe.throw(_("Default Address Template cannot be deleted")) | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_default_address_template(): | def get_default_address_template(): | ||||
'''Get default address template (translated)''' | |||||
return '''{{ address_line1 }}<br>{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}\ | |||||
"""Get default address template (translated)""" | |||||
return ( | |||||
"""{{ address_line1 }}<br>{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}\ | |||||
{{ city }}<br> | {{ city }}<br> | ||||
{% if state %}{{ state }}<br>{% endif -%} | {% if state %}{{ state }}<br>{% endif -%} | ||||
{% if pincode %}{{ pincode }}<br>{% endif -%} | {% if pincode %}{{ pincode }}<br>{% endif -%} | ||||
{{ country }}<br> | {{ country }}<br> | ||||
{% if phone %}'''+_('Phone')+''': {{ phone }}<br>{% endif -%} | |||||
{% if fax %}'''+_('Fax')+''': {{ fax }}<br>{% endif -%} | |||||
{% if email_id %}'''+_('Email')+''': {{ email_id }}<br>{% endif -%}''' | |||||
{% if phone %}""" | |||||
+ _("Phone") | |||||
+ """: {{ phone }}<br>{% endif -%} | |||||
{% if fax %}""" | |||||
+ _("Fax") | |||||
+ """: {{ fax }}<br>{% endif -%} | |||||
{% if email_id %}""" | |||||
+ _("Email") | |||||
+ """: {{ email_id }}<br>{% endif -%}""" | |||||
) |
@@ -1,7 +1,10 @@ | |||||
# -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
# Copyright (c) 2015, Frappe Technologies and Contributors | # Copyright (c) 2015, Frappe Technologies and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe, unittest | |||||
import unittest | |||||
import frappe | |||||
class TestAddressTemplate(unittest.TestCase): | class TestAddressTemplate(unittest.TestCase): | ||||
def setUp(self): | def setUp(self): | ||||
@@ -27,17 +30,12 @@ class TestAddressTemplate(unittest.TestCase): | |||||
def make_default_address_template(self): | def make_default_address_template(self): | ||||
template = """{{ address_line1 }}<br>{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}{{ city }}<br>{% if state %}{{ state }}<br>{% endif -%}{% if pincode %}{{ pincode }}<br>{% endif -%}{{ country }}<br>{% if phone %}Phone: {{ phone }}<br>{% endif -%}{% if fax %}Fax: {{ fax }}<br>{% endif -%}{% if email_id %}Email: {{ email_id }}<br>{% endif -%}""" | template = """{{ address_line1 }}<br>{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}{{ city }}<br>{% if state %}{{ state }}<br>{% endif -%}{% if pincode %}{{ pincode }}<br>{% endif -%}{{ country }}<br>{% if phone %}Phone: {{ phone }}<br>{% endif -%}{% if fax %}Fax: {{ fax }}<br>{% endif -%}{% if email_id %}Email: {{ email_id }}<br>{% endif -%}""" | ||||
if not frappe.db.exists('Address Template', 'India'): | |||||
frappe.get_doc({ | |||||
"doctype": "Address Template", | |||||
"country": 'India', | |||||
"is_default": 1, | |||||
"template": template | |||||
}).insert() | |||||
if not frappe.db.exists('Address Template', 'Brazil'): | |||||
frappe.get_doc({ | |||||
"doctype": "Address Template", | |||||
"country": 'Brazil', | |||||
"template": template | |||||
}).insert() | |||||
if not frappe.db.exists("Address Template", "India"): | |||||
frappe.get_doc( | |||||
{"doctype": "Address Template", "country": "India", "is_default": 1, "template": template} | |||||
).insert() | |||||
if not frappe.db.exists("Address Template", "Brazil"): | |||||
frappe.get_doc( | |||||
{"doctype": "Address Template", "country": "Brazil", "template": template} | |||||
).insert() |
@@ -1,26 +1,27 @@ | |||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | import frappe | ||||
from frappe.utils import cstr, has_gravatar | |||||
from frappe import _ | from frappe import _ | ||||
from frappe.model.document import Document | |||||
from frappe.contacts.address_and_contact import set_link_title | |||||
from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links | from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links | ||||
from frappe.model.document import Document | |||||
from frappe.model.naming import append_number_if_name_exists | from frappe.model.naming import append_number_if_name_exists | ||||
from frappe.contacts.address_and_contact import set_link_title | |||||
from frappe.utils import cstr, has_gravatar | |||||
class Contact(Document): | class Contact(Document): | ||||
def autoname(self): | def autoname(self): | ||||
# concat first and last name | # concat first and last name | ||||
self.name = " ".join(filter(None, | |||||
[cstr(self.get(f)).strip() for f in ["first_name", "last_name"]])) | |||||
self.name = " ".join( | |||||
filter(None, [cstr(self.get(f)).strip() for f in ["first_name", "last_name"]]) | |||||
) | |||||
if frappe.db.exists("Contact", self.name): | if frappe.db.exists("Contact", self.name): | ||||
self.name = append_number_if_name_exists('Contact', self.name) | |||||
self.name = append_number_if_name_exists("Contact", self.name) | |||||
# concat party name if reqd | # concat party name if reqd | ||||
for link in self.links: | for link in self.links: | ||||
self.name = self.name + '-' + link.link_name.strip() | |||||
self.name = self.name + "-" + link.link_name.strip() | |||||
break | break | ||||
def validate(self): | def validate(self): | ||||
@@ -45,7 +46,7 @@ class Contact(Document): | |||||
self.user = frappe.db.get_value("User", {"email": self.email_id}) | self.user = frappe.db.get_value("User", {"email": self.email_id}) | ||||
def get_link_for(self, link_doctype): | def get_link_for(self, link_doctype): | ||||
'''Return the link name, if exists for the given link DocType''' | |||||
"""Return the link name, if exists for the given link DocType""" | |||||
for link in self.links: | for link in self.links: | ||||
if link.link_doctype == link_doctype: | if link.link_doctype == link_doctype: | ||||
return link.link_name | return link.link_name | ||||
@@ -65,21 +66,21 @@ class Contact(Document): | |||||
def add_email(self, email_id, is_primary=0, autosave=False): | def add_email(self, email_id, is_primary=0, autosave=False): | ||||
if not frappe.db.exists("Contact Email", {"email_id": email_id, "parent": self.name}): | if not frappe.db.exists("Contact Email", {"email_id": email_id, "parent": self.name}): | ||||
self.append("email_ids", { | |||||
"email_id": email_id, | |||||
"is_primary": is_primary | |||||
}) | |||||
self.append("email_ids", {"email_id": email_id, "is_primary": is_primary}) | |||||
if autosave: | if autosave: | ||||
self.save(ignore_permissions=True) | self.save(ignore_permissions=True) | ||||
def add_phone(self, phone, is_primary_phone=0, is_primary_mobile_no=0, autosave=False): | def add_phone(self, phone, is_primary_phone=0, is_primary_mobile_no=0, autosave=False): | ||||
if not frappe.db.exists("Contact Phone", {"phone": phone, "parent": self.name}): | if not frappe.db.exists("Contact Phone", {"phone": phone, "parent": self.name}): | ||||
self.append("phone_nos", { | |||||
"phone": phone, | |||||
"is_primary_phone": is_primary_phone, | |||||
"is_primary_mobile_no": is_primary_mobile_no | |||||
}) | |||||
self.append( | |||||
"phone_nos", | |||||
{ | |||||
"phone": phone, | |||||
"is_primary_phone": is_primary_phone, | |||||
"is_primary_mobile_no": is_primary_mobile_no, | |||||
}, | |||||
) | |||||
if autosave: | if autosave: | ||||
self.save(ignore_permissions=True) | self.save(ignore_permissions=True) | ||||
@@ -113,7 +114,9 @@ class Contact(Document): | |||||
is_primary = [phone.phone for phone in self.phone_nos if phone.get(field_name)] | is_primary = [phone.phone for phone in self.phone_nos if phone.get(field_name)] | ||||
if len(is_primary) > 1: | if len(is_primary) > 1: | ||||
frappe.throw(_("Only one {0} can be set as primary.").format(frappe.bold(frappe.unscrub(fieldname)))) | |||||
frappe.throw( | |||||
_("Only one {0} can be set as primary.").format(frappe.bold(frappe.unscrub(fieldname))) | |||||
) | |||||
primary_number_exists = False | primary_number_exists = False | ||||
for d in self.phone_nos: | for d in self.phone_nos: | ||||
@@ -125,9 +128,11 @@ class Contact(Document): | |||||
if not primary_number_exists: | if not primary_number_exists: | ||||
setattr(self, fieldname, "") | setattr(self, fieldname, "") | ||||
def get_default_contact(doctype, name): | def get_default_contact(doctype, name): | ||||
'''Returns default contact for the given doctype, name''' | |||||
out = frappe.db.sql('''select parent, | |||||
"""Returns default contact for the given doctype, name""" | |||||
out = frappe.db.sql( | |||||
'''select parent, | |||||
IFNULL((select is_primary_contact from tabContact c where c.name = dl.parent), 0) | IFNULL((select is_primary_contact from tabContact c where c.name = dl.parent), 0) | ||||
as is_primary_contact | as is_primary_contact | ||||
from | from | ||||
@@ -135,7 +140,10 @@ def get_default_contact(doctype, name): | |||||
where | where | ||||
dl.link_doctype=%s and | dl.link_doctype=%s and | ||||
dl.link_name=%s and | dl.link_name=%s and | ||||
dl.parenttype = "Contact"''', (doctype, name), as_dict=True) | |||||
dl.parenttype = "Contact"''', | |||||
(doctype, name), | |||||
as_dict=True, | |||||
) | |||||
if out: | if out: | ||||
for contact in out: | for contact in out: | ||||
@@ -145,6 +153,7 @@ def get_default_contact(doctype, name): | |||||
else: | else: | ||||
return None | return None | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def invite_user(contact): | def invite_user(contact): | ||||
contact = frappe.get_doc("Contact", contact) | contact = frappe.get_doc("Contact", contact) | ||||
@@ -153,34 +162,39 @@ def invite_user(contact): | |||||
frappe.throw(_("Please set Email Address")) | frappe.throw(_("Please set Email Address")) | ||||
if contact.has_permission("write"): | if contact.has_permission("write"): | ||||
user = frappe.get_doc({ | |||||
"doctype": "User", | |||||
"first_name": contact.first_name, | |||||
"last_name": contact.last_name, | |||||
"email": contact.email_id, | |||||
"user_type": "Website User", | |||||
"send_welcome_email": 1 | |||||
}).insert(ignore_permissions = True) | |||||
user = frappe.get_doc( | |||||
{ | |||||
"doctype": "User", | |||||
"first_name": contact.first_name, | |||||
"last_name": contact.last_name, | |||||
"email": contact.email_id, | |||||
"user_type": "Website User", | |||||
"send_welcome_email": 1, | |||||
} | |||||
).insert(ignore_permissions=True) | |||||
return user.name | return user.name | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_contact_details(contact): | def get_contact_details(contact): | ||||
contact = frappe.get_doc("Contact", contact) | contact = frappe.get_doc("Contact", contact) | ||||
out = { | out = { | ||||
"contact_person": contact.get("name"), | "contact_person": contact.get("name"), | ||||
"contact_display": " ".join(filter(None, | |||||
[contact.get("salutation"), contact.get("first_name"), contact.get("last_name")])), | |||||
"contact_display": " ".join( | |||||
filter(None, [contact.get("salutation"), contact.get("first_name"), contact.get("last_name")]) | |||||
), | |||||
"contact_email": contact.get("email_id"), | "contact_email": contact.get("email_id"), | ||||
"contact_mobile": contact.get("mobile_no"), | "contact_mobile": contact.get("mobile_no"), | ||||
"contact_phone": contact.get("phone"), | "contact_phone": contact.get("phone"), | ||||
"contact_designation": contact.get("designation"), | "contact_designation": contact.get("designation"), | ||||
"contact_department": contact.get("department") | |||||
"contact_department": contact.get("department"), | |||||
} | } | ||||
return out | return out | ||||
def update_contact(doc, method): | def update_contact(doc, method): | ||||
'''Update contact when user is updated, if contact is found. Called via hooks''' | |||||
"""Update contact when user is updated, if contact is found. Called via hooks""" | |||||
contact_name = frappe.db.get_value("Contact", {"email_id": doc.name}) | contact_name = frappe.db.get_value("Contact", {"email_id": doc.name}) | ||||
if contact_name: | if contact_name: | ||||
contact = frappe.get_doc("Contact", contact_name) | contact = frappe.get_doc("Contact", contact_name) | ||||
@@ -190,19 +204,23 @@ def update_contact(doc, method): | |||||
contact.flags.ignore_mandatory = True | contact.flags.ignore_mandatory = True | ||||
contact.save(ignore_permissions=True) | contact.save(ignore_permissions=True) | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
@frappe.validate_and_sanitize_search_inputs | @frappe.validate_and_sanitize_search_inputs | ||||
def contact_query(doctype, txt, searchfield, start, page_len, filters): | def contact_query(doctype, txt, searchfield, start, page_len, filters): | ||||
from frappe.desk.reportview import get_match_cond | from frappe.desk.reportview import get_match_cond | ||||
if not frappe.get_meta("Contact").get_field(searchfield)\ | |||||
and searchfield not in frappe.db.DEFAULT_COLUMNS: | |||||
if ( | |||||
not frappe.get_meta("Contact").get_field(searchfield) | |||||
and searchfield not in frappe.db.DEFAULT_COLUMNS | |||||
): | |||||
return [] | return [] | ||||
link_doctype = filters.pop('link_doctype') | |||||
link_name = filters.pop('link_name') | |||||
link_doctype = filters.pop("link_doctype") | |||||
link_name = filters.pop("link_name") | |||||
return frappe.db.sql("""select | |||||
return frappe.db.sql( | |||||
"""select | |||||
`tabContact`.name, `tabContact`.first_name, `tabContact`.last_name | `tabContact`.name, `tabContact`.first_name, `tabContact`.last_name | ||||
from | from | ||||
`tabContact`, `tabDynamic Link` | `tabContact`, `tabDynamic Link` | ||||
@@ -216,68 +234,90 @@ def contact_query(doctype, txt, searchfield, start, page_len, filters): | |||||
order by | order by | ||||
if(locate(%(_txt)s, `tabContact`.name), locate(%(_txt)s, `tabContact`.name), 99999), | if(locate(%(_txt)s, `tabContact`.name), locate(%(_txt)s, `tabContact`.name), 99999), | ||||
`tabContact`.idx desc, `tabContact`.name | `tabContact`.idx desc, `tabContact`.name | ||||
limit %(start)s, %(page_len)s """.format(mcond=get_match_cond(doctype), key=searchfield), { | |||||
'txt': '%' + txt + '%', | |||||
'_txt': txt.replace("%", ""), | |||||
'start': start, | |||||
'page_len': page_len, | |||||
'link_name': link_name, | |||||
'link_doctype': link_doctype | |||||
}) | |||||
limit %(start)s, %(page_len)s """.format( | |||||
mcond=get_match_cond(doctype), key=searchfield | |||||
), | |||||
{ | |||||
"txt": "%" + txt + "%", | |||||
"_txt": txt.replace("%", ""), | |||||
"start": start, | |||||
"page_len": page_len, | |||||
"link_name": link_name, | |||||
"link_doctype": link_doctype, | |||||
}, | |||||
) | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def address_query(links): | def address_query(links): | ||||
import json | import json | ||||
links = [{"link_doctype": d.get("link_doctype"), "link_name": d.get("link_name")} for d in json.loads(links)] | |||||
links = [ | |||||
{"link_doctype": d.get("link_doctype"), "link_name": d.get("link_name")} | |||||
for d in json.loads(links) | |||||
] | |||||
result = [] | result = [] | ||||
for link in links: | for link in links: | ||||
if not frappe.has_permission(doctype=link.get("link_doctype"), ptype="read", doc=link.get("link_name")): | |||||
if not frappe.has_permission( | |||||
doctype=link.get("link_doctype"), ptype="read", doc=link.get("link_name") | |||||
): | |||||
continue | continue | ||||
res = frappe.db.sql(""" | |||||
res = frappe.db.sql( | |||||
""" | |||||
SELECT `tabAddress`.name | SELECT `tabAddress`.name | ||||
FROM `tabAddress`, `tabDynamic Link` | FROM `tabAddress`, `tabDynamic Link` | ||||
WHERE `tabDynamic Link`.parenttype='Address' | WHERE `tabDynamic Link`.parenttype='Address' | ||||
AND `tabDynamic Link`.parent=`tabAddress`.name | AND `tabDynamic Link`.parent=`tabAddress`.name | ||||
AND `tabDynamic Link`.link_doctype = %(link_doctype)s | AND `tabDynamic Link`.link_doctype = %(link_doctype)s | ||||
AND `tabDynamic Link`.link_name = %(link_name)s | AND `tabDynamic Link`.link_name = %(link_name)s | ||||
""", { | |||||
"link_doctype": link.get("link_doctype"), | |||||
"link_name": link.get("link_name"), | |||||
}, as_dict=True) | |||||
""", | |||||
{ | |||||
"link_doctype": link.get("link_doctype"), | |||||
"link_name": link.get("link_name"), | |||||
}, | |||||
as_dict=True, | |||||
) | |||||
result.extend([l.name for l in res]) | result.extend([l.name for l in res]) | ||||
return result | return result | ||||
def get_contact_with_phone_number(number): | def get_contact_with_phone_number(number): | ||||
if not number: return | |||||
if not number: | |||||
return | |||||
contacts = frappe.get_all('Contact Phone', filters=[ | |||||
['phone', 'like', '%{0}'.format(number)] | |||||
], fields=["parent"], limit=1) | |||||
contacts = frappe.get_all( | |||||
"Contact Phone", filters=[["phone", "like", "%{0}".format(number)]], fields=["parent"], limit=1 | |||||
) | |||||
return contacts[0].parent if contacts else None | return contacts[0].parent if contacts else None | ||||
def get_contact_name(email_id): | def get_contact_name(email_id): | ||||
contact = frappe.get_all("Contact Email", filters={"email_id": email_id}, fields=["parent"], limit=1) | |||||
contact = frappe.get_all( | |||||
"Contact Email", filters={"email_id": email_id}, fields=["parent"], limit=1 | |||||
) | |||||
return contact[0].parent if contact else None | return contact[0].parent if contact else None | ||||
def get_contacts_linking_to(doctype, docname, fields=None): | def get_contacts_linking_to(doctype, docname, fields=None): | ||||
"""Return a list of contacts containing a link to the given document.""" | """Return a list of contacts containing a link to the given document.""" | ||||
return frappe.get_list('Contact', fields=fields, filters=[ | |||||
['Dynamic Link', 'link_doctype', '=', doctype], | |||||
['Dynamic Link', 'link_name', '=', docname] | |||||
]) | |||||
return frappe.get_list( | |||||
"Contact", | |||||
fields=fields, | |||||
filters=[ | |||||
["Dynamic Link", "link_doctype", "=", doctype], | |||||
["Dynamic Link", "link_name", "=", docname], | |||||
], | |||||
) | |||||
def get_contacts_linked_from(doctype, docname, fields=None): | def get_contacts_linked_from(doctype, docname, fields=None): | ||||
"""Return a list of contacts that are contained in (linked from) the given document.""" | """Return a list of contacts that are contained in (linked from) the given document.""" | ||||
link_fields = frappe.get_meta(doctype).get('fields', { | |||||
'fieldtype': 'Link', | |||||
'options': 'Contact' | |||||
}) | |||||
link_fields = frappe.get_meta(doctype).get("fields", {"fieldtype": "Link", "options": "Contact"}) | |||||
if not link_fields: | if not link_fields: | ||||
return [] | return [] | ||||
@@ -285,6 +325,4 @@ def get_contacts_linked_from(doctype, docname, fields=None): | |||||
if not contact_names: | if not contact_names: | ||||
return [] | return [] | ||||
return frappe.get_list('Contact', fields=fields, filters={ | |||||
'name': ('in', contact_names) | |||||
}) | |||||
return frappe.get_list("Contact", fields=fields, filters={"name": ("in", contact_names)}) |
@@ -1,13 +1,14 @@ | |||||
# -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
# Copyright (c) 2017, Frappe Technologies and Contributors | # Copyright (c) 2017, Frappe Technologies and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | |||||
import unittest | import unittest | ||||
test_dependencies = ['Contact', 'Salutation'] | |||||
import frappe | |||||
class TestContact(unittest.TestCase): | |||||
test_dependencies = ["Contact", "Salutation"] | |||||
class TestContact(unittest.TestCase): | |||||
def test_check_default_email(self): | def test_check_default_email(self): | ||||
emails = [ | emails = [ | ||||
{"email": "test1@example.com", "is_primary": 0}, | {"email": "test1@example.com", "is_primary": 0}, | ||||
@@ -32,13 +33,11 @@ class TestContact(unittest.TestCase): | |||||
self.assertEqual(contact.phone, "+91 0000000002") | self.assertEqual(contact.phone, "+91 0000000002") | ||||
self.assertEqual(contact.mobile_no, "+91 0000000003") | self.assertEqual(contact.mobile_no, "+91 0000000003") | ||||
def create_contact(name, salutation, emails=None, phones=None, save=True): | def create_contact(name, salutation, emails=None, phones=None, save=True): | ||||
doc = frappe.get_doc({ | |||||
"doctype": "Contact", | |||||
"first_name": name, | |||||
"status": "Open", | |||||
"salutation": salutation | |||||
}) | |||||
doc = frappe.get_doc( | |||||
{"doctype": "Contact", "first_name": name, "status": "Open", "salutation": salutation} | |||||
) | |||||
if emails: | if emails: | ||||
for d in emails: | for d in emails: | ||||
@@ -5,5 +5,6 @@ | |||||
# import frappe | # import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class ContactEmail(Document): | class ContactEmail(Document): | ||||
pass | pass |
@@ -5,5 +5,6 @@ | |||||
# import frappe | # import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class ContactPhone(Document): | class ContactPhone(Document): | ||||
pass | pass |
@@ -4,5 +4,6 @@ | |||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class Gender(Document): | class Gender(Document): | ||||
pass | pass |
@@ -3,5 +3,6 @@ | |||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import unittest | import unittest | ||||
class TestGender(unittest.TestCase): | class TestGender(unittest.TestCase): | ||||
pass | pass |
@@ -4,5 +4,6 @@ | |||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class Salutation(Document): | class Salutation(Document): | ||||
pass | pass |
@@ -3,5 +3,6 @@ | |||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import unittest | import unittest | ||||
class TestSalutation(unittest.TestCase): | class TestSalutation(unittest.TestCase): | ||||
pass | pass |
@@ -4,17 +4,37 @@ import frappe | |||||
from frappe import _ | from frappe import _ | ||||
field_map = { | field_map = { | ||||
"Contact": ["first_name", "last_name", "address", "phone", "mobile_no", "email_id", "is_primary_contact"], | |||||
"Address": ["address_line1", "address_line2", "city", "state", "pincode", "country", "is_primary_address"] | |||||
"Contact": [ | |||||
"first_name", | |||||
"last_name", | |||||
"address", | |||||
"phone", | |||||
"mobile_no", | |||||
"email_id", | |||||
"is_primary_contact", | |||||
], | |||||
"Address": [ | |||||
"address_line1", | |||||
"address_line2", | |||||
"city", | |||||
"state", | |||||
"pincode", | |||||
"country", | |||||
"is_primary_address", | |||||
], | |||||
} | } | ||||
def execute(filters=None): | def execute(filters=None): | ||||
columns, data = get_columns(filters), get_data(filters) | columns, data = get_columns(filters), get_data(filters) | ||||
return columns, data | return columns, data | ||||
def get_columns(filters): | def get_columns(filters): | ||||
return [ | return [ | ||||
"{reference_doctype}:Link/{reference_doctype}".format(reference_doctype=filters.get("reference_doctype")), | |||||
"{reference_doctype}:Link/{reference_doctype}".format( | |||||
reference_doctype=filters.get("reference_doctype") | |||||
), | |||||
"Address Line 1", | "Address Line 1", | ||||
"Address Line 2", | "Address Line 2", | ||||
"City", | "City", | ||||
@@ -27,9 +47,10 @@ def get_columns(filters): | |||||
"Address", | "Address", | ||||
"Phone", | "Phone", | ||||
"Email Id", | "Email Id", | ||||
"Is Primary Contact:Check" | |||||
"Is Primary Contact:Check", | |||||
] | ] | ||||
def get_data(filters): | def get_data(filters): | ||||
data = [] | data = [] | ||||
reference_doctype = filters.get("reference_doctype") | reference_doctype = filters.get("reference_doctype") | ||||
@@ -37,6 +58,7 @@ def get_data(filters): | |||||
return get_reference_addresses_and_contact(reference_doctype, reference_name) | return get_reference_addresses_and_contact(reference_doctype, reference_name) | ||||
def get_reference_addresses_and_contact(reference_doctype, reference_name): | def get_reference_addresses_and_contact(reference_doctype, reference_name): | ||||
data = [] | data = [] | ||||
filters = None | filters = None | ||||
@@ -48,16 +70,22 @@ def get_reference_addresses_and_contact(reference_doctype, reference_name): | |||||
if reference_name: | if reference_name: | ||||
filters = {"name": reference_name} | filters = {"name": reference_name} | ||||
reference_list = [d[0] for d in frappe.get_list(reference_doctype, filters=filters, fields=["name"], as_list=True)] | |||||
reference_list = [ | |||||
d[0] for d in frappe.get_list(reference_doctype, filters=filters, fields=["name"], as_list=True) | |||||
] | |||||
for d in reference_list: | for d in reference_list: | ||||
reference_details.setdefault(d, frappe._dict()) | reference_details.setdefault(d, frappe._dict()) | ||||
reference_details = get_reference_details(reference_doctype, "Address", reference_list, reference_details) | |||||
reference_details = get_reference_details(reference_doctype, "Contact", reference_list, reference_details) | |||||
reference_details = get_reference_details( | |||||
reference_doctype, "Address", reference_list, reference_details | |||||
) | |||||
reference_details = get_reference_details( | |||||
reference_doctype, "Contact", reference_list, reference_details | |||||
) | |||||
for reference_name, details in reference_details.items(): | for reference_name, details in reference_details.items(): | ||||
addresses = details.get("address", []) | addresses = details.get("address", []) | ||||
contacts = details.get("contact", []) | |||||
contacts = details.get("contact", []) | |||||
if not any([addresses, contacts]): | if not any([addresses, contacts]): | ||||
result = [reference_name] | result = [reference_name] | ||||
result.extend(add_blank_columns_for("Address")) | result.extend(add_blank_columns_for("Address")) | ||||
@@ -78,10 +106,11 @@ def get_reference_addresses_and_contact(reference_doctype, reference_name): | |||||
return data | return data | ||||
def get_reference_details(reference_doctype, doctype, reference_list, reference_details): | def get_reference_details(reference_doctype, doctype, reference_list, reference_details): | ||||
filters = [ | |||||
filters = [ | |||||
["Dynamic Link", "link_doctype", "=", reference_doctype], | ["Dynamic Link", "link_doctype", "=", reference_doctype], | ||||
["Dynamic Link", "link_name", "in", reference_list] | |||||
["Dynamic Link", "link_name", "in", reference_list], | |||||
] | ] | ||||
fields = ["`tabDynamic Link`.link_name"] + field_map.get(doctype, []) | fields = ["`tabDynamic Link`.link_name"] + field_map.get(doctype, []) | ||||
@@ -97,5 +126,6 @@ def get_reference_details(reference_doctype, doctype, reference_list, reference_ | |||||
reference_details[reference_list[0]][frappe.scrub(doctype)] = temp_records | reference_details[reference_list[0]][frappe.scrub(doctype)] = temp_records | ||||
return reference_details | return reference_details | ||||
def add_blank_columns_for(doctype): | def add_blank_columns_for(doctype): | ||||
return ["" for field in field_map.get(doctype, [])] | return ["" for field in field_map.get(doctype, [])] |
@@ -1,95 +1,87 @@ | |||||
import unittest | |||||
import frappe | import frappe | ||||
import frappe.defaults | import frappe.defaults | ||||
import unittest | |||||
from frappe.contacts.report.addresses_and_contacts.addresses_and_contacts import get_data | from frappe.contacts.report.addresses_and_contacts.addresses_and_contacts import get_data | ||||
def get_custom_linked_doctype(): | def get_custom_linked_doctype(): | ||||
if bool(frappe.get_all("DocType", filters={'name':'Test Custom Doctype'})): | |||||
if bool(frappe.get_all("DocType", filters={"name": "Test Custom Doctype"})): | |||||
return | return | ||||
doc = frappe.get_doc({ | |||||
"doctype": "DocType", | |||||
"module": "Core", | |||||
"custom": 1, | |||||
"fields": [{ | |||||
"label": "Test Field", | |||||
"fieldname": "test_field", | |||||
"fieldtype": "Data" | |||||
}, | |||||
{ | |||||
"label": "Contact HTML", | |||||
"fieldname": "contact_html", | |||||
"fieldtype": "HTML" | |||||
}, | |||||
doc = frappe.get_doc( | |||||
{ | { | ||||
"label": "Address HTML", | |||||
"fieldname": "address_html", | |||||
"fieldtype": "HTML" | |||||
}], | |||||
"permissions": [{ | |||||
"role": "System Manager", | |||||
"read": 1 | |||||
}], | |||||
"name": "Test Custom Doctype", | |||||
}) | |||||
"doctype": "DocType", | |||||
"module": "Core", | |||||
"custom": 1, | |||||
"fields": [ | |||||
{"label": "Test Field", "fieldname": "test_field", "fieldtype": "Data"}, | |||||
{"label": "Contact HTML", "fieldname": "contact_html", "fieldtype": "HTML"}, | |||||
{"label": "Address HTML", "fieldname": "address_html", "fieldtype": "HTML"}, | |||||
], | |||||
"permissions": [{"role": "System Manager", "read": 1}], | |||||
"name": "Test Custom Doctype", | |||||
} | |||||
) | |||||
doc.insert() | doc.insert() | ||||
def get_custom_doc_for_address_and_contacts(): | def get_custom_doc_for_address_and_contacts(): | ||||
get_custom_linked_doctype() | get_custom_linked_doctype() | ||||
linked_doc = frappe.get_doc({ | |||||
"doctype": "Test Custom Doctype", | |||||
"test_field": "Hello", | |||||
}).insert() | |||||
linked_doc = frappe.get_doc( | |||||
{ | |||||
"doctype": "Test Custom Doctype", | |||||
"test_field": "Hello", | |||||
} | |||||
).insert() | |||||
return linked_doc | return linked_doc | ||||
def create_linked_address(link_list): | def create_linked_address(link_list): | ||||
if frappe.flags.test_address_created: | if frappe.flags.test_address_created: | ||||
return | return | ||||
address = frappe.get_doc({ | |||||
"doctype": "Address", | |||||
"address_title": "_Test Address", | |||||
"address_type": "Billing", | |||||
"address_line1": "test address line 1", | |||||
"address_line2": "test address line 2", | |||||
"city": "Milan", | |||||
"country": "Italy" | |||||
}) | |||||
address = frappe.get_doc( | |||||
{ | |||||
"doctype": "Address", | |||||
"address_title": "_Test Address", | |||||
"address_type": "Billing", | |||||
"address_line1": "test address line 1", | |||||
"address_line2": "test address line 2", | |||||
"city": "Milan", | |||||
"country": "Italy", | |||||
} | |||||
) | |||||
for name in link_list: | for name in link_list: | ||||
address.append("links",{ | |||||
'link_doctype': 'Test Custom Doctype', | |||||
'link_name': name | |||||
}) | |||||
address.append("links", {"link_doctype": "Test Custom Doctype", "link_name": name}) | |||||
address.insert() | address.insert() | ||||
frappe.flags.test_address_created = True | frappe.flags.test_address_created = True | ||||
return address.name | return address.name | ||||
def create_linked_contact(link_list, address): | def create_linked_contact(link_list, address): | ||||
if frappe.flags.test_contact_created: | if frappe.flags.test_contact_created: | ||||
return | return | ||||
contact = frappe.get_doc({ | |||||
"doctype": "Contact", | |||||
"salutation": "Mr", | |||||
"first_name": "_Test First Name", | |||||
"last_name": "_Test Last Name", | |||||
"is_primary_contact": 1, | |||||
"address": address, | |||||
"status": "Open" | |||||
}) | |||||
contact = frappe.get_doc( | |||||
{ | |||||
"doctype": "Contact", | |||||
"salutation": "Mr", | |||||
"first_name": "_Test First Name", | |||||
"last_name": "_Test Last Name", | |||||
"is_primary_contact": 1, | |||||
"address": address, | |||||
"status": "Open", | |||||
} | |||||
) | |||||
contact.add_email("test_contact@example.com", is_primary=True) | contact.add_email("test_contact@example.com", is_primary=True) | ||||
contact.add_phone("+91 0000000000", is_primary_phone=True) | contact.add_phone("+91 0000000000", is_primary_phone=True) | ||||
for name in link_list: | for name in link_list: | ||||
contact.append("links",{ | |||||
'link_doctype': 'Test Custom Doctype', | |||||
'link_name': name | |||||
}) | |||||
contact.append("links", {"link_doctype": "Test Custom Doctype", "link_name": name}) | |||||
contact.insert(ignore_permissions=True) | contact.insert(ignore_permissions=True) | ||||
frappe.flags.test_contact_created = True | frappe.flags.test_contact_created = True | ||||
@@ -103,7 +95,23 @@ class TestAddressesAndContacts(unittest.TestCase): | |||||
create_linked_contact(links_list, d) | create_linked_contact(links_list, d) | ||||
report_data = get_data({"reference_doctype": "Test Custom Doctype"}) | report_data = get_data({"reference_doctype": "Test Custom Doctype"}) | ||||
for idx, link in enumerate(links_list): | for idx, link in enumerate(links_list): | ||||
test_item = [link, 'test address line 1', 'test address line 2', 'Milan', None, None, 'Italy', 0, '_Test First Name', '_Test Last Name', '_Test Address-Billing', '+91 0000000000', '', 'test_contact@example.com', 1] | |||||
test_item = [ | |||||
link, | |||||
"test address line 1", | |||||
"test address line 2", | |||||
"Milan", | |||||
None, | |||||
None, | |||||
"Italy", | |||||
0, | |||||
"_Test First Name", | |||||
"_Test Last Name", | |||||
"_Test Address-Billing", | |||||
"+91 0000000000", | |||||
"", | |||||
"test_contact@example.com", | |||||
1, | |||||
] | |||||
self.assertListEqual(test_item, report_data[idx]) | self.assertListEqual(test_item, report_data[idx]) | ||||
def tearDown(self): | def tearDown(self): | ||||
@@ -1,3 +1,2 @@ | |||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
@@ -1,9 +1,10 @@ | |||||
# Copyright (c) 2021, Frappe Technologies and contributors | # Copyright (c) 2021, Frappe Technologies and contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | |||||
from frappe.utils import cstr | |||||
from tenacity import retry, retry_if_exception_type, stop_after_attempt | from tenacity import retry, retry_if_exception_type, stop_after_attempt | ||||
import frappe | |||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe.utils import cstr | |||||
class AccessLog(Document): | class AccessLog(Document): | ||||
@@ -22,14 +23,19 @@ def make_access_log( | |||||
columns=None, | columns=None, | ||||
): | ): | ||||
_make_access_log( | _make_access_log( | ||||
doctype, document, method, file_type, report_name, filters, page, columns, | |||||
doctype, | |||||
document, | |||||
method, | |||||
file_type, | |||||
report_name, | |||||
filters, | |||||
page, | |||||
columns, | |||||
) | ) | ||||
@frappe.write_only() | @frappe.write_only() | ||||
@retry( | |||||
stop=stop_after_attempt(3), retry=retry_if_exception_type(frappe.DuplicateEntryError) | |||||
) | |||||
@retry(stop=stop_after_attempt(3), retry=retry_if_exception_type(frappe.DuplicateEntryError)) | |||||
def _make_access_log( | def _make_access_log( | ||||
doctype=None, | doctype=None, | ||||
document=None, | document=None, | ||||
@@ -43,18 +49,20 @@ def _make_access_log( | |||||
user = frappe.session.user | user = frappe.session.user | ||||
in_request = frappe.request and frappe.request.method == "GET" | in_request = frappe.request and frappe.request.method == "GET" | ||||
frappe.get_doc({ | |||||
"doctype": "Access Log", | |||||
"user": user, | |||||
"export_from": doctype, | |||||
"reference_document": document, | |||||
"file_type": file_type, | |||||
"report_name": report_name, | |||||
"page": page, | |||||
"method": method, | |||||
"filters": cstr(filters) or None, | |||||
"columns": columns, | |||||
}).db_insert() | |||||
frappe.get_doc( | |||||
{ | |||||
"doctype": "Access Log", | |||||
"user": user, | |||||
"export_from": doctype, | |||||
"reference_document": document, | |||||
"file_type": file_type, | |||||
"report_name": report_name, | |||||
"page": page, | |||||
"method": method, | |||||
"filters": cstr(filters) or None, | |||||
"columns": columns, | |||||
} | |||||
).db_insert() | |||||
# `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview` | # `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview` | ||||
# dont commit in test mode. It must be tempting to put this block along with the in_request in the | # dont commit in test mode. It must be tempting to put this block along with the in_request in the | ||||
@@ -2,20 +2,21 @@ | |||||
# Copyright (c) 2019, Frappe Technologies and Contributors | # Copyright (c) 2019, Frappe Technologies and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
# imports - standard imports | |||||
import unittest | |||||
import base64 | import base64 | ||||
import os | import os | ||||
# imports - standard imports | |||||
import unittest | |||||
# imports - third party imports | |||||
import requests | |||||
# imports - module imports | # imports - module imports | ||||
import frappe | import frappe | ||||
from frappe.core.doctype.access_log.access_log import make_access_log | from frappe.core.doctype.access_log.access_log import make_access_log | ||||
from frappe.utils import cstr, get_site_url | |||||
from frappe.core.doctype.data_import.data_import import export_csv | from frappe.core.doctype.data_import.data_import import export_csv | ||||
from frappe.core.doctype.user.user import generate_keys | from frappe.core.doctype.user.user import generate_keys | ||||
# imports - third party imports | |||||
import requests | |||||
from frappe.utils import cstr, get_site_url | |||||
class TestAccessLog(unittest.TestCase): | class TestAccessLog(unittest.TestCase): | ||||
@@ -23,8 +24,9 @@ class TestAccessLog(unittest.TestCase): | |||||
# generate keys for current user to send requests for the following tests | # generate keys for current user to send requests for the following tests | ||||
generate_keys(frappe.session.user) | generate_keys(frappe.session.user) | ||||
frappe.db.commit() | frappe.db.commit() | ||||
generated_secret = frappe.utils.password.get_decrypted_password("User", | |||||
frappe.session.user, fieldname='api_secret') | |||||
generated_secret = frappe.utils.password.get_decrypted_password( | |||||
"User", frappe.session.user, fieldname="api_secret" | |||||
) | |||||
api_key = frappe.db.get_value("User", "Administrator", "api_key") | api_key = frappe.db.get_value("User", "Administrator", "api_key") | ||||
self.header = {"Authorization": "token {}:{}".format(api_key, generated_secret)} | self.header = {"Authorization": "token {}:{}".format(api_key, generated_secret)} | ||||
@@ -101,54 +103,55 @@ class TestAccessLog(unittest.TestCase): | |||||
"party": [], | "party": [], | ||||
"group_by": "Group by Voucher (Consolidated)", | "group_by": "Group by Voucher (Consolidated)", | ||||
"cost_center": [], | "cost_center": [], | ||||
"project": [] | |||||
"project": [], | |||||
} | } | ||||
self.test_doctype = 'File' | |||||
self.test_document = 'Test Document' | |||||
self.test_report_name = 'General Ledger' | |||||
self.test_file_type = 'CSV' | |||||
self.test_method = 'Test Method' | |||||
self.file_name = frappe.utils.random_string(10) + '.txt' | |||||
self.test_doctype = "File" | |||||
self.test_document = "Test Document" | |||||
self.test_report_name = "General Ledger" | |||||
self.test_file_type = "CSV" | |||||
self.test_method = "Test Method" | |||||
self.file_name = frappe.utils.random_string(10) + ".txt" | |||||
self.test_content = frappe.utils.random_string(1024) | self.test_content = frappe.utils.random_string(1024) | ||||
def test_make_full_access_log(self): | def test_make_full_access_log(self): | ||||
self.maxDiff = None | self.maxDiff = None | ||||
# test if all fields maintain data: html page and filters are converted? | # test if all fields maintain data: html page and filters are converted? | ||||
make_access_log(doctype=self.test_doctype, | |||||
make_access_log( | |||||
doctype=self.test_doctype, | |||||
document=self.test_document, | document=self.test_document, | ||||
report_name=self.test_report_name, | report_name=self.test_report_name, | ||||
page=self.test_html_template, | page=self.test_html_template, | ||||
file_type=self.test_file_type, | file_type=self.test_file_type, | ||||
method=self.test_method, | method=self.test_method, | ||||
filters=self.test_filters) | |||||
filters=self.test_filters, | |||||
) | |||||
last_doc = frappe.get_last_doc('Access Log') | |||||
last_doc = frappe.get_last_doc("Access Log") | |||||
self.assertEqual(last_doc.filters, cstr(self.test_filters)) | self.assertEqual(last_doc.filters, cstr(self.test_filters)) | ||||
self.assertEqual(self.test_doctype, last_doc.export_from) | self.assertEqual(self.test_doctype, last_doc.export_from) | ||||
self.assertEqual(self.test_document, last_doc.reference_document) | self.assertEqual(self.test_document, last_doc.reference_document) | ||||
def test_make_export_log(self): | def test_make_export_log(self): | ||||
# export data and delete temp file generated on disk | # export data and delete temp file generated on disk | ||||
export_csv(self.test_doctype, self.file_name) | export_csv(self.test_doctype, self.file_name) | ||||
os.remove(self.file_name) | os.remove(self.file_name) | ||||
# test if the exported data is logged | # test if the exported data is logged | ||||
last_doc = frappe.get_last_doc('Access Log') | |||||
last_doc = frappe.get_last_doc("Access Log") | |||||
self.assertEqual(self.test_doctype, last_doc.export_from) | self.assertEqual(self.test_doctype, last_doc.export_from) | ||||
def test_private_file_download(self): | def test_private_file_download(self): | ||||
# create new private file | # create new private file | ||||
new_private_file = frappe.get_doc({ | |||||
'doctype': self.test_doctype, | |||||
'file_name': self.file_name, | |||||
'content': base64.b64encode(self.test_content.encode('utf-8')), | |||||
'is_private': 1, | |||||
}) | |||||
new_private_file = frappe.get_doc( | |||||
{ | |||||
"doctype": self.test_doctype, | |||||
"file_name": self.file_name, | |||||
"content": base64.b64encode(self.test_content.encode("utf-8")), | |||||
"is_private": 1, | |||||
} | |||||
) | |||||
new_private_file.insert() | new_private_file.insert() | ||||
# access the created file | # access the created file | ||||
@@ -156,7 +159,7 @@ class TestAccessLog(unittest.TestCase): | |||||
try: | try: | ||||
request = requests.post(private_file_link, headers=self.header) | request = requests.post(private_file_link, headers=self.header) | ||||
last_doc = frappe.get_last_doc('Access Log') | |||||
last_doc = frappe.get_last_doc("Access Log") | |||||
if request.ok: | if request.ok: | ||||
# check for the access log of downloaded file | # check for the access log of downloaded file | ||||
@@ -169,6 +172,5 @@ class TestAccessLog(unittest.TestCase): | |||||
# cleanup | # cleanup | ||||
new_private_file.delete() | new_private_file.delete() | ||||
def tearDown(self): | def tearDown(self): | ||||
pass | pass |
@@ -26,20 +26,25 @@ class ActivityLog(Document): | |||||
if self.reference_doctype and self.reference_name: | if self.reference_doctype and self.reference_name: | ||||
self.status = "Linked" | self.status = "Linked" | ||||
def on_doctype_update(): | def on_doctype_update(): | ||||
"""Add indexes in `tabActivity Log`""" | """Add indexes in `tabActivity Log`""" | ||||
frappe.db.add_index("Activity Log", ["reference_doctype", "reference_name"]) | frappe.db.add_index("Activity Log", ["reference_doctype", "reference_name"]) | ||||
frappe.db.add_index("Activity Log", ["timeline_doctype", "timeline_name"]) | frappe.db.add_index("Activity Log", ["timeline_doctype", "timeline_name"]) | ||||
frappe.db.add_index("Activity Log", ["link_doctype", "link_name"]) | frappe.db.add_index("Activity Log", ["link_doctype", "link_name"]) | ||||
def add_authentication_log(subject, user, operation="Login", status="Success"): | def add_authentication_log(subject, user, operation="Login", status="Success"): | ||||
frappe.get_doc({ | |||||
"doctype": "Activity Log", | |||||
"user": user, | |||||
"status": status, | |||||
"subject": subject, | |||||
"operation": operation, | |||||
}).insert(ignore_permissions=True, ignore_links=True) | |||||
frappe.get_doc( | |||||
{ | |||||
"doctype": "Activity Log", | |||||
"user": user, | |||||
"status": status, | |||||
"subject": subject, | |||||
"operation": operation, | |||||
} | |||||
).insert(ignore_permissions=True, ignore_links=True) | |||||
def clear_activity_logs(days=None): | def clear_activity_logs(days=None): | ||||
"""clear 90 day old authentication logs or configured in log settings""" | """clear 90 day old authentication logs or configured in log settings""" | ||||
@@ -47,6 +52,4 @@ def clear_activity_logs(days=None): | |||||
if not days: | if not days: | ||||
days = 90 | days = 90 | ||||
doctype = DocType("Activity Log") | doctype = DocType("Activity Log") | ||||
frappe.db.delete(doctype, filters=( | |||||
doctype.creation < (Now() - Interval(days=days)) | |||||
)) | |||||
frappe.db.delete(doctype, filters=(doctype.creation < (Now() - Interval(days=days)))) |
@@ -3,15 +3,16 @@ | |||||
import frappe | import frappe | ||||
import frappe.permissions | import frappe.permissions | ||||
from frappe.utils import get_fullname | |||||
from frappe import _ | from frappe import _ | ||||
from frappe.core.doctype.activity_log.activity_log import add_authentication_log | from frappe.core.doctype.activity_log.activity_log import add_authentication_log | ||||
from frappe.utils import get_fullname | |||||
def update_feed(doc, method=None): | def update_feed(doc, method=None): | ||||
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import: | if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import: | ||||
return | return | ||||
if doc._action!="save" or doc.flags.ignore_feed: | |||||
if doc._action != "save" or doc.flags.ignore_feed: | |||||
return | return | ||||
if doc.doctype == "Activity Log" or doc.meta.issingle: | if doc.doctype == "Activity Log" or doc.meta.issingle: | ||||
@@ -29,65 +30,75 @@ def update_feed(doc, method=None): | |||||
name = feed.name or doc.name | name = feed.name or doc.name | ||||
# delete earlier feed | # delete earlier feed | ||||
frappe.db.delete("Activity Log", { | |||||
"reference_doctype": doctype, | |||||
"reference_name": name, | |||||
"link_doctype": feed.link_doctype | |||||
}) | |||||
frappe.get_doc({ | |||||
"doctype": "Activity Log", | |||||
"reference_doctype": doctype, | |||||
"reference_name": name, | |||||
"subject": feed.subject, | |||||
"full_name": get_fullname(doc.owner), | |||||
"reference_owner": frappe.db.get_value(doctype, name, "owner"), | |||||
"link_doctype": feed.link_doctype, | |||||
"link_name": feed.link_name | |||||
}).insert(ignore_permissions=True) | |||||
frappe.db.delete( | |||||
"Activity Log", | |||||
{"reference_doctype": doctype, "reference_name": name, "link_doctype": feed.link_doctype}, | |||||
) | |||||
frappe.get_doc( | |||||
{ | |||||
"doctype": "Activity Log", | |||||
"reference_doctype": doctype, | |||||
"reference_name": name, | |||||
"subject": feed.subject, | |||||
"full_name": get_fullname(doc.owner), | |||||
"reference_owner": frappe.db.get_value(doctype, name, "owner"), | |||||
"link_doctype": feed.link_doctype, | |||||
"link_name": feed.link_name, | |||||
} | |||||
).insert(ignore_permissions=True) | |||||
def login_feed(login_manager): | def login_feed(login_manager): | ||||
if login_manager.user != "Guest": | if login_manager.user != "Guest": | ||||
subject = _("{0} logged in").format(get_fullname(login_manager.user)) | subject = _("{0} logged in").format(get_fullname(login_manager.user)) | ||||
add_authentication_log(subject, login_manager.user) | add_authentication_log(subject, login_manager.user) | ||||
def logout_feed(user, reason): | def logout_feed(user, reason): | ||||
if user and user != "Guest": | if user and user != "Guest": | ||||
subject = _("{0} logged out: {1}").format(get_fullname(user), frappe.bold(reason)) | subject = _("{0} logged out: {1}").format(get_fullname(user), frappe.bold(reason)) | ||||
add_authentication_log(subject, user, operation="Logout") | add_authentication_log(subject, user, operation="Logout") | ||||
def get_feed_match_conditions(user=None, doctype='Comment'): | |||||
if not user: user = frappe.session.user | |||||
conditions = ['`tab{doctype}`.owner={user} or `tab{doctype}`.reference_owner={user}'.format( | |||||
user = frappe.db.escape(user), | |||||
doctype = doctype | |||||
)] | |||||
def get_feed_match_conditions(user=None, doctype="Comment"): | |||||
if not user: | |||||
user = frappe.session.user | |||||
conditions = [ | |||||
"`tab{doctype}`.owner={user} or `tab{doctype}`.reference_owner={user}".format( | |||||
user=frappe.db.escape(user), doctype=doctype | |||||
) | |||||
] | |||||
user_permissions = frappe.permissions.get_user_permissions(user) | user_permissions = frappe.permissions.get_user_permissions(user) | ||||
can_read = frappe.get_user().get_can_read() | can_read = frappe.get_user().get_can_read() | ||||
can_read_doctypes = ["'{}'".format(dt) for dt in | |||||
list(set(can_read) - set(list(user_permissions)))] | |||||
can_read_doctypes = [ | |||||
"'{}'".format(dt) for dt in list(set(can_read) - set(list(user_permissions))) | |||||
] | |||||
if can_read_doctypes: | if can_read_doctypes: | ||||
conditions += ["""(`tab{doctype}`.reference_doctype is null | |||||
conditions += [ | |||||
"""(`tab{doctype}`.reference_doctype is null | |||||
or `tab{doctype}`.reference_doctype = '' | or `tab{doctype}`.reference_doctype = '' | ||||
or `tab{doctype}`.reference_doctype | or `tab{doctype}`.reference_doctype | ||||
in ({values}))""".format( | in ({values}))""".format( | ||||
doctype = doctype, | |||||
values =", ".join(can_read_doctypes) | |||||
)] | |||||
doctype=doctype, values=", ".join(can_read_doctypes) | |||||
) | |||||
] | |||||
if user_permissions: | if user_permissions: | ||||
can_read_docs = [] | can_read_docs = [] | ||||
for dt, obj in user_permissions.items(): | for dt, obj in user_permissions.items(): | ||||
for n in obj: | for n in obj: | ||||
can_read_docs.append('{}|{}'.format(frappe.db.escape(dt), frappe.db.escape(n.get('doc', '')))) | |||||
can_read_docs.append("{}|{}".format(frappe.db.escape(dt), frappe.db.escape(n.get("doc", "")))) | |||||
if can_read_docs: | if can_read_docs: | ||||
conditions.append("concat_ws('|', `tab{doctype}`.reference_doctype, `tab{doctype}`.reference_name) in ({values})".format( | |||||
doctype = doctype, | |||||
values = ", ".join(can_read_docs))) | |||||
conditions.append( | |||||
"concat_ws('|', `tab{doctype}`.reference_doctype, `tab{doctype}`.reference_name) in ({values})".format( | |||||
doctype=doctype, values=", ".join(can_read_docs) | |||||
) | |||||
) | |||||
return "(" + " or ".join(conditions) + ")" | |||||
return "(" + " or ".join(conditions) + ")" |
@@ -1,77 +1,74 @@ | |||||
# -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
# Copyright (c) 2015, Frappe Technologies and Contributors | # Copyright (c) 2015, Frappe Technologies and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | |||||
import unittest | |||||
import time | import time | ||||
from frappe.auth import LoginManager, CookieManager | |||||
import unittest | |||||
import frappe | |||||
from frappe.auth import CookieManager, LoginManager | |||||
class TestActivityLog(unittest.TestCase): | class TestActivityLog(unittest.TestCase): | ||||
def test_activity_log(self): | def test_activity_log(self): | ||||
# test user login log | # test user login log | ||||
frappe.local.form_dict = frappe._dict({ | |||||
'cmd': 'login', | |||||
'sid': 'Guest', | |||||
'pwd': 'admin', | |||||
'usr': 'Administrator' | |||||
}) | |||||
frappe.local.form_dict = frappe._dict( | |||||
{"cmd": "login", "sid": "Guest", "pwd": "admin", "usr": "Administrator"} | |||||
) | |||||
frappe.local.cookie_manager = CookieManager() | frappe.local.cookie_manager = CookieManager() | ||||
frappe.local.login_manager = LoginManager() | frappe.local.login_manager = LoginManager() | ||||
auth_log = self.get_auth_log() | auth_log = self.get_auth_log() | ||||
self.assertEqual(auth_log.status, 'Success') | |||||
self.assertEqual(auth_log.status, "Success") | |||||
# test user logout log | # test user logout log | ||||
frappe.local.login_manager.logout() | frappe.local.login_manager.logout() | ||||
auth_log = self.get_auth_log(operation='Logout') | |||||
self.assertEqual(auth_log.status, 'Success') | |||||
auth_log = self.get_auth_log(operation="Logout") | |||||
self.assertEqual(auth_log.status, "Success") | |||||
# test invalid login | # test invalid login | ||||
frappe.form_dict.update({ 'pwd': 'password' }) | |||||
frappe.form_dict.update({"pwd": "password"}) | |||||
self.assertRaises(frappe.AuthenticationError, LoginManager) | self.assertRaises(frappe.AuthenticationError, LoginManager) | ||||
auth_log = self.get_auth_log() | auth_log = self.get_auth_log() | ||||
self.assertEqual(auth_log.status, 'Failed') | |||||
self.assertEqual(auth_log.status, "Failed") | |||||
frappe.local.form_dict = frappe._dict() | frappe.local.form_dict = frappe._dict() | ||||
def get_auth_log(self, operation='Login'): | |||||
names = frappe.db.get_all('Activity Log', filters={ | |||||
'user': 'Administrator', | |||||
'operation': operation, | |||||
}, order_by='`creation` DESC') | |||||
def get_auth_log(self, operation="Login"): | |||||
names = frappe.db.get_all( | |||||
"Activity Log", | |||||
filters={ | |||||
"user": "Administrator", | |||||
"operation": operation, | |||||
}, | |||||
order_by="`creation` DESC", | |||||
) | |||||
name = names[0] | name = names[0] | ||||
auth_log = frappe.get_doc('Activity Log', name) | |||||
auth_log = frappe.get_doc("Activity Log", name) | |||||
return auth_log | return auth_log | ||||
def test_brute_security(self): | def test_brute_security(self): | ||||
update_system_settings({ | |||||
'allow_consecutive_login_attempts': 3, | |||||
'allow_login_after_fail': 5 | |||||
}) | |||||
frappe.local.form_dict = frappe._dict({ | |||||
'cmd': 'login', | |||||
'sid': 'Guest', | |||||
'pwd': 'admin', | |||||
'usr': 'Administrator' | |||||
}) | |||||
update_system_settings({"allow_consecutive_login_attempts": 3, "allow_login_after_fail": 5}) | |||||
frappe.local.form_dict = frappe._dict( | |||||
{"cmd": "login", "sid": "Guest", "pwd": "admin", "usr": "Administrator"} | |||||
) | |||||
frappe.local.cookie_manager = CookieManager() | frappe.local.cookie_manager = CookieManager() | ||||
frappe.local.login_manager = LoginManager() | frappe.local.login_manager = LoginManager() | ||||
auth_log = self.get_auth_log() | auth_log = self.get_auth_log() | ||||
self.assertEqual(auth_log.status, 'Success') | |||||
self.assertEqual(auth_log.status, "Success") | |||||
# test user logout log | # test user logout log | ||||
frappe.local.login_manager.logout() | frappe.local.login_manager.logout() | ||||
auth_log = self.get_auth_log(operation='Logout') | |||||
self.assertEqual(auth_log.status, 'Success') | |||||
auth_log = self.get_auth_log(operation="Logout") | |||||
self.assertEqual(auth_log.status, "Success") | |||||
# test invalid login | # test invalid login | ||||
frappe.form_dict.update({ 'pwd': 'password' }) | |||||
frappe.form_dict.update({"pwd": "password"}) | |||||
self.assertRaises(frappe.AuthenticationError, LoginManager) | self.assertRaises(frappe.AuthenticationError, LoginManager) | ||||
self.assertRaises(frappe.AuthenticationError, LoginManager) | self.assertRaises(frappe.AuthenticationError, LoginManager) | ||||
self.assertRaises(frappe.AuthenticationError, LoginManager) | self.assertRaises(frappe.AuthenticationError, LoginManager) | ||||
@@ -85,8 +82,9 @@ class TestActivityLog(unittest.TestCase): | |||||
frappe.local.form_dict = frappe._dict() | frappe.local.form_dict = frappe._dict() | ||||
def update_system_settings(args): | def update_system_settings(args): | ||||
doc = frappe.get_doc('System Settings') | |||||
doc = frappe.get_doc("System Settings") | |||||
doc.update(args) | doc.update(args) | ||||
doc.flags.ignore_mandatory = 1 | doc.flags.ignore_mandatory = 1 | ||||
doc.save() | doc.save() |
@@ -5,5 +5,6 @@ | |||||
import frappe | import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class BlockModule(Document): | class BlockModule(Document): | ||||
pass | pass |
@@ -1,22 +1,27 @@ | |||||
# -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
# Copyright (c) 2019, Frappe Technologies and contributors | # Copyright (c) 2019, Frappe Technologies and contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import json | |||||
import frappe | import frappe | ||||
from frappe import _ | from frappe import _ | ||||
import json | |||||
from frappe.model.document import Document | |||||
from frappe.core.doctype.user.user import extract_mentions | from frappe.core.doctype.user.user import extract_mentions | ||||
from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification,\ | |||||
get_title, get_title_html | |||||
from frappe.utils import get_fullname | |||||
from frappe.website.utils import clear_cache | |||||
from frappe.database.schema import add_column | from frappe.database.schema import add_column | ||||
from frappe.desk.doctype.notification_log.notification_log import ( | |||||
enqueue_create_notification, | |||||
get_title, | |||||
get_title_html, | |||||
) | |||||
from frappe.exceptions import ImplicitCommitError | from frappe.exceptions import ImplicitCommitError | ||||
from frappe.model.document import Document | |||||
from frappe.utils import get_fullname | |||||
from frappe.website.utils import clear_cache | |||||
class Comment(Document): | class Comment(Document): | ||||
def after_insert(self): | def after_insert(self): | ||||
self.notify_mentions() | self.notify_mentions() | ||||
self.notify_change('add') | |||||
self.notify_change("add") | |||||
def validate(self): | def validate(self): | ||||
if not self.comment_email: | if not self.comment_email: | ||||
@@ -26,34 +31,35 @@ class Comment(Document): | |||||
def on_update(self): | def on_update(self): | ||||
update_comment_in_doc(self) | update_comment_in_doc(self) | ||||
if self.is_new(): | if self.is_new(): | ||||
self.notify_change('update') | |||||
self.notify_change("update") | |||||
def on_trash(self): | def on_trash(self): | ||||
self.remove_comment_from_cache() | self.remove_comment_from_cache() | ||||
self.notify_change('delete') | |||||
self.notify_change("delete") | |||||
def notify_change(self, action): | def notify_change(self, action): | ||||
key_map = { | key_map = { | ||||
'Like': 'like_logs', | |||||
'Assigned': 'assignment_logs', | |||||
'Assignment Completed': 'assignment_logs', | |||||
'Comment': 'comments', | |||||
'Attachment': 'attachment_logs', | |||||
'Attachment Removed': 'attachment_logs', | |||||
"Like": "like_logs", | |||||
"Assigned": "assignment_logs", | |||||
"Assignment Completed": "assignment_logs", | |||||
"Comment": "comments", | |||||
"Attachment": "attachment_logs", | |||||
"Attachment Removed": "attachment_logs", | |||||
} | } | ||||
key = key_map.get(self.comment_type) | key = key_map.get(self.comment_type) | ||||
if not key: return | |||||
if not key: | |||||
return | |||||
frappe.publish_realtime('update_docinfo_for_{}_{}'.format(self.reference_doctype, self.reference_name), { | |||||
'doc': self.as_dict(), | |||||
'key': key, | |||||
'action': action | |||||
}, after_commit=True) | |||||
frappe.publish_realtime( | |||||
"update_docinfo_for_{}_{}".format(self.reference_doctype, self.reference_name), | |||||
{"doc": self.as_dict(), "key": key, "action": action}, | |||||
after_commit=True, | |||||
) | |||||
def remove_comment_from_cache(self): | def remove_comment_from_cache(self): | ||||
_comments = get_comments_from_parent(self) | _comments = get_comments_from_parent(self) | ||||
for c in _comments: | for c in _comments: | ||||
if c.get("name")==self.name: | |||||
if c.get("name") == self.name: | |||||
_comments.remove(c) | _comments.remove(c) | ||||
update_comments_in_parent(self.reference_doctype, self.reference_name, _comments) | update_comments_in_parent(self.reference_doctype, self.reference_name, _comments) | ||||
@@ -68,19 +74,26 @@ class Comment(Document): | |||||
sender_fullname = get_fullname(frappe.session.user) | sender_fullname = get_fullname(frappe.session.user) | ||||
title = get_title(self.reference_doctype, self.reference_name) | title = get_title(self.reference_doctype, self.reference_name) | ||||
recipients = [frappe.db.get_value("User", {"enabled": 1, "name": name, "user_type": "System User", "allowed_in_mentions": 1}, "email") | |||||
for name in mentions] | |||||
recipients = [ | |||||
frappe.db.get_value( | |||||
"User", | |||||
{"enabled": 1, "name": name, "user_type": "System User", "allowed_in_mentions": 1}, | |||||
"email", | |||||
) | |||||
for name in mentions | |||||
] | |||||
notification_message = _('''{0} mentioned you in a comment in {1} {2}''')\ | |||||
.format(frappe.bold(sender_fullname), frappe.bold(self.reference_doctype), get_title_html(title)) | |||||
notification_message = _("""{0} mentioned you in a comment in {1} {2}""").format( | |||||
frappe.bold(sender_fullname), frappe.bold(self.reference_doctype), get_title_html(title) | |||||
) | |||||
notification_doc = { | notification_doc = { | ||||
'type': 'Mention', | |||||
'document_type': self.reference_doctype, | |||||
'document_name': self.reference_name, | |||||
'subject': notification_message, | |||||
'from_user': frappe.session.user, | |||||
'email_content': self.content | |||||
"type": "Mention", | |||||
"document_type": self.reference_doctype, | |||||
"document_name": self.reference_name, | |||||
"subject": notification_message, | |||||
"from_user": frappe.session.user, | |||||
"email_content": self.content, | |||||
} | } | ||||
enqueue_create_notification(recipients, notification_doc) | enqueue_create_notification(recipients, notification_doc) | ||||
@@ -99,45 +112,46 @@ def update_comment_in_doc(doc): | |||||
`_comments` format | `_comments` format | ||||
{ | |||||
"comment": [String], | |||||
"by": [user], | |||||
"name": [Comment Document name] | |||||
}""" | |||||
{ | |||||
"comment": [String], | |||||
"by": [user], | |||||
"name": [Comment Document name] | |||||
}""" | |||||
# only comments get updates, not likes, assignments etc. | # only comments get updates, not likes, assignments etc. | ||||
if doc.doctype == 'Comment' and doc.comment_type != 'Comment': | |||||
if doc.doctype == "Comment" and doc.comment_type != "Comment": | |||||
return | return | ||||
def get_truncated(content): | def get_truncated(content): | ||||
return (content[:97] + '...') if len(content) > 100 else content | |||||
return (content[:97] + "...") if len(content) > 100 else content | |||||
if doc.reference_doctype and doc.reference_name and doc.content: | if doc.reference_doctype and doc.reference_name and doc.content: | ||||
_comments = get_comments_from_parent(doc) | _comments = get_comments_from_parent(doc) | ||||
updated = False | updated = False | ||||
for c in _comments: | for c in _comments: | ||||
if c.get("name")==doc.name: | |||||
if c.get("name") == doc.name: | |||||
c["comment"] = get_truncated(doc.content) | c["comment"] = get_truncated(doc.content) | ||||
updated = True | updated = True | ||||
if not updated: | if not updated: | ||||
_comments.append({ | |||||
"comment": get_truncated(doc.content), | |||||
# "comment_email" for Comment and "sender" for Communication | |||||
"by": getattr(doc, 'comment_email', None) or getattr(doc, 'sender', None) or doc.owner, | |||||
"name": doc.name | |||||
}) | |||||
_comments.append( | |||||
{ | |||||
"comment": get_truncated(doc.content), | |||||
# "comment_email" for Comment and "sender" for Communication | |||||
"by": getattr(doc, "comment_email", None) or getattr(doc, "sender", None) or doc.owner, | |||||
"name": doc.name, | |||||
} | |||||
) | |||||
update_comments_in_parent(doc.reference_doctype, doc.reference_name, _comments) | update_comments_in_parent(doc.reference_doctype, doc.reference_name, _comments) | ||||
def get_comments_from_parent(doc): | def get_comments_from_parent(doc): | ||||
''' | |||||
""" | |||||
get the list of comments cached in the document record in the column | get the list of comments cached in the document record in the column | ||||
`_comments` | `_comments` | ||||
''' | |||||
""" | |||||
try: | try: | ||||
_comments = frappe.db.get_value(doc.reference_doctype, doc.reference_name, "_comments") or "[]" | _comments = frappe.db.get_value(doc.reference_doctype, doc.reference_name, "_comments") or "[]" | ||||
@@ -153,23 +167,32 @@ def get_comments_from_parent(doc): | |||||
except ValueError: | except ValueError: | ||||
return [] | return [] | ||||
def update_comments_in_parent(reference_doctype, reference_name, _comments): | def update_comments_in_parent(reference_doctype, reference_name, _comments): | ||||
"""Updates `_comments` property in parent Document with given dict. | """Updates `_comments` property in parent Document with given dict. | ||||
:param _comments: Dict of comments.""" | :param _comments: Dict of comments.""" | ||||
if not reference_doctype or not reference_name or frappe.db.get_value("DocType", reference_doctype, "issingle") or frappe.db.get_value("DocType", reference_doctype, "is_virtual"): | |||||
if ( | |||||
not reference_doctype | |||||
or not reference_name | |||||
or frappe.db.get_value("DocType", reference_doctype, "issingle") | |||||
or frappe.db.get_value("DocType", reference_doctype, "is_virtual") | |||||
): | |||||
return | return | ||||
try: | try: | ||||
# use sql, so that we do not mess with the timestamp | # use sql, so that we do not mess with the timestamp | ||||
frappe.db.sql("""update `tab{0}` set `_comments`=%s where name=%s""".format(reference_doctype), # nosec | |||||
(json.dumps(_comments[-100:]), reference_name)) | |||||
frappe.db.sql( | |||||
"""update `tab{0}` set `_comments`=%s where name=%s""".format(reference_doctype), # nosec | |||||
(json.dumps(_comments[-100:]), reference_name), | |||||
) | |||||
except Exception as e: | except Exception as e: | ||||
if frappe.db.is_column_missing(e) and getattr(frappe.local, 'request', None): | |||||
if frappe.db.is_column_missing(e) and getattr(frappe.local, "request", None): | |||||
# missing column and in request, add column and update after commit | # missing column and in request, add column and update after commit | ||||
frappe.local._comments = (getattr(frappe.local, "_comments", []) | |||||
+ [(reference_doctype, reference_name, _comments)]) | |||||
frappe.local._comments = getattr(frappe.local, "_comments", []) + [ | |||||
(reference_doctype, reference_name, _comments) | |||||
] | |||||
elif frappe.db.is_data_too_long(e): | elif frappe.db.is_data_too_long(e): | ||||
raise frappe.DataTooLongException | raise frappe.DataTooLongException | ||||
@@ -183,6 +206,7 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments): | |||||
if getattr(reference_doc, "route", None): | if getattr(reference_doc, "route", None): | ||||
clear_cache(reference_doc.route) | clear_cache(reference_doc.route) | ||||
def update_comments_in_parent_after_request(): | def update_comments_in_parent_after_request(): | ||||
"""update _comments in parent if _comments column is missing""" | """update _comments in parent if _comments column is missing""" | ||||
if hasattr(frappe.local, "_comments"): | if hasattr(frappe.local, "_comments"): | ||||
@@ -1,9 +1,12 @@ | |||||
# -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
# Copyright (c) 2019, Frappe Technologies and Contributors | # Copyright (c) 2019, Frappe Technologies and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe, json | |||||
import json | |||||
import unittest | import unittest | ||||
import frappe | |||||
class TestComment(unittest.TestCase): | class TestComment(unittest.TestCase): | ||||
def tearDown(self): | def tearDown(self): | ||||
frappe.form_dict.comment = None | frappe.form_dict.comment = None | ||||
@@ -15,75 +18,88 @@ class TestComment(unittest.TestCase): | |||||
frappe.local.request_ip = None | frappe.local.request_ip = None | ||||
def test_comment_creation(self): | def test_comment_creation(self): | ||||
test_doc = frappe.get_doc(dict(doctype = 'ToDo', description = 'test')) | |||||
test_doc = frappe.get_doc(dict(doctype="ToDo", description="test")) | |||||
test_doc.insert() | test_doc.insert() | ||||
comment = test_doc.add_comment('Comment', 'test comment') | |||||
comment = test_doc.add_comment("Comment", "test comment") | |||||
test_doc.reload() | test_doc.reload() | ||||
# check if updated in _comments cache | # check if updated in _comments cache | ||||
comments = json.loads(test_doc.get('_comments')) | |||||
self.assertEqual(comments[0].get('name'), comment.name) | |||||
self.assertEqual(comments[0].get('comment'), comment.content) | |||||
comments = json.loads(test_doc.get("_comments")) | |||||
self.assertEqual(comments[0].get("name"), comment.name) | |||||
self.assertEqual(comments[0].get("comment"), comment.content) | |||||
# check document creation | # check document creation | ||||
comment_1 = frappe.get_all('Comment', fields = ['*'], filters = dict( | |||||
reference_doctype = test_doc.doctype, | |||||
reference_name = test_doc.name | |||||
))[0] | |||||
comment_1 = frappe.get_all( | |||||
"Comment", | |||||
fields=["*"], | |||||
filters=dict(reference_doctype=test_doc.doctype, reference_name=test_doc.name), | |||||
)[0] | |||||
self.assertEqual(comment_1.content, 'test comment') | |||||
self.assertEqual(comment_1.content, "test comment") | |||||
# test via blog | # test via blog | ||||
def test_public_comment(self): | def test_public_comment(self): | ||||
from frappe.website.doctype.blog_post.test_blog_post import make_test_blog | from frappe.website.doctype.blog_post.test_blog_post import make_test_blog | ||||
test_blog = make_test_blog() | test_blog = make_test_blog() | ||||
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"}) | frappe.db.delete("Comment", {"reference_doctype": "Blog Post"}) | ||||
from frappe.templates.includes.comments.comments import add_comment | from frappe.templates.includes.comments.comments import add_comment | ||||
frappe.form_dict.comment = 'Good comment with 10 chars' | |||||
frappe.form_dict.comment_email = 'test@test.com' | |||||
frappe.form_dict.comment_by = 'Good Tester' | |||||
frappe.form_dict.reference_doctype = 'Blog Post' | |||||
frappe.form_dict.comment = "Good comment with 10 chars" | |||||
frappe.form_dict.comment_email = "test@test.com" | |||||
frappe.form_dict.comment_by = "Good Tester" | |||||
frappe.form_dict.reference_doctype = "Blog Post" | |||||
frappe.form_dict.reference_name = test_blog.name | frappe.form_dict.reference_name = test_blog.name | ||||
frappe.form_dict.route = test_blog.route | frappe.form_dict.route = test_blog.route | ||||
frappe.local.request_ip = '127.0.0.1' | |||||
frappe.local.request_ip = "127.0.0.1" | |||||
add_comment() | add_comment() | ||||
self.assertEqual(frappe.get_all('Comment', fields = ['*'], filters = dict( | |||||
reference_doctype = test_blog.doctype, | |||||
reference_name = test_blog.name | |||||
))[0].published, 1) | |||||
self.assertEqual( | |||||
frappe.get_all( | |||||
"Comment", | |||||
fields=["*"], | |||||
filters=dict(reference_doctype=test_blog.doctype, reference_name=test_blog.name), | |||||
)[0].published, | |||||
1, | |||||
) | |||||
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"}) | frappe.db.delete("Comment", {"reference_doctype": "Blog Post"}) | ||||
frappe.form_dict.comment = 'pleez vizits my site http://mysite.com' | |||||
frappe.form_dict.comment_by = 'bad commentor' | |||||
frappe.form_dict.comment = "pleez vizits my site http://mysite.com" | |||||
frappe.form_dict.comment_by = "bad commentor" | |||||
add_comment() | add_comment() | ||||
self.assertEqual(len(frappe.get_all('Comment', fields = ['*'], filters = dict( | |||||
reference_doctype = test_blog.doctype, | |||||
reference_name = test_blog.name | |||||
))), 0) | |||||
self.assertEqual( | |||||
len( | |||||
frappe.get_all( | |||||
"Comment", | |||||
fields=["*"], | |||||
filters=dict(reference_doctype=test_blog.doctype, reference_name=test_blog.name), | |||||
) | |||||
), | |||||
0, | |||||
) | |||||
# test for filtering html and css injection elements | # test for filtering html and css injection elements | ||||
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"}) | frappe.db.delete("Comment", {"reference_doctype": "Blog Post"}) | ||||
frappe.form_dict.comment = '<script>alert(1)</script>Comment' | |||||
frappe.form_dict.comment_by = 'hacker' | |||||
frappe.form_dict.comment = "<script>alert(1)</script>Comment" | |||||
frappe.form_dict.comment_by = "hacker" | |||||
add_comment() | add_comment() | ||||
self.assertEqual(frappe.get_all('Comment', fields = ['content'], filters = dict( | |||||
reference_doctype = test_blog.doctype, | |||||
reference_name = test_blog.name | |||||
))[0]['content'], 'Comment') | |||||
self.assertEqual( | |||||
frappe.get_all( | |||||
"Comment", | |||||
fields=["content"], | |||||
filters=dict(reference_doctype=test_blog.doctype, reference_name=test_blog.name), | |||||
)[0]["content"], | |||||
"Comment", | |||||
) | |||||
test_blog.delete() | test_blog.delete() | ||||
@@ -1,3 +1,2 @@ | |||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
@@ -2,49 +2,67 @@ | |||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
from collections import Counter | from collections import Counter | ||||
from email.utils import getaddresses | |||||
from typing import List | from typing import List | ||||
from urllib.parse import unquote | |||||
from parse import compile | |||||
import frappe | import frappe | ||||
from frappe import _ | from frappe import _ | ||||
from frappe.model.document import Document | |||||
from frappe.utils import validate_email_address, strip_html, cstr, time_diff_in_seconds | |||||
from frappe.automation.doctype.assignment_rule.assignment_rule import ( | |||||
apply as apply_assignment_rule, | |||||
) | |||||
from frappe.contacts.doctype.contact.contact import get_contact_name | |||||
from frappe.core.doctype.comment.comment import update_comment_in_doc | |||||
from frappe.core.doctype.communication.email import validate_email | from frappe.core.doctype.communication.email import validate_email | ||||
from frappe.core.doctype.communication.mixins import CommunicationEmailMixin | from frappe.core.doctype.communication.mixins import CommunicationEmailMixin | ||||
from frappe.core.utils import get_parent_doc | from frappe.core.utils import get_parent_doc | ||||
from frappe.utils import parse_addr, split_emails | |||||
from frappe.core.doctype.comment.comment import update_comment_in_doc | |||||
from email.utils import getaddresses | |||||
from urllib.parse import unquote | |||||
from frappe.model.document import Document | |||||
from frappe.utils import ( | |||||
cstr, | |||||
parse_addr, | |||||
split_emails, | |||||
strip_html, | |||||
time_diff_in_seconds, | |||||
validate_email_address, | |||||
) | |||||
from frappe.utils.user import is_system_user | from frappe.utils.user import is_system_user | ||||
from frappe.contacts.doctype.contact.contact import get_contact_name | |||||
from frappe.automation.doctype.assignment_rule.assignment_rule import apply as apply_assignment_rule | |||||
from parse import compile | |||||
exclude_from_linked_with = True | exclude_from_linked_with = True | ||||
class Communication(Document, CommunicationEmailMixin): | class Communication(Document, CommunicationEmailMixin): | ||||
"""Communication represents an external communication like Email. | |||||
""" | |||||
"""Communication represents an external communication like Email.""" | |||||
no_feed_on_delete = True | no_feed_on_delete = True | ||||
DOCTYPE = 'Communication' | |||||
DOCTYPE = "Communication" | |||||
def onload(self): | def onload(self): | ||||
"""create email flag queue""" | """create email flag queue""" | ||||
if self.communication_type == "Communication" and self.communication_medium == "Email" \ | |||||
and self.sent_or_received == "Received" and self.uid and self.uid != -1: | |||||
email_flag_queue = frappe.db.get_value("Email Flag Queue", { | |||||
"communication": self.name, | |||||
"is_completed": 0}) | |||||
if ( | |||||
self.communication_type == "Communication" | |||||
and self.communication_medium == "Email" | |||||
and self.sent_or_received == "Received" | |||||
and self.uid | |||||
and self.uid != -1 | |||||
): | |||||
email_flag_queue = frappe.db.get_value( | |||||
"Email Flag Queue", {"communication": self.name, "is_completed": 0} | |||||
) | |||||
if email_flag_queue: | if email_flag_queue: | ||||
return | return | ||||
frappe.get_doc({ | |||||
"doctype": "Email Flag Queue", | |||||
"action": "Read", | |||||
"communication": self.name, | |||||
"uid": self.uid, | |||||
"email_account": self.email_account | |||||
}).insert(ignore_permissions=True) | |||||
frappe.get_doc( | |||||
{ | |||||
"doctype": "Email Flag Queue", | |||||
"action": "Read", | |||||
"communication": self.name, | |||||
"uid": self.uid, | |||||
"email_account": self.email_account, | |||||
} | |||||
).insert(ignore_permissions=True) | |||||
frappe.db.commit() | frappe.db.commit() | ||||
def validate(self): | def validate(self): | ||||
@@ -74,25 +92,33 @@ class Communication(Document, CommunicationEmailMixin): | |||||
def validate_reference(self): | def validate_reference(self): | ||||
if self.reference_doctype and self.reference_name: | if self.reference_doctype and self.reference_name: | ||||
if not self.reference_owner: | if not self.reference_owner: | ||||
self.reference_owner = frappe.db.get_value(self.reference_doctype, self.reference_name, "owner") | |||||
self.reference_owner = frappe.db.get_value( | |||||
self.reference_doctype, self.reference_name, "owner" | |||||
) | |||||
# prevent communication against a child table | # prevent communication against a child table | ||||
if frappe.get_meta(self.reference_doctype).istable: | if frappe.get_meta(self.reference_doctype).istable: | ||||
frappe.throw(_("Cannot create a {0} against a child document: {1}") | |||||
.format(_(self.communication_type), _(self.reference_doctype))) | |||||
frappe.throw( | |||||
_("Cannot create a {0} against a child document: {1}").format( | |||||
_(self.communication_type), _(self.reference_doctype) | |||||
) | |||||
) | |||||
# Prevent circular linking of Communication DocTypes | # Prevent circular linking of Communication DocTypes | ||||
if self.reference_doctype == "Communication": | if self.reference_doctype == "Communication": | ||||
circular_linking = False | circular_linking = False | ||||
doc = get_parent_doc(self) | doc = get_parent_doc(self) | ||||
while doc.reference_doctype == "Communication": | while doc.reference_doctype == "Communication": | ||||
if get_parent_doc(doc).name==self.name: | |||||
if get_parent_doc(doc).name == self.name: | |||||
circular_linking = True | circular_linking = True | ||||
break | break | ||||
doc = get_parent_doc(doc) | doc = get_parent_doc(doc) | ||||
if circular_linking: | if circular_linking: | ||||
frappe.throw(_("Please make sure the Reference Communication Docs are not circularly linked."), frappe.CircularLinkingError) | |||||
frappe.throw( | |||||
_("Please make sure the Reference Communication Docs are not circularly linked."), | |||||
frappe.CircularLinkingError, | |||||
) | |||||
def after_insert(self): | def after_insert(self): | ||||
if not (self.reference_doctype and self.reference_name): | if not (self.reference_doctype and self.reference_name): | ||||
@@ -102,21 +128,21 @@ class Communication(Document, CommunicationEmailMixin): | |||||
frappe.db.set_value("Communication", self.reference_name, "status", "Replied") | frappe.db.set_value("Communication", self.reference_name, "status", "Replied") | ||||
if self.communication_type == "Communication": | if self.communication_type == "Communication": | ||||
self.notify_change('add') | |||||
self.notify_change("add") | |||||
elif self.communication_type in ("Chat", "Notification"): | elif self.communication_type in ("Chat", "Notification"): | ||||
if self.reference_name == frappe.session.user: | if self.reference_name == frappe.session.user: | ||||
message = self.as_dict() | message = self.as_dict() | ||||
message['broadcast'] = True | |||||
frappe.publish_realtime('new_message', message, after_commit=True) | |||||
message["broadcast"] = True | |||||
frappe.publish_realtime("new_message", message, after_commit=True) | |||||
else: | else: | ||||
# reference_name contains the user who is addressed in the messages' page comment | # reference_name contains the user who is addressed in the messages' page comment | ||||
frappe.publish_realtime('new_message', self.as_dict(), | |||||
user=self.reference_name, after_commit=True) | |||||
frappe.publish_realtime( | |||||
"new_message", self.as_dict(), user=self.reference_name, after_commit=True | |||||
) | |||||
def set_signature_in_email_content(self): | def set_signature_in_email_content(self): | ||||
"""Set sender's User.email_signature or default outgoing's EmailAccount.signature to the email | |||||
""" | |||||
"""Set sender's User.email_signature or default outgoing's EmailAccount.signature to the email""" | |||||
if not self.content: | if not self.content: | ||||
return | return | ||||
@@ -128,11 +154,15 @@ class Communication(Document, CommunicationEmailMixin): | |||||
email_body = email_body[0] | email_body = email_body[0] | ||||
user_email_signature = frappe.db.get_value( | |||||
"User", | |||||
self.sender, | |||||
"email_signature", | |||||
) if self.sender else None | |||||
user_email_signature = ( | |||||
frappe.db.get_value( | |||||
"User", | |||||
self.sender, | |||||
"email_signature", | |||||
) | |||||
if self.sender | |||||
else None | |||||
) | |||||
signature = user_email_signature or frappe.db.get_value( | signature = user_email_signature or frappe.db.get_value( | ||||
"Email Account", | "Email Account", | ||||
@@ -157,19 +187,19 @@ class Communication(Document, CommunicationEmailMixin): | |||||
# comments count for the list view | # comments count for the list view | ||||
update_comment_in_doc(self) | update_comment_in_doc(self) | ||||
if self.comment_type != 'Updated': | |||||
if self.comment_type != "Updated": | |||||
update_parent_document_on_communication(self) | update_parent_document_on_communication(self) | ||||
def on_trash(self): | def on_trash(self): | ||||
if self.communication_type == "Communication": | if self.communication_type == "Communication": | ||||
self.notify_change('delete') | |||||
self.notify_change("delete") | |||||
@property | @property | ||||
def sender_mailid(self): | def sender_mailid(self): | ||||
return parse_addr(self.sender)[1] if self.sender else "" | return parse_addr(self.sender)[1] if self.sender else "" | ||||
@staticmethod | @staticmethod | ||||
def _get_emails_list(emails=None, exclude_displayname = False): | |||||
def _get_emails_list(emails=None, exclude_displayname=False): | |||||
"""Returns list of emails from given email string. | """Returns list of emails from given email string. | ||||
* Removes duplicate mailids | * Removes duplicate mailids | ||||
@@ -180,35 +210,32 @@ class Communication(Document, CommunicationEmailMixin): | |||||
return [email.lower() for email in set([parse_addr(email)[1] for email in emails]) if email] | return [email.lower() for email in set([parse_addr(email)[1] for email in emails]) if email] | ||||
return [email.lower() for email in set(emails) if email] | return [email.lower() for email in set(emails) if email] | ||||
def to_list(self, exclude_displayname = True): | |||||
"""Returns to list. | |||||
""" | |||||
def to_list(self, exclude_displayname=True): | |||||
"""Returns to list.""" | |||||
return self._get_emails_list(self.recipients, exclude_displayname=exclude_displayname) | return self._get_emails_list(self.recipients, exclude_displayname=exclude_displayname) | ||||
def cc_list(self, exclude_displayname = True): | |||||
"""Returns cc list. | |||||
""" | |||||
def cc_list(self, exclude_displayname=True): | |||||
"""Returns cc list.""" | |||||
return self._get_emails_list(self.cc, exclude_displayname=exclude_displayname) | return self._get_emails_list(self.cc, exclude_displayname=exclude_displayname) | ||||
def bcc_list(self, exclude_displayname = True): | |||||
"""Returns bcc list. | |||||
""" | |||||
def bcc_list(self, exclude_displayname=True): | |||||
"""Returns bcc list.""" | |||||
return self._get_emails_list(self.bcc, exclude_displayname=exclude_displayname) | return self._get_emails_list(self.bcc, exclude_displayname=exclude_displayname) | ||||
def get_attachments(self): | def get_attachments(self): | ||||
attachments = frappe.get_all( | attachments = frappe.get_all( | ||||
"File", | "File", | ||||
fields=["name", "file_name", "file_url", "is_private"], | fields=["name", "file_name", "file_url", "is_private"], | ||||
filters = {"attached_to_name": self.name, "attached_to_doctype": self.DOCTYPE} | |||||
filters={"attached_to_name": self.name, "attached_to_doctype": self.DOCTYPE}, | |||||
) | ) | ||||
return attachments | return attachments | ||||
def notify_change(self, action): | def notify_change(self, action): | ||||
frappe.publish_realtime('update_docinfo_for_{}_{}'.format(self.reference_doctype, self.reference_name), { | |||||
'doc': self.as_dict(), | |||||
'key': 'communications', | |||||
'action': action | |||||
}, after_commit=True) | |||||
frappe.publish_realtime( | |||||
"update_docinfo_for_{}_{}".format(self.reference_doctype, self.reference_name), | |||||
{"doc": self.as_dict(), "key": "communications", "action": action}, | |||||
after_commit=True, | |||||
) | |||||
def set_status(self): | def set_status(self): | ||||
if not self.is_new(): | if not self.is_new(): | ||||
@@ -216,15 +243,19 @@ class Communication(Document, CommunicationEmailMixin): | |||||
if self.reference_doctype and self.reference_name: | if self.reference_doctype and self.reference_name: | ||||
self.status = "Linked" | self.status = "Linked" | ||||
elif self.communication_type=="Communication": | |||||
elif self.communication_type == "Communication": | |||||
self.status = "Open" | self.status = "Open" | ||||
else: | else: | ||||
self.status = "Closed" | self.status = "Closed" | ||||
# set email status to spam | # set email status to spam | ||||
email_rule = frappe.db.get_value("Email Rule", { "email_id": self.sender, "is_spam":1 }) | |||||
if self.communication_type == "Communication" and self.communication_medium == "Email" \ | |||||
and self.sent_or_received == "Sent" and email_rule: | |||||
email_rule = frappe.db.get_value("Email Rule", {"email_id": self.sender, "is_spam": 1}) | |||||
if ( | |||||
self.communication_type == "Communication" | |||||
and self.communication_medium == "Email" | |||||
and self.sent_or_received == "Sent" | |||||
and email_rule | |||||
): | |||||
self.email_status = "Spam" | self.email_status = "Spam" | ||||
@@ -254,7 +285,7 @@ class Communication(Document, CommunicationEmailMixin): | |||||
self.sender_full_name = self.sender | self.sender_full_name = self.sender | ||||
self.sender = None | self.sender = None | ||||
else: | else: | ||||
if self.sent_or_received=='Sent': | |||||
if self.sent_or_received == "Sent": | |||||
validate_email_address(self.sender, throw=True) | validate_email_address(self.sender, throw=True) | ||||
sender_name, sender_email = parse_addr(self.sender) | sender_name, sender_email = parse_addr(self.sender) | ||||
if sender_name == sender_email: | if sender_name == sender_email: | ||||
@@ -264,40 +295,41 @@ class Communication(Document, CommunicationEmailMixin): | |||||
self.sender_full_name = sender_name | self.sender_full_name = sender_name | ||||
if not self.sender_full_name: | if not self.sender_full_name: | ||||
self.sender_full_name = frappe.db.get_value('User', self.sender, 'full_name') | |||||
self.sender_full_name = frappe.db.get_value("User", self.sender, "full_name") | |||||
if not self.sender_full_name: | if not self.sender_full_name: | ||||
first_name, last_name = frappe.db.get_value('Contact', | |||||
filters={'email_id': sender_email}, | |||||
fieldname=['first_name', 'last_name'] | |||||
first_name, last_name = frappe.db.get_value( | |||||
"Contact", filters={"email_id": sender_email}, fieldname=["first_name", "last_name"] | |||||
) or [None, None] | ) or [None, None] | ||||
self.sender_full_name = (first_name or '') + (last_name or '') | |||||
self.sender_full_name = (first_name or "") + (last_name or "") | |||||
if not self.sender_full_name: | if not self.sender_full_name: | ||||
self.sender_full_name = sender_email | self.sender_full_name = sender_email | ||||
def set_delivery_status(self, commit=False): | def set_delivery_status(self, commit=False): | ||||
'''Look into the status of Email Queue linked to this Communication and set the Delivery Status of this Communication''' | |||||
"""Look into the status of Email Queue linked to this Communication and set the Delivery Status of this Communication""" | |||||
delivery_status = None | delivery_status = None | ||||
status_counts = Counter(frappe.get_all("Email Queue", pluck="status", filters={"communication": self.name})) | |||||
status_counts = Counter( | |||||
frappe.get_all("Email Queue", pluck="status", filters={"communication": self.name}) | |||||
) | |||||
if self.sent_or_received == "Received": | if self.sent_or_received == "Received": | ||||
return | return | ||||
if status_counts.get('Not Sent') or status_counts.get('Sending'): | |||||
delivery_status = 'Sending' | |||||
if status_counts.get("Not Sent") or status_counts.get("Sending"): | |||||
delivery_status = "Sending" | |||||
elif status_counts.get('Error'): | |||||
delivery_status = 'Error' | |||||
elif status_counts.get("Error"): | |||||
delivery_status = "Error" | |||||
elif status_counts.get('Expired'): | |||||
delivery_status = 'Expired' | |||||
elif status_counts.get("Expired"): | |||||
delivery_status = "Expired" | |||||
elif status_counts.get('Sent'): | |||||
delivery_status = 'Sent' | |||||
elif status_counts.get("Sent"): | |||||
delivery_status = "Sent" | |||||
if delivery_status: | if delivery_status: | ||||
self.db_set('delivery_status', delivery_status) | |||||
self.notify_change('update') | |||||
self.db_set("delivery_status", delivery_status) | |||||
self.notify_change("update") | |||||
# for list views and forms | # for list views and forms | ||||
self.notify_update() | self.notify_update() | ||||
@@ -311,13 +343,17 @@ class Communication(Document, CommunicationEmailMixin): | |||||
# Timeline Links | # Timeline Links | ||||
def set_timeline_links(self): | def set_timeline_links(self): | ||||
contacts = [] | contacts = [] | ||||
create_contact_enabled = self.email_account and frappe.db.get_value("Email Account", self.email_account, "create_contact") | |||||
contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc], auto_create_contact=create_contact_enabled) | |||||
create_contact_enabled = self.email_account and frappe.db.get_value( | |||||
"Email Account", self.email_account, "create_contact" | |||||
) | |||||
contacts = get_contacts( | |||||
[self.sender, self.recipients, self.cc, self.bcc], auto_create_contact=create_contact_enabled | |||||
) | |||||
for contact_name in contacts: | for contact_name in contacts: | ||||
self.add_link('Contact', contact_name) | |||||
self.add_link("Contact", contact_name) | |||||
#link contact's dynamic links to communication | |||||
# link contact's dynamic links to communication | |||||
add_contact_links_to_communication(self, contact_name) | add_contact_links_to_communication(self, contact_name) | ||||
def deduplicate_timeline_links(self): | def deduplicate_timeline_links(self): | ||||
@@ -332,17 +368,12 @@ class Communication(Document, CommunicationEmailMixin): | |||||
duplicate = True | duplicate = True | ||||
if duplicate: | if duplicate: | ||||
del self.timeline_links[:] # make it python 2 compatible as list.clear() is python 3 only | |||||
del self.timeline_links[:] # make it python 2 compatible as list.clear() is python 3 only | |||||
for l in links: | for l in links: | ||||
self.add_link(link_doctype=l[0], link_name=l[1]) | self.add_link(link_doctype=l[0], link_name=l[1]) | ||||
def add_link(self, link_doctype, link_name, autosave=False): | def add_link(self, link_doctype, link_name, autosave=False): | ||||
self.append("timeline_links", | |||||
{ | |||||
"link_doctype": link_doctype, | |||||
"link_name": link_name | |||||
} | |||||
) | |||||
self.append("timeline_links", {"link_doctype": link_doctype, "link_name": link_name}) | |||||
if autosave: | if autosave: | ||||
self.save(ignore_permissions=True) | self.save(ignore_permissions=True) | ||||
@@ -358,13 +389,15 @@ class Communication(Document, CommunicationEmailMixin): | |||||
if autosave: | if autosave: | ||||
self.save(ignore_permissions=ignore_permissions) | self.save(ignore_permissions=ignore_permissions) | ||||
def on_doctype_update(): | def on_doctype_update(): | ||||
"""Add indexes in `tabCommunication`""" | """Add indexes in `tabCommunication`""" | ||||
frappe.db.add_index("Communication", ["reference_doctype", "reference_name"]) | frappe.db.add_index("Communication", ["reference_doctype", "reference_name"]) | ||||
frappe.db.add_index("Communication", ["status", "communication_type"]) | frappe.db.add_index("Communication", ["status", "communication_type"]) | ||||
def has_permission(doc, ptype, user): | def has_permission(doc, ptype, user): | ||||
if ptype=="read": | |||||
if ptype == "read": | |||||
if doc.reference_doctype == "Communication" and doc.reference_name == doc.name: | if doc.reference_doctype == "Communication" and doc.reference_name == doc.name: | ||||
return | return | ||||
@@ -372,24 +405,28 @@ def has_permission(doc, ptype, user): | |||||
if frappe.has_permission(doc.reference_doctype, ptype="read", doc=doc.reference_name): | if frappe.has_permission(doc.reference_doctype, ptype="read", doc=doc.reference_name): | ||||
return True | return True | ||||
def get_permission_query_conditions_for_communication(user): | def get_permission_query_conditions_for_communication(user): | ||||
if not user: user = frappe.session.user | |||||
if not user: | |||||
user = frappe.session.user | |||||
roles = frappe.get_roles(user) | roles = frappe.get_roles(user) | ||||
if "Super Email User" in roles or "System Manager" in roles: | if "Super Email User" in roles or "System Manager" in roles: | ||||
return None | return None | ||||
else: | else: | ||||
accounts = frappe.get_all("User Email", filters={ "parent": user }, | |||||
fields=["email_account"], | |||||
distinct=True, order_by="idx") | |||||
accounts = frappe.get_all( | |||||
"User Email", filters={"parent": user}, fields=["email_account"], distinct=True, order_by="idx" | |||||
) | |||||
if not accounts: | if not accounts: | ||||
return """`tabCommunication`.communication_medium!='Email'""" | return """`tabCommunication`.communication_medium!='Email'""" | ||||
email_accounts = [ '"%s"'%account.get("email_account") for account in accounts ] | |||||
return """`tabCommunication`.email_account in ({email_accounts})"""\ | |||||
.format(email_accounts=','.join(email_accounts)) | |||||
email_accounts = ['"%s"' % account.get("email_account") for account in accounts] | |||||
return """`tabCommunication`.email_account in ({email_accounts})""".format( | |||||
email_accounts=",".join(email_accounts) | |||||
) | |||||
def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[str]: | def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[str]: | ||||
email_addrs = get_emails(email_strings) | email_addrs = get_emails(email_strings) | ||||
@@ -403,12 +440,12 @@ def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[st | |||||
first_name = frappe.unscrub(email_parts[0]) | first_name = frappe.unscrub(email_parts[0]) | ||||
try: | try: | ||||
contact_name = '{0}-{1}'.format(first_name, email_parts[1]) if first_name == 'Contact' else first_name | |||||
contact = frappe.get_doc({ | |||||
"doctype": "Contact", | |||||
"first_name": contact_name, | |||||
"name": contact_name | |||||
}) | |||||
contact_name = ( | |||||
"{0}-{1}".format(first_name, email_parts[1]) if first_name == "Contact" else first_name | |||||
) | |||||
contact = frappe.get_doc( | |||||
{"doctype": "Contact", "first_name": contact_name, "name": contact_name} | |||||
) | |||||
contact.add_email(email_id=email, is_primary=True) | contact.add_email(email_id=email, is_primary=True) | ||||
contact.insert(ignore_permissions=True) | contact.insert(ignore_permissions=True) | ||||
contact_name = contact.name | contact_name = contact.name | ||||
@@ -421,6 +458,7 @@ def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[st | |||||
return contacts | return contacts | ||||
def get_emails(email_strings: List[str]) -> List[str]: | def get_emails(email_strings: List[str]) -> List[str]: | ||||
email_addrs = [] | email_addrs = [] | ||||
@@ -432,22 +470,25 @@ def get_emails(email_strings: List[str]) -> List[str]: | |||||
return email_addrs | return email_addrs | ||||
def add_contact_links_to_communication(communication, contact_name): | def add_contact_links_to_communication(communication, contact_name): | ||||
contact_links = frappe.get_all("Dynamic Link", filters={ | |||||
"parenttype": "Contact", | |||||
"parent": contact_name | |||||
}, fields=["link_doctype", "link_name"]) | |||||
contact_links = frappe.get_all( | |||||
"Dynamic Link", | |||||
filters={"parenttype": "Contact", "parent": contact_name}, | |||||
fields=["link_doctype", "link_name"], | |||||
) | |||||
if contact_links: | if contact_links: | ||||
for contact_link in contact_links: | for contact_link in contact_links: | ||||
communication.add_link(contact_link.link_doctype, contact_link.link_name) | communication.add_link(contact_link.link_doctype, contact_link.link_name) | ||||
def parse_email(communication, email_strings): | def parse_email(communication, email_strings): | ||||
""" | """ | ||||
Parse email to add timeline links. | |||||
When automatic email linking is enabled, an email from email_strings can contain | |||||
a doctype and docname ie in the format `admin+doctype+docname@example.com`, | |||||
the email is parsed and doctype and docname is extracted and timeline link is added. | |||||
Parse email to add timeline links. | |||||
When automatic email linking is enabled, an email from email_strings can contain | |||||
a doctype and docname ie in the format `admin+doctype+docname@example.com`, | |||||
the email is parsed and doctype and docname is extracted and timeline link is added. | |||||
""" | """ | ||||
if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}): | if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}): | ||||
return | return | ||||
@@ -469,10 +510,11 @@ def parse_email(communication, email_strings): | |||||
if doctype and docname and frappe.db.exists(doctype, docname): | if doctype and docname and frappe.db.exists(doctype, docname): | ||||
communication.add_link(doctype, docname) | communication.add_link(doctype, docname) | ||||
def get_email_without_link(email): | def get_email_without_link(email): | ||||
""" | """ | ||||
returns email address without doctype links | |||||
returns admin@example.com for email admin+doctype+docname@example.com | |||||
returns email address without doctype links | |||||
returns admin@example.com for email admin+doctype+docname@example.com | |||||
""" | """ | ||||
if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}): | if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}): | ||||
return email | return email | ||||
@@ -486,6 +528,7 @@ def get_email_without_link(email): | |||||
return "{0}@{1}".format(email_id, email_host) | return "{0}@{1}".format(email_id, email_host) | ||||
def update_parent_document_on_communication(doc): | def update_parent_document_on_communication(doc): | ||||
"""Update mins_to_first_communication of parent document based on who is replying.""" | """Update mins_to_first_communication of parent document based on who is replying.""" | ||||
@@ -516,6 +559,7 @@ def update_parent_document_on_communication(doc): | |||||
parent.run_method("notify_communication", doc) | parent.run_method("notify_communication", doc) | ||||
parent.notify_update() | parent.notify_update() | ||||
def update_first_response_time(parent, communication): | def update_first_response_time(parent, communication): | ||||
if parent.meta.has_field("first_response_time") and not parent.get("first_response_time"): | if parent.meta.has_field("first_response_time") and not parent.get("first_response_time"): | ||||
if is_system_user(communication.sender): | if is_system_user(communication.sender): | ||||
@@ -526,25 +570,29 @@ def update_first_response_time(parent, communication): | |||||
first_response_time = round(time_diff_in_seconds(first_responded_on, parent.creation), 2) | first_response_time = round(time_diff_in_seconds(first_responded_on, parent.creation), 2) | ||||
parent.db_set("first_response_time", first_response_time) | parent.db_set("first_response_time", first_response_time) | ||||
def set_avg_response_time(parent, communication): | def set_avg_response_time(parent, communication): | ||||
if parent.meta.has_field("avg_response_time") and communication.sent_or_received == "Sent": | if parent.meta.has_field("avg_response_time") and communication.sent_or_received == "Sent": | ||||
# avg response time for all the responses | # avg response time for all the responses | ||||
communications = frappe.get_list("Communication", filters={ | |||||
"reference_doctype": parent.doctype, | |||||
"reference_name": parent.name | |||||
}, | |||||
communications = frappe.get_list( | |||||
"Communication", | |||||
filters={"reference_doctype": parent.doctype, "reference_name": parent.name}, | |||||
fields=["sent_or_received", "name", "creation"], | fields=["sent_or_received", "name", "creation"], | ||||
order_by="creation" | |||||
order_by="creation", | |||||
) | ) | ||||
if len(communications): | if len(communications): | ||||
response_times = [] | response_times = [] | ||||
for i in range(len(communications)): | for i in range(len(communications)): | ||||
if communications[i].sent_or_received == "Sent" and communications[i-1].sent_or_received == "Received": | |||||
response_time = round(time_diff_in_seconds(communications[i].creation, communications[i-1].creation), 2) | |||||
if ( | |||||
communications[i].sent_or_received == "Sent" | |||||
and communications[i - 1].sent_or_received == "Received" | |||||
): | |||||
response_time = round( | |||||
time_diff_in_seconds(communications[i].creation, communications[i - 1].creation), 2 | |||||
) | |||||
if response_time > 0: | if response_time > 0: | ||||
response_times.append(response_time) | response_times.append(response_time) | ||||
if response_times: | if response_times: | ||||
avg_response_time = sum(response_times) / len(response_times) | avg_response_time = sum(response_times) / len(response_times) | ||||
parent.db_set("avg_response_time", avg_response_time) | parent.db_set("avg_response_time", avg_response_time) | ||||
@@ -8,17 +8,25 @@ import frappe | |||||
import frappe.email.smtp | import frappe.email.smtp | ||||
from frappe import _ | from frappe import _ | ||||
from frappe.email.email_body import get_message_id | from frappe.email.email_body import get_message_id | ||||
from frappe.utils import (cint, get_datetime, get_formatted_email, | |||||
list_to_str, split_emails, validate_email_address) | |||||
from frappe.utils import ( | |||||
cint, | |||||
get_datetime, | |||||
get_formatted_email, | |||||
list_to_str, | |||||
split_emails, | |||||
validate_email_address, | |||||
) | |||||
if TYPE_CHECKING: | if TYPE_CHECKING: | ||||
from frappe.core.doctype.communication.communication import Communication | from frappe.core.doctype.communication.communication import Communication | ||||
OUTGOING_EMAIL_ACCOUNT_MISSING = _(""" | |||||
OUTGOING_EMAIL_ACCOUNT_MISSING = _( | |||||
""" | |||||
Unable to send mail because of a missing email account. | Unable to send mail because of a missing email account. | ||||
Please setup default Email Account from Setup > Email > Email Account | Please setup default Email Account from Setup > Email > Email Account | ||||
""") | |||||
""" | |||||
) | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
@@ -64,16 +72,15 @@ def make( | |||||
""" | """ | ||||
if kwargs: | if kwargs: | ||||
from frappe.utils.commands import warn | from frappe.utils.commands import warn | ||||
warn( | warn( | ||||
f"Options {kwargs} used in frappe.core.doctype.communication.email.make " | f"Options {kwargs} used in frappe.core.doctype.communication.email.make " | ||||
"are deprecated or unsupported", | "are deprecated or unsupported", | ||||
category=DeprecationWarning | |||||
category=DeprecationWarning, | |||||
) | ) | ||||
if doctype and name and not frappe.has_permission(doctype=doctype, ptype="email", doc=name): | if doctype and name and not frappe.has_permission(doctype=doctype, ptype="email", doc=name): | ||||
raise frappe.PermissionError( | |||||
f"You are not allowed to send emails related to: {doctype} {name}" | |||||
) | |||||
raise frappe.PermissionError(f"You are not allowed to send emails related to: {doctype} {name}") | |||||
return _make( | return _make( | ||||
doctype=doctype, | doctype=doctype, | ||||
@@ -123,33 +130,34 @@ def _make( | |||||
communication_type=None, | communication_type=None, | ||||
add_signature=True, | add_signature=True, | ||||
) -> Dict[str, str]: | ) -> Dict[str, str]: | ||||
"""Internal method to make a new communication that ignores Permission checks. | |||||
""" | |||||
"""Internal method to make a new communication that ignores Permission checks.""" | |||||
sender = sender or get_formatted_email(frappe.session.user) | sender = sender or get_formatted_email(frappe.session.user) | ||||
recipients = list_to_str(recipients) if isinstance(recipients, list) else recipients | recipients = list_to_str(recipients) if isinstance(recipients, list) else recipients | ||||
cc = list_to_str(cc) if isinstance(cc, list) else cc | cc = list_to_str(cc) if isinstance(cc, list) else cc | ||||
bcc = list_to_str(bcc) if isinstance(bcc, list) else bcc | bcc = list_to_str(bcc) if isinstance(bcc, list) else bcc | ||||
comm: "Communication" = frappe.get_doc({ | |||||
"doctype":"Communication", | |||||
"subject": subject, | |||||
"content": content, | |||||
"sender": sender, | |||||
"sender_full_name":sender_full_name, | |||||
"recipients": recipients, | |||||
"cc": cc or None, | |||||
"bcc": bcc or None, | |||||
"communication_medium": communication_medium, | |||||
"sent_or_received": sent_or_received, | |||||
"reference_doctype": doctype, | |||||
"reference_name": name, | |||||
"email_template": email_template, | |||||
"message_id":get_message_id().strip(" <>"), | |||||
"read_receipt":read_receipt, | |||||
"has_attachment": 1 if attachments else 0, | |||||
"communication_type": communication_type, | |||||
}) | |||||
comm: "Communication" = frappe.get_doc( | |||||
{ | |||||
"doctype": "Communication", | |||||
"subject": subject, | |||||
"content": content, | |||||
"sender": sender, | |||||
"sender_full_name": sender_full_name, | |||||
"recipients": recipients, | |||||
"cc": cc or None, | |||||
"bcc": bcc or None, | |||||
"communication_medium": communication_medium, | |||||
"sent_or_received": sent_or_received, | |||||
"reference_doctype": doctype, | |||||
"reference_name": name, | |||||
"email_template": email_template, | |||||
"message_id": get_message_id().strip(" <>"), | |||||
"read_receipt": read_receipt, | |||||
"has_attachment": 1 if attachments else 0, | |||||
"communication_type": communication_type, | |||||
} | |||||
) | |||||
comm.flags.skip_add_signature = not add_signature | comm.flags.skip_add_signature = not add_signature | ||||
comm.insert(ignore_permissions=True) | comm.insert(ignore_permissions=True) | ||||
@@ -161,9 +169,7 @@ def _make( | |||||
if cint(send_email): | if cint(send_email): | ||||
if not comm.get_outgoing_email_account(): | if not comm.get_outgoing_email_account(): | ||||
frappe.throw( | |||||
msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError | |||||
) | |||||
frappe.throw(msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError) | |||||
comm.send_email( | comm.send_email( | ||||
print_html=print_html, | print_html=print_html, | ||||
@@ -179,7 +185,10 @@ def _make( | |||||
def validate_email(doc: "Communication") -> None: | def validate_email(doc: "Communication") -> None: | ||||
"""Validate Email Addresses of Recipients and CC""" | """Validate Email Addresses of Recipients and CC""" | ||||
if not (doc.communication_type=="Communication" and doc.communication_medium == "Email") or doc.flags.in_receive: | |||||
if ( | |||||
not (doc.communication_type == "Communication" and doc.communication_medium == "Email") | |||||
or doc.flags.in_receive | |||||
): | |||||
return | return | ||||
# validate recipients | # validate recipients | ||||
@@ -193,36 +202,45 @@ def validate_email(doc: "Communication") -> None: | |||||
for email in split_emails(doc.bcc): | for email in split_emails(doc.bcc): | ||||
validate_email_address(email, throw=True) | validate_email_address(email, throw=True) | ||||
def set_incoming_outgoing_accounts(doc): | def set_incoming_outgoing_accounts(doc): | ||||
from frappe.email.doctype.email_account.email_account import EmailAccount | from frappe.email.doctype.email_account.email_account import EmailAccount | ||||
incoming_email_account = EmailAccount.find_incoming( | incoming_email_account = EmailAccount.find_incoming( | ||||
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype) | |||||
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype | |||||
) | |||||
doc.incoming_email_account = incoming_email_account.email_id if incoming_email_account else None | doc.incoming_email_account = incoming_email_account.email_id if incoming_email_account else None | ||||
doc.outgoing_email_account = EmailAccount.find_outgoing( | doc.outgoing_email_account = EmailAccount.find_outgoing( | ||||
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype) | |||||
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype | |||||
) | |||||
if doc.sent_or_received == "Sent": | if doc.sent_or_received == "Sent": | ||||
doc.db_set("email_account", doc.outgoing_email_account.name) | doc.db_set("email_account", doc.outgoing_email_account.name) | ||||
def add_attachments(name, attachments): | def add_attachments(name, attachments): | ||||
'''Add attachments to the given Communication''' | |||||
"""Add attachments to the given Communication""" | |||||
# loop through attachments | # loop through attachments | ||||
for a in attachments: | for a in attachments: | ||||
if isinstance(a, str): | if isinstance(a, str): | ||||
attach = frappe.db.get_value("File", {"name":a}, | |||||
["file_name", "file_url", "is_private"], as_dict=1) | |||||
attach = frappe.db.get_value( | |||||
"File", {"name": a}, ["file_name", "file_url", "is_private"], as_dict=1 | |||||
) | |||||
# save attachments to new doc | # save attachments to new doc | ||||
_file = frappe.get_doc({ | |||||
"doctype": "File", | |||||
"file_url": attach.file_url, | |||||
"attached_to_doctype": "Communication", | |||||
"attached_to_name": name, | |||||
"folder": "Home/Attachments", | |||||
"is_private": attach.is_private | |||||
}) | |||||
_file = frappe.get_doc( | |||||
{ | |||||
"doctype": "File", | |||||
"file_url": attach.file_url, | |||||
"attached_to_doctype": "Communication", | |||||
"attached_to_name": name, | |||||
"folder": "Home/Attachments", | |||||
"is_private": attach.is_private, | |||||
} | |||||
) | |||||
_file.save(ignore_permissions=True) | _file.save(ignore_permissions=True) | ||||
@frappe.whitelist(allow_guest=True, methods=("GET",)) | @frappe.whitelist(allow_guest=True, methods=("GET",)) | ||||
def mark_email_as_seen(name: str = None): | def mark_email_as_seen(name: str = None): | ||||
try: | try: | ||||
@@ -233,33 +251,31 @@ def mark_email_as_seen(name: str = None): | |||||
frappe.log_error(frappe.get_traceback()) | frappe.log_error(frappe.get_traceback()) | ||||
finally: | finally: | ||||
frappe.response.update({ | |||||
"type": "binary", | |||||
"filename": "imaginary_pixel.png", | |||||
"filecontent": ( | |||||
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00" | |||||
b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\r" | |||||
b"IDATx\x9cc\xf8\xff\xff?\x03\x00\x08\xfc\x02\xfe\xa7\x9a\xa0" | |||||
b"\xa0\x00\x00\x00\x00IEND\xaeB`\x82" | |||||
) | |||||
}) | |||||
frappe.response.update( | |||||
{ | |||||
"type": "binary", | |||||
"filename": "imaginary_pixel.png", | |||||
"filecontent": ( | |||||
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00" | |||||
b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\r" | |||||
b"IDATx\x9cc\xf8\xff\xff?\x03\x00\x08\xfc\x02\xfe\xa7\x9a\xa0" | |||||
b"\xa0\x00\x00\x00\x00IEND\xaeB`\x82" | |||||
), | |||||
} | |||||
) | |||||
def update_communication_as_read(name): | def update_communication_as_read(name): | ||||
if not name or not isinstance(name, str): | if not name or not isinstance(name, str): | ||||
return | return | ||||
communication = frappe.db.get_value( | |||||
"Communication", | |||||
name, | |||||
"read_by_recipient", | |||||
as_dict=True | |||||
) | |||||
communication = frappe.db.get_value("Communication", name, "read_by_recipient", as_dict=True) | |||||
if not communication or communication.read_by_recipient: | if not communication or communication.read_by_recipient: | ||||
return | return | ||||
frappe.db.set_value("Communication", name, { | |||||
"read_by_recipient": 1, | |||||
"delivery_status": "Read", | |||||
"read_by_recipient_on": get_datetime() | |||||
}) | |||||
frappe.db.set_value( | |||||
"Communication", | |||||
name, | |||||
{"read_by_recipient": 1, "delivery_status": "Read", "read_by_recipient_on": get_datetime()}, | |||||
) |
@@ -1,33 +1,34 @@ | |||||
from typing import List | from typing import List | ||||
import frappe | import frappe | ||||
from frappe import _ | from frappe import _ | ||||
from frappe.core.utils import get_parent_doc | from frappe.core.utils import get_parent_doc | ||||
from frappe.utils import parse_addr, get_formatted_email, get_url | |||||
from frappe.email.doctype.email_account.email_account import EmailAccount | |||||
from frappe.desk.doctype.todo.todo import ToDo | from frappe.desk.doctype.todo.todo import ToDo | ||||
from frappe.email.doctype.email_account.email_account import EmailAccount | |||||
from frappe.utils import get_formatted_email, get_url, parse_addr | |||||
class CommunicationEmailMixin: | class CommunicationEmailMixin: | ||||
"""Mixin class to handle communication mails. | |||||
""" | |||||
"""Mixin class to handle communication mails.""" | |||||
def is_email_communication(self): | def is_email_communication(self): | ||||
return self.communication_type=="Communication" and self.communication_medium == "Email" | |||||
return self.communication_type == "Communication" and self.communication_medium == "Email" | |||||
def get_owner(self): | def get_owner(self): | ||||
"""Get owner of the communication docs parent. | |||||
""" | |||||
"""Get owner of the communication docs parent.""" | |||||
parent_doc = get_parent_doc(self) | parent_doc = get_parent_doc(self) | ||||
return parent_doc.owner if parent_doc else None | return parent_doc.owner if parent_doc else None | ||||
def get_all_email_addresses(self, exclude_displayname=False): | def get_all_email_addresses(self, exclude_displayname=False): | ||||
"""Get all Email addresses mentioned in the doc along with display name. | |||||
""" | |||||
return self.to_list(exclude_displayname=exclude_displayname) + \ | |||||
self.cc_list(exclude_displayname=exclude_displayname) + \ | |||||
self.bcc_list(exclude_displayname=exclude_displayname) | |||||
"""Get all Email addresses mentioned in the doc along with display name.""" | |||||
return ( | |||||
self.to_list(exclude_displayname=exclude_displayname) | |||||
+ self.cc_list(exclude_displayname=exclude_displayname) | |||||
+ self.bcc_list(exclude_displayname=exclude_displayname) | |||||
) | |||||
def get_email_with_displayname(self, email_address): | def get_email_with_displayname(self, email_address): | ||||
"""Returns email address after adding displayname. | |||||
""" | |||||
"""Returns email address after adding displayname.""" | |||||
display_name, email = parse_addr(email_address) | display_name, email = parse_addr(email_address) | ||||
if display_name and display_name != email: | if display_name and display_name != email: | ||||
return email_address | return email_address | ||||
@@ -37,26 +38,24 @@ class CommunicationEmailMixin: | |||||
return email_map.get(email, email) | return email_map.get(email, email) | ||||
def mail_recipients(self, is_inbound_mail_communcation=False): | def mail_recipients(self, is_inbound_mail_communcation=False): | ||||
"""Build to(recipient) list to send an email. | |||||
""" | |||||
"""Build to(recipient) list to send an email.""" | |||||
# Incase of inbound mail, recipients already received the mail, no need to send again. | # Incase of inbound mail, recipients already received the mail, no need to send again. | ||||
if is_inbound_mail_communcation: | if is_inbound_mail_communcation: | ||||
return [] | return [] | ||||
if hasattr(self, '_final_recipients'): | |||||
if hasattr(self, "_final_recipients"): | |||||
return self._final_recipients | return self._final_recipients | ||||
to = self.to_list() | to = self.to_list() | ||||
self._final_recipients = list(filter(lambda id: id != 'Administrator', to)) | |||||
self._final_recipients = list(filter(lambda id: id != "Administrator", to)) | |||||
return self._final_recipients | return self._final_recipients | ||||
def get_mail_recipients_with_displayname(self, is_inbound_mail_communcation=False): | def get_mail_recipients_with_displayname(self, is_inbound_mail_communcation=False): | ||||
"""Build to(recipient) list to send an email including displayname in email. | |||||
""" | |||||
"""Build to(recipient) list to send an email including displayname in email.""" | |||||
to_list = self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation) | to_list = self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation) | ||||
return [self.get_email_with_displayname(email) for email in to_list] | return [self.get_email_with_displayname(email) for email in to_list] | ||||
def mail_cc(self, is_inbound_mail_communcation=False, include_sender = False): | |||||
def mail_cc(self, is_inbound_mail_communcation=False, include_sender=False): | |||||
"""Build cc list to send an email. | """Build cc list to send an email. | ||||
* if email copy is requested by sender, then add sender to CC. | * if email copy is requested by sender, then add sender to CC. | ||||
@@ -67,7 +66,7 @@ class CommunicationEmailMixin: | |||||
* FixMe: Removed adding TODO owners to cc list. Check if that is needed. | * FixMe: Removed adding TODO owners to cc list. Check if that is needed. | ||||
""" | """ | ||||
if hasattr(self, '_final_cc'): | |||||
if hasattr(self, "_final_cc"): | |||||
return self._final_cc | return self._final_cc | ||||
cc = self.cc_list() | cc = self.cc_list() | ||||
@@ -88,11 +87,13 @@ class CommunicationEmailMixin: | |||||
if is_inbound_mail_communcation: | if is_inbound_mail_communcation: | ||||
cc = cc - set(self.cc_list() + self.to_list()) | cc = cc - set(self.cc_list() + self.to_list()) | ||||
self._final_cc = list(filter(lambda id: id != 'Administrator', cc)) | |||||
self._final_cc = list(filter(lambda id: id != "Administrator", cc)) | |||||
return self._final_cc | return self._final_cc | ||||
def get_mail_cc_with_displayname(self, is_inbound_mail_communcation=False, include_sender = False): | |||||
cc_list = self.mail_cc(is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender = include_sender) | |||||
def get_mail_cc_with_displayname(self, is_inbound_mail_communcation=False, include_sender=False): | |||||
cc_list = self.mail_cc( | |||||
is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=include_sender | |||||
) | |||||
return [self.get_email_with_displayname(email) for email in cc_list] | return [self.get_email_with_displayname(email) for email in cc_list] | ||||
def mail_bcc(self, is_inbound_mail_communcation=False): | def mail_bcc(self, is_inbound_mail_communcation=False): | ||||
@@ -102,7 +103,7 @@ class CommunicationEmailMixin: | |||||
* User must be enabled in the system | * User must be enabled in the system | ||||
* remove_administrator_from_email_list | * remove_administrator_from_email_list | ||||
""" | """ | ||||
if hasattr(self, '_final_bcc'): | |||||
if hasattr(self, "_final_bcc"): | |||||
return self._final_bcc | return self._final_bcc | ||||
bcc = set(self.bcc_list()) | bcc = set(self.bcc_list()) | ||||
@@ -116,7 +117,7 @@ class CommunicationEmailMixin: | |||||
if is_inbound_mail_communcation: | if is_inbound_mail_communcation: | ||||
bcc = bcc - set(self.bcc_list() + self.to_list()) | bcc = bcc - set(self.bcc_list() + self.to_list()) | ||||
self._final_bcc = list(filter(lambda id: id != 'Administrator', bcc)) | |||||
self._final_bcc = list(filter(lambda id: id != "Administrator", bcc)) | |||||
return self._final_bcc | return self._final_bcc | ||||
def get_mail_bcc_with_displayname(self, is_inbound_mail_communcation=False): | def get_mail_bcc_with_displayname(self, is_inbound_mail_communcation=False): | ||||
@@ -145,22 +146,23 @@ class CommunicationEmailMixin: | |||||
def get_attach_link(self, print_format): | def get_attach_link(self, print_format): | ||||
"""Returns public link for the attachment via `templates/emails/print_link.html`.""" | """Returns public link for the attachment via `templates/emails/print_link.html`.""" | ||||
return frappe.get_template("templates/emails/print_link.html").render({ | |||||
"url": get_url(), | |||||
"doctype": self.reference_doctype, | |||||
"name": self.reference_name, | |||||
"print_format": print_format, | |||||
"key": get_parent_doc(self).get_signature() | |||||
}) | |||||
return frappe.get_template("templates/emails/print_link.html").render( | |||||
{ | |||||
"url": get_url(), | |||||
"doctype": self.reference_doctype, | |||||
"name": self.reference_name, | |||||
"print_format": print_format, | |||||
"key": get_parent_doc(self).get_signature(), | |||||
} | |||||
) | |||||
def get_outgoing_email_account(self): | def get_outgoing_email_account(self): | ||||
if not hasattr(self, '_outgoing_email_account'): | |||||
if not hasattr(self, "_outgoing_email_account"): | |||||
if self.email_account: | if self.email_account: | ||||
self._outgoing_email_account = EmailAccount.find(self.email_account) | self._outgoing_email_account = EmailAccount.find(self.email_account) | ||||
else: | else: | ||||
self._outgoing_email_account = EmailAccount.find_outgoing( | self._outgoing_email_account = EmailAccount.find_outgoing( | ||||
match_by_email=self.sender_mailid, | |||||
match_by_doctype=self.reference_doctype | |||||
match_by_email=self.sender_mailid, match_by_doctype=self.reference_doctype | |||||
) | ) | ||||
if self.sent_or_received == "Sent" and self._outgoing_email_account: | if self.sent_or_received == "Sent" and self._outgoing_email_account: | ||||
@@ -169,10 +171,9 @@ class CommunicationEmailMixin: | |||||
return self._outgoing_email_account | return self._outgoing_email_account | ||||
def get_incoming_email_account(self): | def get_incoming_email_account(self): | ||||
if not hasattr(self, '_incoming_email_account'): | |||||
if not hasattr(self, "_incoming_email_account"): | |||||
self._incoming_email_account = EmailAccount.find_incoming( | self._incoming_email_account = EmailAccount.find_incoming( | ||||
match_by_email=self.sender_mailid, | |||||
match_by_doctype=self.reference_doctype | |||||
match_by_email=self.sender_mailid, match_by_doctype=self.reference_doctype | |||||
) | ) | ||||
return self._incoming_email_account | return self._incoming_email_account | ||||
@@ -180,12 +181,17 @@ class CommunicationEmailMixin: | |||||
final_attachments = [] | final_attachments = [] | ||||
if print_format or print_html: | if print_format or print_html: | ||||
d = {'print_format': print_format, 'html': print_html, 'print_format_attachment': 1, | |||||
'doctype': self.reference_doctype, 'name': self.reference_name} | |||||
d = { | |||||
"print_format": print_format, | |||||
"html": print_html, | |||||
"print_format_attachment": 1, | |||||
"doctype": self.reference_doctype, | |||||
"name": self.reference_name, | |||||
} | |||||
final_attachments.append(d) | final_attachments.append(d) | ||||
for a in self.get_attachments() or []: | for a in self.get_attachments() or []: | ||||
final_attachments.append({"fid": a['name']}) | |||||
final_attachments.append({"fid": a["name"]}) | |||||
return final_attachments | return final_attachments | ||||
@@ -193,48 +199,57 @@ class CommunicationEmailMixin: | |||||
email_account = self.get_outgoing_email_account() | email_account = self.get_outgoing_email_account() | ||||
if email_account and email_account.send_unsubscribe_message: | if email_account and email_account.send_unsubscribe_message: | ||||
return _("Leave this conversation") | return _("Leave this conversation") | ||||
return '' | |||||
return "" | |||||
def exclude_emails_list(self, is_inbound_mail_communcation=False, include_sender=False) -> List: | def exclude_emails_list(self, is_inbound_mail_communcation=False, include_sender=False) -> List: | ||||
"""List of mail id's excluded while sending mail. | |||||
""" | |||||
"""List of mail id's excluded while sending mail.""" | |||||
all_ids = self.get_all_email_addresses(exclude_displayname=True) | all_ids = self.get_all_email_addresses(exclude_displayname=True) | ||||
final_ids = ( | final_ids = ( | ||||
self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation) | self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation) | ||||
+ self.mail_bcc(is_inbound_mail_communcation=is_inbound_mail_communcation) | + self.mail_bcc(is_inbound_mail_communcation=is_inbound_mail_communcation) | ||||
+ self.mail_cc(is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=include_sender) | |||||
+ self.mail_cc( | |||||
is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=include_sender | |||||
) | |||||
) | ) | ||||
return list(set(all_ids) - set(final_ids)) | return list(set(all_ids) - set(final_ids)) | ||||
def get_assignees(self): | def get_assignees(self): | ||||
"""Get owners of the reference document. | |||||
""" | |||||
filters = {'status': 'Open', 'reference_name': self.reference_name, | |||||
'reference_type': self.reference_doctype} | |||||
"""Get owners of the reference document.""" | |||||
filters = { | |||||
"status": "Open", | |||||
"reference_name": self.reference_name, | |||||
"reference_type": self.reference_doctype, | |||||
} | |||||
return ToDo.get_owners(filters) | return ToDo.get_owners(filters) | ||||
@staticmethod | @staticmethod | ||||
def filter_thread_notification_disbled_users(emails): | def filter_thread_notification_disbled_users(emails): | ||||
"""Filter users based on notifications for email threads setting is disabled. | |||||
""" | |||||
"""Filter users based on notifications for email threads setting is disabled.""" | |||||
if not emails: | if not emails: | ||||
return [] | return [] | ||||
return frappe.get_all("User", pluck="email", filters={"email": ["in", emails], "thread_notify": 0}) | |||||
return frappe.get_all( | |||||
"User", pluck="email", filters={"email": ["in", emails], "thread_notify": 0} | |||||
) | |||||
@staticmethod | @staticmethod | ||||
def filter_disabled_users(emails): | def filter_disabled_users(emails): | ||||
""" | |||||
""" | |||||
""" """ | |||||
if not emails: | if not emails: | ||||
return [] | return [] | ||||
return frappe.get_all("User", pluck="email", filters={"email": ["in", emails], "enabled": 0}) | return frappe.get_all("User", pluck="email", filters={"email": ["in", emails], "enabled": 0}) | ||||
def sendmail_input_dict(self, print_html=None, print_format=None, | |||||
send_me_a_copy=None, print_letterhead=None, is_inbound_mail_communcation=None): | |||||
def sendmail_input_dict( | |||||
self, | |||||
print_html=None, | |||||
print_format=None, | |||||
send_me_a_copy=None, | |||||
print_letterhead=None, | |||||
is_inbound_mail_communcation=None, | |||||
): | |||||
outgoing_email_account = self.get_outgoing_email_account() | outgoing_email_account = self.get_outgoing_email_account() | ||||
if not outgoing_email_account: | if not outgoing_email_account: | ||||
@@ -244,8 +259,7 @@ class CommunicationEmailMixin: | |||||
is_inbound_mail_communcation=is_inbound_mail_communcation | is_inbound_mail_communcation=is_inbound_mail_communcation | ||||
) | ) | ||||
cc = self.get_mail_cc_with_displayname( | cc = self.get_mail_cc_with_displayname( | ||||
is_inbound_mail_communcation=is_inbound_mail_communcation, | |||||
include_sender = send_me_a_copy | |||||
is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=send_me_a_copy | |||||
) | ) | ||||
bcc = self.get_mail_bcc_with_displayname( | bcc = self.get_mail_bcc_with_displayname( | ||||
is_inbound_mail_communcation=is_inbound_mail_communcation | is_inbound_mail_communcation=is_inbound_mail_communcation | ||||
@@ -273,18 +287,24 @@ class CommunicationEmailMixin: | |||||
"delayed": True, | "delayed": True, | ||||
"communication": self.name, | "communication": self.name, | ||||
"read_receipt": self.read_receipt, | "read_receipt": self.read_receipt, | ||||
"is_notification": (self.sent_or_received =="Received" and True) or False, | |||||
"print_letterhead": print_letterhead | |||||
"is_notification": (self.sent_or_received == "Received" and True) or False, | |||||
"print_letterhead": print_letterhead, | |||||
} | } | ||||
def send_email(self, print_html=None, print_format=None, | |||||
send_me_a_copy=None, print_letterhead=None, is_inbound_mail_communcation=None): | |||||
def send_email( | |||||
self, | |||||
print_html=None, | |||||
print_format=None, | |||||
send_me_a_copy=None, | |||||
print_letterhead=None, | |||||
is_inbound_mail_communcation=None, | |||||
): | |||||
input_dict = self.sendmail_input_dict( | input_dict = self.sendmail_input_dict( | ||||
print_html=print_html, | print_html=print_html, | ||||
print_format=print_format, | print_format=print_format, | ||||
send_me_a_copy=send_me_a_copy, | send_me_a_copy=send_me_a_copy, | ||||
print_letterhead=print_letterhead, | print_letterhead=print_letterhead, | ||||
is_inbound_mail_communcation=is_inbound_mail_communcation | |||||
is_inbound_mail_communcation=is_inbound_mail_communcation, | |||||
) | ) | ||||
if input_dict: | if input_dict: | ||||
@@ -7,20 +7,30 @@ import frappe | |||||
from frappe.core.doctype.communication.communication import get_emails | from frappe.core.doctype.communication.communication import get_emails | ||||
from frappe.email.doctype.email_queue.email_queue import EmailQueue | from frappe.email.doctype.email_queue.email_queue import EmailQueue | ||||
test_records = frappe.get_test_records('Communication') | |||||
test_records = frappe.get_test_records("Communication") | |||||
class TestCommunication(unittest.TestCase): | |||||
class TestCommunication(unittest.TestCase): | |||||
def test_email(self): | def test_email(self): | ||||
valid_email_list = ["Full Name <full@example.com>", | |||||
'"Full Name with quotes and <weird@chars.com>" <weird@example.com>', | |||||
"Surname, Name <name.surname@domain.com>", | |||||
"Purchase@ABC <purchase@abc.com>", "xyz@abc2.com <xyz@abc.com>", | |||||
"Name [something else] <name@domain.com>"] | |||||
invalid_email_list = ["[invalid!email]", "invalid-email", | |||||
"tes2", "e", "rrrrrrrr", "manas","[[[sample]]]", | |||||
"[invalid!email].com"] | |||||
valid_email_list = [ | |||||
"Full Name <full@example.com>", | |||||
'"Full Name with quotes and <weird@chars.com>" <weird@example.com>', | |||||
"Surname, Name <name.surname@domain.com>", | |||||
"Purchase@ABC <purchase@abc.com>", | |||||
"xyz@abc2.com <xyz@abc.com>", | |||||
"Name [something else] <name@domain.com>", | |||||
] | |||||
invalid_email_list = [ | |||||
"[invalid!email]", | |||||
"invalid-email", | |||||
"tes2", | |||||
"e", | |||||
"rrrrrrrr", | |||||
"manas", | |||||
"[[[sample]]]", | |||||
"[invalid!email].com", | |||||
] | |||||
for x in valid_email_list: | for x in valid_email_list: | ||||
self.assertTrue(frappe.utils.parse_addr(x)[1]) | self.assertTrue(frappe.utils.parse_addr(x)[1]) | ||||
@@ -29,15 +39,25 @@ class TestCommunication(unittest.TestCase): | |||||
self.assertFalse(frappe.utils.parse_addr(x)[0]) | self.assertFalse(frappe.utils.parse_addr(x)[0]) | ||||
def test_name(self): | def test_name(self): | ||||
valid_email_list = ["Full Name <full@example.com>", | |||||
'"Full Name with quotes and <weird@chars.com>" <weird@example.com>', | |||||
"Surname, Name <name.surname@domain.com>", | |||||
"Purchase@ABC <purchase@abc.com>", "xyz@abc2.com <xyz@abc.com>", | |||||
"Name [something else] <name@domain.com>"] | |||||
invalid_email_list = ["[invalid!email]", "invalid-email", | |||||
"tes2", "e", "rrrrrrrr", "manas","[[[sample]]]", | |||||
"[invalid!email].com"] | |||||
valid_email_list = [ | |||||
"Full Name <full@example.com>", | |||||
'"Full Name with quotes and <weird@chars.com>" <weird@example.com>', | |||||
"Surname, Name <name.surname@domain.com>", | |||||
"Purchase@ABC <purchase@abc.com>", | |||||
"xyz@abc2.com <xyz@abc.com>", | |||||
"Name [something else] <name@domain.com>", | |||||
] | |||||
invalid_email_list = [ | |||||
"[invalid!email]", | |||||
"invalid-email", | |||||
"tes2", | |||||
"e", | |||||
"rrrrrrrr", | |||||
"manas", | |||||
"[[[sample]]]", | |||||
"[invalid!email].com", | |||||
] | |||||
for x in valid_email_list: | for x in valid_email_list: | ||||
self.assertTrue(frappe.utils.parse_addr(x)[0]) | self.assertTrue(frappe.utils.parse_addr(x)[0]) | ||||
@@ -46,27 +66,33 @@ class TestCommunication(unittest.TestCase): | |||||
self.assertFalse(frappe.utils.parse_addr(x)[0]) | self.assertFalse(frappe.utils.parse_addr(x)[0]) | ||||
def test_circular_linking(self): | def test_circular_linking(self): | ||||
a = frappe.get_doc({ | |||||
"doctype": "Communication", | |||||
"communication_type": "Communication", | |||||
"content": "This was created to test circular linking: Communication A", | |||||
}).insert(ignore_permissions=True) | |||||
b = frappe.get_doc({ | |||||
"doctype": "Communication", | |||||
"communication_type": "Communication", | |||||
"content": "This was created to test circular linking: Communication B", | |||||
"reference_doctype": "Communication", | |||||
"reference_name": a.name | |||||
}).insert(ignore_permissions=True) | |||||
c = frappe.get_doc({ | |||||
"doctype": "Communication", | |||||
"communication_type": "Communication", | |||||
"content": "This was created to test circular linking: Communication C", | |||||
"reference_doctype": "Communication", | |||||
"reference_name": b.name | |||||
}).insert(ignore_permissions=True) | |||||
a = frappe.get_doc( | |||||
{ | |||||
"doctype": "Communication", | |||||
"communication_type": "Communication", | |||||
"content": "This was created to test circular linking: Communication A", | |||||
} | |||||
).insert(ignore_permissions=True) | |||||
b = frappe.get_doc( | |||||
{ | |||||
"doctype": "Communication", | |||||
"communication_type": "Communication", | |||||
"content": "This was created to test circular linking: Communication B", | |||||
"reference_doctype": "Communication", | |||||
"reference_name": a.name, | |||||
} | |||||
).insert(ignore_permissions=True) | |||||
c = frappe.get_doc( | |||||
{ | |||||
"doctype": "Communication", | |||||
"communication_type": "Communication", | |||||
"content": "This was created to test circular linking: Communication C", | |||||
"reference_doctype": "Communication", | |||||
"reference_name": b.name, | |||||
} | |||||
).insert(ignore_permissions=True) | |||||
a = frappe.get_doc("Communication", a.name) | a = frappe.get_doc("Communication", a.name) | ||||
a.reference_doctype = "Communication" | a.reference_doctype = "Communication" | ||||
@@ -77,20 +103,24 @@ class TestCommunication(unittest.TestCase): | |||||
def test_deduplication_timeline_links(self): | def test_deduplication_timeline_links(self): | ||||
frappe.delete_doc_if_exists("Note", "deduplication timeline links") | frappe.delete_doc_if_exists("Note", "deduplication timeline links") | ||||
note = frappe.get_doc({ | |||||
"doctype": "Note", | |||||
"title": "deduplication timeline links", | |||||
"content": "deduplication timeline links" | |||||
}).insert(ignore_permissions=True) | |||||
comm = frappe.get_doc({ | |||||
"doctype": "Communication", | |||||
"communication_type": "Communication", | |||||
"content": "Deduplication of Links", | |||||
"communication_medium": "Email" | |||||
}).insert(ignore_permissions=True) | |||||
#adding same link twice | |||||
note = frappe.get_doc( | |||||
{ | |||||
"doctype": "Note", | |||||
"title": "deduplication timeline links", | |||||
"content": "deduplication timeline links", | |||||
} | |||||
).insert(ignore_permissions=True) | |||||
comm = frappe.get_doc( | |||||
{ | |||||
"doctype": "Communication", | |||||
"communication_type": "Communication", | |||||
"content": "Deduplication of Links", | |||||
"communication_medium": "Email", | |||||
} | |||||
).insert(ignore_permissions=True) | |||||
# adding same link twice | |||||
comm.add_link(link_doctype="Note", link_name=note.name, autosave=True) | comm.add_link(link_doctype="Note", link_name=note.name, autosave=True) | ||||
comm.add_link(link_doctype="Note", link_name=note.name, autosave=True) | comm.add_link(link_doctype="Note", link_name=note.name, autosave=True) | ||||
@@ -99,35 +129,43 @@ class TestCommunication(unittest.TestCase): | |||||
self.assertNotEqual(2, len(comm.timeline_links)) | self.assertNotEqual(2, len(comm.timeline_links)) | ||||
def test_contacts_attached(self): | def test_contacts_attached(self): | ||||
contact_sender = frappe.get_doc({ | |||||
"doctype": "Contact", | |||||
"first_name": "contact_sender", | |||||
}) | |||||
contact_sender = frappe.get_doc( | |||||
{ | |||||
"doctype": "Contact", | |||||
"first_name": "contact_sender", | |||||
} | |||||
) | |||||
contact_sender.add_email("comm_sender@example.com") | contact_sender.add_email("comm_sender@example.com") | ||||
contact_sender.insert(ignore_permissions=True) | contact_sender.insert(ignore_permissions=True) | ||||
contact_recipient = frappe.get_doc({ | |||||
"doctype": "Contact", | |||||
"first_name": "contact_recipient", | |||||
}) | |||||
contact_recipient = frappe.get_doc( | |||||
{ | |||||
"doctype": "Contact", | |||||
"first_name": "contact_recipient", | |||||
} | |||||
) | |||||
contact_recipient.add_email("comm_recipient@example.com") | contact_recipient.add_email("comm_recipient@example.com") | ||||
contact_recipient.insert(ignore_permissions=True) | contact_recipient.insert(ignore_permissions=True) | ||||
contact_cc = frappe.get_doc({ | |||||
"doctype": "Contact", | |||||
"first_name": "contact_cc", | |||||
}) | |||||
contact_cc = frappe.get_doc( | |||||
{ | |||||
"doctype": "Contact", | |||||
"first_name": "contact_cc", | |||||
} | |||||
) | |||||
contact_cc.add_email("comm_cc@example.com") | contact_cc.add_email("comm_cc@example.com") | ||||
contact_cc.insert(ignore_permissions=True) | contact_cc.insert(ignore_permissions=True) | ||||
comm = frappe.get_doc({ | |||||
"doctype": "Communication", | |||||
"communication_medium": "Email", | |||||
"subject": "Contacts Attached Test", | |||||
"sender": "comm_sender@example.com", | |||||
"recipients": "comm_recipient@example.com", | |||||
"cc": "comm_cc@example.com" | |||||
}).insert(ignore_permissions=True) | |||||
comm = frappe.get_doc( | |||||
{ | |||||
"doctype": "Communication", | |||||
"communication_medium": "Email", | |||||
"subject": "Contacts Attached Test", | |||||
"sender": "comm_sender@example.com", | |||||
"recipients": "comm_recipient@example.com", | |||||
"cc": "comm_cc@example.com", | |||||
} | |||||
).insert(ignore_permissions=True) | |||||
comm = frappe.get_doc("Communication", comm.name) | comm = frappe.get_doc("Communication", comm.name) | ||||
@@ -144,27 +182,29 @@ class TestCommunication(unittest.TestCase): | |||||
frappe.delete_doc_if_exists("Note", "get communication data") | frappe.delete_doc_if_exists("Note", "get communication data") | ||||
note = frappe.get_doc({ | |||||
"doctype": "Note", | |||||
"title": "get communication data", | |||||
"content": "get communication data" | |||||
}).insert(ignore_permissions=True) | |||||
note = frappe.get_doc( | |||||
{"doctype": "Note", "title": "get communication data", "content": "get communication data"} | |||||
).insert(ignore_permissions=True) | |||||
comm_note_1 = frappe.get_doc({ | |||||
"doctype": "Communication", | |||||
"communication_type": "Communication", | |||||
"content": "Test Get Communication Data 1", | |||||
"communication_medium": "Email" | |||||
}).insert(ignore_permissions=True) | |||||
comm_note_1 = frappe.get_doc( | |||||
{ | |||||
"doctype": "Communication", | |||||
"communication_type": "Communication", | |||||
"content": "Test Get Communication Data 1", | |||||
"communication_medium": "Email", | |||||
} | |||||
).insert(ignore_permissions=True) | |||||
comm_note_1.add_link(link_doctype="Note", link_name=note.name, autosave=True) | comm_note_1.add_link(link_doctype="Note", link_name=note.name, autosave=True) | ||||
comm_note_2 = frappe.get_doc({ | |||||
"doctype": "Communication", | |||||
"communication_type": "Communication", | |||||
"content": "Test Get Communication Data 2", | |||||
"communication_medium": "Email" | |||||
}).insert(ignore_permissions=True) | |||||
comm_note_2 = frappe.get_doc( | |||||
{ | |||||
"doctype": "Communication", | |||||
"communication_type": "Communication", | |||||
"content": "Test Get Communication Data 2", | |||||
"communication_medium": "Email", | |||||
} | |||||
).insert(ignore_permissions=True) | |||||
comm_note_2.add_link(link_doctype="Note", link_name=note.name, autosave=True) | comm_note_2.add_link(link_doctype="Note", link_name=note.name, autosave=True) | ||||
@@ -182,19 +222,23 @@ class TestCommunication(unittest.TestCase): | |||||
create_email_account() | create_email_account() | ||||
note = frappe.get_doc({ | |||||
"doctype": "Note", | |||||
"title": "test document link in email", | |||||
"content": "test document link in email" | |||||
}).insert(ignore_permissions=True) | |||||
comm = frappe.get_doc({ | |||||
"doctype": "Communication", | |||||
"communication_medium": "Email", | |||||
"subject": "Document Link in Email", | |||||
"sender": "comm_sender@example.com", | |||||
"recipients": "comm_recipient+{0}+{1}@example.com".format(quote("Note"), quote(note.name)), | |||||
}).insert(ignore_permissions=True) | |||||
note = frappe.get_doc( | |||||
{ | |||||
"doctype": "Note", | |||||
"title": "test document link in email", | |||||
"content": "test document link in email", | |||||
} | |||||
).insert(ignore_permissions=True) | |||||
comm = frappe.get_doc( | |||||
{ | |||||
"doctype": "Communication", | |||||
"communication_medium": "Email", | |||||
"subject": "Document Link in Email", | |||||
"sender": "comm_sender@example.com", | |||||
"recipients": "comm_recipient+{0}+{1}@example.com".format(quote("Note"), quote(note.name)), | |||||
} | |||||
).insert(ignore_permissions=True) | |||||
doc_links = [] | doc_links = [] | ||||
for timeline_link in comm.timeline_links: | for timeline_link in comm.timeline_links: | ||||
@@ -205,9 +249,9 @@ class TestCommunication(unittest.TestCase): | |||||
def test_parse_emails(self): | def test_parse_emails(self): | ||||
emails = get_emails( | emails = get_emails( | ||||
[ | [ | ||||
'comm_recipient+DocType+DocName@example.com', | |||||
"comm_recipient+DocType+DocName@example.com", | |||||
'"First, LastName" <first.lastname@email.com>', | '"First, LastName" <first.lastname@email.com>', | ||||
'test@user.com' | |||||
"test@user.com", | |||||
] | ] | ||||
) | ) | ||||
@@ -215,99 +259,108 @@ class TestCommunication(unittest.TestCase): | |||||
self.assertEqual(emails[1], "first.lastname@email.com") | self.assertEqual(emails[1], "first.lastname@email.com") | ||||
self.assertEqual(emails[2], "test@user.com") | self.assertEqual(emails[2], "test@user.com") | ||||
class TestCommunicationEmailMixin(unittest.TestCase): | class TestCommunicationEmailMixin(unittest.TestCase): | ||||
def new_communication(self, recipients=None, cc=None, bcc=None): | def new_communication(self, recipients=None, cc=None, bcc=None): | ||||
recipients = ', '.join(recipients or []) | |||||
cc = ', '.join(cc or []) | |||||
bcc = ', '.join(bcc or []) | |||||
comm = frappe.get_doc({ | |||||
"doctype": "Communication", | |||||
"communication_type": "Communication", | |||||
"communication_medium": "Email", | |||||
"content": "Test content", | |||||
"recipients": recipients, | |||||
"cc": cc, | |||||
"bcc": bcc | |||||
}).insert(ignore_permissions=True) | |||||
recipients = ", ".join(recipients or []) | |||||
cc = ", ".join(cc or []) | |||||
bcc = ", ".join(bcc or []) | |||||
comm = frappe.get_doc( | |||||
{ | |||||
"doctype": "Communication", | |||||
"communication_type": "Communication", | |||||
"communication_medium": "Email", | |||||
"content": "Test content", | |||||
"recipients": recipients, | |||||
"cc": cc, | |||||
"bcc": bcc, | |||||
} | |||||
).insert(ignore_permissions=True) | |||||
return comm | return comm | ||||
def new_user(self, email, **user_data): | def new_user(self, email, **user_data): | ||||
user_data.setdefault('first_name', 'first_name') | |||||
user = frappe.new_doc('User') | |||||
user_data.setdefault("first_name", "first_name") | |||||
user = frappe.new_doc("User") | |||||
user.email = email | user.email = email | ||||
user.update(user_data) | user.update(user_data) | ||||
user.insert(ignore_permissions=True, ignore_if_duplicate=True) | user.insert(ignore_permissions=True, ignore_if_duplicate=True) | ||||
return user | return user | ||||
def test_recipients(self): | def test_recipients(self): | ||||
to_list = ['to@test.com', 'receiver <to+1@test.com>', 'to@test.com'] | |||||
comm = self.new_communication(recipients = to_list) | |||||
to_list = ["to@test.com", "receiver <to+1@test.com>", "to@test.com"] | |||||
comm = self.new_communication(recipients=to_list) | |||||
res = comm.get_mail_recipients_with_displayname() | res = comm.get_mail_recipients_with_displayname() | ||||
self.assertCountEqual(res, ['to@test.com', 'receiver <to+1@test.com>']) | |||||
self.assertCountEqual(res, ["to@test.com", "receiver <to+1@test.com>"]) | |||||
comm.delete() | comm.delete() | ||||
def test_cc(self): | def test_cc(self): | ||||
to_list = ['to@test.com'] | |||||
cc_list = ['cc+1@test.com', 'cc <cc+2@test.com>', 'to@test.com'] | |||||
user = self.new_user(email='cc+1@test.com', thread_notify=0) | |||||
to_list = ["to@test.com"] | |||||
cc_list = ["cc+1@test.com", "cc <cc+2@test.com>", "to@test.com"] | |||||
user = self.new_user(email="cc+1@test.com", thread_notify=0) | |||||
comm = self.new_communication(recipients=to_list, cc=cc_list) | comm = self.new_communication(recipients=to_list, cc=cc_list) | ||||
res = comm.get_mail_cc_with_displayname() | res = comm.get_mail_cc_with_displayname() | ||||
self.assertCountEqual(res, ['cc <cc+2@test.com>']) | |||||
self.assertCountEqual(res, ["cc <cc+2@test.com>"]) | |||||
user.delete() | user.delete() | ||||
comm.delete() | comm.delete() | ||||
def test_bcc(self): | def test_bcc(self): | ||||
bcc_list = ['bcc+1@test.com', 'cc <bcc+2@test.com>', ] | |||||
user = self.new_user(email='bcc+2@test.com', enabled=0) | |||||
bcc_list = [ | |||||
"bcc+1@test.com", | |||||
"cc <bcc+2@test.com>", | |||||
] | |||||
user = self.new_user(email="bcc+2@test.com", enabled=0) | |||||
comm = self.new_communication(bcc=bcc_list) | comm = self.new_communication(bcc=bcc_list) | ||||
res = comm.get_mail_bcc_with_displayname() | res = comm.get_mail_bcc_with_displayname() | ||||
self.assertCountEqual(res, ['bcc+1@test.com']) | |||||
self.assertCountEqual(res, ["bcc+1@test.com"]) | |||||
user.delete() | user.delete() | ||||
comm.delete() | comm.delete() | ||||
def test_sendmail(self): | def test_sendmail(self): | ||||
to_list = ['to <to@test.com>'] | |||||
cc_list = ['cc <cc+1@test.com>', 'cc <cc+2@test.com>'] | |||||
to_list = ["to <to@test.com>"] | |||||
cc_list = ["cc <cc+1@test.com>", "cc <cc+2@test.com>"] | |||||
comm = self.new_communication(recipients=to_list, cc=cc_list) | comm = self.new_communication(recipients=to_list, cc=cc_list) | ||||
comm.send_email() | comm.send_email() | ||||
doc = EmailQueue.find_one_by_filters(communication=comm.name) | doc = EmailQueue.find_one_by_filters(communication=comm.name) | ||||
mail_receivers = [each.recipient for each in doc.recipients] | mail_receivers = [each.recipient for each in doc.recipients] | ||||
self.assertIsNotNone(doc) | self.assertIsNotNone(doc) | ||||
self.assertCountEqual(to_list+cc_list, mail_receivers) | |||||
self.assertCountEqual(to_list + cc_list, mail_receivers) | |||||
doc.delete() | doc.delete() | ||||
comm.delete() | comm.delete() | ||||
def create_email_account(): | def create_email_account(): | ||||
frappe.delete_doc_if_exists("Email Account", "_Test Comm Account 1") | frappe.delete_doc_if_exists("Email Account", "_Test Comm Account 1") | ||||
frappe.flags.mute_emails = False | frappe.flags.mute_emails = False | ||||
frappe.flags.sent_mail = None | frappe.flags.sent_mail = None | ||||
email_account = frappe.get_doc({ | |||||
"is_default": 1, | |||||
"is_global": 1, | |||||
"doctype": "Email Account", | |||||
"domain":"example.com", | |||||
"append_to": "ToDo", | |||||
"email_account_name": "_Test Comm Account 1", | |||||
"enable_outgoing": 1, | |||||
"smtp_server": "test.example.com", | |||||
"email_id": "test_comm@example.com", | |||||
"password": "password", | |||||
"add_signature": 1, | |||||
"signature": "\nBest Wishes\nTest Signature", | |||||
"enable_auto_reply": 1, | |||||
"auto_reply_message": "", | |||||
"enable_incoming": 1, | |||||
"notify_if_unreplied": 1, | |||||
"unreplied_for_mins": 20, | |||||
"send_notification_to": "test_comm@example.com", | |||||
"pop3_server": "pop.test.example.com", | |||||
"imap_folder": [{"folder_name": "INBOX", "append_to": "ToDo"}], | |||||
"no_remaining":"0", | |||||
"enable_automatic_linking": 1 | |||||
}).insert(ignore_permissions=True) | |||||
email_account = frappe.get_doc( | |||||
{ | |||||
"is_default": 1, | |||||
"is_global": 1, | |||||
"doctype": "Email Account", | |||||
"domain": "example.com", | |||||
"append_to": "ToDo", | |||||
"email_account_name": "_Test Comm Account 1", | |||||
"enable_outgoing": 1, | |||||
"smtp_server": "test.example.com", | |||||
"email_id": "test_comm@example.com", | |||||
"password": "password", | |||||
"add_signature": 1, | |||||
"signature": "\nBest Wishes\nTest Signature", | |||||
"enable_auto_reply": 1, | |||||
"auto_reply_message": "", | |||||
"enable_incoming": 1, | |||||
"notify_if_unreplied": 1, | |||||
"unreplied_for_mins": 20, | |||||
"send_notification_to": "test_comm@example.com", | |||||
"pop3_server": "pop.test.example.com", | |||||
"imap_folder": [{"folder_name": "INBOX", "append_to": "ToDo"}], | |||||
"no_remaining": "0", | |||||
"enable_automatic_linking": 1, | |||||
} | |||||
).insert(ignore_permissions=True) | |||||
return email_account | return email_account |
@@ -5,8 +5,10 @@ | |||||
import frappe | import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class CommunicationLink(Document): | class CommunicationLink(Document): | ||||
pass | pass | ||||
def on_doctype_update(): | def on_doctype_update(): | ||||
frappe.db.add_index("Communication Link", ["link_doctype", "link_name"]) | |||||
frappe.db.add_index("Communication Link", ["link_doctype", "link_name"]) |
@@ -5,6 +5,7 @@ | |||||
import frappe | import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class CustomDocPerm(Document): | class CustomDocPerm(Document): | ||||
def on_update(self): | def on_update(self): | ||||
frappe.clear_cache(doctype = self.parent) | |||||
frappe.clear_cache(doctype=self.parent) |
@@ -1,10 +1,12 @@ | |||||
# -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
# Copyright (c) 2015, Frappe Technologies and Contributors | # Copyright (c) 2015, Frappe Technologies and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | |||||
import unittest | import unittest | ||||
import frappe | |||||
# test_records = frappe.get_test_records('Custom DocPerm') | # test_records = frappe.get_test_records('Custom DocPerm') | ||||
class TestCustomDocPerm(unittest.TestCase): | class TestCustomDocPerm(unittest.TestCase): | ||||
pass | pass |
@@ -5,16 +5,18 @@ | |||||
import frappe | import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class CustomRole(Document): | class CustomRole(Document): | ||||
def validate(self): | def validate(self): | ||||
if self.report and not self.ref_doctype: | if self.report and not self.ref_doctype: | ||||
self.ref_doctype = frappe.db.get_value('Report', self.report, 'ref_doctype') | |||||
self.ref_doctype = frappe.db.get_value("Report", self.report, "ref_doctype") | |||||
def get_custom_allowed_roles(field, name): | def get_custom_allowed_roles(field, name): | ||||
allowed_roles = [] | allowed_roles = [] | ||||
custom_role = frappe.db.get_value('Custom Role', {field: name}, 'name') | |||||
custom_role = frappe.db.get_value("Custom Role", {field: name}, "name") | |||||
if custom_role: | if custom_role: | ||||
custom_role_doc = frappe.get_doc('Custom Role', custom_role) | |||||
custom_role_doc = frappe.get_doc("Custom Role", custom_role) | |||||
allowed_roles = [d.role for d in custom_role_doc.roles] | allowed_roles = [d.role for d in custom_role_doc.roles] | ||||
return allowed_roles | |||||
return allowed_roles |
@@ -1,10 +1,12 @@ | |||||
# -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
# Copyright (c) 2015, Frappe Technologies and Contributors | # Copyright (c) 2015, Frappe Technologies and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | |||||
import unittest | import unittest | ||||
import frappe | |||||
# test_records = frappe.get_test_records('Custom Role') | # test_records = frappe.get_test_records('Custom Role') | ||||
class TestCustomRole(unittest.TestCase): | class TestCustomRole(unittest.TestCase): | ||||
pass | pass |
@@ -4,5 +4,6 @@ | |||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class DataExport(Document): | class DataExport(Document): | ||||
pass | pass |
@@ -1,47 +1,78 @@ | |||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import csv | |||||
import os | |||||
import re | |||||
import frappe | import frappe | ||||
from frappe import _ | |||||
import frappe.permissions | import frappe.permissions | ||||
import re, csv, os | |||||
from frappe.utils.csvutils import UnicodeWriter | |||||
from frappe.utils import cstr, formatdate, format_datetime, parse_json, cint, format_duration | |||||
from frappe import _ | |||||
from frappe.core.doctype.access_log.access_log import make_access_log | from frappe.core.doctype.access_log.access_log import make_access_log | ||||
from frappe.utils import cint, cstr, format_datetime, format_duration, formatdate, parse_json | |||||
from frappe.utils.csvutils import UnicodeWriter | |||||
reflags = {"I": re.I, "L": re.L, "M": re.M, "U": re.U, "S": re.S, "X": re.X, "D": re.DEBUG} | |||||
reflags = { | |||||
"I":re.I, | |||||
"L":re.L, | |||||
"M":re.M, | |||||
"U":re.U, | |||||
"S":re.S, | |||||
"X":re.X, | |||||
"D": re.DEBUG | |||||
} | |||||
def get_data_keys(): | def get_data_keys(): | ||||
return frappe._dict({ | |||||
"data_separator": _('Start entering data below this line'), | |||||
"main_table": _("Table") + ":", | |||||
"parent_table": _("Parent Table") + ":", | |||||
"columns": _("Column Name") + ":", | |||||
"doctype": _("DocType") + ":" | |||||
}) | |||||
return frappe._dict( | |||||
{ | |||||
"data_separator": _("Start entering data below this line"), | |||||
"main_table": _("Table") + ":", | |||||
"parent_table": _("Parent Table") + ":", | |||||
"columns": _("Column Name") + ":", | |||||
"doctype": _("DocType") + ":", | |||||
} | |||||
) | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def export_data(doctype=None, parent_doctype=None, all_doctypes=True, with_data=False, | |||||
select_columns=None, file_type='CSV', template=False, filters=None): | |||||
def export_data( | |||||
doctype=None, | |||||
parent_doctype=None, | |||||
all_doctypes=True, | |||||
with_data=False, | |||||
select_columns=None, | |||||
file_type="CSV", | |||||
template=False, | |||||
filters=None, | |||||
): | |||||
_doctype = doctype | _doctype = doctype | ||||
if isinstance(_doctype, list): | if isinstance(_doctype, list): | ||||
_doctype = _doctype[0] | _doctype = _doctype[0] | ||||
make_access_log(doctype=_doctype, file_type=file_type, columns=select_columns, filters=filters, method=parent_doctype) | |||||
exporter = DataExporter(doctype=doctype, parent_doctype=parent_doctype, all_doctypes=all_doctypes, with_data=with_data, | |||||
select_columns=select_columns, file_type=file_type, template=template, filters=filters) | |||||
make_access_log( | |||||
doctype=_doctype, | |||||
file_type=file_type, | |||||
columns=select_columns, | |||||
filters=filters, | |||||
method=parent_doctype, | |||||
) | |||||
exporter = DataExporter( | |||||
doctype=doctype, | |||||
parent_doctype=parent_doctype, | |||||
all_doctypes=all_doctypes, | |||||
with_data=with_data, | |||||
select_columns=select_columns, | |||||
file_type=file_type, | |||||
template=template, | |||||
filters=filters, | |||||
) | |||||
exporter.build_response() | exporter.build_response() | ||||
class DataExporter: | class DataExporter: | ||||
def __init__(self, doctype=None, parent_doctype=None, all_doctypes=True, with_data=False, | |||||
select_columns=None, file_type='CSV', template=False, filters=None): | |||||
def __init__( | |||||
self, | |||||
doctype=None, | |||||
parent_doctype=None, | |||||
all_doctypes=True, | |||||
with_data=False, | |||||
select_columns=None, | |||||
file_type="CSV", | |||||
template=False, | |||||
filters=None, | |||||
): | |||||
self.doctype = doctype | self.doctype = doctype | ||||
self.parent_doctype = parent_doctype | self.parent_doctype = parent_doctype | ||||
self.all_doctypes = all_doctypes | self.all_doctypes = all_doctypes | ||||
@@ -81,18 +112,18 @@ class DataExporter: | |||||
def build_response(self): | def build_response(self): | ||||
self.writer = UnicodeWriter() | self.writer = UnicodeWriter() | ||||
self.name_field = 'parent' if self.parent_doctype != self.doctype else 'name' | |||||
self.name_field = "parent" if self.parent_doctype != self.doctype else "name" | |||||
if self.template: | if self.template: | ||||
self.add_main_header() | self.add_main_header() | ||||
self.writer.writerow(['']) | |||||
self.writer.writerow([""]) | |||||
self.tablerow = [self.data_keys.doctype] | self.tablerow = [self.data_keys.doctype] | ||||
self.labelrow = [_("Column Labels:")] | self.labelrow = [_("Column Labels:")] | ||||
self.fieldrow = [self.data_keys.columns] | self.fieldrow = [self.data_keys.columns] | ||||
self.mandatoryrow = [_("Mandatory:")] | self.mandatoryrow = [_("Mandatory:")] | ||||
self.typerow = [_('Type:')] | |||||
self.inforow = [_('Info:')] | |||||
self.typerow = [_("Type:")] | |||||
self.inforow = [_("Info:")] | |||||
self.columns = [] | self.columns = [] | ||||
self.build_field_columns(self.doctype) | self.build_field_columns(self.doctype) | ||||
@@ -100,74 +131,99 @@ class DataExporter: | |||||
if self.all_doctypes: | if self.all_doctypes: | ||||
for d in self.child_doctypes: | for d in self.child_doctypes: | ||||
self.append_empty_field_column() | self.append_empty_field_column() | ||||
if (self.select_columns and self.select_columns.get(d['doctype'], None)) or not self.select_columns: | |||||
if ( | |||||
self.select_columns and self.select_columns.get(d["doctype"], None) | |||||
) or not self.select_columns: | |||||
# if atleast one column is selected for this doctype | # if atleast one column is selected for this doctype | ||||
self.build_field_columns(d['doctype'], d['parentfield']) | |||||
self.build_field_columns(d["doctype"], d["parentfield"]) | |||||
self.add_field_headings() | self.add_field_headings() | ||||
self.add_data() | self.add_data() | ||||
if self.with_data and not self.data: | if self.with_data and not self.data: | ||||
frappe.respond_as_web_page(_('No Data'), _('There is no data to be exported'), indicator_color='orange') | |||||
frappe.respond_as_web_page( | |||||
_("No Data"), _("There is no data to be exported"), indicator_color="orange" | |||||
) | |||||
if self.file_type == 'Excel': | |||||
if self.file_type == "Excel": | |||||
self.build_response_as_excel() | self.build_response_as_excel() | ||||
else: | else: | ||||
# write out response as a type csv | # write out response as a type csv | ||||
frappe.response['result'] = cstr(self.writer.getvalue()) | |||||
frappe.response['type'] = 'csv' | |||||
frappe.response['doctype'] = self.doctype | |||||
frappe.response["result"] = cstr(self.writer.getvalue()) | |||||
frappe.response["type"] = "csv" | |||||
frappe.response["doctype"] = self.doctype | |||||
def add_main_header(self): | def add_main_header(self): | ||||
self.writer.writerow([_('Data Import Template')]) | |||||
self.writer.writerow([_("Data Import Template")]) | |||||
self.writer.writerow([self.data_keys.main_table, self.doctype]) | self.writer.writerow([self.data_keys.main_table, self.doctype]) | ||||
if self.parent_doctype != self.doctype: | if self.parent_doctype != self.doctype: | ||||
self.writer.writerow([self.data_keys.parent_table, self.parent_doctype]) | self.writer.writerow([self.data_keys.parent_table, self.parent_doctype]) | ||||
else: | else: | ||||
self.writer.writerow(['']) | |||||
self.writer.writerow(['']) | |||||
self.writer.writerow([_('Notes:')]) | |||||
self.writer.writerow([_('Please do not change the template headings.')]) | |||||
self.writer.writerow([_('First data column must be blank.')]) | |||||
self.writer.writerow([_('If you are uploading new records, leave the "name" (ID) column blank.')]) | |||||
self.writer.writerow([_('If you are uploading new records, "Naming Series" becomes mandatory, if present.')]) | |||||
self.writer.writerow([_('Only mandatory fields are necessary for new records. You can delete non-mandatory columns if you wish.')]) | |||||
self.writer.writerow([_('For updating, you can update only selective columns.')]) | |||||
self.writer.writerow([_('You can only upload upto 5000 records in one go. (may be less in some cases)')]) | |||||
self.writer.writerow([""]) | |||||
self.writer.writerow([""]) | |||||
self.writer.writerow([_("Notes:")]) | |||||
self.writer.writerow([_("Please do not change the template headings.")]) | |||||
self.writer.writerow([_("First data column must be blank.")]) | |||||
self.writer.writerow( | |||||
[_('If you are uploading new records, leave the "name" (ID) column blank.')] | |||||
) | |||||
self.writer.writerow( | |||||
[_('If you are uploading new records, "Naming Series" becomes mandatory, if present.')] | |||||
) | |||||
self.writer.writerow( | |||||
[ | |||||
_( | |||||
"Only mandatory fields are necessary for new records. You can delete non-mandatory columns if you wish." | |||||
) | |||||
] | |||||
) | |||||
self.writer.writerow([_("For updating, you can update only selective columns.")]) | |||||
self.writer.writerow( | |||||
[_("You can only upload upto 5000 records in one go. (may be less in some cases)")] | |||||
) | |||||
if self.name_field == "parent": | if self.name_field == "parent": | ||||
self.writer.writerow([_('"Parent" signifies the parent table in which this row must be added')]) | self.writer.writerow([_('"Parent" signifies the parent table in which this row must be added')]) | ||||
self.writer.writerow([_('If you are updating, please select "Overwrite" else existing rows will not be deleted.')]) | |||||
self.writer.writerow( | |||||
[_('If you are updating, please select "Overwrite" else existing rows will not be deleted.')] | |||||
) | |||||
def build_field_columns(self, dt, parentfield=None): | def build_field_columns(self, dt, parentfield=None): | ||||
meta = frappe.get_meta(dt) | meta = frappe.get_meta(dt) | ||||
# build list of valid docfields | # build list of valid docfields | ||||
tablecolumns = [] | tablecolumns = [] | ||||
table_name = 'tab' + dt | |||||
table_name = "tab" + dt | |||||
for f in frappe.db.get_table_columns_description(table_name): | for f in frappe.db.get_table_columns_description(table_name): | ||||
field = meta.get_field(f.name) | field = meta.get_field(f.name) | ||||
if field and ((self.select_columns and f.name in self.select_columns[dt]) or not self.select_columns): | |||||
if field and ( | |||||
(self.select_columns and f.name in self.select_columns[dt]) or not self.select_columns | |||||
): | |||||
tablecolumns.append(field) | tablecolumns.append(field) | ||||
tablecolumns.sort(key = lambda a: int(a.idx)) | |||||
tablecolumns.sort(key=lambda a: int(a.idx)) | |||||
_column_start_end = frappe._dict(start=0) | _column_start_end = frappe._dict(start=0) | ||||
if dt==self.doctype: | |||||
if (meta.get('autoname') and meta.get('autoname').lower()=='prompt') or (self.with_data): | |||||
if dt == self.doctype: | |||||
if (meta.get("autoname") and meta.get("autoname").lower() == "prompt") or (self.with_data): | |||||
self._append_name_column() | self._append_name_column() | ||||
# if importing only child table for new record, add parent field | # if importing only child table for new record, add parent field | ||||
if meta.get('istable') and not self.with_data: | |||||
self.append_field_column(frappe._dict({ | |||||
"fieldname": "parent", | |||||
"parent": "", | |||||
"label": "Parent", | |||||
"fieldtype": "Data", | |||||
"reqd": 1, | |||||
"info": _("Parent is the name of the document to which the data will get added to.") | |||||
}), True) | |||||
if meta.get("istable") and not self.with_data: | |||||
self.append_field_column( | |||||
frappe._dict( | |||||
{ | |||||
"fieldname": "parent", | |||||
"parent": "", | |||||
"label": "Parent", | |||||
"fieldtype": "Data", | |||||
"reqd": 1, | |||||
"info": _("Parent is the name of the document to which the data will get added to."), | |||||
} | |||||
), | |||||
True, | |||||
) | |||||
_column_start_end = frappe._dict(start=0) | _column_start_end = frappe._dict(start=0) | ||||
else: | else: | ||||
@@ -184,7 +240,7 @@ class DataExporter: | |||||
self.append_field_column(docfield, False) | self.append_field_column(docfield, False) | ||||
# if there is one column, add a blank column (?) | # if there is one column, add a blank column (?) | ||||
if len(self.columns)-_column_start_end.start == 1: | |||||
if len(self.columns) - _column_start_end.start == 1: | |||||
self.append_empty_field_column() | self.append_empty_field_column() | ||||
# append DocType name | # append DocType name | ||||
@@ -204,18 +260,21 @@ class DataExporter: | |||||
return | return | ||||
if not for_mandatory and docfield.reqd: | if not for_mandatory and docfield.reqd: | ||||
return | return | ||||
if docfield.fieldname in ('parenttype', 'trash_reason'): | |||||
if docfield.fieldname in ("parenttype", "trash_reason"): | |||||
return | return | ||||
if docfield.hidden: | if docfield.hidden: | ||||
return | return | ||||
if self.select_columns and docfield.fieldname not in self.select_columns.get(docfield.parent, []) \ | |||||
and docfield.fieldname!="name": | |||||
if ( | |||||
self.select_columns | |||||
and docfield.fieldname not in self.select_columns.get(docfield.parent, []) | |||||
and docfield.fieldname != "name" | |||||
): | |||||
return | return | ||||
self.tablerow.append("") | self.tablerow.append("") | ||||
self.fieldrow.append(docfield.fieldname) | self.fieldrow.append(docfield.fieldname) | ||||
self.labelrow.append(_(docfield.label)) | self.labelrow.append(_(docfield.label)) | ||||
self.mandatoryrow.append(docfield.reqd and 'Yes' or 'No') | |||||
self.mandatoryrow.append(docfield.reqd and "Yes" or "No") | |||||
self.typerow.append(docfield.fieldtype) | self.typerow.append(docfield.fieldtype) | ||||
self.inforow.append(self.getinforow(docfield)) | self.inforow.append(self.getinforow(docfield)) | ||||
self.columns.append(docfield.fieldname) | self.columns.append(docfield.fieldname) | ||||
@@ -232,15 +291,15 @@ class DataExporter: | |||||
@staticmethod | @staticmethod | ||||
def getinforow(docfield): | def getinforow(docfield): | ||||
"""make info comment for options, links etc.""" | """make info comment for options, links etc.""" | ||||
if docfield.fieldtype == 'Select': | |||||
if docfield.fieldtype == "Select": | |||||
if not docfield.options: | if not docfield.options: | ||||
return '' | |||||
return "" | |||||
else: | else: | ||||
return _("One of") + ': %s' % ', '.join(filter(None, docfield.options.split('\n'))) | |||||
elif docfield.fieldtype == 'Link': | |||||
return 'Valid %s' % docfield.options | |||||
elif docfield.fieldtype == 'Int': | |||||
return 'Integer' | |||||
return _("One of") + ": %s" % ", ".join(filter(None, docfield.options.split("\n"))) | |||||
elif docfield.fieldtype == "Link": | |||||
return "Valid %s" % docfield.options | |||||
elif docfield.fieldtype == "Int": | |||||
return "Integer" | |||||
elif docfield.fieldtype == "Check": | elif docfield.fieldtype == "Check": | ||||
return "0 or 1" | return "0 or 1" | ||||
elif docfield.fieldtype in ["Date", "Datetime"]: | elif docfield.fieldtype in ["Date", "Datetime"]: | ||||
@@ -248,7 +307,7 @@ class DataExporter: | |||||
elif hasattr(docfield, "info"): | elif hasattr(docfield, "info"): | ||||
return docfield.info | return docfield.info | ||||
else: | else: | ||||
return '' | |||||
return "" | |||||
def add_field_headings(self): | def add_field_headings(self): | ||||
self.writer.writerow(self.tablerow) | self.writer.writerow(self.tablerow) | ||||
@@ -262,6 +321,7 @@ class DataExporter: | |||||
def add_data(self): | def add_data(self): | ||||
from frappe.query_builder import DocType | from frappe.query_builder import DocType | ||||
if self.template and not self.with_data: | if self.template and not self.with_data: | ||||
return | return | ||||
@@ -270,26 +330,28 @@ class DataExporter: | |||||
# sort nested set doctypes by `lft asc` | # sort nested set doctypes by `lft asc` | ||||
order_by = None | order_by = None | ||||
table_columns = frappe.db.get_table_columns(self.parent_doctype) | table_columns = frappe.db.get_table_columns(self.parent_doctype) | ||||
if 'lft' in table_columns and 'rgt' in table_columns: | |||||
order_by = '`tab{doctype}`.`lft` asc'.format(doctype=self.parent_doctype) | |||||
if "lft" in table_columns and "rgt" in table_columns: | |||||
order_by = "`tab{doctype}`.`lft` asc".format(doctype=self.parent_doctype) | |||||
# get permitted data only | # get permitted data only | ||||
self.data = frappe.get_list(self.doctype, fields=["*"], filters=self.filters, limit_page_length=None, order_by=order_by) | |||||
self.data = frappe.get_list( | |||||
self.doctype, fields=["*"], filters=self.filters, limit_page_length=None, order_by=order_by | |||||
) | |||||
for doc in self.data: | for doc in self.data: | ||||
op = self.docs_to_export.get("op") | op = self.docs_to_export.get("op") | ||||
names = self.docs_to_export.get("name") | names = self.docs_to_export.get("name") | ||||
if names and op: | if names and op: | ||||
if op == '=' and doc.name not in names: | |||||
if op == "=" and doc.name not in names: | |||||
continue | continue | ||||
elif op == '!=' and doc.name in names: | |||||
elif op == "!=" and doc.name in names: | |||||
continue | continue | ||||
elif names: | elif names: | ||||
try: | try: | ||||
sflags = self.docs_to_export.get("flags", "I,U").upper() | sflags = self.docs_to_export.get("flags", "I,U").upper() | ||||
flags = 0 | flags = 0 | ||||
for a in re.split(r'\W+', sflags): | |||||
flags = flags | reflags.get(a,0) | |||||
for a in re.split(r"\W+", sflags): | |||||
flags = flags | reflags.get(a, 0) | |||||
c = re.compile(names, flags) | c = re.compile(names, flags) | ||||
m = c.match(doc.name) | m = c.match(doc.name) | ||||
@@ -315,7 +377,7 @@ class DataExporter: | |||||
.orderby(child_doctype_table.idx) | .orderby(child_doctype_table.idx) | ||||
) | ) | ||||
for ci, child in enumerate(data_row.run(as_dict=True)): | for ci, child in enumerate(data_row.run(as_dict=True)): | ||||
self.add_data_row(rows, c['doctype'], c['parentfield'], child, ci) | |||||
self.add_data_row(rows, c["doctype"], c["parentfield"], child, ci) | |||||
for row in rows: | for row in rows: | ||||
self.writer.writerow(row) | self.writer.writerow(row) | ||||
@@ -333,7 +395,7 @@ class DataExporter: | |||||
_column_start_end = self.column_start_end.get((dt, parentfield)) | _column_start_end = self.column_start_end.get((dt, parentfield)) | ||||
if _column_start_end: | if _column_start_end: | ||||
for i, c in enumerate(self.columns[_column_start_end.start:_column_start_end.end]): | |||||
for i, c in enumerate(self.columns[_column_start_end.start : _column_start_end.end]): | |||||
df = meta.get_field(c) | df = meta.get_field(c) | ||||
fieldtype = df.fieldtype if df else "Data" | fieldtype = df.fieldtype if df else "Data" | ||||
value = d.get(c, "") | value = d.get(c, "") | ||||
@@ -349,27 +411,33 @@ class DataExporter: | |||||
def build_response_as_excel(self): | def build_response_as_excel(self): | ||||
filename = frappe.generate_hash("", 10) | filename = frappe.generate_hash("", 10) | ||||
with open(filename, 'wb') as f: | |||||
f.write(cstr(self.writer.getvalue()).encode('utf-8')) | |||||
with open(filename, "wb") as f: | |||||
f.write(cstr(self.writer.getvalue()).encode("utf-8")) | |||||
f = open(filename) | f = open(filename) | ||||
reader = csv.reader(f) | reader = csv.reader(f) | ||||
from frappe.utils.xlsxutils import make_xlsx | from frappe.utils.xlsxutils import make_xlsx | ||||
xlsx_file = make_xlsx(reader, "Data Import Template" if self.template else 'Data Export') | |||||
xlsx_file = make_xlsx(reader, "Data Import Template" if self.template else "Data Export") | |||||
f.close() | f.close() | ||||
os.remove(filename) | os.remove(filename) | ||||
# write out response as a xlsx type | # write out response as a xlsx type | ||||
frappe.response['filename'] = self.doctype + '.xlsx' | |||||
frappe.response['filecontent'] = xlsx_file.getvalue() | |||||
frappe.response['type'] = 'binary' | |||||
frappe.response["filename"] = self.doctype + ".xlsx" | |||||
frappe.response["filecontent"] = xlsx_file.getvalue() | |||||
frappe.response["type"] = "binary" | |||||
def _append_name_column(self, dt=None): | def _append_name_column(self, dt=None): | ||||
self.append_field_column(frappe._dict({ | |||||
"fieldname": "name" if dt else self.name_field, | |||||
"parent": dt or "", | |||||
"label": "ID", | |||||
"fieldtype": "Data", | |||||
"reqd": 1, | |||||
}), True) | |||||
self.append_field_column( | |||||
frappe._dict( | |||||
{ | |||||
"fieldname": "name" if dt else self.name_field, | |||||
"parent": dt or "", | |||||
"label": "ID", | |||||
"fieldtype": "Data", | |||||
"reqd": 1, | |||||
} | |||||
), | |||||
True, | |||||
) |
@@ -2,13 +2,15 @@ | |||||
# Copyright (c) 2019, Frappe Technologies and Contributors | # Copyright (c) 2019, Frappe Technologies and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import unittest | import unittest | ||||
import frappe | import frappe | ||||
from frappe.core.doctype.data_export.exporter import DataExporter | from frappe.core.doctype.data_export.exporter import DataExporter | ||||
class TestDataExporter(unittest.TestCase): | class TestDataExporter(unittest.TestCase): | ||||
def setUp(self): | def setUp(self): | ||||
self.doctype_name = 'Test DocType for Export Tool' | |||||
self.doc_name = 'Test Data for Export Tool' | |||||
self.doctype_name = "Test DocType for Export Tool" | |||||
self.doc_name = "Test Data for Export Tool" | |||||
self.create_doctype_if_not_exists(doctype_name=self.doctype_name) | self.create_doctype_if_not_exists(doctype_name=self.doctype_name) | ||||
self.create_test_data() | self.create_test_data() | ||||
@@ -17,42 +19,49 @@ class TestDataExporter(unittest.TestCase): | |||||
Helper Function for setting up doctypes | Helper Function for setting up doctypes | ||||
""" | """ | ||||
if force: | if force: | ||||
frappe.delete_doc_if_exists('DocType', doctype_name) | |||||
frappe.delete_doc_if_exists('DocType', 'Child 1 of ' + doctype_name) | |||||
frappe.delete_doc_if_exists("DocType", doctype_name) | |||||
frappe.delete_doc_if_exists("DocType", "Child 1 of " + doctype_name) | |||||
if frappe.db.exists('DocType', doctype_name): | |||||
if frappe.db.exists("DocType", doctype_name): | |||||
return | return | ||||
# Child Table 1 | # Child Table 1 | ||||
table_1_name = 'Child 1 of ' + doctype_name | |||||
frappe.get_doc({ | |||||
'doctype': 'DocType', | |||||
'name': table_1_name, | |||||
'module': 'Custom', | |||||
'custom': 1, | |||||
'istable': 1, | |||||
'fields': [ | |||||
{'label': 'Child Title', 'fieldname': 'child_title', 'reqd': 1, 'fieldtype': 'Data'}, | |||||
{'label': 'Child Number', 'fieldname': 'child_number', 'fieldtype': 'Int'}, | |||||
] | |||||
}).insert() | |||||
table_1_name = "Child 1 of " + doctype_name | |||||
frappe.get_doc( | |||||
{ | |||||
"doctype": "DocType", | |||||
"name": table_1_name, | |||||
"module": "Custom", | |||||
"custom": 1, | |||||
"istable": 1, | |||||
"fields": [ | |||||
{"label": "Child Title", "fieldname": "child_title", "reqd": 1, "fieldtype": "Data"}, | |||||
{"label": "Child Number", "fieldname": "child_number", "fieldtype": "Int"}, | |||||
], | |||||
} | |||||
).insert() | |||||
# Main Table | # Main Table | ||||
frappe.get_doc({ | |||||
'doctype': 'DocType', | |||||
'name': doctype_name, | |||||
'module': 'Custom', | |||||
'custom': 1, | |||||
'autoname': 'field:title', | |||||
'fields': [ | |||||
{'label': 'Title', 'fieldname': 'title', 'reqd': 1, 'fieldtype': 'Data'}, | |||||
{'label': 'Number', 'fieldname': 'number', 'fieldtype': 'Int'}, | |||||
{'label': 'Table Field 1', 'fieldname': 'table_field_1', 'fieldtype': 'Table', 'options': table_1_name}, | |||||
], | |||||
'permissions': [ | |||||
{'role': 'System Manager'} | |||||
] | |||||
}).insert() | |||||
frappe.get_doc( | |||||
{ | |||||
"doctype": "DocType", | |||||
"name": doctype_name, | |||||
"module": "Custom", | |||||
"custom": 1, | |||||
"autoname": "field:title", | |||||
"fields": [ | |||||
{"label": "Title", "fieldname": "title", "reqd": 1, "fieldtype": "Data"}, | |||||
{"label": "Number", "fieldname": "number", "fieldtype": "Int"}, | |||||
{ | |||||
"label": "Table Field 1", | |||||
"fieldname": "table_field_1", | |||||
"fieldtype": "Table", | |||||
"options": table_1_name, | |||||
}, | |||||
], | |||||
"permissions": [{"role": "System Manager"}], | |||||
} | |||||
).insert() | |||||
def create_test_data(self, force=False): | def create_test_data(self, force=False): | ||||
""" | """ | ||||
@@ -69,37 +78,38 @@ class TestDataExporter(unittest.TestCase): | |||||
table_field_1=[ | table_field_1=[ | ||||
{"child_title": "Child Title 1", "child_number": "50"}, | {"child_title": "Child Title 1", "child_number": "50"}, | ||||
{"child_title": "Child Title 2", "child_number": "51"}, | {"child_title": "Child Title 2", "child_number": "51"}, | ||||
] | |||||
], | |||||
).insert() | ).insert() | ||||
else: | else: | ||||
self.doc = frappe.get_doc(self.doctype_name, self.doc_name) | self.doc = frappe.get_doc(self.doctype_name, self.doc_name) | ||||
def test_export_content(self): | def test_export_content(self): | ||||
exp = DataExporter(doctype=self.doctype_name, file_type='CSV') | |||||
exp = DataExporter(doctype=self.doctype_name, file_type="CSV") | |||||
exp.build_response() | exp.build_response() | ||||
self.assertEqual(frappe.response['type'],'csv') | |||||
self.assertEqual(frappe.response['doctype'], self.doctype_name) | |||||
self.assertTrue(frappe.response['result']) | |||||
self.assertIn('Child Title 1\",50',frappe.response['result']) | |||||
self.assertIn('Child Title 2\",51',frappe.response['result']) | |||||
self.assertEqual(frappe.response["type"], "csv") | |||||
self.assertEqual(frappe.response["doctype"], self.doctype_name) | |||||
self.assertTrue(frappe.response["result"]) | |||||
self.assertIn('Child Title 1",50', frappe.response["result"]) | |||||
self.assertIn('Child Title 2",51', frappe.response["result"]) | |||||
def test_export_type(self): | def test_export_type(self): | ||||
for type in ['csv', 'Excel']: | |||||
for type in ["csv", "Excel"]: | |||||
with self.subTest(type=type): | with self.subTest(type=type): | ||||
exp = DataExporter(doctype=self.doctype_name, file_type=type) | exp = DataExporter(doctype=self.doctype_name, file_type=type) | ||||
exp.build_response() | exp.build_response() | ||||
self.assertEqual(frappe.response['doctype'], self.doctype_name) | |||||
self.assertTrue(frappe.response['result']) | |||||
self.assertEqual(frappe.response["doctype"], self.doctype_name) | |||||
self.assertTrue(frappe.response["result"]) | |||||
if type == 'csv': | |||||
self.assertEqual(frappe.response['type'],'csv') | |||||
elif type == 'Excel': | |||||
self.assertEqual(frappe.response['type'],'binary') | |||||
self.assertEqual(frappe.response['filename'], self.doctype_name+'.xlsx') # 'Test DocType for Export Tool.xlsx') | |||||
self.assertTrue(frappe.response['filecontent']) | |||||
if type == "csv": | |||||
self.assertEqual(frappe.response["type"], "csv") | |||||
elif type == "Excel": | |||||
self.assertEqual(frappe.response["type"], "binary") | |||||
self.assertEqual( | |||||
frappe.response["filename"], self.doctype_name + ".xlsx" | |||||
) # 'Test DocType for Export Tool.xlsx') | |||||
self.assertTrue(frappe.response["filecontent"]) | |||||
def tearDown(self): | def tearDown(self): | ||||
pass | pass | ||||
@@ -64,9 +64,7 @@ class DataImport(Document): | |||||
from frappe.utils.scheduler import is_scheduler_inactive | from frappe.utils.scheduler import is_scheduler_inactive | ||||
if is_scheduler_inactive() and not frappe.flags.in_test: | if is_scheduler_inactive() and not frappe.flags.in_test: | ||||
frappe.throw( | |||||
_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive") | |||||
) | |||||
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive")) | |||||
enqueued_jobs = [d.get("job_name") for d in get_info()] | enqueued_jobs = [d.get("job_name") for d in get_info()] | ||||
@@ -100,6 +98,7 @@ def get_preview_from_template(data_import, import_file=None, google_sheets_url=N | |||||
import_file, google_sheets_url | import_file, google_sheets_url | ||||
) | ) | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def form_start_import(data_import): | def form_start_import(data_import): | ||||
return frappe.get_doc("Data Import", data_import).start_import() | return frappe.get_doc("Data Import", data_import).start_import() | ||||
@@ -127,11 +126,11 @@ def download_template( | |||||
): | ): | ||||
""" | """ | ||||
Download template from Exporter | Download template from Exporter | ||||
:param doctype: Document Type | |||||
:param export_fields=None: Fields to export as dict {'Sales Invoice': ['name', 'customer'], 'Sales Invoice Item': ['item_code']} | |||||
:param export_records=None: One of 'all', 'by_filter', 'blank_template' | |||||
:param export_filters: Filter dict | |||||
:param file_type: File type to export into | |||||
:param doctype: Document Type | |||||
:param export_fields=None: Fields to export as dict {'Sales Invoice': ['name', 'customer'], 'Sales Invoice Item': ['item_code']} | |||||
:param export_records=None: One of 'all', 'by_filter', 'blank_template' | |||||
:param export_filters: Filter dict | |||||
:param file_type: File type to export into | |||||
""" | """ | ||||
export_fields = frappe.parse_json(export_fields) | export_fields = frappe.parse_json(export_fields) | ||||
@@ -154,34 +153,38 @@ def download_errored_template(data_import_name): | |||||
data_import = frappe.get_doc("Data Import", data_import_name) | data_import = frappe.get_doc("Data Import", data_import_name) | ||||
data_import.export_errored_rows() | data_import.export_errored_rows() | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def download_import_log(data_import_name): | def download_import_log(data_import_name): | ||||
data_import = frappe.get_doc("Data Import", data_import_name) | data_import = frappe.get_doc("Data Import", data_import_name) | ||||
data_import.download_import_log() | data_import.download_import_log() | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_import_status(data_import_name): | def get_import_status(data_import_name): | ||||
import_status = {} | import_status = {} | ||||
logs = frappe.get_all('Data Import Log', fields=['count(*) as count', 'success'], | |||||
filters={'data_import': data_import_name}, | |||||
group_by='success') | |||||
logs = frappe.get_all( | |||||
"Data Import Log", | |||||
fields=["count(*) as count", "success"], | |||||
filters={"data_import": data_import_name}, | |||||
group_by="success", | |||||
) | |||||
total_payload_count = frappe.db.get_value('Data Import', data_import_name, 'payload_count') | |||||
total_payload_count = frappe.db.get_value("Data Import", data_import_name, "payload_count") | |||||
for log in logs: | for log in logs: | ||||
if log.get('success'): | |||||
import_status['success'] = log.get('count') | |||||
if log.get("success"): | |||||
import_status["success"] = log.get("count") | |||||
else: | else: | ||||
import_status['failed'] = log.get('count') | |||||
import_status["failed"] = log.get("count") | |||||
import_status['total_records'] = total_payload_count | |||||
import_status["total_records"] = total_payload_count | |||||
return import_status | return import_status | ||||
def import_file( | |||||
doctype, file_path, import_type, submit_after_import=False, console=False | |||||
): | |||||
def import_file(doctype, file_path, import_type, submit_after_import=False, console=False): | |||||
""" | """ | ||||
Import documents in from CSV or XLSX using data import. | Import documents in from CSV or XLSX using data import. | ||||
@@ -198,9 +201,7 @@ def import_file( | |||||
"Insert New Records" if import_type.lower() == "insert" else "Update Existing Records" | "Insert New Records" if import_type.lower() == "insert" else "Update Existing Records" | ||||
) | ) | ||||
i = Importer( | |||||
doctype=doctype, file_path=file_path, data_import=data_import, console=console | |||||
) | |||||
i = Importer(doctype=doctype, file_path=file_path, data_import=data_import, console=console) | |||||
i.import_data() | i.import_data() | ||||
@@ -214,11 +215,7 @@ def import_doc(path, pre_process=None): | |||||
if f.endswith(".json"): | if f.endswith(".json"): | ||||
frappe.flags.mute_emails = True | frappe.flags.mute_emails = True | ||||
import_file_by_path( | import_file_by_path( | ||||
f, | |||||
data_import=True, | |||||
force=True, | |||||
pre_process=pre_process, | |||||
reset_permissions=True | |||||
f, data_import=True, force=True, pre_process=pre_process, reset_permissions=True | |||||
) | ) | ||||
frappe.flags.mute_emails = False | frappe.flags.mute_emails = False | ||||
frappe.db.commit() | frappe.db.commit() | ||||
@@ -226,9 +223,7 @@ def import_doc(path, pre_process=None): | |||||
raise NotImplementedError("Only .json files can be imported") | raise NotImplementedError("Only .json files can be imported") | ||||
def export_json( | |||||
doctype, path, filters=None, or_filters=None, name=None, order_by="creation asc" | |||||
): | |||||
def export_json(doctype, path, filters=None, or_filters=None, name=None, order_by="creation asc"): | |||||
def post_process(out): | def post_process(out): | ||||
# Note on Tree DocTypes: | # Note on Tree DocTypes: | ||||
# The tree structure is maintained in the database via the fields "lft" | # The tree structure is maintained in the database via the fields "lft" | ||||
@@ -6,11 +6,8 @@ import typing | |||||
import frappe | import frappe | ||||
from frappe import _ | from frappe import _ | ||||
from frappe.model import ( | |||||
display_fieldtypes, | |||||
no_value_fields, | |||||
table_fields as table_fieldtypes, | |||||
) | |||||
from frappe.model import display_fieldtypes, no_value_fields | |||||
from frappe.model import table_fields as table_fieldtypes | |||||
from frappe.utils import flt, format_duration, groupby_metric | from frappe.utils import flt, format_duration, groupby_metric | ||||
from frappe.utils.csvutils import build_csv_response | from frappe.utils.csvutils import build_csv_response | ||||
from frappe.utils.xlsxutils import build_xlsx_response | from frappe.utils.xlsxutils import build_xlsx_response | ||||
@@ -28,11 +25,11 @@ class Exporter: | |||||
): | ): | ||||
""" | """ | ||||
Exports records of a DocType for use with Importer | Exports records of a DocType for use with Importer | ||||
:param doctype: Document Type to export | |||||
:param export_fields=None: One of 'All', 'Mandatory' or {'DocType': ['field1', 'field2'], 'Child DocType': ['childfield1']} | |||||
:param export_data=False: Whether to export data as well | |||||
:param export_filters=None: The filters (dict or list) which is used to query the records | |||||
:param file_type: One of 'Excel' or 'CSV' | |||||
:param doctype: Document Type to export | |||||
:param export_fields=None: One of 'All', 'Mandatory' or {'DocType': ['field1', 'field2'], 'Child DocType': ['childfield1']} | |||||
:param export_data=False: Whether to export data as well | |||||
:param export_filters=None: The filters (dict or list) which is used to query the records | |||||
:param file_type: One of 'Excel' or 'CSV' | |||||
""" | """ | ||||
self.doctype = doctype | self.doctype = doctype | ||||
self.meta = frappe.get_meta(doctype) | self.meta = frappe.get_meta(doctype) | ||||
@@ -168,9 +165,7 @@ class Exporter: | |||||
else: | else: | ||||
order_by = "`tab{0}`.`creation` DESC".format(self.doctype) | order_by = "`tab{0}`.`creation` DESC".format(self.doctype) | ||||
parent_fields = [ | |||||
format_column_name(df) for df in self.fields if df.parent == self.doctype | |||||
] | |||||
parent_fields = [format_column_name(df) for df in self.fields if df.parent == self.doctype] | |||||
parent_data = frappe.db.get_list( | parent_data = frappe.db.get_list( | ||||
self.doctype, | self.doctype, | ||||
filters=filters, | filters=filters, | ||||
@@ -188,9 +183,7 @@ class Exporter: | |||||
child_table_df = self.meta.get_field(key) | child_table_df = self.meta.get_field(key) | ||||
child_table_doctype = child_table_df.options | child_table_doctype = child_table_df.options | ||||
child_fields = ["name", "idx", "parent", "parentfield"] + list( | child_fields = ["name", "idx", "parent", "parentfield"] + list( | ||||
set( | |||||
[format_column_name(df) for df in self.fields if df.parent == child_table_doctype] | |||||
) | |||||
set([format_column_name(df) for df in self.fields if df.parent == child_table_doctype]) | |||||
) | ) | ||||
data = frappe.db.get_all( | data = frappe.db.get_all( | ||||
child_table_doctype, | child_table_doctype, | ||||
@@ -261,4 +254,4 @@ class Exporter: | |||||
build_xlsx_response(self.get_csv_array_for_export(), _(self.doctype)) | build_xlsx_response(self.get_csv_array_for_export(), _(self.doctype)) | ||||
def group_children_data_by_parent(self, children_data: typing.Dict[str, list]): | def group_children_data_by_parent(self, children_data: typing.Dict[str, list]): | ||||
return groupby_metric(children_data, key='parent') | |||||
return groupby_metric(children_data, key="parent") |
@@ -1,21 +1,23 @@ | |||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import os | |||||
import io | import io | ||||
import frappe | |||||
import timeit | |||||
import json | import json | ||||
from datetime import datetime, date | |||||
import os | |||||
import timeit | |||||
from datetime import date, datetime | |||||
import frappe | |||||
from frappe import _ | from frappe import _ | ||||
from frappe.utils import cint, flt, update_progress_bar, cstr, duration_to_seconds | |||||
from frappe.utils.csvutils import read_csv_content, get_csv_content_from_google_sheets | |||||
from frappe.core.doctype.version.version import get_diff | |||||
from frappe.model import no_value_fields | |||||
from frappe.model import table_fields as table_fieldtypes | |||||
from frappe.utils import cint, cstr, duration_to_seconds, flt, update_progress_bar | |||||
from frappe.utils.csvutils import get_csv_content_from_google_sheets, read_csv_content | |||||
from frappe.utils.xlsxutils import ( | from frappe.utils.xlsxutils import ( | ||||
read_xlsx_file_from_attached_file, | |||||
read_xls_file_from_attached_file, | read_xls_file_from_attached_file, | ||||
read_xlsx_file_from_attached_file, | |||||
) | ) | ||||
from frappe.model import no_value_fields, table_fields as table_fieldtypes | |||||
from frappe.core.doctype.version.version import get_diff | |||||
INVALID_VALUES = ("", None) | INVALID_VALUES = ("", None) | ||||
MAX_ROWS_IN_PREVIEW = 10 | MAX_ROWS_IN_PREVIEW = 10 | ||||
@@ -24,9 +26,7 @@ UPDATE = "Update Existing Records" | |||||
class Importer: | class Importer: | ||||
def __init__( | |||||
self, doctype, data_import=None, file_path=None, import_type=None, console=False | |||||
): | |||||
def __init__(self, doctype, data_import=None, file_path=None, import_type=None, console=False): | |||||
self.doctype = doctype | self.doctype = doctype | ||||
self.console = console | self.console = console | ||||
@@ -49,9 +49,13 @@ class Importer: | |||||
def get_data_for_import_preview(self): | def get_data_for_import_preview(self): | ||||
out = self.import_file.get_data_for_import_preview() | out = self.import_file.get_data_for_import_preview() | ||||
out.import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success"], | |||||
out.import_log = frappe.db.get_all( | |||||
"Data Import Log", | |||||
fields=["row_indexes", "success"], | |||||
filters={"data_import": self.data_import.name}, | filters={"data_import": self.data_import.name}, | ||||
order_by="log_index", limit=10) | |||||
order_by="log_index", | |||||
limit=10, | |||||
) | |||||
return out | return out | ||||
@@ -84,14 +88,23 @@ class Importer: | |||||
return | return | ||||
# setup import log | # setup import log | ||||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "log_index"], | |||||
filters={"data_import": self.data_import.name}, | |||||
order_by="log_index") or [] | |||||
import_log = ( | |||||
frappe.db.get_all( | |||||
"Data Import Log", | |||||
fields=["row_indexes", "success", "log_index"], | |||||
filters={"data_import": self.data_import.name}, | |||||
order_by="log_index", | |||||
) | |||||
or [] | |||||
) | |||||
log_index = 0 | log_index = 0 | ||||
# Do not remove rows in case of retry after an error or pending data import | # Do not remove rows in case of retry after an error or pending data import | ||||
if self.data_import.status == "Partial Success" and len(import_log) >= self.data_import.payload_count: | |||||
if ( | |||||
self.data_import.status == "Partial Success" | |||||
and len(import_log) >= self.data_import.payload_count | |||||
): | |||||
# remove previous failures from import log only in case of retry after partial success | # remove previous failures from import log only in case of retry after partial success | ||||
import_log = [log for log in import_log if log.get("success")] | import_log = [log for log in import_log if log.get("success")] | ||||
@@ -108,9 +121,7 @@ class Importer: | |||||
total_payload_count = len(payloads) | total_payload_count = len(payloads) | ||||
batch_size = frappe.conf.data_import_batch_size or 1000 | batch_size = frappe.conf.data_import_batch_size or 1000 | ||||
for batch_index, batched_payloads in enumerate( | |||||
frappe.utils.create_batch(payloads, batch_size) | |||||
): | |||||
for batch_index, batched_payloads in enumerate(frappe.utils.create_batch(payloads, batch_size)): | |||||
for i, payload in enumerate(batched_payloads): | for i, payload in enumerate(batched_payloads): | ||||
doc = payload.doc | doc = payload.doc | ||||
row_indexes = [row.row_number for row in payload.rows] | row_indexes = [row.row_number for row in payload.rows] | ||||
@@ -156,11 +167,11 @@ class Importer: | |||||
}, | }, | ||||
) | ) | ||||
create_import_log(self.data_import.name, log_index, { | |||||
'success': True, | |||||
'docname': doc.name, | |||||
'row_indexes': row_indexes | |||||
}) | |||||
create_import_log( | |||||
self.data_import.name, | |||||
log_index, | |||||
{"success": True, "docname": doc.name, "row_indexes": row_indexes}, | |||||
) | |||||
log_index += 1 | log_index += 1 | ||||
@@ -177,19 +188,29 @@ class Importer: | |||||
# rollback if exception | # rollback if exception | ||||
frappe.db.rollback() | frappe.db.rollback() | ||||
create_import_log(self.data_import.name, log_index, { | |||||
'success': False, | |||||
'exception': frappe.get_traceback(), | |||||
'messages': messages, | |||||
'row_indexes': row_indexes | |||||
}) | |||||
create_import_log( | |||||
self.data_import.name, | |||||
log_index, | |||||
{ | |||||
"success": False, | |||||
"exception": frappe.get_traceback(), | |||||
"messages": messages, | |||||
"row_indexes": row_indexes, | |||||
}, | |||||
) | |||||
log_index += 1 | log_index += 1 | ||||
# Logs are db inserted directly so will have to be fetched again | # Logs are db inserted directly so will have to be fetched again | ||||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "log_index"], | |||||
filters={"data_import": self.data_import.name}, | |||||
order_by="log_index") or [] | |||||
import_log = ( | |||||
frappe.db.get_all( | |||||
"Data Import Log", | |||||
fields=["row_indexes", "success", "log_index"], | |||||
filters={"data_import": self.data_import.name}, | |||||
order_by="log_index", | |||||
) | |||||
or [] | |||||
) | |||||
# set status | # set status | ||||
failures = [log for log in import_log if not log.get("success")] | failures = [log for log in import_log if not log.get("success")] | ||||
@@ -274,9 +295,15 @@ class Importer: | |||||
if not self.data_import: | if not self.data_import: | ||||
return | return | ||||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success"], | |||||
filters={"data_import": self.data_import.name}, | |||||
order_by="log_index") or [] | |||||
import_log = ( | |||||
frappe.db.get_all( | |||||
"Data Import Log", | |||||
fields=["row_indexes", "success"], | |||||
filters={"data_import": self.data_import.name}, | |||||
order_by="log_index", | |||||
) | |||||
or [] | |||||
) | |||||
failures = [log for log in import_log if not log.get("success")] | failures = [log for log in import_log if not log.get("success")] | ||||
row_indexes = [] | row_indexes = [] | ||||
@@ -299,9 +326,12 @@ class Importer: | |||||
if not self.data_import: | if not self.data_import: | ||||
return | return | ||||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "messages", "exception", "docname"], | |||||
import_log = frappe.db.get_all( | |||||
"Data Import Log", | |||||
fields=["row_indexes", "success", "messages", "exception", "docname"], | |||||
filters={"data_import": self.data_import.name}, | filters={"data_import": self.data_import.name}, | ||||
order_by="log_index") | |||||
order_by="log_index", | |||||
) | |||||
header_row = ["Row Numbers", "Status", "Message", "Exception"] | header_row = ["Row Numbers", "Status", "Message", "Exception"] | ||||
@@ -309,10 +339,13 @@ class Importer: | |||||
for log in import_log: | for log in import_log: | ||||
row_number = json.loads(log.get("row_indexes"))[0] | row_number = json.loads(log.get("row_indexes"))[0] | ||||
status = "Success" if log.get('success') else "Failure" | |||||
message = "Successfully Imported {0}".format(log.get('docname')) if log.get('success') else \ | |||||
log.get("messages") | |||||
exception = frappe.utils.cstr(log.get("exception", '')) | |||||
status = "Success" if log.get("success") else "Failure" | |||||
message = ( | |||||
"Successfully Imported {0}".format(log.get("docname")) | |||||
if log.get("success") | |||||
else log.get("messages") | |||||
) | |||||
exception = frappe.utils.cstr(log.get("exception", "")) | |||||
rows += [[row_number, status, message, exception]] | rows += [[row_number, status, message, exception]] | ||||
build_csv_response(rows, self.doctype) | build_csv_response(rows, self.doctype) | ||||
@@ -324,9 +357,7 @@ class Importer: | |||||
if successful_records: | if successful_records: | ||||
print() | print() | ||||
print( | print( | ||||
"Successfully imported {0} records out of {1}".format( | |||||
len(successful_records), len(import_log) | |||||
) | |||||
"Successfully imported {0} records out of {1}".format(len(successful_records), len(import_log)) | |||||
) | ) | ||||
if failed_records: | if failed_records: | ||||
@@ -363,9 +394,7 @@ class Importer: | |||||
class ImportFile: | class ImportFile: | ||||
def __init__(self, doctype, file, template_options=None, import_type=None): | def __init__(self, doctype, file, template_options=None, import_type=None): | ||||
self.doctype = doctype | self.doctype = doctype | ||||
self.template_options = template_options or frappe._dict( | |||||
column_to_field_map=frappe._dict() | |||||
) | |||||
self.template_options = template_options or frappe._dict(column_to_field_map=frappe._dict()) | |||||
self.column_to_field_map = self.template_options.column_to_field_map | self.column_to_field_map = self.template_options.column_to_field_map | ||||
self.import_type = import_type | self.import_type = import_type | ||||
self.warnings = [] | self.warnings = [] | ||||
@@ -556,9 +585,7 @@ class ImportFile: | |||||
def read_content(self, content, extension): | def read_content(self, content, extension): | ||||
error_title = _("Template Error") | error_title = _("Template Error") | ||||
if extension not in ("csv", "xlsx", "xls"): | if extension not in ("csv", "xlsx", "xls"): | ||||
frappe.throw( | |||||
_("Import template should be of type .csv, .xlsx or .xls"), title=error_title | |||||
) | |||||
frappe.throw(_("Import template should be of type .csv, .xlsx or .xls"), title=error_title) | |||||
if extension == "csv": | if extension == "csv": | ||||
data = read_csv_content(content) | data = read_csv_content(content) | ||||
@@ -587,12 +614,13 @@ class Row: | |||||
if len_row != len_columns: | if len_row != len_columns: | ||||
less_than_columns = len_row < len_columns | less_than_columns = len_row < len_columns | ||||
message = ( | message = ( | ||||
"Row has less values than columns" | |||||
if less_than_columns | |||||
else "Row has more values than columns" | |||||
"Row has less values than columns" if less_than_columns else "Row has more values than columns" | |||||
) | ) | ||||
self.warnings.append( | self.warnings.append( | ||||
{"row": self.row_number, "message": message,} | |||||
{ | |||||
"row": self.row_number, | |||||
"message": message, | |||||
} | |||||
) | ) | ||||
def parse_doc(self, doctype, parent_doc=None, table_df=None): | def parse_doc(self, doctype, parent_doc=None, table_df=None): | ||||
@@ -662,18 +690,24 @@ class Row: | |||||
options_string = ", ".join(frappe.bold(d) for d in select_options) | options_string = ", ".join(frappe.bold(d) for d in select_options) | ||||
msg = _("Value must be one of {0}").format(options_string) | msg = _("Value must be one of {0}").format(options_string) | ||||
self.warnings.append( | self.warnings.append( | ||||
{"row": self.row_number, "field": df_as_json(df), "message": msg,} | |||||
{ | |||||
"row": self.row_number, | |||||
"field": df_as_json(df), | |||||
"message": msg, | |||||
} | |||||
) | ) | ||||
return | return | ||||
elif df.fieldtype == "Link": | elif df.fieldtype == "Link": | ||||
exists = self.link_exists(value, df) | exists = self.link_exists(value, df) | ||||
if not exists: | if not exists: | ||||
msg = _("Value {0} missing for {1}").format( | |||||
frappe.bold(value), frappe.bold(df.options) | |||||
) | |||||
msg = _("Value {0} missing for {1}").format(frappe.bold(value), frappe.bold(df.options)) | |||||
self.warnings.append( | self.warnings.append( | ||||
{"row": self.row_number, "field": df_as_json(df), "message": msg,} | |||||
{ | |||||
"row": self.row_number, | |||||
"field": df_as_json(df), | |||||
"message": msg, | |||||
} | |||||
) | ) | ||||
return | return | ||||
elif df.fieldtype in ["Date", "Datetime"]: | elif df.fieldtype in ["Date", "Datetime"]: | ||||
@@ -693,6 +727,7 @@ class Row: | |||||
return | return | ||||
elif df.fieldtype == "Duration": | elif df.fieldtype == "Duration": | ||||
import re | import re | ||||
is_valid_duration = re.match(r"^(?:(\d+d)?((^|\s)\d+h)?((^|\s)\d+m)?((^|\s)\d+s)?)$", value) | is_valid_duration = re.match(r"^(?:(\d+d)?((^|\s)\d+h)?((^|\s)\d+m)?((^|\s)\d+s)?)$", value) | ||||
if not is_valid_duration: | if not is_valid_duration: | ||||
self.warnings.append( | self.warnings.append( | ||||
@@ -702,7 +737,7 @@ class Row: | |||||
"field": df_as_json(df), | "field": df_as_json(df), | ||||
"message": _("Value {0} must be in the valid duration format: d h m s").format( | "message": _("Value {0} must be in the valid duration format: d h m s").format( | ||||
frappe.bold(value) | frappe.bold(value) | ||||
) | |||||
), | |||||
} | } | ||||
) | ) | ||||
@@ -789,9 +824,7 @@ class Header(Row): | |||||
else: | else: | ||||
doctypes.append((col.df.parent, col.df.child_table_df)) | doctypes.append((col.df.parent, col.df.child_table_df)) | ||||
self.doctypes = sorted( | |||||
list(set(doctypes)), key=lambda x: -1 if x[0] == self.doctype else 1 | |||||
) | |||||
self.doctypes = sorted(list(set(doctypes)), key=lambda x: -1 if x[0] == self.doctype else 1) | |||||
def get_column_indexes(self, doctype, tablefield=None): | def get_column_indexes(self, doctype, tablefield=None): | ||||
def is_table_field(df): | def is_table_field(df): | ||||
@@ -802,10 +835,7 @@ class Header(Row): | |||||
return [ | return [ | ||||
col.index | col.index | ||||
for col in self.columns | for col in self.columns | ||||
if not col.skip_import | |||||
and col.df | |||||
and col.df.parent == doctype | |||||
and is_table_field(col.df) | |||||
if not col.skip_import and col.df and col.df.parent == doctype and is_table_field(col.df) | |||||
] | ] | ||||
def get_columns(self, indexes): | def get_columns(self, indexes): | ||||
@@ -893,9 +923,7 @@ class Column: | |||||
self.warnings.append( | self.warnings.append( | ||||
{ | { | ||||
"col": column_number, | "col": column_number, | ||||
"message": _("Cannot match column {0} with any field").format( | |||||
frappe.bold(header_title) | |||||
), | |||||
"message": _("Cannot match column {0} with any field").format(frappe.bold(header_title)), | |||||
"type": "info", | "type": "info", | ||||
} | } | ||||
) | ) | ||||
@@ -958,9 +986,7 @@ class Column: | |||||
if self.df.fieldtype == "Link": | if self.df.fieldtype == "Link": | ||||
# find all values that dont exist | # find all values that dont exist | ||||
values = list({cstr(v) for v in self.column_values[1:] if v}) | values = list({cstr(v) for v in self.column_values[1:] if v}) | ||||
exists = [ | |||||
d.name for d in frappe.db.get_all(self.df.options, filters={"name": ("in", values)}) | |||||
] | |||||
exists = [d.name for d in frappe.db.get_all(self.df.options, filters={"name": ("in", values)})] | |||||
not_exists = list(set(values) - set(exists)) | not_exists = list(set(values) - set(exists)) | ||||
if not_exists: | if not_exists: | ||||
missing_values = ", ".join(not_exists) | missing_values = ", ".join(not_exists) | ||||
@@ -968,9 +994,7 @@ class Column: | |||||
{ | { | ||||
"col": self.column_number, | "col": self.column_number, | ||||
"message": ( | "message": ( | ||||
"The following values do not exist for {}: {}".format( | |||||
self.df.options, missing_values | |||||
) | |||||
"The following values do not exist for {}: {}".format(self.df.options, missing_values) | |||||
), | ), | ||||
"type": "warning", | "type": "warning", | ||||
} | } | ||||
@@ -983,7 +1007,9 @@ class Column: | |||||
self.warnings.append( | self.warnings.append( | ||||
{ | { | ||||
"col": self.column_number, | "col": self.column_number, | ||||
"message": _("Date format could not be determined from the values in this column. Defaulting to yyyy-mm-dd."), | |||||
"message": _( | |||||
"Date format could not be determined from the values in this column. Defaulting to yyyy-mm-dd." | |||||
), | |||||
"type": "info", | "type": "info", | ||||
} | } | ||||
) | ) | ||||
@@ -1027,12 +1053,12 @@ def build_fields_dict_for_column_matching(parent_doctype): | |||||
Build a dict with various keys to match with column headers and value as docfield | Build a dict with various keys to match with column headers and value as docfield | ||||
The keys can be label or fieldname | The keys can be label or fieldname | ||||
{ | { | ||||
'Customer': df1, | |||||
'customer': df1, | |||||
'Due Date': df2, | |||||
'due_date': df2, | |||||
'Item Code (Sales Invoice Item)': df3, | |||||
'Sales Invoice Item:item_code': df3, | |||||
'Customer': df1, | |||||
'customer': df1, | |||||
'Due Date': df2, | |||||
'due_date': df2, | |||||
'Item Code (Sales Invoice Item)': df3, | |||||
'Sales Invoice Item:item_code': df3, | |||||
} | } | ||||
""" | """ | ||||
@@ -1062,9 +1088,7 @@ def build_fields_dict_for_column_matching(parent_doctype): | |||||
out = {} | out = {} | ||||
# doctypes and fieldname if it is a child doctype | # doctypes and fieldname if it is a child doctype | ||||
doctypes = [(parent_doctype, None)] + [ | |||||
(df.options, df) for df in parent_meta.get_table_fields() | |||||
] | |||||
doctypes = [(parent_doctype, None)] + [(df.options, df) for df in parent_meta.get_table_fields()] | |||||
for doctype, table_df in doctypes: | for doctype, table_df in doctypes: | ||||
translated_table_label = _(table_df.label) if table_df else None | translated_table_label = _(table_df.label) if table_df else None | ||||
@@ -1082,15 +1106,15 @@ def build_fields_dict_for_column_matching(parent_doctype): | |||||
if doctype == parent_doctype: | if doctype == parent_doctype: | ||||
name_headers = ( | name_headers = ( | ||||
"name", # fieldname | |||||
"ID", # label | |||||
_("ID"), # translated label | |||||
"name", # fieldname | |||||
"ID", # label | |||||
_("ID"), # translated label | |||||
) | ) | ||||
else: | else: | ||||
name_headers = ( | name_headers = ( | ||||
"{0}.name".format(table_df.fieldname), # fieldname | |||||
"ID ({0})".format(table_df.label), # label | |||||
"{0} ({1})".format(_("ID"), translated_table_label), # translated label | |||||
"{0}.name".format(table_df.fieldname), # fieldname | |||||
"ID ({0})".format(table_df.label), # label | |||||
"{0} ({1})".format(_("ID"), translated_table_label), # translated label | |||||
) | ) | ||||
name_df.is_child_table_field = True | name_df.is_child_table_field = True | ||||
@@ -1122,7 +1146,7 @@ def build_fields_dict_for_column_matching(parent_doctype): | |||||
for header in ( | for header in ( | ||||
df.fieldname, | df.fieldname, | ||||
f"{label} ({df.fieldname})", | f"{label} ({df.fieldname})", | ||||
f"{translated_label} ({df.fieldname})" | |||||
f"{translated_label} ({df.fieldname})", | |||||
): | ): | ||||
out[header] = df | out[header] = df | ||||
@@ -1155,9 +1179,8 @@ def build_fields_dict_for_column_matching(parent_doctype): | |||||
autoname_field = get_autoname_field(parent_doctype) | autoname_field = get_autoname_field(parent_doctype) | ||||
if autoname_field: | if autoname_field: | ||||
for header in ( | for header in ( | ||||
"ID ({})".format(autoname_field.label), # label | |||||
"{0} ({1})".format(_("ID"), _(autoname_field.label)), # translated label | |||||
"ID ({})".format(autoname_field.label), # label | |||||
"{0} ({1})".format(_("ID"), _(autoname_field.label)), # translated label | |||||
# ID field should also map to the autoname field | # ID field should also map to the autoname field | ||||
"ID", | "ID", | ||||
_("ID"), | _("ID"), | ||||
@@ -1205,10 +1228,7 @@ def get_item_at_index(_list, i, default=None): | |||||
def get_user_format(date_format): | def get_user_format(date_format): | ||||
return ( | return ( | ||||
date_format.replace("%Y", "yyyy") | |||||
.replace("%y", "yy") | |||||
.replace("%m", "mm") | |||||
.replace("%d", "dd") | |||||
date_format.replace("%Y", "yyyy").replace("%y", "yy").replace("%m", "mm").replace("%d", "dd") | |||||
) | ) | ||||
@@ -1226,16 +1246,17 @@ def df_as_json(df): | |||||
def get_select_options(df): | def get_select_options(df): | ||||
return [d for d in (df.options or "").split("\n") if d] | return [d for d in (df.options or "").split("\n") if d] | ||||
def create_import_log(data_import, log_index, log_details): | |||||
frappe.get_doc({ | |||||
'doctype': 'Data Import Log', | |||||
'log_index': log_index, | |||||
'success': log_details.get('success'), | |||||
'data_import': data_import, | |||||
'row_indexes': json.dumps(log_details.get('row_indexes')), | |||||
'docname': log_details.get('docname'), | |||||
'messages': json.dumps(log_details.get('messages', '[]')), | |||||
'exception': log_details.get('exception') | |||||
}).db_insert() | |||||
def create_import_log(data_import, log_index, log_details): | |||||
frappe.get_doc( | |||||
{ | |||||
"doctype": "Data Import Log", | |||||
"log_index": log_index, | |||||
"success": log_details.get("success"), | |||||
"data_import": data_import, | |||||
"row_indexes": json.dumps(log_details.get("row_indexes")), | |||||
"docname": log_details.get("docname"), | |||||
"messages": json.dumps(log_details.get("messages", "[]")), | |||||
"exception": log_details.get("exception"), | |||||
} | |||||
).db_insert() |
@@ -4,5 +4,6 @@ | |||||
# import frappe | # import frappe | ||||
import unittest | import unittest | ||||
class TestDataImport(unittest.TestCase): | class TestDataImport(unittest.TestCase): | ||||
pass | pass |
@@ -2,13 +2,13 @@ | |||||
# Copyright (c) 2019, Frappe Technologies and Contributors | # Copyright (c) 2019, Frappe Technologies and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import unittest | import unittest | ||||
import frappe | import frappe | ||||
from frappe.core.doctype.data_import.exporter import Exporter | from frappe.core.doctype.data_import.exporter import Exporter | ||||
from frappe.core.doctype.data_import.test_importer import ( | |||||
create_doctype_if_not_exists, | |||||
) | |||||
from frappe.core.doctype.data_import.test_importer import create_doctype_if_not_exists | |||||
doctype_name = "DocType for Export" | |||||
doctype_name = 'DocType for Export' | |||||
class TestExporter(unittest.TestCase): | class TestExporter(unittest.TestCase): | ||||
def setUp(self): | def setUp(self): | ||||
@@ -93,10 +93,10 @@ class TestExporter(unittest.TestCase): | |||||
doctype_name, | doctype_name, | ||||
export_fields={doctype_name: ["title", "description"]}, | export_fields={doctype_name: ["title", "description"]}, | ||||
export_data=True, | export_data=True, | ||||
file_type="CSV" | |||||
file_type="CSV", | |||||
) | ) | ||||
e.build_response() | e.build_response() | ||||
self.assertTrue(frappe.response['result']) | |||||
self.assertEqual(frappe.response['doctype'], doctype_name) | |||||
self.assertEqual(frappe.response['type'], "csv") | |||||
self.assertTrue(frappe.response["result"]) | |||||
self.assertEqual(frappe.response["doctype"], doctype_name) | |||||
self.assertEqual(frappe.response["type"], "csv") |
@@ -2,53 +2,57 @@ | |||||
# Copyright (c) 2019, Frappe Technologies and Contributors | # Copyright (c) 2019, Frappe Technologies and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import unittest | import unittest | ||||
import frappe | import frappe | ||||
from frappe.core.doctype.data_import.importer import Importer | from frappe.core.doctype.data_import.importer import Importer | ||||
from frappe.tests.test_query_builder import db_type_is, run_only_if | from frappe.tests.test_query_builder import db_type_is, run_only_if | ||||
from frappe.utils import getdate, format_duration | |||||
from frappe.utils import format_duration, getdate | |||||
doctype_name = "DocType for Import" | |||||
doctype_name = 'DocType for Import' | |||||
class TestImporter(unittest.TestCase): | class TestImporter(unittest.TestCase): | ||||
@classmethod | @classmethod | ||||
def setUpClass(cls): | def setUpClass(cls): | ||||
create_doctype_if_not_exists(doctype_name,) | |||||
create_doctype_if_not_exists( | |||||
doctype_name, | |||||
) | |||||
def test_data_import_from_file(self): | def test_data_import_from_file(self): | ||||
import_file = get_import_file('sample_import_file') | |||||
import_file = get_import_file("sample_import_file") | |||||
data_import = self.get_importer(doctype_name, import_file) | data_import = self.get_importer(doctype_name, import_file) | ||||
data_import.start_import() | data_import.start_import() | ||||
doc1 = frappe.get_doc(doctype_name, 'Test') | |||||
doc2 = frappe.get_doc(doctype_name, 'Test 2') | |||||
doc3 = frappe.get_doc(doctype_name, 'Test 3') | |||||
doc1 = frappe.get_doc(doctype_name, "Test") | |||||
doc2 = frappe.get_doc(doctype_name, "Test 2") | |||||
doc3 = frappe.get_doc(doctype_name, "Test 3") | |||||
self.assertEqual(doc1.description, 'test description') | |||||
self.assertEqual(doc1.description, "test description") | |||||
self.assertEqual(doc1.number, 1) | self.assertEqual(doc1.number, 1) | ||||
self.assertEqual(format_duration(doc1.duration), '3h') | |||||
self.assertEqual(format_duration(doc1.duration), "3h") | |||||
self.assertEqual(doc1.table_field_1[0].child_title, 'child title') | |||||
self.assertEqual(doc1.table_field_1[0].child_description, 'child description') | |||||
self.assertEqual(doc1.table_field_1[0].child_title, "child title") | |||||
self.assertEqual(doc1.table_field_1[0].child_description, "child description") | |||||
self.assertEqual(doc1.table_field_1[1].child_title, 'child title 2') | |||||
self.assertEqual(doc1.table_field_1[1].child_description, 'child description 2') | |||||
self.assertEqual(doc1.table_field_1[1].child_title, "child title 2") | |||||
self.assertEqual(doc1.table_field_1[1].child_description, "child description 2") | |||||
self.assertEqual(doc1.table_field_2[1].child_2_title, 'title child') | |||||
self.assertEqual(doc1.table_field_2[1].child_2_date, getdate('2019-10-30')) | |||||
self.assertEqual(doc1.table_field_2[1].child_2_title, "title child") | |||||
self.assertEqual(doc1.table_field_2[1].child_2_date, getdate("2019-10-30")) | |||||
self.assertEqual(doc1.table_field_2[1].child_2_another_number, 5) | self.assertEqual(doc1.table_field_2[1].child_2_another_number, 5) | ||||
self.assertEqual(doc1.table_field_1_again[0].child_title, 'child title again') | |||||
self.assertEqual(doc1.table_field_1_again[1].child_title, 'child title again 2') | |||||
self.assertEqual(doc1.table_field_1_again[1].child_date, getdate('2021-09-22')) | |||||
self.assertEqual(doc1.table_field_1_again[0].child_title, "child title again") | |||||
self.assertEqual(doc1.table_field_1_again[1].child_title, "child title again 2") | |||||
self.assertEqual(doc1.table_field_1_again[1].child_date, getdate("2021-09-22")) | |||||
self.assertEqual(doc2.description, 'test description 2') | |||||
self.assertEqual(format_duration(doc2.duration), '4d 3h') | |||||
self.assertEqual(doc2.description, "test description 2") | |||||
self.assertEqual(format_duration(doc2.duration), "4d 3h") | |||||
self.assertEqual(doc3.another_number, 5) | self.assertEqual(doc3.another_number, 5) | ||||
self.assertEqual(format_duration(doc3.duration), '5d 5h 45m') | |||||
self.assertEqual(format_duration(doc3.duration), "5d 5h 45m") | |||||
def test_data_import_preview(self): | def test_data_import_preview(self): | ||||
import_file = get_import_file('sample_import_file') | |||||
import_file = get_import_file("sample_import_file") | |||||
data_import = self.get_importer(doctype_name, import_file) | data_import = self.get_importer(doctype_name, import_file) | ||||
preview = data_import.get_preview_from_template() | preview = data_import.get_preview_from_template() | ||||
@@ -58,35 +62,49 @@ class TestImporter(unittest.TestCase): | |||||
# ignored on postgres because myisam doesn't exist on pg | # ignored on postgres because myisam doesn't exist on pg | ||||
@run_only_if(db_type_is.MARIADB) | @run_only_if(db_type_is.MARIADB) | ||||
def test_data_import_without_mandatory_values(self): | def test_data_import_without_mandatory_values(self): | ||||
import_file = get_import_file('sample_import_file_without_mandatory') | |||||
import_file = get_import_file("sample_import_file_without_mandatory") | |||||
data_import = self.get_importer(doctype_name, import_file) | data_import = self.get_importer(doctype_name, import_file) | ||||
frappe.local.message_log = [] | frappe.local.message_log = [] | ||||
data_import.start_import() | data_import.start_import() | ||||
data_import.reload() | data_import.reload() | ||||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "messages", "exception", "docname"], | |||||
import_log = frappe.db.get_all( | |||||
"Data Import Log", | |||||
fields=["row_indexes", "success", "messages", "exception", "docname"], | |||||
filters={"data_import": data_import.name}, | filters={"data_import": data_import.name}, | ||||
order_by="log_index") | |||||
order_by="log_index", | |||||
) | |||||
self.assertEqual(frappe.parse_json(import_log[0]['row_indexes']), [2,3]) | |||||
expected_error = "Error: <strong>Child 1 of DocType for Import</strong> Row #1: Value missing for: Child Title" | |||||
self.assertEqual(frappe.parse_json(frappe.parse_json(import_log[0]['messages'])[0])['message'], expected_error) | |||||
expected_error = "Error: <strong>Child 1 of DocType for Import</strong> Row #2: Value missing for: Child Title" | |||||
self.assertEqual(frappe.parse_json(frappe.parse_json(import_log[0]['messages'])[1])['message'], expected_error) | |||||
self.assertEqual(frappe.parse_json(import_log[0]["row_indexes"]), [2, 3]) | |||||
expected_error = ( | |||||
"Error: <strong>Child 1 of DocType for Import</strong> Row #1: Value missing for: Child Title" | |||||
) | |||||
self.assertEqual( | |||||
frappe.parse_json(frappe.parse_json(import_log[0]["messages"])[0])["message"], expected_error | |||||
) | |||||
expected_error = ( | |||||
"Error: <strong>Child 1 of DocType for Import</strong> Row #2: Value missing for: Child Title" | |||||
) | |||||
self.assertEqual( | |||||
frappe.parse_json(frappe.parse_json(import_log[0]["messages"])[1])["message"], expected_error | |||||
) | |||||
self.assertEqual(frappe.parse_json(import_log[1]['row_indexes']), [4]) | |||||
self.assertEqual(frappe.parse_json(frappe.parse_json(import_log[1]['messages'])[0])['message'], "Title is required") | |||||
self.assertEqual(frappe.parse_json(import_log[1]["row_indexes"]), [4]) | |||||
self.assertEqual( | |||||
frappe.parse_json(frappe.parse_json(import_log[1]["messages"])[0])["message"], | |||||
"Title is required", | |||||
) | |||||
def test_data_import_update(self): | def test_data_import_update(self): | ||||
existing_doc = frappe.get_doc( | existing_doc = frappe.get_doc( | ||||
doctype=doctype_name, | doctype=doctype_name, | ||||
title=frappe.generate_hash(doctype_name, 8), | title=frappe.generate_hash(doctype_name, 8), | ||||
table_field_1=[{'child_title': 'child title to update'}] | |||||
table_field_1=[{"child_title": "child title to update"}], | |||||
) | ) | ||||
existing_doc.save() | existing_doc.save() | ||||
frappe.db.commit() | frappe.db.commit() | ||||
import_file = get_import_file('sample_import_file_for_update') | |||||
import_file = get_import_file("sample_import_file_for_update") | |||||
data_import = self.get_importer(doctype_name, import_file, update=True) | data_import = self.get_importer(doctype_name, import_file, update=True) | ||||
i = Importer(data_import.reference_doctype, data_import=data_import) | i = Importer(data_import.reference_doctype, data_import=data_import) | ||||
@@ -104,15 +122,15 @@ class TestImporter(unittest.TestCase): | |||||
updated_doc = frappe.get_doc(doctype_name, existing_doc.name) | updated_doc = frappe.get_doc(doctype_name, existing_doc.name) | ||||
self.assertEqual(existing_doc.title, updated_doc.title) | self.assertEqual(existing_doc.title, updated_doc.title) | ||||
self.assertEqual(updated_doc.description, 'test description') | |||||
self.assertEqual(updated_doc.table_field_1[0].child_title, 'child title') | |||||
self.assertEqual(updated_doc.description, "test description") | |||||
self.assertEqual(updated_doc.table_field_1[0].child_title, "child title") | |||||
self.assertEqual(updated_doc.table_field_1[0].name, existing_doc.table_field_1[0].name) | self.assertEqual(updated_doc.table_field_1[0].name, existing_doc.table_field_1[0].name) | ||||
self.assertEqual(updated_doc.table_field_1[0].child_description, 'child description') | |||||
self.assertEqual(updated_doc.table_field_1_again[0].child_title, 'child title again') | |||||
self.assertEqual(updated_doc.table_field_1[0].child_description, "child description") | |||||
self.assertEqual(updated_doc.table_field_1_again[0].child_title, "child title again") | |||||
def get_importer(self, doctype, import_file, update=False): | def get_importer(self, doctype, import_file, update=False): | ||||
data_import = frappe.new_doc('Data Import') | |||||
data_import.import_type = 'Insert New Records' if not update else 'Update Existing Records' | |||||
data_import = frappe.new_doc("Data Import") | |||||
data_import.import_type = "Insert New Records" if not update else "Update Existing Records" | |||||
data_import.reference_doctype = doctype | data_import.reference_doctype = doctype | ||||
data_import.import_file = import_file.file_url | data_import.import_file = import_file.file_url | ||||
data_import.insert() | data_import.insert() | ||||
@@ -121,88 +139,109 @@ class TestImporter(unittest.TestCase): | |||||
return data_import | return data_import | ||||
def create_doctype_if_not_exists(doctype_name, force=False): | def create_doctype_if_not_exists(doctype_name, force=False): | ||||
if force: | if force: | ||||
frappe.delete_doc_if_exists('DocType', doctype_name) | |||||
frappe.delete_doc_if_exists('DocType', 'Child 1 of ' + doctype_name) | |||||
frappe.delete_doc_if_exists('DocType', 'Child 2 of ' + doctype_name) | |||||
frappe.delete_doc_if_exists("DocType", doctype_name) | |||||
frappe.delete_doc_if_exists("DocType", "Child 1 of " + doctype_name) | |||||
frappe.delete_doc_if_exists("DocType", "Child 2 of " + doctype_name) | |||||
if frappe.db.exists('DocType', doctype_name): | |||||
if frappe.db.exists("DocType", doctype_name): | |||||
return | return | ||||
# Child Table 1 | # Child Table 1 | ||||
table_1_name = 'Child 1 of ' + doctype_name | |||||
frappe.get_doc({ | |||||
'doctype': 'DocType', | |||||
'name': table_1_name, | |||||
'module': 'Custom', | |||||
'custom': 1, | |||||
'istable': 1, | |||||
'fields': [ | |||||
{'label': 'Child Title', 'fieldname': 'child_title', 'reqd': 1, 'fieldtype': 'Data'}, | |||||
{'label': 'Child Description', 'fieldname': 'child_description', 'fieldtype': 'Small Text'}, | |||||
{'label': 'Child Date', 'fieldname': 'child_date', 'fieldtype': 'Date'}, | |||||
{'label': 'Child Number', 'fieldname': 'child_number', 'fieldtype': 'Int'}, | |||||
{'label': 'Child Number', 'fieldname': 'child_another_number', 'fieldtype': 'Int'}, | |||||
] | |||||
}).insert() | |||||
table_1_name = "Child 1 of " + doctype_name | |||||
frappe.get_doc( | |||||
{ | |||||
"doctype": "DocType", | |||||
"name": table_1_name, | |||||
"module": "Custom", | |||||
"custom": 1, | |||||
"istable": 1, | |||||
"fields": [ | |||||
{"label": "Child Title", "fieldname": "child_title", "reqd": 1, "fieldtype": "Data"}, | |||||
{"label": "Child Description", "fieldname": "child_description", "fieldtype": "Small Text"}, | |||||
{"label": "Child Date", "fieldname": "child_date", "fieldtype": "Date"}, | |||||
{"label": "Child Number", "fieldname": "child_number", "fieldtype": "Int"}, | |||||
{"label": "Child Number", "fieldname": "child_another_number", "fieldtype": "Int"}, | |||||
], | |||||
} | |||||
).insert() | |||||
# Child Table 2 | # Child Table 2 | ||||
table_2_name = 'Child 2 of ' + doctype_name | |||||
frappe.get_doc({ | |||||
'doctype': 'DocType', | |||||
'name': table_2_name, | |||||
'module': 'Custom', | |||||
'custom': 1, | |||||
'istable': 1, | |||||
'fields': [ | |||||
{'label': 'Child 2 Title', 'fieldname': 'child_2_title', 'reqd': 1, 'fieldtype': 'Data'}, | |||||
{'label': 'Child 2 Description', 'fieldname': 'child_2_description', 'fieldtype': 'Small Text'}, | |||||
{'label': 'Child 2 Date', 'fieldname': 'child_2_date', 'fieldtype': 'Date'}, | |||||
{'label': 'Child 2 Number', 'fieldname': 'child_2_number', 'fieldtype': 'Int'}, | |||||
{'label': 'Child 2 Number', 'fieldname': 'child_2_another_number', 'fieldtype': 'Int'}, | |||||
] | |||||
}).insert() | |||||
table_2_name = "Child 2 of " + doctype_name | |||||
frappe.get_doc( | |||||
{ | |||||
"doctype": "DocType", | |||||
"name": table_2_name, | |||||
"module": "Custom", | |||||
"custom": 1, | |||||
"istable": 1, | |||||
"fields": [ | |||||
{"label": "Child 2 Title", "fieldname": "child_2_title", "reqd": 1, "fieldtype": "Data"}, | |||||
{ | |||||
"label": "Child 2 Description", | |||||
"fieldname": "child_2_description", | |||||
"fieldtype": "Small Text", | |||||
}, | |||||
{"label": "Child 2 Date", "fieldname": "child_2_date", "fieldtype": "Date"}, | |||||
{"label": "Child 2 Number", "fieldname": "child_2_number", "fieldtype": "Int"}, | |||||
{"label": "Child 2 Number", "fieldname": "child_2_another_number", "fieldtype": "Int"}, | |||||
], | |||||
} | |||||
).insert() | |||||
# Main Table | # Main Table | ||||
frappe.get_doc({ | |||||
'doctype': 'DocType', | |||||
'name': doctype_name, | |||||
'module': 'Custom', | |||||
'custom': 1, | |||||
'autoname': 'field:title', | |||||
'fields': [ | |||||
{'label': 'Title', 'fieldname': 'title', 'reqd': 1, 'fieldtype': 'Data'}, | |||||
{'label': 'Description', 'fieldname': 'description', 'fieldtype': 'Small Text'}, | |||||
{'label': 'Date', 'fieldname': 'date', 'fieldtype': 'Date'}, | |||||
{'label': 'Duration', 'fieldname': 'duration', 'fieldtype': 'Duration'}, | |||||
{'label': 'Number', 'fieldname': 'number', 'fieldtype': 'Int'}, | |||||
{'label': 'Number', 'fieldname': 'another_number', 'fieldtype': 'Int'}, | |||||
{'label': 'Table Field 1', 'fieldname': 'table_field_1', 'fieldtype': 'Table', 'options': table_1_name}, | |||||
{'label': 'Table Field 2', 'fieldname': 'table_field_2', 'fieldtype': 'Table', 'options': table_2_name}, | |||||
{'label': 'Table Field 1 Again', 'fieldname': 'table_field_1_again', 'fieldtype': 'Table', 'options': table_1_name}, | |||||
], | |||||
'permissions': [ | |||||
{'role': 'System Manager'} | |||||
] | |||||
}).insert() | |||||
frappe.get_doc( | |||||
{ | |||||
"doctype": "DocType", | |||||
"name": doctype_name, | |||||
"module": "Custom", | |||||
"custom": 1, | |||||
"autoname": "field:title", | |||||
"fields": [ | |||||
{"label": "Title", "fieldname": "title", "reqd": 1, "fieldtype": "Data"}, | |||||
{"label": "Description", "fieldname": "description", "fieldtype": "Small Text"}, | |||||
{"label": "Date", "fieldname": "date", "fieldtype": "Date"}, | |||||
{"label": "Duration", "fieldname": "duration", "fieldtype": "Duration"}, | |||||
{"label": "Number", "fieldname": "number", "fieldtype": "Int"}, | |||||
{"label": "Number", "fieldname": "another_number", "fieldtype": "Int"}, | |||||
{ | |||||
"label": "Table Field 1", | |||||
"fieldname": "table_field_1", | |||||
"fieldtype": "Table", | |||||
"options": table_1_name, | |||||
}, | |||||
{ | |||||
"label": "Table Field 2", | |||||
"fieldname": "table_field_2", | |||||
"fieldtype": "Table", | |||||
"options": table_2_name, | |||||
}, | |||||
{ | |||||
"label": "Table Field 1 Again", | |||||
"fieldname": "table_field_1_again", | |||||
"fieldtype": "Table", | |||||
"options": table_1_name, | |||||
}, | |||||
], | |||||
"permissions": [{"role": "System Manager"}], | |||||
} | |||||
).insert() | |||||
def get_import_file(csv_file_name, force=False): | def get_import_file(csv_file_name, force=False): | ||||
file_name = csv_file_name + '.csv' | |||||
_file = frappe.db.exists('File', {'file_name': file_name}) | |||||
file_name = csv_file_name + ".csv" | |||||
_file = frappe.db.exists("File", {"file_name": file_name}) | |||||
if force and _file: | if force and _file: | ||||
frappe.delete_doc_if_exists('File', _file) | |||||
frappe.delete_doc_if_exists("File", _file) | |||||
if frappe.db.exists('File', {'file_name': file_name}): | |||||
f = frappe.get_doc('File', {'file_name': file_name}) | |||||
if frappe.db.exists("File", {"file_name": file_name}): | |||||
f = frappe.get_doc("File", {"file_name": file_name}) | |||||
else: | else: | ||||
full_path = get_csv_file_path(file_name) | full_path = get_csv_file_path(file_name) | ||||
f = frappe.get_doc( | f = frappe.get_doc( | ||||
doctype='File', | |||||
content=frappe.read_file(full_path), | |||||
file_name=file_name, | |||||
is_private=1 | |||||
doctype="File", content=frappe.read_file(full_path), file_name=file_name, is_private=1 | |||||
) | ) | ||||
f.save(ignore_permissions=True) | f.save(ignore_permissions=True) | ||||
@@ -210,4 +249,4 @@ def get_import_file(csv_file_name, force=False): | |||||
def get_csv_file_path(file_name): | def get_csv_file_path(file_name): | ||||
return frappe.get_app_path('frappe', 'core', 'doctype', 'data_import', 'fixtures', file_name) | |||||
return frappe.get_app_path("frappe", "core", "doctype", "data_import", "fixtures", file_name) |
@@ -4,5 +4,6 @@ | |||||
# import frappe | # import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class DataImportLog(Document): | class DataImportLog(Document): | ||||
pass | pass |
@@ -4,5 +4,6 @@ | |||||
# import frappe | # import frappe | ||||
import unittest | import unittest | ||||
class TestDataImportLog(unittest.TestCase): | class TestDataImportLog(unittest.TestCase): | ||||
pass | pass |
@@ -1,3 +1,2 @@ | |||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
@@ -2,19 +2,24 @@ | |||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class DefaultValue(Document): | class DefaultValue(Document): | ||||
pass | pass | ||||
def on_doctype_update(): | def on_doctype_update(): | ||||
"""Create indexes for `tabDefaultValue` on `(parent, defkey)`""" | """Create indexes for `tabDefaultValue` on `(parent, defkey)`""" | ||||
frappe.db.commit() | frappe.db.commit() | ||||
frappe.db.add_index(doctype='DefaultValue', | |||||
fields=['parent', 'defkey'], | |||||
index_name='defaultvalue_parent_defkey_index') | |||||
frappe.db.add_index( | |||||
doctype="DefaultValue", | |||||
fields=["parent", "defkey"], | |||||
index_name="defaultvalue_parent_defkey_index", | |||||
) | |||||
frappe.db.add_index(doctype='DefaultValue', | |||||
fields=['parent', 'parenttype'], | |||||
index_name='defaultvalue_parent_parenttype_index') | |||||
frappe.db.add_index( | |||||
doctype="DefaultValue", | |||||
fields=["parent", "parenttype"], | |||||
index_name="defaultvalue_parent_parenttype_index", | |||||
) |
@@ -2,11 +2,12 @@ | |||||
# Copyright (c) 2015, Frappe Technologies and contributors | # Copyright (c) 2015, Frappe Technologies and contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | |||||
import json | import json | ||||
import frappe | |||||
from frappe import _ | |||||
from frappe.desk.doctype.bulk_update.bulk_update import show_progress | from frappe.desk.doctype.bulk_update.bulk_update import show_progress | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe import _ | |||||
class DeletedDocument(Document): | class DeletedDocument(Document): | ||||
@@ -15,7 +16,7 @@ class DeletedDocument(Document): | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def restore(name, alert=True): | def restore(name, alert=True): | ||||
deleted = frappe.get_doc('Deleted Document', name) | |||||
deleted = frappe.get_doc("Deleted Document", name) | |||||
if deleted.restored: | if deleted.restored: | ||||
frappe.throw(_("Document {0} Already Restored").format(name), exc=frappe.DocumentAlreadyRestored) | frappe.throw(_("Document {0} Already Restored").format(name), exc=frappe.DocumentAlreadyRestored) | ||||
@@ -29,20 +30,20 @@ def restore(name, alert=True): | |||||
doc.docstatus = 0 | doc.docstatus = 0 | ||||
doc.insert() | doc.insert() | ||||
doc.add_comment('Edit', _('restored {0} as {1}').format(deleted.deleted_name, doc.name)) | |||||
doc.add_comment("Edit", _("restored {0} as {1}").format(deleted.deleted_name, doc.name)) | |||||
deleted.new_name = doc.name | deleted.new_name = doc.name | ||||
deleted.restored = 1 | deleted.restored = 1 | ||||
deleted.db_update() | deleted.db_update() | ||||
if alert: | if alert: | ||||
frappe.msgprint(_('Document Restored')) | |||||
frappe.msgprint(_("Document Restored")) | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def bulk_restore(docnames): | def bulk_restore(docnames): | ||||
docnames = frappe.parse_json(docnames) | docnames = frappe.parse_json(docnames) | ||||
message = _('Restoring Deleted Document') | |||||
message = _("Restoring Deleted Document") | |||||
restored, invalid, failed = [], [], [] | restored, invalid, failed = [], [], [] | ||||
for i, d in enumerate(docnames): | for i, d in enumerate(docnames): | ||||
@@ -61,8 +62,4 @@ def bulk_restore(docnames): | |||||
failed.append(d) | failed.append(d) | ||||
frappe.db.rollback() | frappe.db.rollback() | ||||
return { | |||||
"restored": restored, | |||||
"invalid": invalid, | |||||
"failed": failed | |||||
} | |||||
return {"restored": restored, "invalid": invalid, "failed": failed} |
@@ -1,10 +1,12 @@ | |||||
# -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
# Copyright (c) 2015, Frappe Technologies and Contributors | # Copyright (c) 2015, Frappe Technologies and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | |||||
import unittest | import unittest | ||||
import frappe | |||||
# test_records = frappe.get_test_records('Deleted Document') | # test_records = frappe.get_test_records('Deleted Document') | ||||
class TestDeletedDocument(unittest.TestCase): | class TestDeletedDocument(unittest.TestCase): | ||||
pass | pass |
@@ -1,3 +1,2 @@ | |||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
@@ -4,28 +4,28 @@ | |||||
import frappe | import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class DocField(Document): | class DocField(Document): | ||||
def get_link_doctype(self): | def get_link_doctype(self): | ||||
'''Returns the Link doctype for the docfield (if applicable) | |||||
"""Returns the Link doctype for the docfield (if applicable) | |||||
if fieldtype is Link: Returns "options" | if fieldtype is Link: Returns "options" | ||||
if fieldtype is Table MultiSelect: Returns "options" of the Link field in the Child Table | if fieldtype is Table MultiSelect: Returns "options" of the Link field in the Child Table | ||||
''' | |||||
if self.fieldtype == 'Link': | |||||
""" | |||||
if self.fieldtype == "Link": | |||||
return self.options | return self.options | ||||
if self.fieldtype == 'Table MultiSelect': | |||||
if self.fieldtype == "Table MultiSelect": | |||||
table_doctype = self.options | table_doctype = self.options | ||||
link_doctype = frappe.db.get_value('DocField', { | |||||
'fieldtype': 'Link', | |||||
'parenttype': 'DocType', | |||||
'parent': table_doctype, | |||||
'in_list_view': 1 | |||||
}, 'options') | |||||
link_doctype = frappe.db.get_value( | |||||
"DocField", | |||||
{"fieldtype": "Link", "parenttype": "DocType", "parent": table_doctype, "in_list_view": 1}, | |||||
"options", | |||||
) | |||||
return link_doctype | return link_doctype | ||||
def get_select_options(self): | def get_select_options(self): | ||||
if self.fieldtype == 'Select': | |||||
options = self.options or '' | |||||
return [d for d in options.split('\n') if d] | |||||
if self.fieldtype == "Select": | |||||
options = self.options or "" | |||||
return [d for d in options.split("\n") if d] |
@@ -1,3 +1,2 @@ | |||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
@@ -2,8 +2,8 @@ | |||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class DocPerm(Document): | class DocPerm(Document): | ||||
pass | pass |
@@ -2,12 +2,13 @@ | |||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | import frappe | ||||
from frappe.model.document import Document | |||||
from frappe import _ | from frappe import _ | ||||
from frappe.utils import get_fullname, cint | |||||
from frappe.model.document import Document | |||||
from frappe.utils import cint, get_fullname | |||||
exclude_from_linked_with = True | exclude_from_linked_with = True | ||||
class DocShare(Document): | class DocShare(Document): | ||||
no_feed_on_delete = True | no_feed_on_delete = True | ||||
@@ -36,15 +37,21 @@ class DocShare(Document): | |||||
frappe.throw(_("User is mandatory for Share"), frappe.MandatoryError) | frappe.throw(_("User is mandatory for Share"), frappe.MandatoryError) | ||||
def check_share_permission(self): | def check_share_permission(self): | ||||
if (not self.flags.ignore_share_permission and | |||||
not frappe.has_permission(self.share_doctype, "share", self.get_doc())): | |||||
if not self.flags.ignore_share_permission and not frappe.has_permission( | |||||
self.share_doctype, "share", self.get_doc() | |||||
): | |||||
frappe.throw(_('You need to have "Share" permission'), frappe.PermissionError) | frappe.throw(_('You need to have "Share" permission'), frappe.PermissionError) | ||||
def check_is_submittable(self): | def check_is_submittable(self): | ||||
if self.submit and not cint(frappe.db.get_value("DocType", self.share_doctype, "is_submittable")): | |||||
frappe.throw(_("Cannot share {0} with submit permission as the doctype {1} is not submittable").format( | |||||
frappe.bold(self.share_name), frappe.bold(self.share_doctype))) | |||||
if self.submit and not cint( | |||||
frappe.db.get_value("DocType", self.share_doctype, "is_submittable") | |||||
): | |||||
frappe.throw( | |||||
_("Cannot share {0} with submit permission as the doctype {1} is not submittable").format( | |||||
frappe.bold(self.share_name), frappe.bold(self.share_doctype) | |||||
) | |||||
) | |||||
def after_insert(self): | def after_insert(self): | ||||
doc = self.get_doc() | doc = self.get_doc() | ||||
@@ -53,14 +60,21 @@ class DocShare(Document): | |||||
if self.everyone: | if self.everyone: | ||||
doc.add_comment("Shared", _("{0} shared this document with everyone").format(owner)) | doc.add_comment("Shared", _("{0} shared this document with everyone").format(owner)) | ||||
else: | else: | ||||
doc.add_comment("Shared", _("{0} shared this document with {1}").format(owner, get_fullname(self.user))) | |||||
doc.add_comment( | |||||
"Shared", _("{0} shared this document with {1}").format(owner, get_fullname(self.user)) | |||||
) | |||||
def on_trash(self): | def on_trash(self): | ||||
if not self.flags.ignore_share_permission: | if not self.flags.ignore_share_permission: | ||||
self.check_share_permission() | self.check_share_permission() | ||||
self.get_doc().add_comment("Unshared", | |||||
_("{0} un-shared this document with {1}").format(get_fullname(self.owner), get_fullname(self.user))) | |||||
self.get_doc().add_comment( | |||||
"Unshared", | |||||
_("{0} un-shared this document with {1}").format( | |||||
get_fullname(self.owner), get_fullname(self.user) | |||||
), | |||||
) | |||||
def on_doctype_update(): | def on_doctype_update(): | ||||
"""Add index in `tabDocShare` for `(user, share_doctype)`""" | """Add index in `tabDocShare` for `(user, share_doctype)`""" | ||||
@@ -1,20 +1,26 @@ | |||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import unittest | |||||
import frappe | import frappe | ||||
import frappe.share | import frappe.share | ||||
import unittest | |||||
from frappe.automation.doctype.auto_repeat.test_auto_repeat import create_submittable_doctype | from frappe.automation.doctype.auto_repeat.test_auto_repeat import create_submittable_doctype | ||||
test_dependencies = ['User'] | |||||
test_dependencies = ["User"] | |||||
class TestDocShare(unittest.TestCase): | class TestDocShare(unittest.TestCase): | ||||
def setUp(self): | def setUp(self): | ||||
self.user = "test@example.com" | self.user = "test@example.com" | ||||
self.event = frappe.get_doc({"doctype": "Event", | |||||
"subject": "test share event", | |||||
"starts_on": "2015-01-01 10:00:00", | |||||
"event_type": "Private"}).insert() | |||||
self.event = frappe.get_doc( | |||||
{ | |||||
"doctype": "Event", | |||||
"subject": "test share event", | |||||
"starts_on": "2015-01-01 10:00:00", | |||||
"event_type": "Private", | |||||
} | |||||
).insert() | |||||
def tearDown(self): | def tearDown(self): | ||||
frappe.set_user("Administrator") | frappe.set_user("Administrator") | ||||
@@ -98,7 +104,9 @@ class TestDocShare(unittest.TestCase): | |||||
doctype = "Test DocShare with Submit" | doctype = "Test DocShare with Submit" | ||||
create_submittable_doctype(doctype, submit_perms=0) | create_submittable_doctype(doctype, submit_perms=0) | ||||
submittable_doc = frappe.get_doc(dict(doctype=doctype, test="test docshare with submit")).insert() | |||||
submittable_doc = frappe.get_doc( | |||||
dict(doctype=doctype, test="test docshare with submit") | |||||
).insert() | |||||
frappe.set_user(self.user) | frappe.set_user(self.user) | ||||
self.assertFalse(frappe.has_permission(doctype, "submit", user=self.user)) | self.assertFalse(frappe.has_permission(doctype, "submit", user=self.user)) | ||||
@@ -107,10 +115,14 @@ class TestDocShare(unittest.TestCase): | |||||
frappe.share.add(doctype, submittable_doc.name, self.user, submit=1) | frappe.share.add(doctype, submittable_doc.name, self.user, submit=1) | ||||
frappe.set_user(self.user) | frappe.set_user(self.user) | ||||
self.assertTrue(frappe.has_permission(doctype, "submit", doc=submittable_doc.name, user=self.user)) | |||||
self.assertTrue( | |||||
frappe.has_permission(doctype, "submit", doc=submittable_doc.name, user=self.user) | |||||
) | |||||
# test cascade | # test cascade | ||||
self.assertTrue(frappe.has_permission(doctype, "read", doc=submittable_doc.name, user=self.user)) | self.assertTrue(frappe.has_permission(doctype, "read", doc=submittable_doc.name, user=self.user)) | ||||
self.assertTrue(frappe.has_permission(doctype, "write", doc=submittable_doc.name, user=self.user)) | |||||
self.assertTrue( | |||||
frappe.has_permission(doctype, "write", doc=submittable_doc.name, user=self.user) | |||||
) | |||||
frappe.share.remove(doctype, submittable_doc.name, self.user) | frappe.share.remove(doctype, submittable_doc.name, self.user) |
@@ -1,3 +1,2 @@ | |||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
@@ -1,7 +1,8 @@ | |||||
import frappe | import frappe | ||||
from frappe.desk.utils import slug | from frappe.desk.utils import slug | ||||
def execute(): | def execute(): | ||||
for doctype in frappe.get_all('DocType', ['name', 'route'], dict(istable=0)): | |||||
if not doctype.route: | |||||
frappe.db.set_value('DocType', doctype.name, 'route', slug(doctype.name), update_modified = False) | |||||
for doctype in frappe.get_all("DocType", ["name", "route"], dict(istable=0)): | |||||
if not doctype.route: | |||||
frappe.db.set_value("DocType", doctype.name, "route", slug(doctype.name), update_modified=False) |
@@ -1,21 +1,24 @@ | |||||
# -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | |||||
import unittest | import unittest | ||||
from frappe.core.doctype.doctype.doctype import (UniqueFieldnameError, | |||||
IllegalMandatoryError, | |||||
import frappe | |||||
from frappe.core.doctype.doctype.doctype import ( | |||||
CannotIndexedError, | |||||
DoctypeLinkError, | DoctypeLinkError, | ||||
WrongOptionsDoctypeLinkError, | |||||
HiddenAndMandatoryWithoutDefaultError, | HiddenAndMandatoryWithoutDefaultError, | ||||
CannotIndexedError, | |||||
IllegalMandatoryError, | |||||
InvalidFieldNameError, | InvalidFieldNameError, | ||||
validate_links_table_fieldnames) | |||||
UniqueFieldnameError, | |||||
WrongOptionsDoctypeLinkError, | |||||
validate_links_table_fieldnames, | |||||
) | |||||
# test_records = frappe.get_test_records('DocType') | # test_records = frappe.get_test_records('DocType') | ||||
class TestDocType(unittest.TestCase): | |||||
class TestDocType(unittest.TestCase): | |||||
def tearDown(self): | def tearDown(self): | ||||
frappe.db.rollback() | frappe.db.rollback() | ||||
@@ -23,7 +26,10 @@ class TestDocType(unittest.TestCase): | |||||
self.assertRaises(frappe.NameError, new_doctype("_Some DocType").insert) | self.assertRaises(frappe.NameError, new_doctype("_Some DocType").insert) | ||||
self.assertRaises(frappe.NameError, new_doctype("8Some DocType").insert) | self.assertRaises(frappe.NameError, new_doctype("8Some DocType").insert) | ||||
self.assertRaises(frappe.NameError, new_doctype("Some (DocType)").insert) | self.assertRaises(frappe.NameError, new_doctype("Some (DocType)").insert) | ||||
self.assertRaises(frappe.NameError, new_doctype("Some Doctype with a name whose length is more than 61 characters").insert) | |||||
self.assertRaises( | |||||
frappe.NameError, | |||||
new_doctype("Some Doctype with a name whose length is more than 61 characters").insert, | |||||
) | |||||
for name in ("Some DocType", "Some_DocType", "Some-DocType"): | for name in ("Some DocType", "Some_DocType", "Some-DocType"): | ||||
if frappe.db.exists("DocType", name): | if frappe.db.exists("DocType", name): | ||||
frappe.delete_doc("DocType", name) | frappe.delete_doc("DocType", name) | ||||
@@ -86,19 +92,33 @@ class TestDocType(unittest.TestCase): | |||||
def test_all_depends_on_fields_conditions(self): | def test_all_depends_on_fields_conditions(self): | ||||
import re | import re | ||||
docfields = frappe.get_all("DocField", | |||||
or_filters={ | |||||
"ifnull(depends_on, '')": ("!=", ''), | |||||
"ifnull(collapsible_depends_on, '')": ("!=", ''), | |||||
"ifnull(mandatory_depends_on, '')": ("!=", ''), | |||||
"ifnull(read_only_depends_on, '')": ("!=", '') | |||||
docfields = frappe.get_all( | |||||
"DocField", | |||||
or_filters={ | |||||
"ifnull(depends_on, '')": ("!=", ""), | |||||
"ifnull(collapsible_depends_on, '')": ("!=", ""), | |||||
"ifnull(mandatory_depends_on, '')": ("!=", ""), | |||||
"ifnull(read_only_depends_on, '')": ("!=", ""), | |||||
}, | }, | ||||
fields=["parent", "depends_on", "collapsible_depends_on", "mandatory_depends_on",\ | |||||
"read_only_depends_on", "fieldname", "fieldtype"]) | |||||
fields=[ | |||||
"parent", | |||||
"depends_on", | |||||
"collapsible_depends_on", | |||||
"mandatory_depends_on", | |||||
"read_only_depends_on", | |||||
"fieldname", | |||||
"fieldtype", | |||||
], | |||||
) | |||||
pattern = r'[\w\.:_]+\s*={1}\s*[\w\.@\'"]+' | pattern = r'[\w\.:_]+\s*={1}\s*[\w\.@\'"]+' | ||||
for field in docfields: | for field in docfields: | ||||
for depends_on in ["depends_on", "collapsible_depends_on", "mandatory_depends_on", "read_only_depends_on"]: | |||||
for depends_on in [ | |||||
"depends_on", | |||||
"collapsible_depends_on", | |||||
"mandatory_depends_on", | |||||
"read_only_depends_on", | |||||
]: | |||||
condition = field.get(depends_on) | condition = field.get(depends_on) | ||||
if condition: | if condition: | ||||
self.assertFalse(re.match(pattern, condition)) | self.assertFalse(re.match(pattern, condition)) | ||||
@@ -108,18 +128,18 @@ class TestDocType(unittest.TestCase): | |||||
valid_data_field_options = frappe.model.data_field_options + ("",) | valid_data_field_options = frappe.model.data_field_options + ("",) | ||||
invalid_data_field_options = ("Invalid Option 1", frappe.utils.random_string(5)) | invalid_data_field_options = ("Invalid Option 1", frappe.utils.random_string(5)) | ||||
for field_option in (valid_data_field_options + invalid_data_field_options): | |||||
test_doctype = frappe.get_doc({ | |||||
"doctype": "DocType", | |||||
"name": doctype_name, | |||||
"module": "Core", | |||||
"custom": 1, | |||||
"fields": [{ | |||||
"fieldname": "{0}_field".format(field_option), | |||||
"fieldtype": "Data", | |||||
"options": field_option | |||||
}] | |||||
}) | |||||
for field_option in valid_data_field_options + invalid_data_field_options: | |||||
test_doctype = frappe.get_doc( | |||||
{ | |||||
"doctype": "DocType", | |||||
"name": doctype_name, | |||||
"module": "Core", | |||||
"custom": 1, | |||||
"fields": [ | |||||
{"fieldname": "{0}_field".format(field_option), "fieldtype": "Data", "options": field_option} | |||||
], | |||||
} | |||||
) | |||||
if field_option in invalid_data_field_options: | if field_option in invalid_data_field_options: | ||||
# assert that only data options in frappe.model.data_field_options are valid | # assert that only data options in frappe.model.data_field_options are valid | ||||
@@ -130,45 +150,29 @@ class TestDocType(unittest.TestCase): | |||||
test_doctype.delete() | test_doctype.delete() | ||||
def test_sync_field_order(self): | def test_sync_field_order(self): | ||||
from frappe.modules.import_file import get_file_path | |||||
import os | import os | ||||
from frappe.modules.import_file import get_file_path | |||||
# create test doctype | # create test doctype | ||||
test_doctype = frappe.get_doc({ | |||||
"doctype": "DocType", | |||||
"module": "Core", | |||||
"fields": [ | |||||
{ | |||||
"label": "Field 1", | |||||
"fieldname": "field_1", | |||||
"fieldtype": "Data" | |||||
}, | |||||
{ | |||||
"label": "Field 2", | |||||
"fieldname": "field_2", | |||||
"fieldtype": "Data" | |||||
}, | |||||
{ | |||||
"label": "Field 3", | |||||
"fieldname": "field_3", | |||||
"fieldtype": "Data" | |||||
}, | |||||
{ | |||||
"label": "Field 4", | |||||
"fieldname": "field_4", | |||||
"fieldtype": "Data" | |||||
} | |||||
], | |||||
"permissions": [{ | |||||
"role": "System Manager", | |||||
"read": 1 | |||||
}], | |||||
"name": "Test Field Order DocType", | |||||
"__islocal": 1 | |||||
}) | |||||
test_doctype = frappe.get_doc( | |||||
{ | |||||
"doctype": "DocType", | |||||
"module": "Core", | |||||
"fields": [ | |||||
{"label": "Field 1", "fieldname": "field_1", "fieldtype": "Data"}, | |||||
{"label": "Field 2", "fieldname": "field_2", "fieldtype": "Data"}, | |||||
{"label": "Field 3", "fieldname": "field_3", "fieldtype": "Data"}, | |||||
{"label": "Field 4", "fieldname": "field_4", "fieldtype": "Data"}, | |||||
], | |||||
"permissions": [{"role": "System Manager", "read": 1}], | |||||
"name": "Test Field Order DocType", | |||||
"__islocal": 1, | |||||
} | |||||
) | |||||
path = get_file_path(test_doctype.module, test_doctype.doctype, test_doctype.name) | path = get_file_path(test_doctype.module, test_doctype.doctype, test_doctype.name) | ||||
initial_fields_order = ['field_1', 'field_2', 'field_3', 'field_4'] | |||||
initial_fields_order = ["field_1", "field_2", "field_3", "field_4"] | |||||
frappe.delete_doc_if_exists("DocType", "Test Field Order DocType") | frappe.delete_doc_if_exists("DocType", "Test Field Order DocType") | ||||
if os.path.isfile(path): | if os.path.isfile(path): | ||||
@@ -181,14 +185,18 @@ class TestDocType(unittest.TestCase): | |||||
# assert that field_order list is being created with the default order | # assert that field_order list is being created with the default order | ||||
test_doctype_json = frappe.get_file_json(path) | test_doctype_json = frappe.get_file_json(path) | ||||
self.assertTrue(test_doctype_json.get("field_order")) | self.assertTrue(test_doctype_json.get("field_order")) | ||||
self.assertEqual(len(test_doctype_json['fields']), len(test_doctype_json['field_order'])) | |||||
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], test_doctype_json['field_order']) | |||||
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], initial_fields_order) | |||||
self.assertListEqual(test_doctype_json['field_order'], initial_fields_order) | |||||
self.assertEqual(len(test_doctype_json["fields"]), len(test_doctype_json["field_order"])) | |||||
self.assertListEqual( | |||||
[f["fieldname"] for f in test_doctype_json["fields"]], test_doctype_json["field_order"] | |||||
) | |||||
self.assertListEqual( | |||||
[f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order | |||||
) | |||||
self.assertListEqual(test_doctype_json["field_order"], initial_fields_order) | |||||
# remove field_order to test reload_doc/sync/migrate is backwards compatible without field_order | # remove field_order to test reload_doc/sync/migrate is backwards compatible without field_order | ||||
del test_doctype_json['field_order'] | |||||
with open(path, 'w+') as txtfile: | |||||
del test_doctype_json["field_order"] | |||||
with open(path, "w+") as txtfile: | |||||
txtfile.write(frappe.as_json(test_doctype_json)) | txtfile.write(frappe.as_json(test_doctype_json)) | ||||
# assert that field_order is actually removed from the json file | # assert that field_order is actually removed from the json file | ||||
@@ -203,10 +211,14 @@ class TestDocType(unittest.TestCase): | |||||
test_doctype.save() | test_doctype.save() | ||||
test_doctype_json = frappe.get_file_json(path) | test_doctype_json = frappe.get_file_json(path) | ||||
self.assertTrue(test_doctype_json.get("field_order")) | self.assertTrue(test_doctype_json.get("field_order")) | ||||
self.assertEqual(len(test_doctype_json['fields']), len(test_doctype_json['field_order'])) | |||||
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], test_doctype_json['field_order']) | |||||
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], initial_fields_order) | |||||
self.assertListEqual(test_doctype_json['field_order'], initial_fields_order) | |||||
self.assertEqual(len(test_doctype_json["fields"]), len(test_doctype_json["field_order"])) | |||||
self.assertListEqual( | |||||
[f["fieldname"] for f in test_doctype_json["fields"]], test_doctype_json["field_order"] | |||||
) | |||||
self.assertListEqual( | |||||
[f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order | |||||
) | |||||
self.assertListEqual(test_doctype_json["field_order"], initial_fields_order) | |||||
# reorder fields: swap row 1 and 3 | # reorder fields: swap row 1 and 3 | ||||
test_doctype.fields[0], test_doctype.fields[2] = test_doctype.fields[2], test_doctype.fields[0] | test_doctype.fields[0], test_doctype.fields[2] = test_doctype.fields[2], test_doctype.fields[0] | ||||
@@ -216,25 +228,30 @@ class TestDocType(unittest.TestCase): | |||||
# assert that reordering fields only affects `field_order` rather than `fields` attr | # assert that reordering fields only affects `field_order` rather than `fields` attr | ||||
test_doctype.save() | test_doctype.save() | ||||
test_doctype_json = frappe.get_file_json(path) | test_doctype_json = frappe.get_file_json(path) | ||||
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], initial_fields_order) | |||||
self.assertListEqual(test_doctype_json['field_order'], ['field_3', 'field_2', 'field_1', 'field_4']) | |||||
self.assertListEqual( | |||||
[f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order | |||||
) | |||||
self.assertListEqual( | |||||
test_doctype_json["field_order"], ["field_3", "field_2", "field_1", "field_4"] | |||||
) | |||||
# reorder `field_order` in the json file: swap row 2 and 4 | # reorder `field_order` in the json file: swap row 2 and 4 | ||||
test_doctype_json['field_order'][1], test_doctype_json['field_order'][3] = test_doctype_json['field_order'][3], test_doctype_json['field_order'][1] | |||||
with open(path, 'w+') as txtfile: | |||||
test_doctype_json["field_order"][1], test_doctype_json["field_order"][3] = ( | |||||
test_doctype_json["field_order"][3], | |||||
test_doctype_json["field_order"][1], | |||||
) | |||||
with open(path, "w+") as txtfile: | |||||
txtfile.write(frappe.as_json(test_doctype_json)) | txtfile.write(frappe.as_json(test_doctype_json)) | ||||
# assert that reordering `field_order` from json file is reflected in DocType upon migrate/sync | # assert that reordering `field_order` from json file is reflected in DocType upon migrate/sync | ||||
frappe.reload_doctype(test_doctype.name, force=True) | frappe.reload_doctype(test_doctype.name, force=True) | ||||
test_doctype.reload() | test_doctype.reload() | ||||
self.assertListEqual([f.fieldname for f in test_doctype.fields], ['field_3', 'field_4', 'field_1', 'field_2']) | |||||
self.assertListEqual( | |||||
[f.fieldname for f in test_doctype.fields], ["field_3", "field_4", "field_1", "field_2"] | |||||
) | |||||
# insert row in the middle and remove first row (field 3) | # insert row in the middle and remove first row (field 3) | ||||
test_doctype.append("fields", { | |||||
"label": "Field 5", | |||||
"fieldname": "field_5", | |||||
"fieldtype": "Data" | |||||
}) | |||||
test_doctype.append("fields", {"label": "Field 5", "fieldname": "field_5", "fieldtype": "Data"}) | |||||
test_doctype.fields[4], test_doctype.fields[3] = test_doctype.fields[3], test_doctype.fields[4] | test_doctype.fields[4], test_doctype.fields[3] = test_doctype.fields[3], test_doctype.fields[4] | ||||
test_doctype.fields[3], test_doctype.fields[2] = test_doctype.fields[2], test_doctype.fields[3] | test_doctype.fields[3], test_doctype.fields[2] = test_doctype.fields[2], test_doctype.fields[3] | ||||
test_doctype.remove(test_doctype.fields[0]) | test_doctype.remove(test_doctype.fields[0]) | ||||
@@ -243,115 +260,121 @@ class TestDocType(unittest.TestCase): | |||||
test_doctype.save() | test_doctype.save() | ||||
test_doctype_json = frappe.get_file_json(path) | test_doctype_json = frappe.get_file_json(path) | ||||
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], ['field_1', 'field_2', 'field_4', 'field_5']) | |||||
self.assertListEqual(test_doctype_json['field_order'], ['field_4', 'field_5', 'field_1', 'field_2']) | |||||
self.assertListEqual( | |||||
[f["fieldname"] for f in test_doctype_json["fields"]], | |||||
["field_1", "field_2", "field_4", "field_5"], | |||||
) | |||||
self.assertListEqual( | |||||
test_doctype_json["field_order"], ["field_4", "field_5", "field_1", "field_2"] | |||||
) | |||||
except: | except: | ||||
raise | raise | ||||
finally: | finally: | ||||
frappe.flags.allow_doctype_export = 0 | frappe.flags.allow_doctype_export = 0 | ||||
def test_unique_field_name_for_two_fields(self): | def test_unique_field_name_for_two_fields(self): | ||||
doc = new_doctype('Test Unique Field') | |||||
field_1 = doc.append('fields', {}) | |||||
field_1.fieldname = 'some_fieldname_1' | |||||
field_1.fieldtype = 'Data' | |||||
doc = new_doctype("Test Unique Field") | |||||
field_1 = doc.append("fields", {}) | |||||
field_1.fieldname = "some_fieldname_1" | |||||
field_1.fieldtype = "Data" | |||||
field_2 = doc.append('fields', {}) | |||||
field_2.fieldname = 'some_fieldname_1' | |||||
field_2.fieldtype = 'Data' | |||||
field_2 = doc.append("fields", {}) | |||||
field_2.fieldname = "some_fieldname_1" | |||||
field_2.fieldtype = "Data" | |||||
self.assertRaises(UniqueFieldnameError, doc.insert) | self.assertRaises(UniqueFieldnameError, doc.insert) | ||||
def test_fieldname_is_not_name(self): | def test_fieldname_is_not_name(self): | ||||
doc = new_doctype('Test Name Field') | |||||
field_1 = doc.append('fields', {}) | |||||
field_1.label = 'Name' | |||||
field_1.fieldtype = 'Data' | |||||
doc = new_doctype("Test Name Field") | |||||
field_1 = doc.append("fields", {}) | |||||
field_1.label = "Name" | |||||
field_1.fieldtype = "Data" | |||||
doc.insert() | doc.insert() | ||||
self.assertEqual(doc.fields[1].fieldname, "name1") | self.assertEqual(doc.fields[1].fieldname, "name1") | ||||
doc.fields[1].fieldname = 'name' | |||||
doc.fields[1].fieldname = "name" | |||||
self.assertRaises(InvalidFieldNameError, doc.save) | self.assertRaises(InvalidFieldNameError, doc.save) | ||||
def test_illegal_mandatory_validation(self): | def test_illegal_mandatory_validation(self): | ||||
doc = new_doctype('Test Illegal mandatory') | |||||
field_1 = doc.append('fields', {}) | |||||
field_1.fieldname = 'some_fieldname_1' | |||||
field_1.fieldtype = 'Section Break' | |||||
doc = new_doctype("Test Illegal mandatory") | |||||
field_1 = doc.append("fields", {}) | |||||
field_1.fieldname = "some_fieldname_1" | |||||
field_1.fieldtype = "Section Break" | |||||
field_1.reqd = 1 | field_1.reqd = 1 | ||||
self.assertRaises(IllegalMandatoryError, doc.insert) | self.assertRaises(IllegalMandatoryError, doc.insert) | ||||
def test_link_with_wrong_and_no_options(self): | def test_link_with_wrong_and_no_options(self): | ||||
doc = new_doctype('Test link') | |||||
field_1 = doc.append('fields', {}) | |||||
field_1.fieldname = 'some_fieldname_1' | |||||
field_1.fieldtype = 'Link' | |||||
doc = new_doctype("Test link") | |||||
field_1 = doc.append("fields", {}) | |||||
field_1.fieldname = "some_fieldname_1" | |||||
field_1.fieldtype = "Link" | |||||
self.assertRaises(DoctypeLinkError, doc.insert) | self.assertRaises(DoctypeLinkError, doc.insert) | ||||
field_1.options = 'wrongdoctype' | |||||
field_1.options = "wrongdoctype" | |||||
self.assertRaises(WrongOptionsDoctypeLinkError, doc.insert) | self.assertRaises(WrongOptionsDoctypeLinkError, doc.insert) | ||||
def test_hidden_and_mandatory_without_default(self): | def test_hidden_and_mandatory_without_default(self): | ||||
doc = new_doctype('Test hidden and mandatory') | |||||
field_1 = doc.append('fields', {}) | |||||
field_1.fieldname = 'some_fieldname_1' | |||||
field_1.fieldtype = 'Data' | |||||
doc = new_doctype("Test hidden and mandatory") | |||||
field_1 = doc.append("fields", {}) | |||||
field_1.fieldname = "some_fieldname_1" | |||||
field_1.fieldtype = "Data" | |||||
field_1.reqd = 1 | field_1.reqd = 1 | ||||
field_1.hidden = 1 | field_1.hidden = 1 | ||||
self.assertRaises(HiddenAndMandatoryWithoutDefaultError, doc.insert) | self.assertRaises(HiddenAndMandatoryWithoutDefaultError, doc.insert) | ||||
def test_field_can_not_be_indexed_validation(self): | def test_field_can_not_be_indexed_validation(self): | ||||
doc = new_doctype('Test index') | |||||
field_1 = doc.append('fields', {}) | |||||
field_1.fieldname = 'some_fieldname_1' | |||||
field_1.fieldtype = 'Long Text' | |||||
doc = new_doctype("Test index") | |||||
field_1 = doc.append("fields", {}) | |||||
field_1.fieldname = "some_fieldname_1" | |||||
field_1.fieldtype = "Long Text" | |||||
field_1.search_index = 1 | field_1.search_index = 1 | ||||
self.assertRaises(CannotIndexedError, doc.insert) | self.assertRaises(CannotIndexedError, doc.insert) | ||||
def test_cancel_link_doctype(self): | def test_cancel_link_doctype(self): | ||||
import json | import json | ||||
from frappe.desk.form.linked_with import get_submitted_linked_docs, cancel_all_linked_docs | |||||
#create doctype | |||||
link_doc = new_doctype('Test Linked Doctype') | |||||
from frappe.desk.form.linked_with import cancel_all_linked_docs, get_submitted_linked_docs | |||||
# create doctype | |||||
link_doc = new_doctype("Test Linked Doctype") | |||||
link_doc.is_submittable = 1 | link_doc.is_submittable = 1 | ||||
for data in link_doc.get('permissions'): | |||||
for data in link_doc.get("permissions"): | |||||
data.submit = 1 | data.submit = 1 | ||||
data.cancel = 1 | data.cancel = 1 | ||||
link_doc.insert() | link_doc.insert() | ||||
doc = new_doctype('Test Doctype') | |||||
doc = new_doctype("Test Doctype") | |||||
doc.is_submittable = 1 | doc.is_submittable = 1 | ||||
field_2 = doc.append('fields', {}) | |||||
field_2.label = 'Test Linked Doctype' | |||||
field_2.fieldname = 'test_linked_doctype' | |||||
field_2.fieldtype = 'Link' | |||||
field_2.options = 'Test Linked Doctype' | |||||
for data in link_doc.get('permissions'): | |||||
field_2 = doc.append("fields", {}) | |||||
field_2.label = "Test Linked Doctype" | |||||
field_2.fieldname = "test_linked_doctype" | |||||
field_2.fieldtype = "Link" | |||||
field_2.options = "Test Linked Doctype" | |||||
for data in link_doc.get("permissions"): | |||||
data.submit = 1 | data.submit = 1 | ||||
data.cancel = 1 | data.cancel = 1 | ||||
doc.insert() | doc.insert() | ||||
# create doctype data | # create doctype data | ||||
data_link_doc = frappe.new_doc('Test Linked Doctype') | |||||
data_link_doc.some_fieldname = 'Data1' | |||||
data_link_doc = frappe.new_doc("Test Linked Doctype") | |||||
data_link_doc.some_fieldname = "Data1" | |||||
data_link_doc.insert() | data_link_doc.insert() | ||||
data_link_doc.save() | data_link_doc.save() | ||||
data_link_doc.submit() | data_link_doc.submit() | ||||
data_doc = frappe.new_doc('Test Doctype') | |||||
data_doc.some_fieldname = 'Data1' | |||||
data_doc = frappe.new_doc("Test Doctype") | |||||
data_doc.some_fieldname = "Data1" | |||||
data_doc.test_linked_doctype = data_link_doc.name | data_doc.test_linked_doctype = data_link_doc.name | ||||
data_doc.insert() | data_doc.insert() | ||||
data_doc.save() | data_doc.save() | ||||
data_doc.submit() | data_doc.submit() | ||||
docs = get_submitted_linked_docs(link_doc.name, data_link_doc.name) | docs = get_submitted_linked_docs(link_doc.name, data_link_doc.name) | ||||
dump_docs = json.dumps(docs.get('docs')) | |||||
dump_docs = json.dumps(docs.get("docs")) | |||||
cancel_all_linked_docs(dump_docs) | cancel_all_linked_docs(dump_docs) | ||||
data_link_doc.cancel() | data_link_doc.cancel() | ||||
data_doc.load_from_db() | data_doc.load_from_db() | ||||
@@ -369,69 +392,70 @@ class TestDocType(unittest.TestCase): | |||||
def test_ignore_cancelation_of_linked_doctype_during_cancel(self): | def test_ignore_cancelation_of_linked_doctype_during_cancel(self): | ||||
import json | import json | ||||
from frappe.desk.form.linked_with import get_submitted_linked_docs, cancel_all_linked_docs | |||||
#create linked doctype | |||||
link_doc = new_doctype('Test Linked Doctype 1') | |||||
from frappe.desk.form.linked_with import cancel_all_linked_docs, get_submitted_linked_docs | |||||
# create linked doctype | |||||
link_doc = new_doctype("Test Linked Doctype 1") | |||||
link_doc.is_submittable = 1 | link_doc.is_submittable = 1 | ||||
for data in link_doc.get('permissions'): | |||||
for data in link_doc.get("permissions"): | |||||
data.submit = 1 | data.submit = 1 | ||||
data.cancel = 1 | data.cancel = 1 | ||||
link_doc.insert() | link_doc.insert() | ||||
#create first parent doctype | |||||
test_doc_1 = new_doctype('Test Doctype 1') | |||||
# create first parent doctype | |||||
test_doc_1 = new_doctype("Test Doctype 1") | |||||
test_doc_1.is_submittable = 1 | test_doc_1.is_submittable = 1 | ||||
field_2 = test_doc_1.append('fields', {}) | |||||
field_2.label = 'Test Linked Doctype 1' | |||||
field_2.fieldname = 'test_linked_doctype_a' | |||||
field_2.fieldtype = 'Link' | |||||
field_2.options = 'Test Linked Doctype 1' | |||||
field_2 = test_doc_1.append("fields", {}) | |||||
field_2.label = "Test Linked Doctype 1" | |||||
field_2.fieldname = "test_linked_doctype_a" | |||||
field_2.fieldtype = "Link" | |||||
field_2.options = "Test Linked Doctype 1" | |||||
for data in test_doc_1.get('permissions'): | |||||
for data in test_doc_1.get("permissions"): | |||||
data.submit = 1 | data.submit = 1 | ||||
data.cancel = 1 | data.cancel = 1 | ||||
test_doc_1.insert() | test_doc_1.insert() | ||||
#crete second parent doctype | |||||
doc = new_doctype('Test Doctype 2') | |||||
# crete second parent doctype | |||||
doc = new_doctype("Test Doctype 2") | |||||
doc.is_submittable = 1 | doc.is_submittable = 1 | ||||
field_2 = doc.append('fields', {}) | |||||
field_2.label = 'Test Linked Doctype 1' | |||||
field_2.fieldname = 'test_linked_doctype_a' | |||||
field_2.fieldtype = 'Link' | |||||
field_2.options = 'Test Linked Doctype 1' | |||||
field_2 = doc.append("fields", {}) | |||||
field_2.label = "Test Linked Doctype 1" | |||||
field_2.fieldname = "test_linked_doctype_a" | |||||
field_2.fieldtype = "Link" | |||||
field_2.options = "Test Linked Doctype 1" | |||||
for data in link_doc.get('permissions'): | |||||
for data in link_doc.get("permissions"): | |||||
data.submit = 1 | data.submit = 1 | ||||
data.cancel = 1 | data.cancel = 1 | ||||
doc.insert() | doc.insert() | ||||
# create doctype data | # create doctype data | ||||
data_link_doc_1 = frappe.new_doc('Test Linked Doctype 1') | |||||
data_link_doc_1.some_fieldname = 'Data1' | |||||
data_link_doc_1 = frappe.new_doc("Test Linked Doctype 1") | |||||
data_link_doc_1.some_fieldname = "Data1" | |||||
data_link_doc_1.insert() | data_link_doc_1.insert() | ||||
data_link_doc_1.save() | data_link_doc_1.save() | ||||
data_link_doc_1.submit() | data_link_doc_1.submit() | ||||
data_doc_2 = frappe.new_doc('Test Doctype 1') | |||||
data_doc_2.some_fieldname = 'Data1' | |||||
data_doc_2 = frappe.new_doc("Test Doctype 1") | |||||
data_doc_2.some_fieldname = "Data1" | |||||
data_doc_2.test_linked_doctype_a = data_link_doc_1.name | data_doc_2.test_linked_doctype_a = data_link_doc_1.name | ||||
data_doc_2.insert() | data_doc_2.insert() | ||||
data_doc_2.save() | data_doc_2.save() | ||||
data_doc_2.submit() | data_doc_2.submit() | ||||
data_doc = frappe.new_doc('Test Doctype 2') | |||||
data_doc.some_fieldname = 'Data1' | |||||
data_doc = frappe.new_doc("Test Doctype 2") | |||||
data_doc.some_fieldname = "Data1" | |||||
data_doc.test_linked_doctype_a = data_link_doc_1.name | data_doc.test_linked_doctype_a = data_link_doc_1.name | ||||
data_doc.insert() | data_doc.insert() | ||||
data_doc.save() | data_doc.save() | ||||
data_doc.submit() | data_doc.submit() | ||||
docs = get_submitted_linked_docs(link_doc.name, data_link_doc_1.name) | docs = get_submitted_linked_docs(link_doc.name, data_link_doc_1.name) | ||||
dump_docs = json.dumps(docs.get('docs')) | |||||
dump_docs = json.dumps(docs.get("docs")) | |||||
cancel_all_linked_docs(dump_docs, ignore_doctypes_on_cancel_all=["Test Doctype 2"]) | cancel_all_linked_docs(dump_docs, ignore_doctypes_on_cancel_all=["Test Doctype 2"]) | ||||
@@ -442,10 +466,10 @@ class TestDocType(unittest.TestCase): | |||||
data_doc_2.load_from_db() | data_doc_2.load_from_db() | ||||
self.assertEqual(data_link_doc_1.docstatus, 2) | self.assertEqual(data_link_doc_1.docstatus, 2) | ||||
#linked doc is canceled | |||||
# linked doc is canceled | |||||
self.assertEqual(data_doc_2.docstatus, 2) | self.assertEqual(data_doc_2.docstatus, 2) | ||||
#ignored doctype 2 during cancel | |||||
# ignored doctype 2 during cancel | |||||
self.assertEqual(data_doc.docstatus, 1) | self.assertEqual(data_doc.docstatus, 1) | ||||
# delete doctype record | # delete doctype record | ||||
@@ -464,42 +488,35 @@ class TestDocType(unittest.TestCase): | |||||
doc = new_doctype("Test Links Table Validation") | doc = new_doctype("Test Links Table Validation") | ||||
# check valid data | # check valid data | ||||
doc.append("links", { | |||||
'link_doctype': "User", | |||||
'link_fieldname': "first_name" | |||||
}) | |||||
validate_links_table_fieldnames(doc) # no error | |||||
doc.links = [] # reset links table | |||||
doc.append("links", {"link_doctype": "User", "link_fieldname": "first_name"}) | |||||
validate_links_table_fieldnames(doc) # no error | |||||
doc.links = [] # reset links table | |||||
# check invalid doctype | # check invalid doctype | ||||
doc.append("links", { | |||||
'link_doctype': "User2", | |||||
'link_fieldname': "first_name" | |||||
}) | |||||
doc.append("links", {"link_doctype": "User2", "link_fieldname": "first_name"}) | |||||
self.assertRaises(frappe.DoesNotExistError, validate_links_table_fieldnames, doc) | self.assertRaises(frappe.DoesNotExistError, validate_links_table_fieldnames, doc) | ||||
doc.links = [] # reset links table | |||||
doc.links = [] # reset links table | |||||
# check invalid fieldname | # check invalid fieldname | ||||
doc.append("links", { | |||||
'link_doctype': "User", | |||||
'link_fieldname': "a_field_that_does_not_exists" | |||||
}) | |||||
doc.append("links", {"link_doctype": "User", "link_fieldname": "a_field_that_does_not_exists"}) | |||||
self.assertRaises(InvalidFieldNameError, validate_links_table_fieldnames, doc) | self.assertRaises(InvalidFieldNameError, validate_links_table_fieldnames, doc) | ||||
def test_create_virtual_doctype(self): | def test_create_virtual_doctype(self): | ||||
"""Test virtual DOcTYpe.""" | """Test virtual DOcTYpe.""" | ||||
virtual_doc = new_doctype('Test Virtual Doctype') | |||||
virtual_doc = new_doctype("Test Virtual Doctype") | |||||
virtual_doc.is_virtual = 1 | virtual_doc.is_virtual = 1 | ||||
virtual_doc.insert() | virtual_doc.insert() | ||||
virtual_doc.save() | virtual_doc.save() | ||||
doc = frappe.get_doc("DocType", "Test Virtual Doctype") | doc = frappe.get_doc("DocType", "Test Virtual Doctype") | ||||
self.assertEqual(doc.is_virtual, 1) | self.assertEqual(doc.is_virtual, 1) | ||||
self.assertFalse(frappe.db.table_exists('Test Virtual Doctype')) | |||||
self.assertFalse(frappe.db.table_exists("Test Virtual Doctype")) | |||||
def test_default_fieldname(self): | def test_default_fieldname(self): | ||||
fields = [{"label": "title", "fieldname": "title", "fieldtype": "Data", "default": "{some_fieldname}"}] | |||||
fields = [ | |||||
{"label": "title", "fieldname": "title", "fieldtype": "Data", "default": "{some_fieldname}"} | |||||
] | |||||
dt = new_doctype("DT with default field", fields=fields) | dt = new_doctype("DT with default field", fields=fields) | ||||
dt.insert() | dt.insert() | ||||
@@ -521,28 +538,34 @@ class TestDocType(unittest.TestCase): | |||||
dt.delete(ignore_permissions=True) | dt.delete(ignore_permissions=True) | ||||
def new_doctype(name, unique=0, depends_on='', fields=None, autoincremented=False): | |||||
doc = frappe.get_doc({ | |||||
"doctype": "DocType", | |||||
"module": "Core", | |||||
"custom": 1, | |||||
"fields": [{ | |||||
"label": "Some Field", | |||||
"fieldname": "some_fieldname", | |||||
"fieldtype": "Data", | |||||
"unique": unique, | |||||
"depends_on": depends_on, | |||||
}], | |||||
"permissions": [{ | |||||
"role": "System Manager", | |||||
"read": 1, | |||||
}], | |||||
"name": name, | |||||
"autoname": "autoincrement" if autoincremented else "" | |||||
}) | |||||
def new_doctype(name, unique=0, depends_on="", fields=None, autoincremented=False): | |||||
doc = frappe.get_doc( | |||||
{ | |||||
"doctype": "DocType", | |||||
"module": "Core", | |||||
"custom": 1, | |||||
"fields": [ | |||||
{ | |||||
"label": "Some Field", | |||||
"fieldname": "some_fieldname", | |||||
"fieldtype": "Data", | |||||
"unique": unique, | |||||
"depends_on": depends_on, | |||||
} | |||||
], | |||||
"permissions": [ | |||||
{ | |||||
"role": "System Manager", | |||||
"read": 1, | |||||
} | |||||
], | |||||
"name": name, | |||||
"autoname": "autoincrement" if autoincremented else "", | |||||
} | |||||
) | |||||
if fields: | if fields: | ||||
for f in fields: | for f in fields: | ||||
doc.append('fields', f) | |||||
doc.append("fields", f) | |||||
return doc | return doc |
@@ -5,5 +5,6 @@ | |||||
# import frappe | # import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class DocTypeAction(Document): | class DocTypeAction(Document): | ||||
pass | pass |
@@ -5,5 +5,6 @@ | |||||
# import frappe | # import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class DocTypeLink(Document): | class DocTypeLink(Document): | ||||
pass | pass |
@@ -4,5 +4,6 @@ | |||||
# import frappe | # import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class DocTypeState(Document): | class DocTypeState(Document): | ||||
pass | pass |
@@ -3,10 +3,11 @@ | |||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | import frappe | ||||
from frappe import _ | |||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe.utils.data import evaluate_filters | |||||
from frappe.model.naming import parse_naming_series | from frappe.model.naming import parse_naming_series | ||||
from frappe import _ | |||||
from frappe.utils.data import evaluate_filters | |||||
class DocumentNamingRule(Document): | class DocumentNamingRule(Document): | ||||
def validate(self): | def validate(self): | ||||
@@ -17,23 +18,30 @@ class DocumentNamingRule(Document): | |||||
docfields = [x.fieldname for x in frappe.get_meta(self.document_type).fields] | docfields = [x.fieldname for x in frappe.get_meta(self.document_type).fields] | ||||
for condition in self.conditions: | for condition in self.conditions: | ||||
if condition.field not in docfields: | if condition.field not in docfields: | ||||
frappe.throw(_("{0} is not a field of doctype {1}").format(frappe.bold(condition.field), frappe.bold(self.document_type))) | |||||
frappe.throw( | |||||
_("{0} is not a field of doctype {1}").format( | |||||
frappe.bold(condition.field), frappe.bold(self.document_type) | |||||
) | |||||
) | |||||
def apply(self, doc): | def apply(self, doc): | ||||
''' | |||||
""" | |||||
Apply naming rules for the given document. Will set `name` if the rule is matched. | Apply naming rules for the given document. Will set `name` if the rule is matched. | ||||
''' | |||||
""" | |||||
if self.conditions: | if self.conditions: | ||||
if not evaluate_filters(doc, [(self.document_type, d.field, d.condition, d.value) for d in self.conditions]): | |||||
if not evaluate_filters( | |||||
doc, [(self.document_type, d.field, d.condition, d.value) for d in self.conditions] | |||||
): | |||||
return | return | ||||
counter = frappe.db.get_value(self.doctype, self.name, 'counter', for_update=True) or 0 | |||||
counter = frappe.db.get_value(self.doctype, self.name, "counter", for_update=True) or 0 | |||||
naming_series = parse_naming_series(self.prefix, doc=doc) | naming_series = parse_naming_series(self.prefix, doc=doc) | ||||
doc.name = naming_series + ('%0'+str(self.prefix_digits)+'d') % (counter + 1) | |||||
frappe.db.set_value(self.doctype, self.name, 'counter', counter + 1) | |||||
doc.name = naming_series + ("%0" + str(self.prefix_digits) + "d") % (counter + 1) | |||||
frappe.db.set_value(self.doctype, self.name, "counter", counter + 1) | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def update_current(name, new_counter): | def update_current(name, new_counter): | ||||
frappe.only_for('System Manager') | |||||
frappe.db.set_value('Document Naming Rule', name, 'counter', new_counter) | |||||
frappe.only_for("System Manager") | |||||
frappe.db.set_value("Document Naming Rule", name, "counter", new_counter) |
@@ -1,79 +1,68 @@ | |||||
# -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
# Copyright (c) 2020, Frappe Technologies and Contributors | # Copyright (c) 2020, Frappe Technologies and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | |||||
import unittest | import unittest | ||||
import frappe | |||||
class TestDocumentNamingRule(unittest.TestCase): | class TestDocumentNamingRule(unittest.TestCase): | ||||
def test_naming_rule_by_series(self): | def test_naming_rule_by_series(self): | ||||
naming_rule = frappe.get_doc(dict( | |||||
doctype = 'Document Naming Rule', | |||||
document_type = 'ToDo', | |||||
prefix = 'test-todo-', | |||||
prefix_digits = 5 | |||||
)).insert() | |||||
naming_rule = frappe.get_doc( | |||||
dict(doctype="Document Naming Rule", document_type="ToDo", prefix="test-todo-", prefix_digits=5) | |||||
).insert() | |||||
todo = frappe.get_doc(dict( | |||||
doctype = 'ToDo', | |||||
description = 'Is this my name ' + frappe.generate_hash() | |||||
)).insert() | |||||
todo = frappe.get_doc( | |||||
dict(doctype="ToDo", description="Is this my name " + frappe.generate_hash()) | |||||
).insert() | |||||
self.assertEqual(todo.name, 'test-todo-00001') | |||||
self.assertEqual(todo.name, "test-todo-00001") | |||||
naming_rule.delete() | naming_rule.delete() | ||||
todo.delete() | todo.delete() | ||||
def test_naming_rule_by_condition(self): | def test_naming_rule_by_condition(self): | ||||
naming_rule = frappe.get_doc(dict( | |||||
doctype = 'Document Naming Rule', | |||||
document_type = 'ToDo', | |||||
prefix = 'test-high-', | |||||
prefix_digits = 5, | |||||
priority = 10, | |||||
conditions = [dict( | |||||
field = 'priority', | |||||
condition = '=', | |||||
value = 'High' | |||||
)] | |||||
)).insert() | |||||
naming_rule = frappe.get_doc( | |||||
dict( | |||||
doctype="Document Naming Rule", | |||||
document_type="ToDo", | |||||
prefix="test-high-", | |||||
prefix_digits=5, | |||||
priority=10, | |||||
conditions=[dict(field="priority", condition="=", value="High")], | |||||
) | |||||
).insert() | |||||
# another rule | # another rule | ||||
naming_rule_1 = frappe.copy_doc(naming_rule) | naming_rule_1 = frappe.copy_doc(naming_rule) | ||||
naming_rule_1.prefix = 'test-medium-' | |||||
naming_rule_1.conditions[0].value = 'Medium' | |||||
naming_rule_1.prefix = "test-medium-" | |||||
naming_rule_1.conditions[0].value = "Medium" | |||||
naming_rule_1.insert() | naming_rule_1.insert() | ||||
# default rule with low priority - should not get applied for rules | # default rule with low priority - should not get applied for rules | ||||
# with higher priority | # with higher priority | ||||
naming_rule_2 = frappe.copy_doc(naming_rule) | naming_rule_2 = frappe.copy_doc(naming_rule) | ||||
naming_rule_2.prefix = 'test-low-' | |||||
naming_rule_2.prefix = "test-low-" | |||||
naming_rule_2.priority = 0 | naming_rule_2.priority = 0 | ||||
naming_rule_2.conditions = [] | naming_rule_2.conditions = [] | ||||
naming_rule_2.insert() | naming_rule_2.insert() | ||||
todo = frappe.get_doc( | |||||
dict(doctype="ToDo", priority="High", description="Is this my name " + frappe.generate_hash()) | |||||
).insert() | |||||
todo = frappe.get_doc(dict( | |||||
doctype = 'ToDo', | |||||
priority = 'High', | |||||
description = 'Is this my name ' + frappe.generate_hash() | |||||
)).insert() | |||||
todo_1 = frappe.get_doc(dict( | |||||
doctype = 'ToDo', | |||||
priority = 'Medium', | |||||
description = 'Is this my name ' + frappe.generate_hash() | |||||
)).insert() | |||||
todo_1 = frappe.get_doc( | |||||
dict(doctype="ToDo", priority="Medium", description="Is this my name " + frappe.generate_hash()) | |||||
).insert() | |||||
todo_2 = frappe.get_doc(dict( | |||||
doctype = 'ToDo', | |||||
priority = 'Low', | |||||
description = 'Is this my name ' + frappe.generate_hash() | |||||
)).insert() | |||||
todo_2 = frappe.get_doc( | |||||
dict(doctype="ToDo", priority="Low", description="Is this my name " + frappe.generate_hash()) | |||||
).insert() | |||||
try: | try: | ||||
self.assertEqual(todo.name, 'test-high-00001') | |||||
self.assertEqual(todo_1.name, 'test-medium-00001') | |||||
self.assertEqual(todo_2.name, 'test-low-00001') | |||||
self.assertEqual(todo.name, "test-high-00001") | |||||
self.assertEqual(todo_1.name, "test-medium-00001") | |||||
self.assertEqual(todo_2.name, "test-low-00001") | |||||
finally: | finally: | ||||
naming_rule.delete() | naming_rule.delete() | ||||
naming_rule_1.delete() | naming_rule_1.delete() | ||||
@@ -5,5 +5,6 @@ | |||||
# import frappe | # import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class DocumentNamingRuleCondition(Document): | class DocumentNamingRuleCondition(Document): | ||||
pass | pass |
@@ -4,5 +4,6 @@ | |||||
# import frappe | # import frappe | ||||
import unittest | import unittest | ||||
class TestDocumentNamingRuleCondition(unittest.TestCase): | class TestDocumentNamingRuleCondition(unittest.TestCase): | ||||
pass | pass |
@@ -3,16 +3,17 @@ | |||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | import frappe | ||||
from frappe.model.document import Document | |||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields | from frappe.custom.doctype.custom_field.custom_field import create_custom_fields | ||||
from frappe.model.document import Document | |||||
class Domain(Document): | class Domain(Document): | ||||
'''Domain documents are created automatically when DocTypes | |||||
"""Domain documents are created automatically when DocTypes | |||||
with "Restricted" domains are imported during | with "Restricted" domains are imported during | ||||
installation or migration''' | |||||
installation or migration""" | |||||
def setup_domain(self): | def setup_domain(self): | ||||
'''Setup domain icons, permissions, custom fields etc.''' | |||||
"""Setup domain icons, permissions, custom fields etc.""" | |||||
self.setup_data() | self.setup_data() | ||||
self.setup_roles() | self.setup_roles() | ||||
self.setup_properties() | self.setup_properties() | ||||
@@ -31,20 +32,20 @@ class Domain(Document): | |||||
frappe.get_attr(self.data.on_setup)() | frappe.get_attr(self.data.on_setup)() | ||||
def remove_domain(self): | def remove_domain(self): | ||||
'''Unset domain settings''' | |||||
"""Unset domain settings""" | |||||
self.setup_data() | self.setup_data() | ||||
if self.data.restricted_roles: | if self.data.restricted_roles: | ||||
for role_name in self.data.restricted_roles: | for role_name in self.data.restricted_roles: | ||||
if frappe.db.exists('Role', role_name): | |||||
role = frappe.get_doc('Role', role_name) | |||||
if frappe.db.exists("Role", role_name): | |||||
role = frappe.get_doc("Role", role_name) | |||||
role.disabled = 1 | role.disabled = 1 | ||||
role.save() | role.save() | ||||
self.remove_custom_field() | self.remove_custom_field() | ||||
def remove_custom_field(self): | def remove_custom_field(self): | ||||
'''Remove custom_fields when disabling domain''' | |||||
"""Remove custom_fields when disabling domain""" | |||||
if self.data.custom_fields: | if self.data.custom_fields: | ||||
for doctype in self.data.custom_fields: | for doctype in self.data.custom_fields: | ||||
custom_fields = self.data.custom_fields[doctype] | custom_fields = self.data.custom_fields[doctype] | ||||
@@ -54,47 +55,48 @@ class Domain(Document): | |||||
custom_fields = [custom_fields] | custom_fields = [custom_fields] | ||||
for custom_field_detail in custom_fields: | for custom_field_detail in custom_fields: | ||||
custom_field_name = frappe.db.get_value('Custom Field', | |||||
dict(dt=doctype, fieldname=custom_field_detail.get('fieldname'))) | |||||
custom_field_name = frappe.db.get_value( | |||||
"Custom Field", dict(dt=doctype, fieldname=custom_field_detail.get("fieldname")) | |||||
) | |||||
if custom_field_name: | if custom_field_name: | ||||
frappe.delete_doc('Custom Field', custom_field_name) | |||||
frappe.delete_doc("Custom Field", custom_field_name) | |||||
def setup_roles(self): | def setup_roles(self): | ||||
'''Enable roles that are restricted to this domain''' | |||||
"""Enable roles that are restricted to this domain""" | |||||
if self.data.restricted_roles: | if self.data.restricted_roles: | ||||
user = frappe.get_doc("User", frappe.session.user) | user = frappe.get_doc("User", frappe.session.user) | ||||
for role_name in self.data.restricted_roles: | for role_name in self.data.restricted_roles: | ||||
user.append("roles", {"role": role_name}) | user.append("roles", {"role": role_name}) | ||||
if not frappe.db.get_value('Role', role_name): | |||||
frappe.get_doc(dict(doctype='Role', role_name=role_name)).insert() | |||||
if not frappe.db.get_value("Role", role_name): | |||||
frappe.get_doc(dict(doctype="Role", role_name=role_name)).insert() | |||||
continue | continue | ||||
role = frappe.get_doc('Role', role_name) | |||||
role = frappe.get_doc("Role", role_name) | |||||
role.disabled = 0 | role.disabled = 0 | ||||
role.save() | role.save() | ||||
user.save() | user.save() | ||||
def setup_data(self, domain=None): | def setup_data(self, domain=None): | ||||
'''Load domain info via hooks''' | |||||
"""Load domain info via hooks""" | |||||
self.data = frappe.get_domain_data(self.name) | self.data = frappe.get_domain_data(self.name) | ||||
def get_domain_data(self, module): | def get_domain_data(self, module): | ||||
return frappe.get_attr(frappe.get_hooks('domains')[self.name] + '.data') | |||||
return frappe.get_attr(frappe.get_hooks("domains")[self.name] + ".data") | |||||
def set_default_portal_role(self): | def set_default_portal_role(self): | ||||
'''Set default portal role based on domain''' | |||||
if self.data.get('default_portal_role'): | |||||
frappe.db.set_value('Portal Settings', None, 'default_role', | |||||
self.data.get('default_portal_role')) | |||||
"""Set default portal role based on domain""" | |||||
if self.data.get("default_portal_role"): | |||||
frappe.db.set_value( | |||||
"Portal Settings", None, "default_role", self.data.get("default_portal_role") | |||||
) | |||||
def setup_properties(self): | def setup_properties(self): | ||||
if self.data.properties: | if self.data.properties: | ||||
for args in self.data.properties: | for args in self.data.properties: | ||||
frappe.make_property_setter(args) | frappe.make_property_setter(args) | ||||
def set_values(self): | def set_values(self): | ||||
'''set values based on `data.set_value`''' | |||||
"""set values based on `data.set_value`""" | |||||
if self.data.set_value: | if self.data.set_value: | ||||
for args in self.data.set_value: | for args in self.data.set_value: | ||||
frappe.reload_doctype(args[0]) | frappe.reload_doctype(args[0]) | ||||
@@ -103,19 +105,27 @@ class Domain(Document): | |||||
doc.save() | doc.save() | ||||
def setup_sidebar_items(self): | def setup_sidebar_items(self): | ||||
'''Enable / disable sidebar items''' | |||||
"""Enable / disable sidebar items""" | |||||
if self.data.allow_sidebar_items: | if self.data.allow_sidebar_items: | ||||
# disable all | # disable all | ||||
frappe.db.sql('update `tabPortal Menu Item` set enabled=0') | |||||
frappe.db.sql("update `tabPortal Menu Item` set enabled=0") | |||||
# enable | # enable | ||||
frappe.db.sql('''update `tabPortal Menu Item` set enabled=1 | |||||
where route in ({0})'''.format(', '.join('"{0}"'.format(d) for d in self.data.allow_sidebar_items))) | |||||
frappe.db.sql( | |||||
"""update `tabPortal Menu Item` set enabled=1 | |||||
where route in ({0})""".format( | |||||
", ".join('"{0}"'.format(d) for d in self.data.allow_sidebar_items) | |||||
) | |||||
) | |||||
if self.data.remove_sidebar_items: | if self.data.remove_sidebar_items: | ||||
# disable all | # disable all | ||||
frappe.db.sql('update `tabPortal Menu Item` set enabled=1') | |||||
frappe.db.sql("update `tabPortal Menu Item` set enabled=1") | |||||
# enable | # enable | ||||
frappe.db.sql('''update `tabPortal Menu Item` set enabled=0 | |||||
where route in ({0})'''.format(', '.join('"{0}"'.format(d) for d in self.data.remove_sidebar_items))) | |||||
frappe.db.sql( | |||||
"""update `tabPortal Menu Item` set enabled=0 | |||||
where route in ({0})""".format( | |||||
", ".join('"{0}"'.format(d) for d in self.data.remove_sidebar_items) | |||||
) | |||||
) |
@@ -1,8 +1,10 @@ | |||||
# -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
# Copyright (c) 2017, Frappe Technologies and Contributors | # Copyright (c) 2017, Frappe Technologies and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | |||||
import unittest | import unittest | ||||
import frappe | |||||
class TestDomain(unittest.TestCase): | class TestDomain(unittest.TestCase): | ||||
pass | pass |
@@ -5,13 +5,14 @@ | |||||
import frappe | import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class DomainSettings(Document): | class DomainSettings(Document): | ||||
def set_active_domains(self, domains): | def set_active_domains(self, domains): | ||||
active_domains = [d.domain for d in self.active_domains] | active_domains = [d.domain for d in self.active_domains] | ||||
added = False | added = False | ||||
for d in domains: | for d in domains: | ||||
if not d in active_domains: | if not d in active_domains: | ||||
self.append('active_domains', dict(domain=d)) | |||||
self.append("active_domains", dict(domain=d)) | |||||
added = True | added = True | ||||
if added: | if added: | ||||
@@ -22,49 +23,52 @@ class DomainSettings(Document): | |||||
# set the flag to update the the desktop icons of all domains | # set the flag to update the the desktop icons of all domains | ||||
if i >= 1: | if i >= 1: | ||||
frappe.flags.keep_desktop_icons = True | frappe.flags.keep_desktop_icons = True | ||||
domain = frappe.get_doc('Domain', d.domain) | |||||
domain = frappe.get_doc("Domain", d.domain) | |||||
domain.setup_domain() | domain.setup_domain() | ||||
self.restrict_roles_and_modules() | self.restrict_roles_and_modules() | ||||
frappe.clear_cache() | frappe.clear_cache() | ||||
def restrict_roles_and_modules(self): | def restrict_roles_and_modules(self): | ||||
'''Disable all restricted roles and set `restrict_to_domain` property in Module Def''' | |||||
"""Disable all restricted roles and set `restrict_to_domain` property in Module Def""" | |||||
active_domains = frappe.get_active_domains() | active_domains = frappe.get_active_domains() | ||||
all_domains = list((frappe.get_hooks('domains') or {})) | |||||
all_domains = list((frappe.get_hooks("domains") or {})) | |||||
def remove_role(role): | def remove_role(role): | ||||
frappe.db.delete("Has Role", {"role": role}) | frappe.db.delete("Has Role", {"role": role}) | ||||
frappe.set_value('Role', role, 'disabled', 1) | |||||
frappe.set_value("Role", role, "disabled", 1) | |||||
for domain in all_domains: | for domain in all_domains: | ||||
data = frappe.get_domain_data(domain) | data = frappe.get_domain_data(domain) | ||||
if not frappe.db.get_value('Domain', domain): | |||||
frappe.get_doc(dict(doctype='Domain', domain=domain)).insert() | |||||
if 'modules' in data: | |||||
for module in data.get('modules'): | |||||
frappe.db.set_value('Module Def', module, 'restrict_to_domain', domain) | |||||
if 'restricted_roles' in data: | |||||
for role in data['restricted_roles']: | |||||
if not frappe.db.get_value('Role', role): | |||||
frappe.get_doc(dict(doctype='Role', role_name=role)).insert() | |||||
frappe.db.set_value('Role', role, 'restrict_to_domain', domain) | |||||
if not frappe.db.get_value("Domain", domain): | |||||
frappe.get_doc(dict(doctype="Domain", domain=domain)).insert() | |||||
if "modules" in data: | |||||
for module in data.get("modules"): | |||||
frappe.db.set_value("Module Def", module, "restrict_to_domain", domain) | |||||
if "restricted_roles" in data: | |||||
for role in data["restricted_roles"]: | |||||
if not frappe.db.get_value("Role", role): | |||||
frappe.get_doc(dict(doctype="Role", role_name=role)).insert() | |||||
frappe.db.set_value("Role", role, "restrict_to_domain", domain) | |||||
if domain not in active_domains: | if domain not in active_domains: | ||||
remove_role(role) | remove_role(role) | ||||
if 'custom_fields' in data: | |||||
if "custom_fields" in data: | |||||
if domain not in active_domains: | if domain not in active_domains: | ||||
inactive_domain = frappe.get_doc("Domain", domain) | inactive_domain = frappe.get_doc("Domain", domain) | ||||
inactive_domain.setup_data() | inactive_domain.setup_data() | ||||
inactive_domain.remove_custom_field() | inactive_domain.remove_custom_field() | ||||
def get_active_domains(): | def get_active_domains(): | ||||
""" get the domains set in the Domain Settings as active domain """ | |||||
"""get the domains set in the Domain Settings as active domain""" | |||||
def _get_active_domains(): | def _get_active_domains(): | ||||
domains = frappe.get_all("Has Domain", filters={ "parent": "Domain Settings" }, | |||||
fields=["domain"], distinct=True) | |||||
domains = frappe.get_all( | |||||
"Has Domain", filters={"parent": "Domain Settings"}, fields=["domain"], distinct=True | |||||
) | |||||
active_domains = [row.get("domain") for row in domains] | active_domains = [row.get("domain") for row in domains] | ||||
active_domains.append("") | active_domains.append("") | ||||
@@ -72,14 +76,16 @@ def get_active_domains(): | |||||
return frappe.cache().get_value("active_domains", _get_active_domains) | return frappe.cache().get_value("active_domains", _get_active_domains) | ||||
def get_active_modules(): | def get_active_modules(): | ||||
""" get the active modules from Module Def""" | |||||
"""get the active modules from Module Def""" | |||||
def _get_active_modules(): | def _get_active_modules(): | ||||
active_modules = [] | active_modules = [] | ||||
active_domains = get_active_domains() | active_domains = get_active_domains() | ||||
for m in frappe.get_all("Module Def", fields=['name', 'restrict_to_domain']): | |||||
for m in frappe.get_all("Module Def", fields=["name", "restrict_to_domain"]): | |||||
if (not m.restrict_to_domain) or (m.restrict_to_domain in active_domains): | if (not m.restrict_to_domain) or (m.restrict_to_domain in active_domains): | ||||
active_modules.append(m.name) | active_modules.append(m.name) | ||||
return active_modules | return active_modules | ||||
return frappe.cache().get_value('active_modules', _get_active_modules) | |||||
return frappe.cache().get_value("active_modules", _get_active_modules) |
@@ -5,12 +5,15 @@ | |||||
import frappe | import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class DynamicLink(Document): | class DynamicLink(Document): | ||||
pass | pass | ||||
def on_doctype_update(): | def on_doctype_update(): | ||||
frappe.db.add_index("Dynamic Link", ["link_doctype", "link_name"]) | frappe.db.add_index("Dynamic Link", ["link_doctype", "link_name"]) | ||||
def deduplicate_dynamic_links(doc): | def deduplicate_dynamic_links(doc): | ||||
links, duplicate = [], False | links, duplicate = [], False | ||||
for l in doc.links or []: | for l in doc.links or []: | ||||
@@ -23,4 +26,4 @@ def deduplicate_dynamic_links(doc): | |||||
if duplicate: | if duplicate: | ||||
doc.links = [] | doc.links = [] | ||||
for l in links: | for l in links: | ||||
doc.append('links', dict(link_doctype=l[0], link_name=l[1])) | |||||
doc.append("links", dict(link_doctype=l[0], link_name=l[1])) |
@@ -5,19 +5,24 @@ | |||||
import frappe | import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
class ErrorLog(Document): | class ErrorLog(Document): | ||||
def onload(self): | def onload(self): | ||||
if not self.seen: | if not self.seen: | ||||
self.db_set('seen', 1, update_modified=0) | |||||
self.db_set("seen", 1, update_modified=0) | |||||
frappe.db.commit() | frappe.db.commit() | ||||
def set_old_logs_as_seen(): | def set_old_logs_as_seen(): | ||||
# set logs as seen | # set logs as seen | ||||
frappe.db.sql("""UPDATE `tabError Log` SET `seen`=1 | |||||
WHERE `seen`=0 AND `creation` < (NOW() - INTERVAL '7' DAY)""") | |||||
frappe.db.sql( | |||||
"""UPDATE `tabError Log` SET `seen`=1 | |||||
WHERE `seen`=0 AND `creation` < (NOW() - INTERVAL '7' DAY)""" | |||||
) | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def clear_error_logs(): | def clear_error_logs(): | ||||
'''Flush all Error Logs''' | |||||
frappe.only_for('System Manager') | |||||
"""Flush all Error Logs""" | |||||
frappe.only_for("System Manager") | |||||
frappe.db.truncate("Error Log") | frappe.db.truncate("Error Log") |
@@ -1,10 +1,12 @@ | |||||
# -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
# Copyright (c) 2015, Frappe Technologies and Contributors | # Copyright (c) 2015, Frappe Technologies and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | |||||
import unittest | import unittest | ||||
import frappe | |||||
# test_records = frappe.get_test_records('Error Log') | # test_records = frappe.get_test_records('Error Log') | ||||
class TestErrorLog(unittest.TestCase): | class TestErrorLog(unittest.TestCase): | ||||
pass | pass |