Merge branch develop into phone_field_control
This commit is contained in:
commit
c2f2fc10e7
927 changed files with 36495 additions and 25747 deletions
|
|
@ -19,3 +19,6 @@ fe20515c23a3ac41f1092bf0eaf0a0a452ec2e85
|
|||
|
||||
# Clean up whitespace
|
||||
b2fc959307c7c79f5584625569d5aed04133ba13
|
||||
|
||||
# Format codebase and sort imports
|
||||
c0c5b2ebdddbe8898ce2d5e5365f4931ff73b6bf
|
||||
|
|
|
|||
|
|
@ -16,6 +16,17 @@ repos:
|
|||
- id: check-merge-conflict
|
||||
- id: check-ast
|
||||
|
||||
- repo: https://github.com/adityahase/black
|
||||
rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119
|
||||
hooks:
|
||||
- id: black
|
||||
additional_dependencies: ['click==8.0.4']
|
||||
|
||||
- repo: https://github.com/timothycrosley/isort
|
||||
rev: 5.9.1
|
||||
hooks:
|
||||
- id: isort
|
||||
exclude: ".*setup.py$"
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: weekly
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ context('Awesome Bar', () => {
|
|||
it('navigates to doctype list', () => {
|
||||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)').type('todo', { delay: 700 });
|
||||
cy.get('.awesomplete').findByRole('listbox').should('be.visible');
|
||||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)').type('{downarrow}{enter}', { delay: 700 });
|
||||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)').type('{enter}', { delay: 700 });
|
||||
|
||||
cy.get('.title-text').should('contain', 'To Do');
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ context('Awesome Bar', () => {
|
|||
|
||||
it('find text in doctype list', () => {
|
||||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)')
|
||||
.type('test in todo{downarrow}{enter}', { delay: 700 });
|
||||
.type('test in todo{enter}', { delay: 700 });
|
||||
|
||||
cy.get('.title-text').should('contain', 'To Do');
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ context('Awesome Bar', () => {
|
|||
|
||||
it('navigates to new form', () => {
|
||||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)')
|
||||
.type('new blog post{downarrow}{enter}', { delay: 700 });
|
||||
.type('new blog post{enter}', { delay: 700 });
|
||||
|
||||
cy.get('.title-text:visible').should('have.text', 'New Blog Post');
|
||||
});
|
||||
|
|
|
|||
90
cypress/integration/control_attach.js
Normal file
90
cypress/integration/control_attach.js
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
context('Attach Control', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/doctype');
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.xcall('frappe.tests.ui_test_helpers.create_doctype', {
|
||||
name: 'Test Attach Control',
|
||||
fields: [
|
||||
{
|
||||
"label": "Attach File or Image",
|
||||
"fieldname": "attach",
|
||||
"fieldtype": "Attach",
|
||||
"in_list_view": 1,
|
||||
},
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
it('Checking functionality for "Link" button in the "Attach" fieldtype', () => {
|
||||
//Navigating to the new form for the newly created doctype
|
||||
cy.new_form('Test Attach Control');
|
||||
|
||||
//Clicking on the attach button which is displayed as part of creating a doctype with "Attach" fieldtype
|
||||
cy.findByRole('button', {name: 'Attach'}).click();
|
||||
|
||||
//Clicking on "Link" button to attach a file using the "Link" button
|
||||
cy.findByRole('button', {name: 'Link'}).click();
|
||||
cy.findByPlaceholderText('Attach a web link').type('https://wallpaperplay.com/walls/full/8/2/b/72402.jpg');
|
||||
|
||||
//Clicking on the Upload button to upload the file
|
||||
cy.intercept("POST", "/api/method/upload_file").as("upload_image");
|
||||
cy.get('.modal-footer').findByRole("button", {name: "Upload"}).click({delay: 500});
|
||||
cy.wait("@upload_image");
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
|
||||
//Checking if the URL of the attached image is getting displayed in the field of the newly created doctype
|
||||
cy.get('.attached-file > .ellipsis > .attached-file-link')
|
||||
.should('have.attr', 'href')
|
||||
.and('equal', 'https://wallpaperplay.com/walls/full/8/2/b/72402.jpg');
|
||||
|
||||
//Clicking on the "Clear" button
|
||||
cy.get('[data-action="clear_attachment"]').click();
|
||||
|
||||
//Checking if clicking on the clear button clears the field of the doctype form and again displays the attach button
|
||||
cy.get('.control-input > .btn-sm').should('contain', 'Attach');
|
||||
|
||||
//Deleting the doc
|
||||
cy.go_to_list('Test Attach Control');
|
||||
cy.get('.list-row-checkbox').eq(0).click();
|
||||
cy.get('.actions-btn-group > .btn').contains('Actions').click();
|
||||
cy.get('.actions-btn-group > .dropdown-menu [data-label="Delete"]').click();
|
||||
cy.click_modal_primary_button('Yes');
|
||||
});
|
||||
|
||||
it('Checking functionality for "Library" button in the "Attach" fieldtype', () => {
|
||||
//Navigating to the new form for the newly created doctype
|
||||
cy.new_form('Test Attach Control');
|
||||
|
||||
//Clicking on the attach button which is displayed as part of creating a doctype with "Attach" fieldtype
|
||||
cy.findByRole('button', {name: 'Attach'}).click();
|
||||
|
||||
//Clicking on "Library" button to attach a file using the "Library" button
|
||||
cy.findByRole('button', {name: 'Library'}).click();
|
||||
cy.contains('72402.jpg').click();
|
||||
|
||||
//Clicking on the Upload button to upload the file
|
||||
cy.intercept("POST", "/api/method/upload_file").as("upload_image");
|
||||
cy.get('.modal-footer').findByRole("button", {name: "Upload"}).click({delay: 500});
|
||||
cy.wait("@upload_image");
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
|
||||
//Checking if the URL of the attached image is getting displayed in the field of the newly created doctype
|
||||
cy.get('.attached-file > .ellipsis > .attached-file-link')
|
||||
.should('have.attr', 'href')
|
||||
.and('equal', 'https://wallpaperplay.com/walls/full/8/2/b/72402.jpg');
|
||||
|
||||
//Clicking on the "Clear" button
|
||||
cy.get('[data-action="clear_attachment"]').click();
|
||||
|
||||
//Checking if clicking on the clear button clears the field of the doctype form and again displays the attach button
|
||||
cy.get('.control-input > .btn-sm').should('contain', 'Attach');
|
||||
|
||||
//Deleting the doc
|
||||
cy.go_to_list('Test Attach Control');
|
||||
cy.get('.list-row-checkbox').eq(0).click();
|
||||
cy.get('.actions-btn-group > .btn').contains('Actions').click();
|
||||
cy.get('.actions-btn-group > .dropdown-menu [data-label="Delete"]').click();
|
||||
cy.click_modal_primary_button('Yes');
|
||||
});
|
||||
});
|
||||
30
cypress/integration/theme_switcher_dialog.js
Normal file
30
cypress/integration/theme_switcher_dialog.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
context('Theme Switcher Shortcut', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app');
|
||||
});
|
||||
beforeEach(() => {
|
||||
cy.reload();
|
||||
});
|
||||
it('Check Toggle', () => {
|
||||
cy.open_theme_dialog('{ctrl+shift+g}');
|
||||
cy.get('.modal-backdrop').should('exist');
|
||||
cy.get('.theme-grid > div').first().click();
|
||||
cy.close_theme('{ctrl+shift+g}');
|
||||
cy.get('.modal-backdrop').should('not.exist');
|
||||
});
|
||||
it('Check Enter', () => {
|
||||
cy.open_theme_dialog('{ctrl+shift+g}');
|
||||
cy.get('.theme-grid > div').first().click();
|
||||
cy.close_theme('{enter}');
|
||||
cy.get('.modal-backdrop').should('not.exist');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Cypress.Commands.add('open_theme_dialog', (shortcut_keys) => {
|
||||
cy.get('body').type(shortcut_keys);
|
||||
});
|
||||
Cypress.Commands.add('close_theme', (shortcut_keys) => {
|
||||
cy.get('.modal-header').type(shortcut_keys);
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -9,8 +9,8 @@ import frappe
|
|||
import frappe.client
|
||||
import frappe.handler
|
||||
from frappe import _
|
||||
from frappe.utils.response import build_response
|
||||
from frappe.utils.data import sbool
|
||||
from frappe.utils.response import build_response
|
||||
|
||||
|
||||
def handle():
|
||||
|
|
@ -22,22 +22,22 @@ def handle():
|
|||
`/api/method/{methodname}` will call a whitelisted method
|
||||
|
||||
`/api/resource/{doctype}` will query a table
|
||||
examples:
|
||||
- `?fields=["name", "owner"]`
|
||||
- `?filters=[["Task", "name", "like", "%005"]]`
|
||||
- `?limit_start=0`
|
||||
- `?limit_page_length=20`
|
||||
examples:
|
||||
- `?fields=["name", "owner"]`
|
||||
- `?filters=[["Task", "name", "like", "%005"]]`
|
||||
- `?limit_start=0`
|
||||
- `?limit_page_length=20`
|
||||
|
||||
`/api/resource/{doctype}/{name}` will point to a resource
|
||||
`GET` will return doclist
|
||||
`POST` will insert
|
||||
`PUT` will update
|
||||
`DELETE` will delete
|
||||
`GET` will return doclist
|
||||
`POST` will insert
|
||||
`PUT` will update
|
||||
`DELETE` will delete
|
||||
|
||||
`/api/resource/{doctype}/{name}?run_method={method}` will run a whitelisted controller method
|
||||
"""
|
||||
|
||||
parts = frappe.request.path[1:].split("/",3)
|
||||
parts = frappe.request.path[1:].split("/", 3)
|
||||
call = doctype = name = None
|
||||
|
||||
if len(parts) > 1:
|
||||
|
|
@ -49,22 +49,22 @@ def handle():
|
|||
if len(parts) > 3:
|
||||
name = parts[3]
|
||||
|
||||
if call=="method":
|
||||
if call == "method":
|
||||
frappe.local.form_dict.cmd = doctype
|
||||
return frappe.handler.handle()
|
||||
|
||||
elif call=="resource":
|
||||
elif call == "resource":
|
||||
if "run_method" in frappe.local.form_dict:
|
||||
method = frappe.local.form_dict.pop("run_method")
|
||||
doc = frappe.get_doc(doctype, name)
|
||||
doc.is_whitelisted(method)
|
||||
|
||||
if frappe.local.request.method=="GET":
|
||||
if frappe.local.request.method == "GET":
|
||||
if not doc.has_permission("read"):
|
||||
frappe.throw(_("Not permitted"), frappe.PermissionError)
|
||||
frappe.local.response.update({"data": doc.run_method(method, **frappe.local.form_dict)})
|
||||
|
||||
if frappe.local.request.method=="POST":
|
||||
if frappe.local.request.method == "POST":
|
||||
if not doc.has_permission("write"):
|
||||
frappe.throw(_("Not permitted"), frappe.PermissionError)
|
||||
|
||||
|
|
@ -73,13 +73,13 @@ def handle():
|
|||
|
||||
else:
|
||||
if name:
|
||||
if frappe.local.request.method=="GET":
|
||||
if frappe.local.request.method == "GET":
|
||||
doc = frappe.get_doc(doctype, name)
|
||||
if not doc.has_permission("read"):
|
||||
raise frappe.PermissionError
|
||||
frappe.local.response.update({"data": doc})
|
||||
|
||||
if frappe.local.request.method=="PUT":
|
||||
if frappe.local.request.method == "PUT":
|
||||
data = get_request_form_data()
|
||||
|
||||
doc = frappe.get_doc(doctype, name, for_update=True)
|
||||
|
|
@ -90,9 +90,7 @@ def handle():
|
|||
# Not checking permissions here because it's checked in doc.save
|
||||
doc.update(data)
|
||||
|
||||
frappe.local.response.update({
|
||||
"data": doc.save().as_dict()
|
||||
})
|
||||
frappe.local.response.update({"data": doc.save().as_dict()})
|
||||
|
||||
# check for child table doctype
|
||||
if doc.get("parenttype"):
|
||||
|
|
@ -183,7 +181,7 @@ def validate_oauth(authorization_header):
|
|||
Authenticate request using OAuth and set session user
|
||||
|
||||
Args:
|
||||
authorization_header (list of str): The 'Authorization' header containing the prefix and token
|
||||
authorization_header (list of str): The 'Authorization' header containing the prefix and token
|
||||
"""
|
||||
|
||||
from frappe.integrations.oauth2 import get_oauth_server
|
||||
|
|
@ -194,7 +192,9 @@ def validate_oauth(authorization_header):
|
|||
req = frappe.request
|
||||
parsed_url = urlparse(req.url)
|
||||
access_token = {"access_token": token}
|
||||
uri = parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + "?" + urlencode(access_token)
|
||||
uri = (
|
||||
parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + "?" + urlencode(access_token)
|
||||
)
|
||||
http_method = req.method
|
||||
headers = req.headers
|
||||
body = req.get_data()
|
||||
|
|
@ -202,8 +202,12 @@ def validate_oauth(authorization_header):
|
|||
body = None
|
||||
|
||||
try:
|
||||
required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(get_url_delimiter())
|
||||
valid, oauthlib_request = get_oauth_server().verify_request(uri, http_method, body, headers, required_scopes)
|
||||
required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(
|
||||
get_url_delimiter()
|
||||
)
|
||||
valid, oauthlib_request = get_oauth_server().verify_request(
|
||||
uri, http_method, body, headers, required_scopes
|
||||
)
|
||||
if valid:
|
||||
frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user"))
|
||||
frappe.local.form_dict = form_dict
|
||||
|
|
@ -216,48 +220,43 @@ def validate_auth_via_api_keys(authorization_header):
|
|||
Authenticate request using API keys and set session user
|
||||
|
||||
Args:
|
||||
authorization_header (list of str): The 'Authorization' header containing the prefix and token
|
||||
authorization_header (list of str): The 'Authorization' header containing the prefix and token
|
||||
"""
|
||||
|
||||
try:
|
||||
auth_type, auth_token = authorization_header
|
||||
authorization_source = frappe.get_request_header("Frappe-Authorization-Source")
|
||||
if auth_type.lower() == 'basic':
|
||||
if auth_type.lower() == "basic":
|
||||
api_key, api_secret = frappe.safe_decode(base64.b64decode(auth_token)).split(":")
|
||||
validate_api_key_secret(api_key, api_secret, authorization_source)
|
||||
elif auth_type.lower() == 'token':
|
||||
elif auth_type.lower() == "token":
|
||||
api_key, api_secret = auth_token.split(":")
|
||||
validate_api_key_secret(api_key, api_secret, authorization_source)
|
||||
except binascii.Error:
|
||||
frappe.throw(_("Failed to decode token, please provide a valid base64-encoded token."), frappe.InvalidAuthorizationToken)
|
||||
frappe.throw(
|
||||
_("Failed to decode token, please provide a valid base64-encoded token."),
|
||||
frappe.InvalidAuthorizationToken,
|
||||
)
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def validate_api_key_secret(api_key, api_secret, frappe_authorization_source=None):
|
||||
"""frappe_authorization_source to provide api key and secret for a doctype apart from User"""
|
||||
doctype = frappe_authorization_source or 'User'
|
||||
doc = frappe.db.get_value(
|
||||
doctype=doctype,
|
||||
filters={"api_key": api_key},
|
||||
fieldname=["name"]
|
||||
)
|
||||
doctype = frappe_authorization_source or "User"
|
||||
doc = frappe.db.get_value(doctype=doctype, filters={"api_key": api_key}, fieldname=["name"])
|
||||
form_dict = frappe.local.form_dict
|
||||
doc_secret = frappe.utils.password.get_decrypted_password(doctype, doc, fieldname='api_secret')
|
||||
doc_secret = frappe.utils.password.get_decrypted_password(doctype, doc, fieldname="api_secret")
|
||||
if api_secret == doc_secret:
|
||||
if doctype == 'User':
|
||||
user = frappe.db.get_value(
|
||||
doctype="User",
|
||||
filters={"api_key": api_key},
|
||||
fieldname=["name"]
|
||||
)
|
||||
if doctype == "User":
|
||||
user = frappe.db.get_value(doctype="User", filters={"api_key": api_key}, fieldname=["name"])
|
||||
else:
|
||||
user = frappe.db.get_value(doctype, doc, 'user')
|
||||
if frappe.local.login_manager.user in ('', 'Guest'):
|
||||
user = frappe.db.get_value(doctype, doc, "user")
|
||||
if frappe.local.login_manager.user in ("", "Guest"):
|
||||
frappe.set_user(user)
|
||||
frappe.local.form_dict = form_dict
|
||||
|
||||
|
||||
def validate_auth_via_hooks():
|
||||
for auth_hook in frappe.get_hooks('auth_hooks', []):
|
||||
for auth_hook in frappe.get_hooks("auth_hooks", []):
|
||||
frappe.get_attr(auth_hook)()
|
||||
|
|
|
|||
194
frappe/app.py
194
frappe/app.py
|
|
@ -2,37 +2,37 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import os
|
||||
import logging
|
||||
import os
|
||||
|
||||
from werkzeug.local import LocalManager
|
||||
from werkzeug.wrappers import Request, Response
|
||||
from werkzeug.exceptions import HTTPException, NotFound
|
||||
from werkzeug.local import LocalManager
|
||||
from werkzeug.middleware.profiler import ProfilerMiddleware
|
||||
from werkzeug.middleware.shared_data import SharedDataMiddleware
|
||||
from werkzeug.wrappers import Request, Response
|
||||
|
||||
import frappe
|
||||
import frappe.handler
|
||||
import frappe.auth
|
||||
import frappe.api
|
||||
import frappe.utils.response
|
||||
from frappe.utils import get_site_name, sanitize_html
|
||||
from frappe.middlewares import StaticDataMiddleware
|
||||
from frappe.website.serve import get_response
|
||||
from frappe.utils.error import make_error_snapshot
|
||||
from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request
|
||||
from frappe import _
|
||||
import frappe.recorder
|
||||
import frappe.auth
|
||||
import frappe.handler
|
||||
import frappe.monitor
|
||||
import frappe.rate_limiter
|
||||
import frappe.recorder
|
||||
import frappe.utils.response
|
||||
from frappe import _
|
||||
from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request
|
||||
from frappe.middlewares import StaticDataMiddleware
|
||||
from frappe.utils import get_site_name, sanitize_html
|
||||
from frappe.utils.error import make_error_snapshot
|
||||
from frappe.website.serve import get_response
|
||||
|
||||
local_manager = LocalManager([frappe.local])
|
||||
|
||||
_site = None
|
||||
_sites_path = os.environ.get("SITES_PATH", ".")
|
||||
|
||||
class RequestContext(object):
|
||||
|
||||
class RequestContext(object):
|
||||
def __init__(self, environ):
|
||||
self.request = Request(environ)
|
||||
|
||||
|
|
@ -42,6 +42,7 @@ class RequestContext(object):
|
|||
def __exit__(self, type, value, traceback):
|
||||
frappe.destroy()
|
||||
|
||||
|
||||
@Request.application
|
||||
def application(request):
|
||||
response = None
|
||||
|
|
@ -65,13 +66,13 @@ def application(request):
|
|||
elif request.path.startswith("/api/"):
|
||||
response = frappe.api.handle()
|
||||
|
||||
elif request.path.startswith('/backups'):
|
||||
elif request.path.startswith("/backups"):
|
||||
response = frappe.utils.response.download_backup(request.path)
|
||||
|
||||
elif request.path.startswith('/private/files/'):
|
||||
elif request.path.startswith("/private/files/"):
|
||||
response = frappe.utils.response.download_private_file(request.path)
|
||||
|
||||
elif request.method in ('GET', 'HEAD', 'POST'):
|
||||
elif request.method in ("GET", "HEAD", "POST"):
|
||||
response = get_response()
|
||||
|
||||
else:
|
||||
|
|
@ -103,41 +104,45 @@ def application(request):
|
|||
|
||||
return response
|
||||
|
||||
|
||||
def init_request(request):
|
||||
frappe.local.request = request
|
||||
frappe.local.is_ajax = frappe.get_request_header("X-Requested-With")=="XMLHttpRequest"
|
||||
frappe.local.is_ajax = frappe.get_request_header("X-Requested-With") == "XMLHttpRequest"
|
||||
|
||||
site = _site or request.headers.get('X-Frappe-Site-Name') or get_site_name(request.host)
|
||||
site = _site or request.headers.get("X-Frappe-Site-Name") or get_site_name(request.host)
|
||||
frappe.init(site=site, sites_path=_sites_path)
|
||||
|
||||
if not (frappe.local.conf and frappe.local.conf.db_name):
|
||||
# site does not exist
|
||||
raise NotFound
|
||||
|
||||
if frappe.local.conf.get('maintenance_mode'):
|
||||
if frappe.local.conf.get("maintenance_mode"):
|
||||
frappe.connect()
|
||||
raise frappe.SessionStopped('Session Stopped')
|
||||
raise frappe.SessionStopped("Session Stopped")
|
||||
else:
|
||||
frappe.connect(set_admin_as_user=False)
|
||||
|
||||
request.max_content_length = frappe.local.conf.get('max_file_size') or 10 * 1024 * 1024
|
||||
request.max_content_length = frappe.local.conf.get("max_file_size") or 10 * 1024 * 1024
|
||||
|
||||
make_form_dict(request)
|
||||
|
||||
if request.method != "OPTIONS":
|
||||
frappe.local.http_request = frappe.auth.HTTPRequest()
|
||||
|
||||
|
||||
def log_request(request, response):
|
||||
if hasattr(frappe.local, 'conf') and frappe.local.conf.enable_frappe_logger:
|
||||
frappe.logger("frappe.web", allow_site=frappe.local.site).info({
|
||||
"site": get_site_name(request.host),
|
||||
"remote_addr": getattr(request, "remote_addr", "NOTFOUND"),
|
||||
"base_url": getattr(request, "base_url", "NOTFOUND"),
|
||||
"full_path": getattr(request, "full_path", "NOTFOUND"),
|
||||
"method": getattr(request, "method", "NOTFOUND"),
|
||||
"scheme": getattr(request, "scheme", "NOTFOUND"),
|
||||
"http_status_code": getattr(response, "status_code", "NOTFOUND")
|
||||
})
|
||||
if hasattr(frappe.local, "conf") and frappe.local.conf.enable_frappe_logger:
|
||||
frappe.logger("frappe.web", allow_site=frappe.local.site).info(
|
||||
{
|
||||
"site": get_site_name(request.host),
|
||||
"remote_addr": getattr(request, "remote_addr", "NOTFOUND"),
|
||||
"base_url": getattr(request, "base_url", "NOTFOUND"),
|
||||
"full_path": getattr(request, "full_path", "NOTFOUND"),
|
||||
"method": getattr(request, "method", "NOTFOUND"),
|
||||
"scheme": getattr(request, "scheme", "NOTFOUND"),
|
||||
"http_status_code": getattr(response, "status_code", "NOTFOUND"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def process_response(response):
|
||||
|
|
@ -145,19 +150,20 @@ def process_response(response):
|
|||
return
|
||||
|
||||
# set cookies
|
||||
if hasattr(frappe.local, 'cookie_manager'):
|
||||
if hasattr(frappe.local, "cookie_manager"):
|
||||
frappe.local.cookie_manager.flush_cookies(response=response)
|
||||
|
||||
# rate limiter headers
|
||||
if hasattr(frappe.local, 'rate_limiter'):
|
||||
if hasattr(frappe.local, "rate_limiter"):
|
||||
response.headers.extend(frappe.local.rate_limiter.headers())
|
||||
|
||||
# CORS headers
|
||||
if hasattr(frappe.local, 'conf') and frappe.conf.allow_cors:
|
||||
if hasattr(frappe.local, "conf") and frappe.conf.allow_cors:
|
||||
set_cors_headers(response)
|
||||
|
||||
|
||||
def set_cors_headers(response):
|
||||
origin = frappe.request.headers.get('Origin')
|
||||
origin = frappe.request.headers.get("Origin")
|
||||
allow_cors = frappe.conf.allow_cors
|
||||
if not (origin and allow_cors):
|
||||
return
|
||||
|
|
@ -169,20 +175,25 @@ def set_cors_headers(response):
|
|||
if origin not in allow_cors:
|
||||
return
|
||||
|
||||
response.headers.extend({
|
||||
'Access-Control-Allow-Origin': origin,
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': ('Authorization,DNT,X-Mx-ReqToken,'
|
||||
'Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,'
|
||||
'Cache-Control,Content-Type')
|
||||
})
|
||||
response.headers.extend(
|
||||
{
|
||||
"Access-Control-Allow-Origin": origin,
|
||||
"Access-Control-Allow-Credentials": "true",
|
||||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||
"Access-Control-Allow-Headers": (
|
||||
"Authorization,DNT,X-Mx-ReqToken,"
|
||||
"Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,"
|
||||
"Cache-Control,Content-Type"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def make_form_dict(request):
|
||||
import json
|
||||
|
||||
request_data = request.get_data(as_text=True)
|
||||
if 'application/json' in (request.content_type or '') and request_data:
|
||||
if "application/json" in (request.content_type or "") and request_data:
|
||||
args = json.loads(request_data)
|
||||
else:
|
||||
args = {}
|
||||
|
|
@ -198,20 +209,19 @@ def make_form_dict(request):
|
|||
# _ is passed by $.ajax so that the request is not cached by the browser. So, remove _ from form_dict
|
||||
frappe.local.form_dict.pop("_")
|
||||
|
||||
|
||||
def handle_exception(e):
|
||||
response = None
|
||||
http_status_code = getattr(e, "http_status_code", 500)
|
||||
return_as_message = False
|
||||
accept_header = frappe.get_request_header("Accept") or ""
|
||||
respond_as_json = (
|
||||
frappe.get_request_header('Accept')
|
||||
and (frappe.local.is_ajax or 'application/json' in accept_header)
|
||||
or (
|
||||
frappe.local.request.path.startswith("/api/") and not accept_header.startswith("text")
|
||||
)
|
||||
frappe.get_request_header("Accept")
|
||||
and (frappe.local.is_ajax or "application/json" in accept_header)
|
||||
or (frappe.local.request.path.startswith("/api/") and not accept_header.startswith("text"))
|
||||
)
|
||||
|
||||
if frappe.conf.get('developer_mode'):
|
||||
if frappe.conf.get("developer_mode"):
|
||||
# don't fail silently
|
||||
print(frappe.get_traceback())
|
||||
|
||||
|
|
@ -220,27 +230,38 @@ def handle_exception(e):
|
|||
# if the request is ajax, send back the trace or error message
|
||||
response = frappe.utils.response.report_error(http_status_code)
|
||||
|
||||
elif (http_status_code==500
|
||||
elif (
|
||||
http_status_code == 500
|
||||
and (frappe.db and isinstance(e, frappe.db.InternalError))
|
||||
and (frappe.db and (frappe.db.is_deadlocked(e) or frappe.db.is_timedout(e)))):
|
||||
http_status_code = 508
|
||||
and (frappe.db and (frappe.db.is_deadlocked(e) or frappe.db.is_timedout(e)))
|
||||
):
|
||||
http_status_code = 508
|
||||
|
||||
elif http_status_code==401:
|
||||
frappe.respond_as_web_page(_("Session Expired"),
|
||||
elif http_status_code == 401:
|
||||
frappe.respond_as_web_page(
|
||||
_("Session Expired"),
|
||||
_("Your session has expired, please login again to continue."),
|
||||
http_status_code=http_status_code, indicator_color='red')
|
||||
http_status_code=http_status_code,
|
||||
indicator_color="red",
|
||||
)
|
||||
return_as_message = True
|
||||
|
||||
elif http_status_code==403:
|
||||
frappe.respond_as_web_page(_("Not Permitted"),
|
||||
elif http_status_code == 403:
|
||||
frappe.respond_as_web_page(
|
||||
_("Not Permitted"),
|
||||
_("You do not have enough permissions to complete the action"),
|
||||
http_status_code=http_status_code, indicator_color='red')
|
||||
http_status_code=http_status_code,
|
||||
indicator_color="red",
|
||||
)
|
||||
return_as_message = True
|
||||
|
||||
elif http_status_code==404:
|
||||
frappe.respond_as_web_page(_("Not Found"),
|
||||
elif http_status_code == 404:
|
||||
frappe.respond_as_web_page(
|
||||
_("Not Found"),
|
||||
_("The resource you are looking for is not available"),
|
||||
http_status_code=http_status_code, indicator_color='red')
|
||||
http_status_code=http_status_code,
|
||||
indicator_color="red",
|
||||
)
|
||||
return_as_message = True
|
||||
|
||||
elif http_status_code == 429:
|
||||
|
|
@ -252,9 +273,9 @@ def handle_exception(e):
|
|||
if frappe.local.flags.disable_traceback and not frappe.local.dev_server:
|
||||
traceback = ""
|
||||
|
||||
frappe.respond_as_web_page("Server Error",
|
||||
traceback, http_status_code=http_status_code,
|
||||
indicator_color='red', width=640)
|
||||
frappe.respond_as_web_page(
|
||||
"Server Error", traceback, http_status_code=http_status_code, indicator_color="red", width=640
|
||||
)
|
||||
return_as_message = True
|
||||
|
||||
if e.__class__ == frappe.AuthenticationError:
|
||||
|
|
@ -269,6 +290,7 @@ def handle_exception(e):
|
|||
|
||||
return response
|
||||
|
||||
|
||||
def after_request(rollback):
|
||||
if (frappe.local.request.method in ("POST", "PUT") or frappe.local.flags.commit) and frappe.db:
|
||||
if frappe.db.transaction_writes:
|
||||
|
|
@ -286,41 +308,47 @@ def after_request(rollback):
|
|||
|
||||
return rollback
|
||||
|
||||
|
||||
application = local_manager.make_middleware(application)
|
||||
|
||||
def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=None, sites_path='.'):
|
||||
|
||||
def serve(
|
||||
port=8000, profile=False, no_reload=False, no_threading=False, site=None, sites_path="."
|
||||
):
|
||||
global application, _site, _sites_path
|
||||
_site = site
|
||||
_sites_path = sites_path
|
||||
|
||||
from werkzeug.serving import run_simple
|
||||
|
||||
if profile or os.environ.get('USE_PROFILER'):
|
||||
application = ProfilerMiddleware(application, sort_by=('cumtime', 'calls'))
|
||||
if profile or os.environ.get("USE_PROFILER"):
|
||||
application = ProfilerMiddleware(application, sort_by=("cumtime", "calls"))
|
||||
|
||||
if not os.environ.get('NO_STATICS'):
|
||||
application = SharedDataMiddleware(application, {
|
||||
str('/assets'): str(os.path.join(sites_path, 'assets'))
|
||||
})
|
||||
if not os.environ.get("NO_STATICS"):
|
||||
application = SharedDataMiddleware(
|
||||
application, {str("/assets"): str(os.path.join(sites_path, "assets"))}
|
||||
)
|
||||
|
||||
application = StaticDataMiddleware(application, {
|
||||
str('/files'): str(os.path.abspath(sites_path))
|
||||
})
|
||||
application = StaticDataMiddleware(
|
||||
application, {str("/files"): str(os.path.abspath(sites_path))}
|
||||
)
|
||||
|
||||
application.debug = True
|
||||
application.config = {
|
||||
'SERVER_NAME': 'localhost:8000'
|
||||
}
|
||||
application.config = {"SERVER_NAME": "localhost:8000"}
|
||||
|
||||
log = logging.getLogger('werkzeug')
|
||||
log = logging.getLogger("werkzeug")
|
||||
log.propagate = False
|
||||
|
||||
in_test_env = os.environ.get('CI')
|
||||
in_test_env = os.environ.get("CI")
|
||||
if in_test_env:
|
||||
log.setLevel(logging.ERROR)
|
||||
|
||||
run_simple('0.0.0.0', int(port), application,
|
||||
run_simple(
|
||||
"0.0.0.0",
|
||||
int(port),
|
||||
application,
|
||||
use_reloader=False if in_test_env else not no_reload,
|
||||
use_debugger=not in_test_env,
|
||||
use_evalex=not in_test_env,
|
||||
threaded=not no_threading)
|
||||
threaded=not no_threading,
|
||||
)
|
||||
|
|
|
|||
191
frappe/auth.py
191
frappe/auth.py
|
|
@ -11,7 +11,12 @@ from frappe.core.doctype.activity_log.activity_log import add_authentication_log
|
|||
from frappe.modules.patch_handler import check_session_stopped
|
||||
from frappe.sessions import Session, clear_sessions, delete_session
|
||||
from frappe.translate import get_language
|
||||
from frappe.twofactor import authenticate_for_2factor, confirm_otp_token, get_cached_user_pass, should_run_2fa
|
||||
from frappe.twofactor import (
|
||||
authenticate_for_2factor,
|
||||
confirm_otp_token,
|
||||
get_cached_user_pass,
|
||||
should_run_2fa,
|
||||
)
|
||||
from frappe.utils import cint, date_diff, datetime, get_datetime, today
|
||||
from frappe.utils.password import check_password
|
||||
from frappe.website.utils import get_home_page
|
||||
|
|
@ -47,20 +52,20 @@ class HTTPRequest:
|
|||
def domain(self):
|
||||
if not getattr(self, "_domain", None):
|
||||
self._domain = frappe.request.host
|
||||
if self._domain and self._domain.startswith('www.'):
|
||||
if self._domain and self._domain.startswith("www."):
|
||||
self._domain = self._domain[4:]
|
||||
|
||||
return self._domain
|
||||
|
||||
def set_request_ip(self):
|
||||
if frappe.get_request_header('X-Forwarded-For'):
|
||||
frappe.local.request_ip = (frappe.get_request_header('X-Forwarded-For').split(",")[0]).strip()
|
||||
if frappe.get_request_header("X-Forwarded-For"):
|
||||
frappe.local.request_ip = (frappe.get_request_header("X-Forwarded-For").split(",")[0]).strip()
|
||||
|
||||
elif frappe.get_request_header('REMOTE_ADDR'):
|
||||
frappe.local.request_ip = frappe.get_request_header('REMOTE_ADDR')
|
||||
elif frappe.get_request_header("REMOTE_ADDR"):
|
||||
frappe.local.request_ip = frappe.get_request_header("REMOTE_ADDR")
|
||||
|
||||
else:
|
||||
frappe.local.request_ip = '127.0.0.1'
|
||||
frappe.local.request_ip = "127.0.0.1"
|
||||
|
||||
def set_cookies(self):
|
||||
frappe.local.cookie_manager = CookieManager()
|
||||
|
|
@ -75,7 +80,7 @@ class HTTPRequest:
|
|||
if (
|
||||
not frappe.local.session.data.csrf_token
|
||||
or frappe.local.session.data.device == "mobile"
|
||||
or frappe.conf.get('ignore_csrf', None)
|
||||
or frappe.conf.get("ignore_csrf", None)
|
||||
):
|
||||
# not via boot
|
||||
return
|
||||
|
|
@ -99,10 +104,10 @@ class HTTPRequest:
|
|||
def connect(self):
|
||||
"""connect to db, from ac_name or db_name"""
|
||||
frappe.local.db = frappe.database.get_db(
|
||||
user=self.get_db_name(),
|
||||
password=getattr(conf, 'db_password', '')
|
||||
user=self.get_db_name(), password=getattr(conf, "db_password", "")
|
||||
)
|
||||
|
||||
|
||||
class LoginManager:
|
||||
def __init__(self):
|
||||
self.user = None
|
||||
|
|
@ -110,13 +115,15 @@ class LoginManager:
|
|||
self.full_name = None
|
||||
self.user_type = None
|
||||
|
||||
if frappe.local.form_dict.get('cmd')=='login' or frappe.local.request.path=="/api/method/login":
|
||||
if (
|
||||
frappe.local.form_dict.get("cmd") == "login" or frappe.local.request.path == "/api/method/login"
|
||||
):
|
||||
if self.login() is False:
|
||||
return
|
||||
self.resume = False
|
||||
|
||||
# run login triggers
|
||||
self.run_trigger('on_session_creation')
|
||||
self.run_trigger("on_session_creation")
|
||||
else:
|
||||
try:
|
||||
self.resume = True
|
||||
|
|
@ -131,12 +138,14 @@ class LoginManager:
|
|||
|
||||
def login(self):
|
||||
# clear cache
|
||||
frappe.clear_cache(user = frappe.form_dict.get('usr'))
|
||||
frappe.clear_cache(user=frappe.form_dict.get("usr"))
|
||||
user, pwd = get_cached_user_pass()
|
||||
self.authenticate(user=user, pwd=pwd)
|
||||
if self.force_user_to_reset_password():
|
||||
doc = frappe.get_doc("User", self.user)
|
||||
frappe.local.response["redirect_to"] = doc.reset_password(send_email=False, password_expired=True)
|
||||
frappe.local.response["redirect_to"] = doc.reset_password(
|
||||
send_email=False, password_expired=True
|
||||
)
|
||||
frappe.local.response["message"] = "Password Reset"
|
||||
return False
|
||||
|
||||
|
|
@ -147,7 +156,7 @@ class LoginManager:
|
|||
self.post_login()
|
||||
|
||||
def post_login(self):
|
||||
self.run_trigger('on_login')
|
||||
self.run_trigger("on_login")
|
||||
validate_ip_address(self.user)
|
||||
self.validate_hour()
|
||||
self.get_user_info()
|
||||
|
|
@ -156,8 +165,9 @@ class LoginManager:
|
|||
self.set_user_info()
|
||||
|
||||
def get_user_info(self):
|
||||
self.info = frappe.db.get_value("User", self.user,
|
||||
["user_type", "first_name", "last_name", "user_image"], as_dict=1)
|
||||
self.info = frappe.db.get_value(
|
||||
"User", self.user, ["user_type", "first_name", "last_name", "user_image"], as_dict=1
|
||||
)
|
||||
|
||||
self.user_type = self.info.user_type
|
||||
|
||||
|
|
@ -170,28 +180,27 @@ class LoginManager:
|
|||
# set sid again
|
||||
frappe.local.cookie_manager.init_cookies()
|
||||
|
||||
self.full_name = " ".join(filter(None, [self.info.first_name,
|
||||
self.info.last_name]))
|
||||
self.full_name = " ".join(filter(None, [self.info.first_name, self.info.last_name]))
|
||||
|
||||
if self.info.user_type=="Website User":
|
||||
if self.info.user_type == "Website User":
|
||||
frappe.local.cookie_manager.set_cookie("system_user", "no")
|
||||
if not resume:
|
||||
frappe.local.response["message"] = "No App"
|
||||
frappe.local.response["home_page"] = '/' + get_home_page()
|
||||
frappe.local.response["home_page"] = "/" + get_home_page()
|
||||
else:
|
||||
frappe.local.cookie_manager.set_cookie("system_user", "yes")
|
||||
if not resume:
|
||||
frappe.local.response['message'] = 'Logged In'
|
||||
frappe.local.response["message"] = "Logged In"
|
||||
frappe.local.response["home_page"] = "/app"
|
||||
|
||||
if not resume:
|
||||
frappe.response["full_name"] = self.full_name
|
||||
|
||||
# redirect information
|
||||
redirect_to = frappe.cache().hget('redirect_after_login', self.user)
|
||||
redirect_to = frappe.cache().hget("redirect_after_login", self.user)
|
||||
if redirect_to:
|
||||
frappe.local.response["redirect_to"] = redirect_to
|
||||
frappe.cache().hdel('redirect_after_login', self.user)
|
||||
frappe.cache().hdel("redirect_after_login", self.user)
|
||||
|
||||
frappe.local.cookie_manager.set_cookie("full_name", self.full_name)
|
||||
frappe.local.cookie_manager.set_cookie("user_id", self.user)
|
||||
|
|
@ -202,8 +211,9 @@ class LoginManager:
|
|||
|
||||
def make_session(self, resume=False):
|
||||
# start session
|
||||
frappe.local.session_obj = Session(user=self.user, resume=resume,
|
||||
full_name=self.full_name, user_type=self.user_type)
|
||||
frappe.local.session_obj = Session(
|
||||
user=self.user, resume=resume, full_name=self.full_name, user_type=self.user_type
|
||||
)
|
||||
|
||||
# reset user if changed to Guest
|
||||
self.user = frappe.local.session_obj.user
|
||||
|
|
@ -212,7 +222,10 @@ class LoginManager:
|
|||
|
||||
def clear_active_sessions(self):
|
||||
"""Clear other sessions of the current user if `deny_multiple_sessions` is not set"""
|
||||
if not (cint(frappe.conf.get("deny_multiple_sessions")) or cint(frappe.db.get_system_setting('deny_multiple_sessions'))):
|
||||
if not (
|
||||
cint(frappe.conf.get("deny_multiple_sessions"))
|
||||
or cint(frappe.db.get_system_setting("deny_multiple_sessions"))
|
||||
):
|
||||
return
|
||||
|
||||
if frappe.session.user != "Guest":
|
||||
|
|
@ -222,27 +235,27 @@ class LoginManager:
|
|||
from frappe.core.doctype.user.user import User
|
||||
|
||||
if not (user and pwd):
|
||||
user, pwd = frappe.form_dict.get('usr'), frappe.form_dict.get('pwd')
|
||||
user, pwd = frappe.form_dict.get("usr"), frappe.form_dict.get("pwd")
|
||||
if not (user and pwd):
|
||||
self.fail(_('Incomplete login details'), user=user)
|
||||
self.fail(_("Incomplete login details"), user=user)
|
||||
|
||||
user = User.find_by_credentials(user, pwd)
|
||||
|
||||
if not user:
|
||||
self.fail('Invalid login credentials')
|
||||
self.fail("Invalid login credentials")
|
||||
|
||||
# Current login flow uses cached credentials for authentication while checking OTP.
|
||||
# Incase of OTP check, tracker for auth needs to be disabled(If not, it can remove tracker history as it is going to succeed anyway)
|
||||
# Tracker is activated for 2FA incase of OTP.
|
||||
ignore_tracker = should_run_2fa(user.name) and ('otp' in frappe.form_dict)
|
||||
ignore_tracker = should_run_2fa(user.name) and ("otp" in frappe.form_dict)
|
||||
tracker = None if ignore_tracker else get_login_attempt_tracker(user.name)
|
||||
|
||||
if not user.is_authenticated:
|
||||
tracker and tracker.add_failure_attempt()
|
||||
self.fail('Invalid login credentials', user=user.name)
|
||||
elif not (user.name == 'Administrator' or user.enabled):
|
||||
self.fail("Invalid login credentials", user=user.name)
|
||||
elif not (user.name == "Administrator" or user.enabled):
|
||||
tracker and tracker.add_failure_attempt()
|
||||
self.fail('User disabled or missing', user=user.name)
|
||||
self.fail("User disabled or missing", user=user.name)
|
||||
else:
|
||||
tracker and tracker.add_success_attempt()
|
||||
self.user = user.name
|
||||
|
|
@ -254,12 +267,14 @@ class LoginManager:
|
|||
if self.user in frappe.STANDARD_USERS:
|
||||
return False
|
||||
|
||||
reset_pwd_after_days = cint(frappe.db.get_single_value("System Settings",
|
||||
"force_user_to_reset_password"))
|
||||
reset_pwd_after_days = cint(
|
||||
frappe.db.get_single_value("System Settings", "force_user_to_reset_password")
|
||||
)
|
||||
|
||||
if reset_pwd_after_days:
|
||||
last_password_reset_date = frappe.db.get_value("User",
|
||||
self.user, "last_password_reset_date") or today()
|
||||
last_password_reset_date = (
|
||||
frappe.db.get_value("User", self.user, "last_password_reset_date") or today()
|
||||
)
|
||||
|
||||
last_pwd_reset_days = date_diff(today(), last_password_reset_date)
|
||||
|
||||
|
|
@ -272,30 +287,31 @@ class LoginManager:
|
|||
# returns user in correct case
|
||||
return check_password(user, pwd)
|
||||
except frappe.AuthenticationError:
|
||||
self.fail('Incorrect password', user=user)
|
||||
self.fail("Incorrect password", user=user)
|
||||
|
||||
def fail(self, message, user=None):
|
||||
if not user:
|
||||
user = _('Unknown User')
|
||||
frappe.local.response['message'] = message
|
||||
user = _("Unknown User")
|
||||
frappe.local.response["message"] = message
|
||||
add_authentication_log(message, user, status="Failed")
|
||||
frappe.db.commit()
|
||||
raise frappe.AuthenticationError
|
||||
|
||||
def run_trigger(self, event='on_login'):
|
||||
def run_trigger(self, event="on_login"):
|
||||
for method in frappe.get_hooks().get(event, []):
|
||||
frappe.call(frappe.get_attr(method), login_manager=self)
|
||||
|
||||
def validate_hour(self):
|
||||
"""check if user is logging in during restricted hours"""
|
||||
login_before = int(frappe.db.get_value('User', self.user, 'login_before', ignore=True) or 0)
|
||||
login_after = int(frappe.db.get_value('User', self.user, 'login_after', ignore=True) or 0)
|
||||
login_before = int(frappe.db.get_value("User", self.user, "login_before", ignore=True) or 0)
|
||||
login_after = int(frappe.db.get_value("User", self.user, "login_after", ignore=True) or 0)
|
||||
|
||||
if not (login_before or login_after):
|
||||
return
|
||||
|
||||
from frappe.utils import now_datetime
|
||||
current_hour = int(now_datetime().strftime('%H'))
|
||||
|
||||
current_hour = int(now_datetime().strftime("%H"))
|
||||
|
||||
if login_before and current_hour > login_before:
|
||||
frappe.throw(_("Login not allowed at this time"), frappe.AuthenticationError)
|
||||
|
|
@ -311,9 +327,10 @@ class LoginManager:
|
|||
self.user = user
|
||||
self.post_login()
|
||||
|
||||
def logout(self, arg='', user=None):
|
||||
if not user: user = frappe.session.user
|
||||
self.run_trigger('on_logout')
|
||||
def logout(self, arg="", user=None):
|
||||
if not user:
|
||||
user = frappe.session.user
|
||||
self.run_trigger("on_logout")
|
||||
|
||||
if user == frappe.session.user:
|
||||
delete_session(frappe.session.sid, user=user, reason="User Manually Logged Out")
|
||||
|
|
@ -324,13 +341,15 @@ class LoginManager:
|
|||
def clear_cookies(self):
|
||||
clear_cookies()
|
||||
|
||||
|
||||
class CookieManager:
|
||||
def __init__(self):
|
||||
self.cookies = {}
|
||||
self.to_delete = []
|
||||
|
||||
def init_cookies(self):
|
||||
if not frappe.local.session.get('sid'): return
|
||||
if not frappe.local.session.get("sid"):
|
||||
return
|
||||
|
||||
# sid expires in 3 days
|
||||
expires = datetime.datetime.now() + datetime.timedelta(days=3)
|
||||
|
|
@ -340,7 +359,7 @@ class CookieManager:
|
|||
self.set_cookie("country", frappe.session.session_country)
|
||||
|
||||
def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Lax"):
|
||||
if not secure and hasattr(frappe.local, 'request'):
|
||||
if not secure and hasattr(frappe.local, "request"):
|
||||
secure = frappe.local.request.scheme == "https"
|
||||
|
||||
# Cordova does not work with Lax
|
||||
|
|
@ -352,7 +371,7 @@ class CookieManager:
|
|||
"expires": expires,
|
||||
"secure": secure,
|
||||
"httponly": httponly,
|
||||
"samesite": samesite
|
||||
"samesite": samesite,
|
||||
}
|
||||
|
||||
def delete_cookie(self, to_delete):
|
||||
|
|
@ -363,11 +382,14 @@ class CookieManager:
|
|||
|
||||
def flush_cookies(self, response):
|
||||
for key, opts in self.cookies.items():
|
||||
response.set_cookie(key, quote((opts.get("value") or "").encode('utf-8')),
|
||||
response.set_cookie(
|
||||
key,
|
||||
quote((opts.get("value") or "").encode("utf-8")),
|
||||
expires=opts.get("expires"),
|
||||
secure=opts.get("secure"),
|
||||
httponly=opts.get("httponly"),
|
||||
samesite=opts.get("samesite"))
|
||||
samesite=opts.get("samesite"),
|
||||
)
|
||||
|
||||
# expires yesterday!
|
||||
expires = datetime.datetime.now() + datetime.timedelta(days=-1)
|
||||
|
|
@ -379,19 +401,29 @@ class CookieManager:
|
|||
def get_logged_user():
|
||||
return frappe.session.user
|
||||
|
||||
|
||||
def clear_cookies():
|
||||
if hasattr(frappe.local, "session"):
|
||||
frappe.session.sid = ""
|
||||
frappe.local.cookie_manager.delete_cookie(["full_name", "user_id", "sid", "user_image", "system_user"])
|
||||
frappe.local.cookie_manager.delete_cookie(
|
||||
["full_name", "user_id", "sid", "user_image", "system_user"]
|
||||
)
|
||||
|
||||
|
||||
def validate_ip_address(user):
|
||||
"""check if IP Address is valid"""
|
||||
user = frappe.get_cached_doc("User", user) if not frappe.flags.in_test else frappe.get_doc("User", user)
|
||||
user = (
|
||||
frappe.get_cached_doc("User", user) if not frappe.flags.in_test else frappe.get_doc("User", user)
|
||||
)
|
||||
ip_list = user.get_restricted_ip_list()
|
||||
if not ip_list:
|
||||
return
|
||||
|
||||
system_settings = frappe.get_cached_doc("System Settings") if not frappe.flags.in_test else frappe.get_single("System Settings")
|
||||
system_settings = (
|
||||
frappe.get_cached_doc("System Settings")
|
||||
if not frappe.flags.in_test
|
||||
else frappe.get_single("System Settings")
|
||||
)
|
||||
# check if bypass restrict ip is enabled for all users
|
||||
bypass_restrict_ip_check = system_settings.bypass_restrict_ip_check_if_2fa_enabled
|
||||
|
||||
|
|
@ -406,6 +438,7 @@ def validate_ip_address(user):
|
|||
|
||||
frappe.throw(_("Access not allowed from this IP Address"), frappe.AuthenticationError)
|
||||
|
||||
|
||||
def get_login_attempt_tracker(user_name: str, raise_locked_exception: bool = True):
|
||||
"""Get login attempt tracker instance.
|
||||
|
||||
|
|
@ -413,18 +446,22 @@ def get_login_attempt_tracker(user_name: str, raise_locked_exception: bool = Tru
|
|||
:param raise_locked_exception: If set, raises an exception incase of user not allowed to login
|
||||
"""
|
||||
sys_settings = frappe.get_doc("System Settings")
|
||||
track_login_attempts = (sys_settings.allow_consecutive_login_attempts >0)
|
||||
track_login_attempts = sys_settings.allow_consecutive_login_attempts > 0
|
||||
tracker_kwargs = {}
|
||||
|
||||
if track_login_attempts:
|
||||
tracker_kwargs['lock_interval'] = sys_settings.allow_login_after_fail
|
||||
tracker_kwargs['max_consecutive_login_attempts'] = sys_settings.allow_consecutive_login_attempts
|
||||
tracker_kwargs["lock_interval"] = sys_settings.allow_login_after_fail
|
||||
tracker_kwargs["max_consecutive_login_attempts"] = sys_settings.allow_consecutive_login_attempts
|
||||
|
||||
tracker = LoginAttemptTracker(user_name, **tracker_kwargs)
|
||||
|
||||
if raise_locked_exception and track_login_attempts and not tracker.is_user_allowed():
|
||||
frappe.throw(_("Your account has been locked and will resume after {0} seconds")
|
||||
.format(sys_settings.allow_login_after_fail), frappe.SecurityException)
|
||||
frappe.throw(
|
||||
_("Your account has been locked and will resume after {0} seconds").format(
|
||||
sys_settings.allow_login_after_fail
|
||||
),
|
||||
frappe.SecurityException,
|
||||
)
|
||||
return tracker
|
||||
|
||||
|
||||
|
|
@ -433,8 +470,11 @@ class LoginAttemptTracker(object):
|
|||
|
||||
Lock the account for s number of seconds if there have been n consecutive unsuccessful attempts to log in.
|
||||
"""
|
||||
def __init__(self, user_name: str, max_consecutive_login_attempts: int=3, lock_interval:int = 5*60):
|
||||
""" Initialize the tracker.
|
||||
|
||||
def __init__(
|
||||
self, user_name: str, max_consecutive_login_attempts: int = 3, lock_interval: int = 5 * 60
|
||||
):
|
||||
"""Initialize the tracker.
|
||||
|
||||
:param user_name: Name of the loggedin user
|
||||
:param max_consecutive_login_attempts: Maximum allowed consecutive failed login attempts
|
||||
|
|
@ -446,15 +486,15 @@ class LoginAttemptTracker(object):
|
|||
|
||||
@property
|
||||
def login_failed_count(self):
|
||||
return frappe.cache().hget('login_failed_count', self.user_name)
|
||||
return frappe.cache().hget("login_failed_count", self.user_name)
|
||||
|
||||
@login_failed_count.setter
|
||||
def login_failed_count(self, count):
|
||||
frappe.cache().hset('login_failed_count', self.user_name, count)
|
||||
frappe.cache().hset("login_failed_count", self.user_name, count)
|
||||
|
||||
@login_failed_count.deleter
|
||||
def login_failed_count(self):
|
||||
frappe.cache().hdel('login_failed_count', self.user_name)
|
||||
frappe.cache().hdel("login_failed_count", self.user_name)
|
||||
|
||||
@property
|
||||
def login_failed_time(self):
|
||||
|
|
@ -462,23 +502,23 @@ class LoginAttemptTracker(object):
|
|||
|
||||
For every user we track only First failed login attempt time within lock interval of time.
|
||||
"""
|
||||
return frappe.cache().hget('login_failed_time', self.user_name)
|
||||
return frappe.cache().hget("login_failed_time", self.user_name)
|
||||
|
||||
@login_failed_time.setter
|
||||
def login_failed_time(self, timestamp):
|
||||
frappe.cache().hset('login_failed_time', self.user_name, timestamp)
|
||||
frappe.cache().hset("login_failed_time", self.user_name, timestamp)
|
||||
|
||||
@login_failed_time.deleter
|
||||
def login_failed_time(self):
|
||||
frappe.cache().hdel('login_failed_time', self.user_name)
|
||||
frappe.cache().hdel("login_failed_time", self.user_name)
|
||||
|
||||
def add_failure_attempt(self):
|
||||
""" Log user failure attempts into the system.
|
||||
"""Log user failure attempts into the system.
|
||||
|
||||
Increase the failure count if new failure is with in current lock interval time period, if not reset the login failure count.
|
||||
"""
|
||||
login_failed_time = self.login_failed_time
|
||||
login_failed_count = self.login_failed_count # Consecutive login failure count
|
||||
login_failed_count = self.login_failed_count # Consecutive login failure count
|
||||
current_time = get_datetime()
|
||||
|
||||
if not (login_failed_time and login_failed_count):
|
||||
|
|
@ -493,8 +533,7 @@ class LoginAttemptTracker(object):
|
|||
self.login_failed_count = login_failed_count
|
||||
|
||||
def add_success_attempt(self):
|
||||
"""Reset login failures.
|
||||
"""
|
||||
"""Reset login failures."""
|
||||
del self.login_failed_count
|
||||
del self.login_failed_time
|
||||
|
||||
|
|
@ -507,6 +546,10 @@ class LoginAttemptTracker(object):
|
|||
login_failed_count = self.login_failed_count or 0
|
||||
current_time = get_datetime()
|
||||
|
||||
if login_failed_time and login_failed_time + self.lock_interval > current_time and login_failed_count > self.max_failed_logins:
|
||||
if (
|
||||
login_failed_time
|
||||
and login_failed_time + self.lock_interval > current_time
|
||||
and login_failed_count > self.max_failed_logins
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -24,9 +24,7 @@ class AssignmentRule(Document):
|
|||
def validate_document_types(self):
|
||||
if self.document_type == "ToDo":
|
||||
frappe.throw(
|
||||
_('Assignment Rule is not allowed on {0} document type').format(
|
||||
frappe.bold("ToDo")
|
||||
)
|
||||
_("Assignment Rule is not allowed on {0} document type").format(frappe.bold("ToDo"))
|
||||
)
|
||||
|
||||
def validate_assignment_days(self):
|
||||
|
|
@ -38,70 +36,70 @@ class AssignmentRule(Document):
|
|||
|
||||
frappe.throw(
|
||||
_("Assignment Day{0} {1} has been repeated.").format(
|
||||
plural,
|
||||
frappe.bold(", ".join(repeated_days))
|
||||
plural, frappe.bold(", ".join(repeated_days))
|
||||
)
|
||||
)
|
||||
|
||||
def apply_unassign(self, doc, assignments):
|
||||
if (self.unassign_condition and
|
||||
self.name in [d.assignment_rule for d in assignments]):
|
||||
if self.unassign_condition and self.name in [d.assignment_rule for d in assignments]:
|
||||
return self.clear_assignment(doc)
|
||||
|
||||
return False
|
||||
|
||||
def apply_assign(self, doc):
|
||||
if self.safe_eval('assign_condition', doc):
|
||||
if self.safe_eval("assign_condition", doc):
|
||||
return self.do_assignment(doc)
|
||||
|
||||
def do_assignment(self, doc):
|
||||
# clear existing assignment, to reassign
|
||||
assign_to.clear(doc.get('doctype'), doc.get('name'))
|
||||
assign_to.clear(doc.get("doctype"), doc.get("name"))
|
||||
|
||||
user = self.get_user(doc)
|
||||
|
||||
if user:
|
||||
assign_to.add(dict(
|
||||
assign_to = [user],
|
||||
doctype = doc.get('doctype'),
|
||||
name = doc.get('name'),
|
||||
description = frappe.render_template(self.description, doc),
|
||||
assignment_rule = self.name,
|
||||
notify = True,
|
||||
date = doc.get(self.due_date_based_on) if self.due_date_based_on else None
|
||||
))
|
||||
assign_to.add(
|
||||
dict(
|
||||
assign_to=[user],
|
||||
doctype=doc.get("doctype"),
|
||||
name=doc.get("name"),
|
||||
description=frappe.render_template(self.description, doc),
|
||||
assignment_rule=self.name,
|
||||
notify=True,
|
||||
date=doc.get(self.due_date_based_on) if self.due_date_based_on else None,
|
||||
)
|
||||
)
|
||||
|
||||
# set for reference in round robin
|
||||
self.db_set('last_user', user)
|
||||
self.db_set("last_user", user)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def clear_assignment(self, doc):
|
||||
'''Clear assignments'''
|
||||
if self.safe_eval('unassign_condition', doc):
|
||||
return assign_to.clear(doc.get('doctype'), doc.get('name'))
|
||||
"""Clear assignments"""
|
||||
if self.safe_eval("unassign_condition", doc):
|
||||
return assign_to.clear(doc.get("doctype"), doc.get("name"))
|
||||
|
||||
def close_assignments(self, doc):
|
||||
'''Close assignments'''
|
||||
if self.safe_eval('close_condition', doc):
|
||||
return assign_to.close_all_assignments(doc.get('doctype'), doc.get('name'))
|
||||
"""Close assignments"""
|
||||
if self.safe_eval("close_condition", doc):
|
||||
return assign_to.close_all_assignments(doc.get("doctype"), doc.get("name"))
|
||||
|
||||
def get_user(self, doc):
|
||||
'''
|
||||
"""
|
||||
Get the next user for assignment
|
||||
'''
|
||||
if self.rule == 'Round Robin':
|
||||
"""
|
||||
if self.rule == "Round Robin":
|
||||
return self.get_user_round_robin()
|
||||
elif self.rule == 'Load Balancing':
|
||||
elif self.rule == "Load Balancing":
|
||||
return self.get_user_load_balancing()
|
||||
elif self.rule == 'Based on Field':
|
||||
elif self.rule == "Based on Field":
|
||||
return self.get_user_based_on_field(doc)
|
||||
|
||||
def get_user_round_robin(self):
|
||||
'''
|
||||
"""
|
||||
Get next user based on round robin
|
||||
'''
|
||||
"""
|
||||
|
||||
# first time, or last in list, pick the first
|
||||
if not self.last_user or self.last_user == self.users[-1].user:
|
||||
|
|
@ -110,32 +108,33 @@ class AssignmentRule(Document):
|
|||
# find out the next user in the list
|
||||
for i, d in enumerate(self.users):
|
||||
if self.last_user == d.user:
|
||||
return self.users[i+1].user
|
||||
return self.users[i + 1].user
|
||||
|
||||
# bad last user, assign to the first one
|
||||
return self.users[0].user
|
||||
|
||||
def get_user_load_balancing(self):
|
||||
'''Assign to the user with least number of open assignments'''
|
||||
"""Assign to the user with least number of open assignments"""
|
||||
counts = []
|
||||
for d in self.users:
|
||||
counts.append(dict(
|
||||
user = d.user,
|
||||
count = frappe.db.count('ToDo', dict(
|
||||
reference_type = self.document_type,
|
||||
allocated_to = d.user,
|
||||
status = "Open"))
|
||||
))
|
||||
counts.append(
|
||||
dict(
|
||||
user=d.user,
|
||||
count=frappe.db.count(
|
||||
"ToDo", dict(reference_type=self.document_type, allocated_to=d.user, status="Open")
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# sort by dict value
|
||||
sorted_counts = sorted(counts, key = lambda k: k['count'])
|
||||
sorted_counts = sorted(counts, key=lambda k: k["count"])
|
||||
|
||||
# pick the first user
|
||||
return sorted_counts[0].get('user')
|
||||
return sorted_counts[0].get("user")
|
||||
|
||||
def get_user_based_on_field(self, doc):
|
||||
val = doc.get(self.field)
|
||||
if frappe.db.exists('User', val):
|
||||
if frappe.db.exists("User", val):
|
||||
return val
|
||||
|
||||
def safe_eval(self, fieldname, doc):
|
||||
|
|
@ -145,12 +144,12 @@ class AssignmentRule(Document):
|
|||
except Exception as e:
|
||||
# when assignment fails, don't block the document as it may be
|
||||
# a part of the email pulling
|
||||
frappe.msgprint(frappe._('Auto assignment failed: {0}').format(str(e)), indicator = 'orange')
|
||||
frappe.msgprint(frappe._("Auto assignment failed: {0}").format(str(e)), indicator="orange")
|
||||
|
||||
return False
|
||||
|
||||
def get_assignment_days(self):
|
||||
return [d.day for d in self.get('assignment_days', [])]
|
||||
return [d.day for d in self.get("assignment_days", [])]
|
||||
|
||||
def is_rule_not_applicable_today(self):
|
||||
today = frappe.flags.assignment_day or frappe.utils.get_weekday()
|
||||
|
|
@ -159,11 +158,14 @@ class AssignmentRule(Document):
|
|||
|
||||
|
||||
def get_assignments(doc) -> List[Dict]:
|
||||
return frappe.get_all('ToDo', fields = ['name', 'assignment_rule'], filters = dict(
|
||||
reference_type = doc.get('doctype'),
|
||||
reference_name = doc.get('name'),
|
||||
status = ('!=', 'Cancelled')
|
||||
), limit=5)
|
||||
return frappe.get_all(
|
||||
"ToDo",
|
||||
fields=["name", "assignment_rule"],
|
||||
filters=dict(
|
||||
reference_type=doc.get("doctype"), reference_name=doc.get("name"), status=("!=", "Cancelled")
|
||||
),
|
||||
limit=5,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -173,21 +175,30 @@ def bulk_apply(doctype, docnames):
|
|||
|
||||
for name in docnames:
|
||||
if background:
|
||||
frappe.enqueue('frappe.automation.doctype.assignment_rule.assignment_rule.apply', doc=None, doctype=doctype, name=name)
|
||||
frappe.enqueue(
|
||||
"frappe.automation.doctype.assignment_rule.assignment_rule.apply",
|
||||
doc=None,
|
||||
doctype=doctype,
|
||||
name=name,
|
||||
)
|
||||
else:
|
||||
apply(doctype=doctype, name=name)
|
||||
|
||||
|
||||
def reopen_closed_assignment(doc):
|
||||
todo_list = frappe.get_all("ToDo", filters={
|
||||
"reference_type": doc.doctype,
|
||||
"reference_name": doc.name,
|
||||
"status": "Closed",
|
||||
}, pluck="name")
|
||||
todo_list = frappe.get_all(
|
||||
"ToDo",
|
||||
filters={
|
||||
"reference_type": doc.doctype,
|
||||
"reference_name": doc.name,
|
||||
"status": "Closed",
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
for todo in todo_list:
|
||||
todo_doc = frappe.get_doc('ToDo', todo)
|
||||
todo_doc.status = 'Open'
|
||||
todo_doc = frappe.get_doc("ToDo", todo)
|
||||
todo_doc.status = "Open"
|
||||
todo_doc.save(ignore_permissions=True)
|
||||
|
||||
return bool(todo_list)
|
||||
|
|
@ -209,13 +220,16 @@ def apply(doc=None, method=None, doctype=None, name=None):
|
|||
if not doc and doctype and name:
|
||||
doc = frappe.get_doc(doctype, name)
|
||||
|
||||
assignment_rules = get_doctype_map("Assignment Rule", doc.doctype, filters={
|
||||
"document_type": doc.doctype, "disabled": 0
|
||||
}, order_by="priority desc")
|
||||
assignment_rules = get_doctype_map(
|
||||
"Assignment Rule",
|
||||
doc.doctype,
|
||||
filters={"document_type": doc.doctype, "disabled": 0},
|
||||
order_by="priority desc",
|
||||
)
|
||||
|
||||
# multiple auto assigns
|
||||
assignment_rule_docs: List[AssignmentRule] = [
|
||||
frappe.get_cached_doc("Assignment Rule", d.get('name')) for d in assignment_rules
|
||||
frappe.get_cached_doc("Assignment Rule", d.get("name")) for d in assignment_rules
|
||||
]
|
||||
|
||||
if not assignment_rule_docs:
|
||||
|
|
@ -224,8 +238,8 @@ def apply(doc=None, method=None, doctype=None, name=None):
|
|||
doc = doc.as_dict()
|
||||
assignments = get_assignments(doc)
|
||||
|
||||
clear = True # are all assignments cleared
|
||||
new_apply = False # are new assignments applied
|
||||
clear = True # are all assignments cleared
|
||||
new_apply = False # are new assignments applied
|
||||
|
||||
if assignments:
|
||||
# first unassign
|
||||
|
|
@ -260,14 +274,18 @@ def apply(doc=None, method=None, doctype=None, name=None):
|
|||
|
||||
if not new_apply:
|
||||
# only reopen if close condition is not satisfied
|
||||
to_close_todos = assignment_rule.safe_eval('close_condition', doc)
|
||||
to_close_todos = assignment_rule.safe_eval("close_condition", doc)
|
||||
|
||||
if to_close_todos:
|
||||
# close todo status
|
||||
todos_to_close = frappe.get_all("ToDo", filters={
|
||||
"reference_type": doc.doctype,
|
||||
"reference_name": doc.name,
|
||||
}, pluck="name")
|
||||
todos_to_close = frappe.get_all(
|
||||
"ToDo",
|
||||
filters={
|
||||
"reference_type": doc.doctype,
|
||||
"reference_name": doc.name,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
for todo in todos_to_close:
|
||||
_todo = frappe.get_doc("ToDo", todo)
|
||||
|
|
@ -286,8 +304,7 @@ def apply(doc=None, method=None, doctype=None, name=None):
|
|||
|
||||
|
||||
def update_due_date(doc, state=None):
|
||||
"""Run on_update on every Document (via hooks.py)
|
||||
"""
|
||||
"""Run on_update on every Document (via hooks.py)"""
|
||||
skip_document_update = (
|
||||
frappe.flags.in_migrate
|
||||
or frappe.flags.in_patch
|
||||
|
|
@ -306,7 +323,7 @@ def update_due_date(doc, state=None):
|
|||
"due_date_based_on": ["is", "set"],
|
||||
"document_type": doc.doctype,
|
||||
"disabled": 0,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
for rule in assignment_rules:
|
||||
|
|
@ -319,20 +336,24 @@ def update_due_date(doc, state=None):
|
|||
)
|
||||
|
||||
if field_updated:
|
||||
assignment_todos = frappe.get_all("ToDo", filters={
|
||||
"assignment_rule": rule.get("name"),
|
||||
"reference_type": doc.doctype,
|
||||
"reference_name": doc.name,
|
||||
"status": "Open",
|
||||
}, pluck="name")
|
||||
assignment_todos = frappe.get_all(
|
||||
"ToDo",
|
||||
filters={
|
||||
"assignment_rule": rule.get("name"),
|
||||
"reference_type": doc.doctype,
|
||||
"reference_name": doc.name,
|
||||
"status": "Open",
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
for todo in assignment_todos:
|
||||
todo_doc = frappe.get_doc('ToDo', todo)
|
||||
todo_doc = frappe.get_doc("ToDo", todo)
|
||||
todo_doc.date = doc.get(due_date_field)
|
||||
todo_doc.flags.updater_reference = {
|
||||
'doctype': 'Assignment Rule',
|
||||
'docname': rule.get('name'),
|
||||
'label': _('via Assignment Rule')
|
||||
"doctype": "Assignment Rule",
|
||||
"docname": rule.get("name"),
|
||||
"label": _("via Assignment Rule"),
|
||||
}
|
||||
todo_doc.save(ignore_permissions=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -20,13 +20,13 @@ class TestAutoAssign(unittest.TestCase):
|
|||
def setUp(self):
|
||||
make_test_records("User")
|
||||
days = [
|
||||
dict(day = 'Sunday'),
|
||||
dict(day = 'Monday'),
|
||||
dict(day = 'Tuesday'),
|
||||
dict(day = 'Wednesday'),
|
||||
dict(day = 'Thursday'),
|
||||
dict(day = 'Friday'),
|
||||
dict(day = 'Saturday'),
|
||||
dict(day="Sunday"),
|
||||
dict(day="Monday"),
|
||||
dict(day="Tuesday"),
|
||||
dict(day="Wednesday"),
|
||||
dict(day="Thursday"),
|
||||
dict(day="Friday"),
|
||||
dict(day="Saturday"),
|
||||
]
|
||||
self.days = days
|
||||
self.assignment_rule = get_assignment_rule([days, days])
|
||||
|
|
@ -36,20 +36,22 @@ class TestAutoAssign(unittest.TestCase):
|
|||
note = make_note(dict(public=1))
|
||||
|
||||
# check if auto assigned to first user
|
||||
self.assertEqual(frappe.db.get_value('ToDo', dict(
|
||||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
), 'allocated_to'), 'test@example.com')
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
|
||||
),
|
||||
"test@example.com",
|
||||
)
|
||||
|
||||
note = make_note(dict(public=1))
|
||||
|
||||
# check if auto assigned to second user
|
||||
self.assertEqual(frappe.db.get_value('ToDo', dict(
|
||||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
), 'allocated_to'), 'test1@example.com')
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
|
||||
),
|
||||
"test1@example.com",
|
||||
)
|
||||
|
||||
clear_assignments()
|
||||
|
||||
|
|
@ -57,35 +59,41 @@ class TestAutoAssign(unittest.TestCase):
|
|||
|
||||
# check if auto assigned to third user, even if
|
||||
# previous assignments where closed
|
||||
self.assertEqual(frappe.db.get_value('ToDo', dict(
|
||||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
), 'allocated_to'), 'test2@example.com')
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
|
||||
),
|
||||
"test2@example.com",
|
||||
)
|
||||
|
||||
# check loop back to first user
|
||||
note = make_note(dict(public=1))
|
||||
|
||||
self.assertEqual(frappe.db.get_value('ToDo', dict(
|
||||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
), 'allocated_to'), 'test@example.com')
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
|
||||
),
|
||||
"test@example.com",
|
||||
)
|
||||
|
||||
def test_load_balancing(self):
|
||||
self.assignment_rule.rule = 'Load Balancing'
|
||||
self.assignment_rule.rule = "Load Balancing"
|
||||
self.assignment_rule.save()
|
||||
|
||||
for _ in range(30):
|
||||
note = make_note(dict(public=1))
|
||||
|
||||
# check if each user has 10 assignments (?)
|
||||
for user in ('test@example.com', 'test1@example.com', 'test2@example.com'):
|
||||
self.assertEqual(len(frappe.get_all('ToDo', dict(allocated_to = user, reference_type = 'Note'))), 10)
|
||||
for user in ("test@example.com", "test1@example.com", "test2@example.com"):
|
||||
self.assertEqual(
|
||||
len(frappe.get_all("ToDo", dict(allocated_to=user, reference_type="Note"))), 10
|
||||
)
|
||||
|
||||
# clear 5 assignments for first user
|
||||
# can't do a limit in "delete" since postgres does not support it
|
||||
for d in frappe.get_all('ToDo', dict(reference_type = 'Note', allocated_to = 'test@example.com'), limit=5):
|
||||
for d in frappe.get_all(
|
||||
"ToDo", dict(reference_type="Note", allocated_to="test@example.com"), limit=5
|
||||
):
|
||||
frappe.db.delete("ToDo", {"name": d.name})
|
||||
|
||||
# add 5 more assignments
|
||||
|
|
@ -93,56 +101,59 @@ class TestAutoAssign(unittest.TestCase):
|
|||
make_note(dict(public=1))
|
||||
|
||||
# check if each user still has 10 assignments
|
||||
for user in ('test@example.com', 'test1@example.com', 'test2@example.com'):
|
||||
self.assertEqual(len(frappe.get_all('ToDo', dict(allocated_to = user, reference_type = 'Note'))), 10)
|
||||
for user in ("test@example.com", "test1@example.com", "test2@example.com"):
|
||||
self.assertEqual(
|
||||
len(frappe.get_all("ToDo", dict(allocated_to=user, reference_type="Note"))), 10
|
||||
)
|
||||
|
||||
def test_based_on_field(self):
|
||||
self.assignment_rule.rule = 'Based on Field'
|
||||
self.assignment_rule.field = 'owner'
|
||||
self.assignment_rule.rule = "Based on Field"
|
||||
self.assignment_rule.field = "owner"
|
||||
self.assignment_rule.save()
|
||||
|
||||
frappe.set_user('test1@example.com')
|
||||
frappe.set_user("test1@example.com")
|
||||
note = make_note(dict(public=1))
|
||||
# check if auto assigned to doc owner, test1@example.com
|
||||
self.assertEqual(frappe.db.get_value('ToDo', dict(
|
||||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
), 'owner'), 'test1@example.com')
|
||||
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')
|
||||
frappe.set_user("test2@example.com")
|
||||
note = make_note(dict(public=1))
|
||||
# check if auto assigned to doc owner, test2@example.com
|
||||
self.assertEqual(frappe.db.get_value('ToDo', dict(
|
||||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
), 'owner'), 'test2@example.com')
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "owner"
|
||||
),
|
||||
"test2@example.com",
|
||||
)
|
||||
|
||||
frappe.set_user('Administrator')
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def test_assign_condition(self):
|
||||
# check condition
|
||||
note = make_note(dict(public=0))
|
||||
|
||||
self.assertEqual(frappe.db.get_value('ToDo', dict(
|
||||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
), 'allocated_to'), None)
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
def test_clear_assignment(self):
|
||||
note = make_note(dict(public=1))
|
||||
|
||||
# check if auto assigned to first user
|
||||
todo = frappe.get_list('ToDo', dict(
|
||||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
), limit=1)[0]
|
||||
todo = frappe.get_list(
|
||||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), limit=1
|
||||
)[0]
|
||||
|
||||
todo = frappe.get_doc('ToDo', todo['name'])
|
||||
self.assertEqual(todo.allocated_to, 'test@example.com')
|
||||
todo = frappe.get_doc("ToDo", todo["name"])
|
||||
self.assertEqual(todo.allocated_to, "test@example.com")
|
||||
|
||||
# test auto unassign
|
||||
note.public = 0
|
||||
|
|
@ -151,99 +162,101 @@ class TestAutoAssign(unittest.TestCase):
|
|||
todo.load_from_db()
|
||||
|
||||
# check if todo is cancelled
|
||||
self.assertEqual(todo.status, 'Cancelled')
|
||||
self.assertEqual(todo.status, "Cancelled")
|
||||
|
||||
def test_close_assignment(self):
|
||||
note = make_note(dict(public=1, content="valid"))
|
||||
|
||||
# check if auto assigned
|
||||
todo = frappe.get_list('ToDo', dict(
|
||||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
), limit=1)[0]
|
||||
todo = frappe.get_list(
|
||||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), limit=1
|
||||
)[0]
|
||||
|
||||
todo = frappe.get_doc('ToDo', todo['name'])
|
||||
self.assertEqual(todo.allocated_to, 'test@example.com')
|
||||
todo = frappe.get_doc("ToDo", todo["name"])
|
||||
self.assertEqual(todo.allocated_to, "test@example.com")
|
||||
|
||||
note.content="Closed"
|
||||
note.content = "Closed"
|
||||
note.save()
|
||||
|
||||
todo.load_from_db()
|
||||
|
||||
# check if todo is closed
|
||||
self.assertEqual(todo.status, 'Closed')
|
||||
self.assertEqual(todo.status, "Closed")
|
||||
# check if closed todo retained assignment
|
||||
self.assertEqual(todo.allocated_to, 'test@example.com')
|
||||
self.assertEqual(todo.allocated_to, "test@example.com")
|
||||
|
||||
def check_multiple_rules(self):
|
||||
note = make_note(dict(public=1, notify_on_login=1))
|
||||
|
||||
# check if auto assigned to test3 (2nd rule is applied, as it has higher priority)
|
||||
self.assertEqual(frappe.db.get_value('ToDo', dict(
|
||||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
), 'allocated_to'), 'test@example.com')
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
|
||||
),
|
||||
"test@example.com",
|
||||
)
|
||||
|
||||
def check_assignment_rule_scheduling(self):
|
||||
frappe.db.delete("Assignment Rule")
|
||||
|
||||
days_1 = [dict(day = 'Sunday'), dict(day = 'Monday'), dict(day = 'Tuesday')]
|
||||
days_1 = [dict(day="Sunday"), dict(day="Monday"), dict(day="Tuesday")]
|
||||
|
||||
days_2 = [dict(day = 'Wednesday'), dict(day = 'Thursday'), dict(day = 'Friday'), dict(day = 'Saturday')]
|
||||
days_2 = [dict(day="Wednesday"), dict(day="Thursday"), dict(day="Friday"), dict(day="Saturday")]
|
||||
|
||||
get_assignment_rule([days_1, days_2], ['public == 1', 'public == 1'])
|
||||
get_assignment_rule([days_1, days_2], ["public == 1", "public == 1"])
|
||||
|
||||
frappe.flags.assignment_day = "Monday"
|
||||
note = make_note(dict(public=1))
|
||||
|
||||
self.assertIn(frappe.db.get_value('ToDo', dict(
|
||||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
), 'allocated_to'), ['test@example.com', 'test1@example.com', 'test2@example.com'])
|
||||
self.assertIn(
|
||||
frappe.db.get_value(
|
||||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
|
||||
),
|
||||
["test@example.com", "test1@example.com", "test2@example.com"],
|
||||
)
|
||||
|
||||
frappe.flags.assignment_day = "Friday"
|
||||
note = make_note(dict(public=1))
|
||||
|
||||
self.assertIn(frappe.db.get_value('ToDo', dict(
|
||||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
), 'allocated_to'), ['test3@example.com'])
|
||||
self.assertIn(
|
||||
frappe.db.get_value(
|
||||
"ToDo", dict(reference_type="Note", reference_name=note.name, status="Open"), "allocated_to"
|
||||
),
|
||||
["test3@example.com"],
|
||||
)
|
||||
|
||||
def test_assignment_rule_condition(self):
|
||||
frappe.db.delete("Assignment Rule")
|
||||
|
||||
# Add expiry_date custom field
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
df = dict(fieldname='expiry_date', label='Expiry Date', fieldtype='Date')
|
||||
create_custom_field('Note', df)
|
||||
|
||||
assignment_rule = frappe.get_doc(dict(
|
||||
name = 'Assignment with Due Date',
|
||||
doctype = 'Assignment Rule',
|
||||
document_type = 'Note',
|
||||
assign_condition = 'public == 0',
|
||||
due_date_based_on = 'expiry_date',
|
||||
assignment_days = self.days,
|
||||
users = [
|
||||
dict(user = 'test@example.com'),
|
||||
]
|
||||
)).insert()
|
||||
df = dict(fieldname="expiry_date", label="Expiry Date", fieldtype="Date")
|
||||
create_custom_field("Note", df)
|
||||
|
||||
assignment_rule = frappe.get_doc(
|
||||
dict(
|
||||
name="Assignment with Due Date",
|
||||
doctype="Assignment Rule",
|
||||
document_type="Note",
|
||||
assign_condition="public == 0",
|
||||
due_date_based_on="expiry_date",
|
||||
assignment_days=self.days,
|
||||
users=[
|
||||
dict(user="test@example.com"),
|
||||
],
|
||||
)
|
||||
).insert()
|
||||
|
||||
expiry_date = frappe.utils.add_days(frappe.utils.nowdate(), 2)
|
||||
note1 = make_note({'expiry_date': expiry_date})
|
||||
note2 = make_note({'expiry_date': expiry_date})
|
||||
note1 = make_note({"expiry_date": expiry_date})
|
||||
note2 = make_note({"expiry_date": expiry_date})
|
||||
|
||||
note1_todo = frappe.get_all('ToDo', filters=dict(
|
||||
reference_type = 'Note',
|
||||
reference_name = note1.name,
|
||||
status = 'Open'
|
||||
))[0]
|
||||
note1_todo = frappe.get_all(
|
||||
"ToDo", filters=dict(reference_type="Note", reference_name=note1.name, status="Open")
|
||||
)[0]
|
||||
|
||||
note1_todo_doc = frappe.get_doc('ToDo', note1_todo.name)
|
||||
note1_todo_doc = frappe.get_doc("ToDo", note1_todo.name)
|
||||
self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), expiry_date)
|
||||
|
||||
# due date should be updated if the reference doc's date is updated.
|
||||
|
|
@ -253,66 +266,67 @@ class TestAutoAssign(unittest.TestCase):
|
|||
self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), note1.expiry_date)
|
||||
|
||||
# saving one note's expiry should not update other note todo's due date
|
||||
note2_todo = frappe.get_all('ToDo', filters=dict(
|
||||
reference_type = 'Note',
|
||||
reference_name = note2.name,
|
||||
status = 'Open'
|
||||
), fields=['name', 'date'])[0]
|
||||
note2_todo = frappe.get_all(
|
||||
"ToDo",
|
||||
filters=dict(reference_type="Note", reference_name=note2.name, status="Open"),
|
||||
fields=["name", "date"],
|
||||
)[0]
|
||||
self.assertNotEqual(frappe.utils.get_date_str(note2_todo.date), note1.expiry_date)
|
||||
self.assertEqual(frappe.utils.get_date_str(note2_todo.date), expiry_date)
|
||||
assignment_rule.delete()
|
||||
|
||||
|
||||
def clear_assignments():
|
||||
frappe.db.delete("ToDo", {"reference_type": "Note"})
|
||||
|
||||
|
||||
def get_assignment_rule(days, assign=None):
|
||||
frappe.delete_doc_if_exists('Assignment Rule', 'For Note 1')
|
||||
frappe.delete_doc_if_exists("Assignment Rule", "For Note 1")
|
||||
|
||||
if not assign:
|
||||
assign = ['public == 1', 'notify_on_login == 1']
|
||||
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()
|
||||
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')
|
||||
frappe.delete_doc_if_exists("Assignment Rule", "For Note 2")
|
||||
|
||||
# 2nd rule
|
||||
frappe.get_doc(dict(
|
||||
name = 'For Note 2',
|
||||
doctype = 'Assignment Rule',
|
||||
priority = 1,
|
||||
document_type = 'Note',
|
||||
assign_condition = assign[1],
|
||||
unassign_condition = 'notify_on_login == 0',
|
||||
rule = 'Round Robin',
|
||||
assignment_days = days[1],
|
||||
users = [
|
||||
dict(user = 'test3@example.com')
|
||||
]
|
||||
)).insert()
|
||||
frappe.get_doc(
|
||||
dict(
|
||||
name="For Note 2",
|
||||
doctype="Assignment Rule",
|
||||
priority=1,
|
||||
document_type="Note",
|
||||
assign_condition=assign[1],
|
||||
unassign_condition="notify_on_login == 0",
|
||||
rule="Round Robin",
|
||||
assignment_days=days[1],
|
||||
users=[dict(user="test3@example.com")],
|
||||
)
|
||||
).insert()
|
||||
|
||||
return assignment_rule
|
||||
|
||||
|
||||
def make_note(values=None):
|
||||
note = frappe.get_doc(dict(
|
||||
doctype = 'Note',
|
||||
title = random_string(10),
|
||||
content = random_string(20)
|
||||
))
|
||||
note = frappe.get_doc(dict(doctype="Note", title=random_string(10), content=random_string(20)))
|
||||
|
||||
if values:
|
||||
note.update(values)
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@
|
|||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class AssignmentRuleDay(Document):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@
|
|||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class AssignmentRuleUser(Document):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -2,23 +2,45 @@
|
|||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from datetime import timedelta
|
||||
from frappe.desk.form import assign_to
|
||||
from frappe.utils.jinja import validate_template
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from frappe.utils.user import get_system_managers
|
||||
from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_day, get_first_day, month_diff
|
||||
from frappe.model.document import Document
|
||||
from frappe.core.doctype.communication.email import make
|
||||
from frappe.utils.background_jobs import get_jobs
|
||||
from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeated
|
||||
from frappe.contacts.doctype.contact.contact import get_contacts_linked_from
|
||||
from frappe.contacts.doctype.contact.contact import get_contacts_linking_to
|
||||
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.model.document import Document
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
cstr,
|
||||
get_first_day,
|
||||
get_last_day,
|
||||
getdate,
|
||||
month_diff,
|
||||
split_emails,
|
||||
today,
|
||||
)
|
||||
from frappe.utils.background_jobs import get_jobs
|
||||
from frappe.utils.jinja import validate_template
|
||||
from frappe.utils.user import get_system_managers
|
||||
|
||||
month_map = {"Monthly": 1, "Quarterly": 3, "Half-yearly": 6, "Yearly": 12}
|
||||
week_map = {
|
||||
"Monday": 0,
|
||||
"Tuesday": 1,
|
||||
"Wednesday": 2,
|
||||
"Thursday": 3,
|
||||
"Friday": 4,
|
||||
"Saturday": 5,
|
||||
"Sunday": 6,
|
||||
}
|
||||
|
||||
month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}
|
||||
week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6}
|
||||
|
||||
class AutoRepeat(Document):
|
||||
def validate(self):
|
||||
|
|
@ -46,7 +68,7 @@ class AutoRepeat(Document):
|
|||
frappe.get_doc(self.reference_doctype, self.reference_document).notify_update()
|
||||
|
||||
def on_trash(self):
|
||||
frappe.db.set_value(self.reference_doctype, self.reference_document, 'auto_repeat', '')
|
||||
frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", "")
|
||||
frappe.get_doc(self.reference_doctype, self.reference_document).notify_update()
|
||||
|
||||
def set_dates(self):
|
||||
|
|
@ -56,29 +78,36 @@ class AutoRepeat(Document):
|
|||
self.next_schedule_date = self.get_next_schedule_date(schedule_date=self.start_date)
|
||||
|
||||
def unlink_if_applicable(self):
|
||||
if self.status == 'Completed' or self.disabled:
|
||||
frappe.db.set_value(self.reference_doctype, self.reference_document, 'auto_repeat', '')
|
||||
if self.status == "Completed" or self.disabled:
|
||||
frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", "")
|
||||
|
||||
def validate_reference_doctype(self):
|
||||
if frappe.flags.in_test or frappe.flags.in_patch:
|
||||
return
|
||||
if not frappe.get_meta(self.reference_doctype).allow_auto_repeat:
|
||||
frappe.throw(_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(self.reference_doctype))
|
||||
frappe.throw(
|
||||
_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(
|
||||
self.reference_doctype
|
||||
)
|
||||
)
|
||||
|
||||
def validate_submit_on_creation(self):
|
||||
if self.submit_on_creation and not frappe.get_meta(self.reference_doctype).is_submittable:
|
||||
frappe.throw(_('Cannot enable {0} for a non-submittable doctype').format(
|
||||
frappe.bold('Submit on Creation')))
|
||||
frappe.throw(
|
||||
_("Cannot enable {0} for a non-submittable doctype").format(frappe.bold("Submit on Creation"))
|
||||
)
|
||||
|
||||
def validate_dates(self):
|
||||
if frappe.flags.in_patch:
|
||||
return
|
||||
|
||||
if self.end_date:
|
||||
self.validate_from_to_dates('start_date', 'end_date')
|
||||
self.validate_from_to_dates("start_date", "end_date")
|
||||
|
||||
if self.end_date == self.start_date:
|
||||
frappe.throw(_('{0} should not be same as {1}').format(frappe.bold('End Date'), frappe.bold('Start Date')))
|
||||
frappe.throw(
|
||||
_("{0} should not be same as {1}").format(frappe.bold("End Date"), frappe.bold("Start Date"))
|
||||
)
|
||||
|
||||
def validate_email_id(self):
|
||||
if self.notify_by_email:
|
||||
|
|
@ -100,17 +129,17 @@ class AutoRepeat(Document):
|
|||
|
||||
frappe.throw(
|
||||
_("Auto Repeat Day{0} {1} has been repeated.").format(
|
||||
plural,
|
||||
frappe.bold(", ".join(repeated_days))
|
||||
plural, frappe.bold(", ".join(repeated_days))
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def update_auto_repeat_id(self):
|
||||
#check if document is already on auto repeat
|
||||
# check if document is already on auto repeat
|
||||
auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat")
|
||||
if auto_repeat and auto_repeat != self.name and not frappe.flags.in_patch:
|
||||
frappe.throw(_("The {0} is already on auto repeat {1}").format(self.reference_document, auto_repeat))
|
||||
frappe.throw(
|
||||
_("The {0} is already on auto repeat {1}").format(self.reference_document, auto_repeat)
|
||||
)
|
||||
else:
|
||||
frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", self.name)
|
||||
|
||||
|
|
@ -136,18 +165,18 @@ class AutoRepeat(Document):
|
|||
row = {
|
||||
"reference_document": self.reference_document,
|
||||
"frequency": self.frequency,
|
||||
"next_scheduled_date": next_date
|
||||
"next_scheduled_date": next_date,
|
||||
}
|
||||
schedule_details.append(row)
|
||||
|
||||
if self.end_date:
|
||||
next_date = self.get_next_schedule_date(schedule_date=start_date, for_full_schedule=True)
|
||||
|
||||
while (getdate(next_date) < getdate(end_date)):
|
||||
while getdate(next_date) < getdate(end_date):
|
||||
row = {
|
||||
"reference_document" : self.reference_document,
|
||||
"frequency" : self.frequency,
|
||||
"next_scheduled_date" : next_date
|
||||
"reference_document": self.reference_document,
|
||||
"frequency": self.frequency,
|
||||
"next_scheduled_date": next_date,
|
||||
}
|
||||
schedule_details.append(row)
|
||||
next_date = self.get_next_schedule_date(schedule_date=next_date, for_full_schedule=True)
|
||||
|
|
@ -169,9 +198,9 @@ class AutoRepeat(Document):
|
|||
|
||||
def make_new_document(self):
|
||||
reference_doc = frappe.get_doc(self.reference_doctype, self.reference_document)
|
||||
new_doc = frappe.copy_doc(reference_doc, ignore_no_copy = False)
|
||||
new_doc = frappe.copy_doc(reference_doc, ignore_no_copy=False)
|
||||
self.update_doc(new_doc, reference_doc)
|
||||
new_doc.insert(ignore_permissions = True)
|
||||
new_doc.insert(ignore_permissions=True)
|
||||
|
||||
if self.submit_on_creation:
|
||||
new_doc.submit()
|
||||
|
|
@ -180,61 +209,72 @@ class AutoRepeat(Document):
|
|||
|
||||
def update_doc(self, new_doc, reference_doc):
|
||||
new_doc.docstatus = 0
|
||||
if new_doc.meta.get_field('set_posting_time'):
|
||||
new_doc.set('set_posting_time', 1)
|
||||
if new_doc.meta.get_field("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)
|
||||
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']:
|
||||
for fieldname in [
|
||||
"naming_series",
|
||||
"ignore_pricing_rule",
|
||||
"posting_time",
|
||||
"select_print_heading",
|
||||
"user_remark",
|
||||
"remarks",
|
||||
"owner",
|
||||
]:
|
||||
if new_doc.meta.get_field(fieldname):
|
||||
new_doc.set(fieldname, reference_doc.get(fieldname))
|
||||
|
||||
for data in new_doc.meta.fields:
|
||||
if data.fieldtype == 'Date' and data.reqd:
|
||||
if data.fieldtype == "Date" and data.reqd:
|
||||
new_doc.set(data.fieldname, self.next_schedule_date)
|
||||
|
||||
self.set_auto_repeat_period(new_doc)
|
||||
|
||||
auto_repeat_doc = frappe.get_doc('Auto Repeat', self.name)
|
||||
auto_repeat_doc = frappe.get_doc("Auto Repeat", self.name)
|
||||
|
||||
#for any action that needs to take place after the recurring document creation
|
||||
#on recurring method of that doctype is triggered
|
||||
new_doc.run_method('on_recurring', reference_doc = reference_doc, auto_repeat_doc = auto_repeat_doc)
|
||||
# for any action that needs to take place after the recurring document creation
|
||||
# on recurring method of that doctype is triggered
|
||||
new_doc.run_method("on_recurring", reference_doc=reference_doc, auto_repeat_doc=auto_repeat_doc)
|
||||
|
||||
def set_auto_repeat_period(self, new_doc):
|
||||
mcount = month_map.get(self.frequency)
|
||||
if mcount and new_doc.meta.get_field('from_date') and new_doc.meta.get_field('to_date'):
|
||||
last_ref_doc = frappe.db.get_all(doctype = self.reference_doctype,
|
||||
fields = ['name', 'from_date', 'to_date'],
|
||||
filters = [
|
||||
['auto_repeat', '=', self.name],
|
||||
['docstatus', '<', 2],
|
||||
if mcount and new_doc.meta.get_field("from_date") and new_doc.meta.get_field("to_date"):
|
||||
last_ref_doc = frappe.db.get_all(
|
||||
doctype=self.reference_doctype,
|
||||
fields=["name", "from_date", "to_date"],
|
||||
filters=[
|
||||
["auto_repeat", "=", self.name],
|
||||
["docstatus", "<", 2],
|
||||
],
|
||||
order_by = 'creation desc',
|
||||
limit = 1)
|
||||
order_by="creation desc",
|
||||
limit=1,
|
||||
)
|
||||
|
||||
if not last_ref_doc:
|
||||
return
|
||||
|
||||
from_date = get_next_date(last_ref_doc[0].from_date, mcount)
|
||||
|
||||
if (cstr(get_first_day(last_ref_doc[0].from_date)) == cstr(last_ref_doc[0].from_date)) and \
|
||||
(cstr(get_last_day(last_ref_doc[0].to_date)) == cstr(last_ref_doc[0].to_date)):
|
||||
if (cstr(get_first_day(last_ref_doc[0].from_date)) == cstr(last_ref_doc[0].from_date)) and (
|
||||
cstr(get_last_day(last_ref_doc[0].to_date)) == cstr(last_ref_doc[0].to_date)
|
||||
):
|
||||
to_date = get_last_day(get_next_date(last_ref_doc[0].to_date, mcount))
|
||||
else:
|
||||
to_date = get_next_date(last_ref_doc[0].to_date, mcount)
|
||||
|
||||
new_doc.set('from_date', from_date)
|
||||
new_doc.set('to_date', to_date)
|
||||
new_doc.set("from_date", from_date)
|
||||
new_doc.set("to_date", to_date)
|
||||
|
||||
def get_next_schedule_date(self, schedule_date, for_full_schedule=False):
|
||||
"""
|
||||
Returns the next schedule date for auto repeat after a recurring document has been created.
|
||||
Adds required offset to the schedule_date param and returns the next schedule date.
|
||||
Returns the next schedule date for auto repeat after a recurring document has been created.
|
||||
Adds required offset to the schedule_date param and returns the next schedule date.
|
||||
|
||||
:param schedule_date: The date when the last recurring document was created.
|
||||
:param for_full_schedule: If True, returns the immediate next schedule date, else the full schedule.
|
||||
:param schedule_date: The date when the last recurring document was created.
|
||||
:param for_full_schedule: If True, returns the immediate next schedule date, else the full schedule.
|
||||
"""
|
||||
if month_map.get(self.frequency):
|
||||
month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1
|
||||
|
|
@ -295,60 +335,75 @@ class AutoRepeat(Document):
|
|||
return 7
|
||||
|
||||
def get_auto_repeat_days(self):
|
||||
return [d.day for d in self.get('repeat_on_days', [])]
|
||||
return [d.day for d in self.get("repeat_on_days", [])]
|
||||
|
||||
def send_notification(self, new_doc):
|
||||
"""Notify concerned people about recurring document generation"""
|
||||
subject = self.subject or ''
|
||||
message = self.message or ''
|
||||
subject = self.subject or ""
|
||||
message = self.message or ""
|
||||
|
||||
if not self.subject:
|
||||
subject = _("New {0}: {1}").format(new_doc.doctype, new_doc.name)
|
||||
elif "{" in self.subject:
|
||||
subject = frappe.render_template(self.subject, {'doc': new_doc})
|
||||
subject = frappe.render_template(self.subject, {"doc": new_doc})
|
||||
|
||||
print_format = self.print_format or 'Standard'
|
||||
print_format = self.print_format or "Standard"
|
||||
error_string = None
|
||||
|
||||
try:
|
||||
attachments = [frappe.attach_print(new_doc.doctype, new_doc.name,
|
||||
file_name=new_doc.name, print_format=print_format)]
|
||||
attachments = [
|
||||
frappe.attach_print(
|
||||
new_doc.doctype, new_doc.name, file_name=new_doc.name, print_format=print_format
|
||||
)
|
||||
]
|
||||
|
||||
except frappe.PermissionError:
|
||||
error_string = _("A recurring {0} {1} has been created for you via Auto Repeat {2}.").format(new_doc.doctype, new_doc.name, self.name)
|
||||
error_string = _("A recurring {0} {1} has been created for you via Auto Repeat {2}.").format(
|
||||
new_doc.doctype, new_doc.name, self.name
|
||||
)
|
||||
error_string += "<br><br>"
|
||||
|
||||
error_string += _("{0}: Failed to attach new recurring document. To enable attaching document in the auto repeat notification email, enable {1} in Print Settings").format(
|
||||
frappe.bold(_('Note')),
|
||||
frappe.bold(_('Allow Print for Draft'))
|
||||
)
|
||||
attachments = '[]'
|
||||
error_string += _(
|
||||
"{0}: Failed to attach new recurring document. To enable attaching document in the auto repeat notification email, enable {1} in Print Settings"
|
||||
).format(frappe.bold(_("Note")), frappe.bold(_("Allow Print for Draft")))
|
||||
attachments = "[]"
|
||||
|
||||
if error_string:
|
||||
message = error_string
|
||||
elif not self.message:
|
||||
message = _("Please find attached {0}: {1}").format(new_doc.doctype, new_doc.name)
|
||||
elif "{" in self.message:
|
||||
message = frappe.render_template(self.message, {'doc': new_doc})
|
||||
message = frappe.render_template(self.message, {"doc": new_doc})
|
||||
|
||||
recipients = self.recipients.split('\n')
|
||||
recipients = self.recipients.split("\n")
|
||||
|
||||
make(doctype=new_doc.doctype, name=new_doc.name, recipients=recipients,
|
||||
subject=subject, content=message, attachments=attachments, send_email=1)
|
||||
make(
|
||||
doctype=new_doc.doctype,
|
||||
name=new_doc.name,
|
||||
recipients=recipients,
|
||||
subject=subject,
|
||||
content=message,
|
||||
attachments=attachments,
|
||||
send_email=1,
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def fetch_linked_contacts(self):
|
||||
if self.reference_doctype and self.reference_document:
|
||||
res = get_contacts_linking_to(self.reference_doctype, self.reference_document, fields=['email_id'])
|
||||
res += get_contacts_linked_from(self.reference_doctype, self.reference_document, fields=['email_id'])
|
||||
res = get_contacts_linking_to(
|
||||
self.reference_doctype, self.reference_document, fields=["email_id"]
|
||||
)
|
||||
res += get_contacts_linked_from(
|
||||
self.reference_doctype, self.reference_document, fields=["email_id"]
|
||||
)
|
||||
email_ids = {d.email_id for d in res}
|
||||
if not email_ids:
|
||||
frappe.msgprint(_('No contacts linked to document'), alert=True)
|
||||
frappe.msgprint(_("No contacts linked to document"), alert=True)
|
||||
else:
|
||||
self.recipients = ', '.join(email_ids)
|
||||
self.recipients = ", ".join(email_ids)
|
||||
|
||||
def disable_auto_repeat(self):
|
||||
frappe.db.set_value('Auto Repeat', self.name, 'disabled', 1)
|
||||
frappe.db.set_value("Auto Repeat", self.name, "disabled", 1)
|
||||
|
||||
def notify_error_to_user(self, error_log):
|
||||
recipients = list(get_system_managers(only_name=True))
|
||||
|
|
@ -356,20 +411,17 @@ class AutoRepeat(Document):
|
|||
subject = _("Auto Repeat Document Creation Failed")
|
||||
|
||||
form_link = frappe.utils.get_link_to_form(self.reference_doctype, self.reference_document)
|
||||
auto_repeat_failed_for = _('Auto Repeat failed for {0}').format(form_link)
|
||||
auto_repeat_failed_for = _("Auto Repeat failed for {0}").format(form_link)
|
||||
|
||||
error_log_link = frappe.utils.get_link_to_form('Error Log', error_log.name)
|
||||
error_log_message = _('Check the Error Log for more information: {0}').format(error_log_link)
|
||||
error_log_link = frappe.utils.get_link_to_form("Error Log", error_log.name)
|
||||
error_log_message = _("Check the Error Log for more information: {0}").format(error_log_link)
|
||||
|
||||
frappe.sendmail(
|
||||
recipients=recipients,
|
||||
subject=subject,
|
||||
template="auto_repeat_fail",
|
||||
args={
|
||||
'auto_repeat_failed_for': auto_repeat_failed_for,
|
||||
'error_log_message': error_log_message
|
||||
},
|
||||
header=[subject, 'red']
|
||||
args={"auto_repeat_failed_for": auto_repeat_failed_for, "error_log_message": error_log_message},
|
||||
header=[subject, "red"],
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -382,18 +434,18 @@ def get_next_date(dt, mcount, day=None):
|
|||
def get_next_weekday(current_schedule_day, weekdays):
|
||||
days = list(week_map.keys())
|
||||
if current_schedule_day > 0:
|
||||
days = days[(current_schedule_day + 1):] + days[:current_schedule_day]
|
||||
days = days[(current_schedule_day + 1) :] + days[:current_schedule_day]
|
||||
else:
|
||||
days = days[(current_schedule_day + 1):]
|
||||
days = days[(current_schedule_day + 1) :]
|
||||
|
||||
for entry in days:
|
||||
if entry in weekdays:
|
||||
return entry
|
||||
|
||||
|
||||
#called through hooks
|
||||
# called through hooks
|
||||
def make_auto_repeat_entry():
|
||||
enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries'
|
||||
enqueued_method = "frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries"
|
||||
jobs = get_jobs()
|
||||
|
||||
if not jobs or enqueued_method not in jobs[frappe.local.site]:
|
||||
|
|
@ -404,7 +456,7 @@ def make_auto_repeat_entry():
|
|||
|
||||
def create_repeated_entries(data):
|
||||
for d in data:
|
||||
doc = frappe.get_doc('Auto Repeat', d.name)
|
||||
doc = frappe.get_doc("Auto Repeat", d.name)
|
||||
|
||||
current_date = getdate(today())
|
||||
schedule_date = getdate(doc.next_schedule_date)
|
||||
|
|
@ -413,33 +465,32 @@ def create_repeated_entries(data):
|
|||
doc.create_documents()
|
||||
schedule_date = doc.get_next_schedule_date(schedule_date=schedule_date)
|
||||
if schedule_date and not doc.disabled:
|
||||
frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date)
|
||||
frappe.db.set_value("Auto Repeat", doc.name, "next_schedule_date", schedule_date)
|
||||
|
||||
|
||||
def get_auto_repeat_entries(date=None):
|
||||
if not date:
|
||||
date = getdate(today())
|
||||
return frappe.db.get_all('Auto Repeat', filters=[
|
||||
['next_schedule_date', '<=', date],
|
||||
['status', '=', 'Active']
|
||||
])
|
||||
return frappe.db.get_all(
|
||||
"Auto Repeat", filters=[["next_schedule_date", "<=", date], ["status", "=", "Active"]]
|
||||
)
|
||||
|
||||
|
||||
#called through hooks
|
||||
# called through hooks
|
||||
def set_auto_repeat_as_completed():
|
||||
auto_repeat = frappe.get_all("Auto Repeat", filters = {'status': ['!=', 'Disabled']})
|
||||
auto_repeat = frappe.get_all("Auto Repeat", filters={"status": ["!=", "Disabled"]})
|
||||
for entry in auto_repeat:
|
||||
doc = frappe.get_doc("Auto Repeat", entry.name)
|
||||
if doc.is_completed():
|
||||
doc.status = 'Completed'
|
||||
doc.status = "Completed"
|
||||
doc.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, end_date = None):
|
||||
def make_auto_repeat(doctype, docname, frequency="Daily", start_date=None, end_date=None):
|
||||
if not start_date:
|
||||
start_date = getdate(today())
|
||||
doc = frappe.new_doc('Auto Repeat')
|
||||
doc = frappe.new_doc("Auto Repeat")
|
||||
doc.reference_doctype = doctype
|
||||
doc.reference_document = docname
|
||||
doc.frequency = frequency
|
||||
|
|
@ -449,24 +500,34 @@ def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, e
|
|||
doc.save()
|
||||
return doc
|
||||
|
||||
|
||||
# method for reference_doctype filter
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_auto_repeat_doctypes(doctype, txt, searchfield, start, page_len, filters):
|
||||
res = frappe.db.get_all('Property Setter', {
|
||||
'property': 'allow_auto_repeat',
|
||||
'value': '1',
|
||||
}, ['doc_type'])
|
||||
res = frappe.db.get_all(
|
||||
"Property Setter",
|
||||
{
|
||||
"property": "allow_auto_repeat",
|
||||
"value": "1",
|
||||
},
|
||||
["doc_type"],
|
||||
)
|
||||
docs = [r.doc_type for r in res]
|
||||
|
||||
res = frappe.db.get_all('DocType', {
|
||||
'allow_auto_repeat': 1,
|
||||
}, ['name'])
|
||||
res = frappe.db.get_all(
|
||||
"DocType",
|
||||
{
|
||||
"allow_auto_repeat": 1,
|
||||
},
|
||||
["name"],
|
||||
)
|
||||
docs += [r.name for r in res]
|
||||
docs = set(list(docs))
|
||||
|
||||
return [[d] for d in docs]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_reference(docname, reference):
|
||||
result = ""
|
||||
|
|
@ -478,13 +539,14 @@ def update_reference(docname, reference):
|
|||
raise e
|
||||
return result
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def generate_message_preview(reference_dt, reference_doc, message=None, subject=None):
|
||||
frappe.has_permission("Auto Repeat", "write", throw=True)
|
||||
doc = frappe.get_doc(reference_dt, reference_doc)
|
||||
subject_preview = _("Please add a subject to your email")
|
||||
msg_preview = frappe.render_template(message, {'doc': doc})
|
||||
msg_preview = frappe.render_template(message, {"doc": doc})
|
||||
if subject:
|
||||
subject_preview = frappe.render_template(subject, {'doc': doc})
|
||||
subject_preview = frappe.render_template(subject, {"doc": doc})
|
||||
|
||||
return {'message': msg_preview, 'subject': subject_preview}
|
||||
return {"message": msg_preview, "subject": subject_preview}
|
||||
|
|
|
|||
|
|
@ -4,24 +4,40 @@
|
|||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.automation.doctype.auto_repeat.auto_repeat import (
|
||||
create_repeated_entries,
|
||||
get_auto_repeat_entries,
|
||||
week_map,
|
||||
)
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries, week_map
|
||||
from frappe.utils import today, add_days, getdate, add_months
|
||||
from frappe.utils import add_days, add_months, getdate, today
|
||||
|
||||
|
||||
def add_custom_fields():
|
||||
df = dict(
|
||||
fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', insert_after='sender',
|
||||
options='Auto Repeat', hidden=1, print_hide=1, read_only=1)
|
||||
create_custom_field('ToDo', df)
|
||||
fieldname="auto_repeat",
|
||||
label="Auto Repeat",
|
||||
fieldtype="Link",
|
||||
insert_after="sender",
|
||||
options="Auto Repeat",
|
||||
hidden=1,
|
||||
print_hide=1,
|
||||
read_only=1,
|
||||
)
|
||||
create_custom_field("ToDo", df)
|
||||
|
||||
|
||||
class TestAutoRepeat(unittest.TestCase):
|
||||
def setUp(self):
|
||||
if not frappe.db.sql("SELECT `fieldname` FROM `tabCustom Field` WHERE `fieldname`='auto_repeat' and `dt`=%s", "Todo"):
|
||||
if not frappe.db.sql(
|
||||
"SELECT `fieldname` FROM `tabCustom Field` WHERE `fieldname`='auto_repeat' and `dt`=%s", "Todo"
|
||||
):
|
||||
add_custom_fields()
|
||||
|
||||
def test_daily_auto_repeat(self):
|
||||
todo = frappe.get_doc(
|
||||
dict(doctype='ToDo', description='test recurring todo', assigned_by='Administrator')).insert()
|
||||
dict(doctype="ToDo", description="test recurring todo", assigned_by="Administrator")
|
||||
).insert()
|
||||
|
||||
doc = make_auto_repeat(reference_document=todo.name)
|
||||
self.assertEqual(doc.next_schedule_date, today())
|
||||
|
|
@ -32,19 +48,25 @@ class TestAutoRepeat(unittest.TestCase):
|
|||
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document)
|
||||
self.assertEqual(todo.auto_repeat, doc.name)
|
||||
|
||||
new_todo = frappe.db.get_value('ToDo',
|
||||
{'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name')
|
||||
new_todo = frappe.db.get_value(
|
||||
"ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name"
|
||||
)
|
||||
|
||||
new_todo = frappe.get_doc('ToDo', new_todo)
|
||||
new_todo = frappe.get_doc("ToDo", new_todo)
|
||||
|
||||
self.assertEqual(todo.get('description'), new_todo.get('description'))
|
||||
self.assertEqual(todo.get("description"), new_todo.get("description"))
|
||||
|
||||
def test_weekly_auto_repeat(self):
|
||||
todo = frappe.get_doc(
|
||||
dict(doctype='ToDo', description='test weekly todo', assigned_by='Administrator')).insert()
|
||||
dict(doctype="ToDo", description="test weekly todo", assigned_by="Administrator")
|
||||
).insert()
|
||||
|
||||
doc = make_auto_repeat(reference_doctype='ToDo',
|
||||
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7))
|
||||
doc = make_auto_repeat(
|
||||
reference_doctype="ToDo",
|
||||
frequency="Weekly",
|
||||
reference_document=todo.name,
|
||||
start_date=add_days(today(), -7),
|
||||
)
|
||||
|
||||
self.assertEqual(doc.next_schedule_date, today())
|
||||
data = get_auto_repeat_entries(getdate(today()))
|
||||
|
|
@ -54,25 +76,29 @@ class TestAutoRepeat(unittest.TestCase):
|
|||
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document)
|
||||
self.assertEqual(todo.auto_repeat, doc.name)
|
||||
|
||||
new_todo = frappe.db.get_value('ToDo',
|
||||
{'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name')
|
||||
new_todo = frappe.db.get_value(
|
||||
"ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name"
|
||||
)
|
||||
|
||||
new_todo = frappe.get_doc('ToDo', new_todo)
|
||||
new_todo = frappe.get_doc("ToDo", new_todo)
|
||||
|
||||
self.assertEqual(todo.get('description'), new_todo.get('description'))
|
||||
self.assertEqual(todo.get("description"), new_todo.get("description"))
|
||||
|
||||
def test_weekly_auto_repeat_with_weekdays(self):
|
||||
todo = frappe.get_doc(
|
||||
dict(doctype='ToDo', description='test auto repeat with weekdays', assigned_by='Administrator')).insert()
|
||||
dict(doctype="ToDo", description="test auto repeat with weekdays", assigned_by="Administrator")
|
||||
).insert()
|
||||
|
||||
weekdays = list(week_map.keys())
|
||||
current_weekday = getdate().weekday()
|
||||
days = [
|
||||
{'day': weekdays[current_weekday]},
|
||||
{'day': weekdays[(current_weekday + 2) % 7]}
|
||||
]
|
||||
doc = make_auto_repeat(reference_doctype='ToDo',
|
||||
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7), days=days)
|
||||
days = [{"day": weekdays[current_weekday]}, {"day": weekdays[(current_weekday + 2) % 7]}]
|
||||
doc = make_auto_repeat(
|
||||
reference_doctype="ToDo",
|
||||
frequency="Weekly",
|
||||
reference_document=todo.name,
|
||||
start_date=add_days(today(), -7),
|
||||
days=days,
|
||||
)
|
||||
|
||||
self.assertEqual(doc.next_schedule_date, today())
|
||||
data = get_auto_repeat_entries(getdate(today()))
|
||||
|
|
@ -90,136 +116,173 @@ class TestAutoRepeat(unittest.TestCase):
|
|||
end_date = add_months(start_date, 12)
|
||||
|
||||
todo = frappe.get_doc(
|
||||
dict(doctype='ToDo', description='test recurring todo', assigned_by='Administrator')).insert()
|
||||
dict(doctype="ToDo", description="test recurring todo", assigned_by="Administrator")
|
||||
).insert()
|
||||
|
||||
self.monthly_auto_repeat('ToDo', todo.name, start_date, end_date)
|
||||
#test without end_date
|
||||
todo = frappe.get_doc(dict(doctype='ToDo', description='test recurring todo without end_date', assigned_by='Administrator')).insert()
|
||||
self.monthly_auto_repeat('ToDo', todo.name, start_date)
|
||||
self.monthly_auto_repeat("ToDo", todo.name, start_date, end_date)
|
||||
# test without end_date
|
||||
todo = frappe.get_doc(
|
||||
dict(
|
||||
doctype="ToDo", description="test recurring todo without end_date", assigned_by="Administrator"
|
||||
)
|
||||
).insert()
|
||||
self.monthly_auto_repeat("ToDo", todo.name, start_date)
|
||||
|
||||
def monthly_auto_repeat(self, doctype, docname, start_date, end_date = None):
|
||||
def monthly_auto_repeat(self, doctype, docname, start_date, end_date=None):
|
||||
def get_months(start, end):
|
||||
diff = (12 * end.year + end.month) - (12 * start.year + start.month)
|
||||
return diff + 1
|
||||
|
||||
doc = make_auto_repeat(
|
||||
reference_doctype=doctype, frequency='Monthly', reference_document=docname, start_date=start_date,
|
||||
end_date=end_date)
|
||||
reference_doctype=doctype,
|
||||
frequency="Monthly",
|
||||
reference_document=docname,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
)
|
||||
|
||||
doc.disable_auto_repeat()
|
||||
|
||||
data = get_auto_repeat_entries(getdate(today()))
|
||||
create_repeated_entries(data)
|
||||
docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name})
|
||||
docnames = frappe.get_all(doc.reference_doctype, {"auto_repeat": doc.name})
|
||||
self.assertEqual(len(docnames), 1)
|
||||
|
||||
doc = frappe.get_doc('Auto Repeat', doc.name)
|
||||
doc.db_set('disabled', 0)
|
||||
doc = frappe.get_doc("Auto Repeat", doc.name)
|
||||
doc.db_set("disabled", 0)
|
||||
|
||||
months = get_months(getdate(start_date), getdate(today()))
|
||||
data = get_auto_repeat_entries(getdate(today()))
|
||||
create_repeated_entries(data)
|
||||
|
||||
docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name})
|
||||
docnames = frappe.get_all(doc.reference_doctype, {"auto_repeat": doc.name})
|
||||
self.assertEqual(len(docnames), months)
|
||||
|
||||
def test_notification_is_attached(self):
|
||||
todo = frappe.get_doc(
|
||||
dict(doctype='ToDo', description='Test recurring notification attachment', assigned_by='Administrator')).insert()
|
||||
dict(
|
||||
doctype="ToDo",
|
||||
description="Test recurring notification attachment",
|
||||
assigned_by="Administrator",
|
||||
)
|
||||
).insert()
|
||||
|
||||
doc = make_auto_repeat(reference_document=todo.name, notify=1, recipients="test@domain.com", subject="New ToDo",
|
||||
message="A new ToDo has just been created for you")
|
||||
doc = make_auto_repeat(
|
||||
reference_document=todo.name,
|
||||
notify=1,
|
||||
recipients="test@domain.com",
|
||||
subject="New ToDo",
|
||||
message="A new ToDo has just been created for you",
|
||||
)
|
||||
data = get_auto_repeat_entries(getdate(today()))
|
||||
create_repeated_entries(data)
|
||||
frappe.db.commit()
|
||||
|
||||
new_todo = frappe.db.get_value('ToDo',
|
||||
{'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name')
|
||||
new_todo = frappe.db.get_value(
|
||||
"ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name"
|
||||
)
|
||||
|
||||
linked_comm = frappe.db.exists("Communication", dict(reference_doctype="ToDo", reference_name=new_todo))
|
||||
linked_comm = frappe.db.exists(
|
||||
"Communication", dict(reference_doctype="ToDo", reference_name=new_todo)
|
||||
)
|
||||
self.assertTrue(linked_comm)
|
||||
|
||||
def test_next_schedule_date(self):
|
||||
current_date = getdate(today())
|
||||
todo = frappe.get_doc(
|
||||
dict(doctype='ToDo', description='test next schedule date for monthly', assigned_by='Administrator')).insert()
|
||||
doc = make_auto_repeat(frequency='Monthly', reference_document=todo.name, start_date=add_months(today(), -2))
|
||||
dict(
|
||||
doctype="ToDo", description="test next schedule date for monthly", assigned_by="Administrator"
|
||||
)
|
||||
).insert()
|
||||
doc = make_auto_repeat(
|
||||
frequency="Monthly", reference_document=todo.name, start_date=add_months(today(), -2)
|
||||
)
|
||||
|
||||
# next_schedule_date is set as on or after current date
|
||||
# it should not be a previous month's date
|
||||
self.assertTrue((doc.next_schedule_date >= current_date))
|
||||
|
||||
todo = frappe.get_doc(
|
||||
dict(doctype='ToDo', description='test next schedule date for daily', assigned_by='Administrator')).insert()
|
||||
doc = make_auto_repeat(frequency='Daily', reference_document=todo.name, start_date=add_days(today(), -2))
|
||||
dict(
|
||||
doctype="ToDo", description="test next schedule date for daily", assigned_by="Administrator"
|
||||
)
|
||||
).insert()
|
||||
doc = make_auto_repeat(
|
||||
frequency="Daily", reference_document=todo.name, start_date=add_days(today(), -2)
|
||||
)
|
||||
self.assertEqual(getdate(doc.next_schedule_date), current_date)
|
||||
|
||||
def test_submit_on_creation(self):
|
||||
doctype = 'Test Submittable DocType'
|
||||
doctype = "Test Submittable DocType"
|
||||
create_submittable_doctype(doctype)
|
||||
|
||||
current_date = getdate()
|
||||
submittable_doc = frappe.get_doc(dict(doctype=doctype, test='test submit on creation')).insert()
|
||||
submittable_doc = frappe.get_doc(dict(doctype=doctype, test="test submit on creation")).insert()
|
||||
submittable_doc.submit()
|
||||
doc = make_auto_repeat(frequency='Daily', reference_doctype=doctype, reference_document=submittable_doc.name,
|
||||
start_date=add_days(current_date, -1), submit_on_creation=1)
|
||||
doc = make_auto_repeat(
|
||||
frequency="Daily",
|
||||
reference_doctype=doctype,
|
||||
reference_document=submittable_doc.name,
|
||||
start_date=add_days(current_date, -1),
|
||||
submit_on_creation=1,
|
||||
)
|
||||
|
||||
data = get_auto_repeat_entries(current_date)
|
||||
create_repeated_entries(data)
|
||||
docnames = frappe.db.get_all(doc.reference_doctype,
|
||||
filters={'auto_repeat': doc.name},
|
||||
fields=['docstatus'],
|
||||
limit=1
|
||||
docnames = frappe.db.get_all(
|
||||
doc.reference_doctype, filters={"auto_repeat": doc.name}, fields=["docstatus"], limit=1
|
||||
)
|
||||
self.assertEqual(docnames[0].docstatus, 1)
|
||||
|
||||
|
||||
def make_auto_repeat(**args):
|
||||
args = frappe._dict(args)
|
||||
doc = frappe.get_doc({
|
||||
'doctype': 'Auto Repeat',
|
||||
'reference_doctype': args.reference_doctype or 'ToDo',
|
||||
'reference_document': args.reference_document or frappe.db.get_value('ToDo', 'name'),
|
||||
'submit_on_creation': args.submit_on_creation or 0,
|
||||
'frequency': args.frequency or 'Daily',
|
||||
'start_date': args.start_date or add_days(today(), -1),
|
||||
'end_date': args.end_date or "",
|
||||
'notify_by_email': args.notify or 0,
|
||||
'recipients': args.recipients or "",
|
||||
'subject': args.subject or "",
|
||||
'message': args.message or "",
|
||||
'repeat_on_days': args.days or []
|
||||
}).insert(ignore_permissions=True)
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Auto Repeat",
|
||||
"reference_doctype": args.reference_doctype or "ToDo",
|
||||
"reference_document": args.reference_document or frappe.db.get_value("ToDo", "name"),
|
||||
"submit_on_creation": args.submit_on_creation or 0,
|
||||
"frequency": args.frequency or "Daily",
|
||||
"start_date": args.start_date or add_days(today(), -1),
|
||||
"end_date": args.end_date or "",
|
||||
"notify_by_email": args.notify or 0,
|
||||
"recipients": args.recipients or "",
|
||||
"subject": args.subject or "",
|
||||
"message": args.message or "",
|
||||
"repeat_on_days": args.days or [],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
def create_submittable_doctype(doctype, submit_perms=1):
|
||||
if frappe.db.exists('DocType', doctype):
|
||||
if frappe.db.exists("DocType", doctype):
|
||||
return
|
||||
else:
|
||||
doc = frappe.get_doc({
|
||||
'doctype': 'DocType',
|
||||
'__newname': doctype,
|
||||
'module': 'Custom',
|
||||
'custom': 1,
|
||||
'is_submittable': 1,
|
||||
'fields': [{
|
||||
'fieldname': 'test',
|
||||
'label': 'Test',
|
||||
'fieldtype': 'Data'
|
||||
}],
|
||||
'permissions': [{
|
||||
'role': 'System Manager',
|
||||
'read': 1,
|
||||
'write': 1,
|
||||
'create': 1,
|
||||
'delete': 1,
|
||||
'submit': submit_perms,
|
||||
'cancel': submit_perms,
|
||||
'amend': submit_perms
|
||||
}]
|
||||
}).insert()
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"__newname": doctype,
|
||||
"module": "Custom",
|
||||
"custom": 1,
|
||||
"is_submittable": 1,
|
||||
"fields": [{"fieldname": "test", "label": "Test", "fieldtype": "Data"}],
|
||||
"permissions": [
|
||||
{
|
||||
"role": "System Manager",
|
||||
"read": 1,
|
||||
"write": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"submit": submit_perms,
|
||||
"cancel": submit_perms,
|
||||
"amend": submit_perms,
|
||||
}
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
doc.allow_auto_repeat = 1
|
||||
doc.save()
|
||||
doc.save()
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@
|
|||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class AutoRepeatDay(Document):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class Milestone(Document):
|
||||
pass
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Milestone", ["reference_type", "reference_name"])
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
#import frappe
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class TestMilestone(unittest.TestCase):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -3,43 +3,50 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
import frappe.cache_manager
|
||||
from frappe.model import log_types
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class MilestoneTracker(Document):
|
||||
def on_update(self):
|
||||
frappe.cache_manager.clear_doctype_map('Milestone Tracker', self.document_type)
|
||||
frappe.cache_manager.clear_doctype_map("Milestone Tracker", self.document_type)
|
||||
|
||||
def on_trash(self):
|
||||
frappe.cache_manager.clear_doctype_map('Milestone Tracker', self.document_type)
|
||||
frappe.cache_manager.clear_doctype_map("Milestone Tracker", self.document_type)
|
||||
|
||||
def apply(self, doc):
|
||||
before_save = doc.get_doc_before_save()
|
||||
from_value = before_save and before_save.get(self.track_field) or None
|
||||
if from_value != doc.get(self.track_field):
|
||||
frappe.get_doc(dict(
|
||||
doctype = 'Milestone',
|
||||
reference_type = doc.doctype,
|
||||
reference_name = doc.name,
|
||||
track_field = self.track_field,
|
||||
from_value = from_value,
|
||||
value = doc.get(self.track_field),
|
||||
milestone_tracker = self.name,
|
||||
)).insert(ignore_permissions=True)
|
||||
frappe.get_doc(
|
||||
dict(
|
||||
doctype="Milestone",
|
||||
reference_type=doc.doctype,
|
||||
reference_name=doc.name,
|
||||
track_field=self.track_field,
|
||||
from_value=from_value,
|
||||
value=doc.get(self.track_field),
|
||||
milestone_tracker=self.name,
|
||||
)
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def evaluate_milestone(doc, event):
|
||||
if (frappe.flags.in_install
|
||||
if (
|
||||
frappe.flags.in_install
|
||||
or frappe.flags.in_migrate
|
||||
or frappe.flags.in_setup_wizard
|
||||
or doc.doctype in log_types):
|
||||
or doc.doctype in log_types
|
||||
):
|
||||
return
|
||||
|
||||
# track milestones related to this doctype
|
||||
for d in get_milestone_trackers(doc.doctype):
|
||||
frappe.get_doc('Milestone Tracker', d.get('name')).apply(doc)
|
||||
frappe.get_doc("Milestone Tracker", d.get("name")).apply(doc)
|
||||
|
||||
|
||||
def get_milestone_trackers(doctype):
|
||||
return frappe.cache_manager.get_doctype_map('Milestone Tracker', doctype,
|
||||
dict(document_type = doctype, disabled=0))
|
||||
|
||||
return frappe.cache_manager.get_doctype_map(
|
||||
"Milestone Tracker", doctype, dict(document_type=doctype, disabled=0)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,48 +1,48 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
import frappe.cache_manager
|
||||
import unittest
|
||||
|
||||
|
||||
class TestMilestoneTracker(unittest.TestCase):
|
||||
def test_milestone(self):
|
||||
frappe.db.delete("Milestone Tracker")
|
||||
|
||||
frappe.cache().delete_key('milestone_tracker_map')
|
||||
frappe.cache().delete_key("milestone_tracker_map")
|
||||
|
||||
milestone_tracker = frappe.get_doc(dict(
|
||||
doctype = 'Milestone Tracker',
|
||||
document_type = 'ToDo',
|
||||
track_field = 'status'
|
||||
)).insert()
|
||||
milestone_tracker = frappe.get_doc(
|
||||
dict(doctype="Milestone Tracker", document_type="ToDo", track_field="status")
|
||||
).insert()
|
||||
|
||||
todo = frappe.get_doc(dict(
|
||||
doctype = 'ToDo',
|
||||
description = 'test milestone',
|
||||
status = 'Open'
|
||||
)).insert()
|
||||
todo = frappe.get_doc(dict(doctype="ToDo", description="test milestone", status="Open")).insert()
|
||||
|
||||
milestones = frappe.get_all('Milestone',
|
||||
fields = ['track_field', 'value', 'milestone_tracker'],
|
||||
filters = dict(reference_type = todo.doctype, reference_name=todo.name))
|
||||
milestones = frappe.get_all(
|
||||
"Milestone",
|
||||
fields=["track_field", "value", "milestone_tracker"],
|
||||
filters=dict(reference_type=todo.doctype, reference_name=todo.name),
|
||||
)
|
||||
|
||||
self.assertEqual(len(milestones), 1)
|
||||
self.assertEqual(milestones[0].track_field, 'status')
|
||||
self.assertEqual(milestones[0].value, 'Open')
|
||||
self.assertEqual(milestones[0].track_field, "status")
|
||||
self.assertEqual(milestones[0].value, "Open")
|
||||
|
||||
todo.status = 'Closed'
|
||||
todo.status = "Closed"
|
||||
todo.save()
|
||||
|
||||
milestones = frappe.get_all('Milestone',
|
||||
fields = ['track_field', 'value', 'milestone_tracker'],
|
||||
filters = dict(reference_type = todo.doctype, reference_name=todo.name),
|
||||
order_by = 'modified desc')
|
||||
milestones = frappe.get_all(
|
||||
"Milestone",
|
||||
fields=["track_field", "value", "milestone_tracker"],
|
||||
filters=dict(reference_type=todo.doctype, reference_name=todo.name),
|
||||
order_by="modified desc",
|
||||
)
|
||||
|
||||
self.assertEqual(len(milestones), 2)
|
||||
self.assertEqual(milestones[0].track_field, 'status')
|
||||
self.assertEqual(milestones[0].value, 'Closed')
|
||||
self.assertEqual(milestones[0].track_field, "status")
|
||||
self.assertEqual(milestones[0].value, "Closed")
|
||||
|
||||
# cleanup
|
||||
frappe.db.delete("Milestone")
|
||||
milestone_tracker.delete()
|
||||
milestone_tracker.delete()
|
||||
|
|
|
|||
257
frappe/boot.py
257
frappe/boot.py
|
|
@ -7,18 +7,24 @@ bootstrap client session
|
|||
import frappe
|
||||
import frappe.defaults
|
||||
import frappe.desk.desk_page
|
||||
from frappe.core.doctype.navbar_settings.navbar_settings import get_app_logo, get_navbar_settings
|
||||
from frappe.desk.doctype.route_history.route_history import frequently_visited_links
|
||||
from frappe.desk.form.load import get_meta_bundle
|
||||
from frappe.utils.change_log import get_versions
|
||||
from frappe.translate import get_lang_dict
|
||||
from frappe.email.inbox import get_email_accounts
|
||||
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled
|
||||
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled
|
||||
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
|
||||
from frappe.model.base_document import get_controller
|
||||
from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings, get_app_logo
|
||||
from frappe.geo.country_info import get_all
|
||||
from frappe.utils import get_time_zone, add_user_info
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.query_builder.functions import Count
|
||||
from frappe.query_builder.terms import subqry
|
||||
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
|
||||
from frappe.social.doctype.energy_point_settings.energy_point_settings import (
|
||||
is_energy_point_enabled,
|
||||
)
|
||||
from frappe.translate import get_lang_dict
|
||||
from frappe.utils import add_user_info, get_time_zone
|
||||
from frappe.utils.change_log import get_versions
|
||||
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled
|
||||
|
||||
|
||||
def get_bootinfo():
|
||||
"""build and return boot info"""
|
||||
|
|
@ -35,9 +41,9 @@ def get_bootinfo():
|
|||
bootinfo.sysdefaults = frappe.defaults.get_defaults()
|
||||
bootinfo.server_date = frappe.utils.nowdate()
|
||||
|
||||
if frappe.session['user'] != 'Guest':
|
||||
if frappe.session["user"] != "Guest":
|
||||
bootinfo.user_info = get_user_info()
|
||||
bootinfo.sid = frappe.session['sid']
|
||||
bootinfo.sid = frappe.session["sid"]
|
||||
|
||||
bootinfo.modules = {}
|
||||
bootinfo.module_list = []
|
||||
|
|
@ -48,8 +54,10 @@ def get_bootinfo():
|
|||
add_layouts(bootinfo)
|
||||
|
||||
bootinfo.module_app = frappe.local.module_app
|
||||
bootinfo.single_types = [d.name for d in frappe.get_all('DocType', {'issingle': 1})]
|
||||
bootinfo.nested_set_doctypes = [d.parent for d in frappe.get_all('DocField', {'fieldname': 'lft'}, ['parent'])]
|
||||
bootinfo.single_types = [d.name for d in frappe.get_all("DocType", {"issingle": 1})]
|
||||
bootinfo.nested_set_doctypes = [
|
||||
d.parent for d in frappe.get_all("DocField", {"fieldname": "lft"}, ["parent"])
|
||||
]
|
||||
add_home_page(bootinfo, doclist)
|
||||
bootinfo.page_info = get_allowed_pages()
|
||||
load_translations(bootinfo)
|
||||
|
|
@ -64,8 +72,8 @@ def get_bootinfo():
|
|||
set_time_zone(bootinfo)
|
||||
|
||||
# ipinfo
|
||||
if frappe.session.data.get('ipinfo'):
|
||||
bootinfo.ipinfo = frappe.session['data']['ipinfo']
|
||||
if frappe.session.data.get("ipinfo"):
|
||||
bootinfo.ipinfo = frappe.session["data"]["ipinfo"]
|
||||
|
||||
# add docs
|
||||
bootinfo.docs = doclist
|
||||
|
|
@ -75,7 +83,7 @@ def get_bootinfo():
|
|||
|
||||
if bootinfo.lang:
|
||||
bootinfo.lang = str(bootinfo.lang)
|
||||
bootinfo.versions = {k: v['version'] for k, v in get_versions().items()}
|
||||
bootinfo.versions = {k: v["version"] for k, v in get_versions().items()}
|
||||
|
||||
bootinfo.error_report_email = frappe.conf.error_report_email
|
||||
bootinfo.calendars = sorted(frappe.get_hooks("calendars"))
|
||||
|
|
@ -95,120 +103,144 @@ def get_bootinfo():
|
|||
|
||||
return bootinfo
|
||||
|
||||
|
||||
def get_letter_heads():
|
||||
letter_heads = {}
|
||||
for letter_head in frappe.get_all("Letter Head", fields = ["name", "content", "footer"]):
|
||||
letter_heads.setdefault(letter_head.name,
|
||||
{'header': letter_head.content, 'footer': letter_head.footer})
|
||||
for letter_head in frappe.get_all("Letter Head", fields=["name", "content", "footer"]):
|
||||
letter_heads.setdefault(
|
||||
letter_head.name, {"header": letter_head.content, "footer": letter_head.footer}
|
||||
)
|
||||
|
||||
return letter_heads
|
||||
|
||||
|
||||
def load_conf_settings(bootinfo):
|
||||
from frappe import conf
|
||||
bootinfo.max_file_size = conf.get('max_file_size') or 10485760
|
||||
for key in ('developer_mode', 'socketio_port', 'file_watcher_port'):
|
||||
if key in conf: bootinfo[key] = conf.get(key)
|
||||
|
||||
bootinfo.max_file_size = conf.get("max_file_size") or 10485760
|
||||
for key in ("developer_mode", "socketio_port", "file_watcher_port"):
|
||||
if key in conf:
|
||||
bootinfo[key] = conf.get(key)
|
||||
|
||||
|
||||
def load_desktop_data(bootinfo):
|
||||
from frappe.desk.desktop import get_workspace_sidebar_items
|
||||
bootinfo.allowed_workspaces = get_workspace_sidebar_items().get('pages')
|
||||
|
||||
bootinfo.allowed_workspaces = get_workspace_sidebar_items().get("pages")
|
||||
bootinfo.module_page_map = get_controller("Workspace").get_module_page_map()
|
||||
bootinfo.dashboards = frappe.get_all("Dashboard")
|
||||
|
||||
|
||||
def get_allowed_pages(cache=False):
|
||||
return get_user_pages_or_reports('Page', cache=cache)
|
||||
return get_user_pages_or_reports("Page", cache=cache)
|
||||
|
||||
|
||||
def get_allowed_reports(cache=False):
|
||||
return get_user_pages_or_reports('Report', cache=cache)
|
||||
return get_user_pages_or_reports("Report", cache=cache)
|
||||
|
||||
|
||||
def get_user_pages_or_reports(parent, cache=False):
|
||||
_cache = frappe.cache()
|
||||
|
||||
if cache:
|
||||
has_role = _cache.get_value('has_role:' + parent, user=frappe.session.user)
|
||||
has_role = _cache.get_value("has_role:" + parent, user=frappe.session.user)
|
||||
if has_role:
|
||||
return has_role
|
||||
|
||||
roles = frappe.get_roles()
|
||||
has_role = {}
|
||||
column = get_column(parent)
|
||||
|
||||
page = DocType("Page")
|
||||
report = DocType("Report")
|
||||
|
||||
if parent == "Report":
|
||||
columns = (report.name.as_("title"), report.ref_doctype, report.report_type)
|
||||
else:
|
||||
columns = (page.title.as_("title"),)
|
||||
|
||||
customRole = DocType("Custom Role")
|
||||
hasRole = DocType("Has Role")
|
||||
parentTable = DocType(parent)
|
||||
|
||||
# get pages or reports set on custom role
|
||||
pages_with_custom_roles = frappe.db.sql("""
|
||||
select
|
||||
`tabCustom Role`.{field} as name,
|
||||
`tabCustom Role`.modified,
|
||||
`tabCustom Role`.ref_doctype,
|
||||
{column}
|
||||
from `tabCustom Role`, `tabHas Role`, `tab{parent}`
|
||||
where
|
||||
`tabHas Role`.parent = `tabCustom Role`.name
|
||||
and `tab{parent}`.name = `tabCustom Role`.{field}
|
||||
and `tabCustom Role`.{field} is not null
|
||||
and `tabHas Role`.role in ({roles})
|
||||
""".format(field=parent.lower(), parent=parent, column=column,
|
||||
roles = ', '.join(['%s']*len(roles))), roles, as_dict=1)
|
||||
pages_with_custom_roles = (
|
||||
frappe.qb.from_(customRole)
|
||||
.from_(hasRole)
|
||||
.from_(parentTable)
|
||||
.select(
|
||||
customRole[parent.lower()].as_("name"), customRole.modified, customRole.ref_doctype, *columns
|
||||
)
|
||||
.where(
|
||||
(hasRole.parent == customRole.name)
|
||||
& (parentTable.name == customRole[parent.lower()])
|
||||
& (customRole[parent.lower()].isnotnull())
|
||||
& (hasRole.role.isin(roles))
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
for p in pages_with_custom_roles:
|
||||
has_role[p.name] = {"modified":p.modified, "title": p.title, "ref_doctype": p.ref_doctype}
|
||||
has_role[p.name] = {"modified": p.modified, "title": p.title, "ref_doctype": p.ref_doctype}
|
||||
|
||||
pages_with_standard_roles = frappe.db.sql("""
|
||||
select distinct
|
||||
`tab{parent}`.name as name,
|
||||
`tab{parent}`.modified,
|
||||
{column}
|
||||
from `tabHas Role`, `tab{parent}`
|
||||
where
|
||||
`tabHas Role`.role in ({roles})
|
||||
and `tabHas Role`.parent = `tab{parent}`.name
|
||||
and `tab{parent}`.`name` not in (
|
||||
select `tabCustom Role`.{field} from `tabCustom Role`
|
||||
where `tabCustom Role`.{field} is not null)
|
||||
{condition}
|
||||
""".format(parent=parent, column=column, roles = ', '.join(['%s']*len(roles)),
|
||||
field=parent.lower(), condition="and `tabReport`.disabled=0" if parent == "Report" else ""),
|
||||
roles, as_dict=True)
|
||||
subq = (
|
||||
frappe.qb.from_(customRole)
|
||||
.select(customRole[parent.lower()])
|
||||
.where(customRole[parent.lower()].isnotnull())
|
||||
)
|
||||
|
||||
pages_with_standard_roles = (
|
||||
frappe.qb.from_(hasRole)
|
||||
.from_(parentTable)
|
||||
.select(parentTable.name.as_("name"), parentTable.modified, *columns)
|
||||
.where(
|
||||
(hasRole.role.isin(roles))
|
||||
& (hasRole.parent == parentTable.name)
|
||||
& (parentTable.name.notin(subq))
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
if parent == "Report":
|
||||
pages_with_standard_roles = pages_with_standard_roles.where(report.disabled == 0)
|
||||
|
||||
pages_with_standard_roles = pages_with_standard_roles.run(as_dict=True)
|
||||
|
||||
for p in pages_with_standard_roles:
|
||||
if p.name not in has_role:
|
||||
has_role[p.name] = {"modified":p.modified, "title": p.title}
|
||||
has_role[p.name] = {"modified": p.modified, "title": p.title}
|
||||
if parent == "Report":
|
||||
has_role[p.name].update({'ref_doctype': p.ref_doctype})
|
||||
has_role[p.name].update({"ref_doctype": p.ref_doctype})
|
||||
|
||||
no_of_roles = (
|
||||
frappe.qb.from_(hasRole).select(Count("*")).where(hasRole.parent == parentTable.name)
|
||||
)
|
||||
|
||||
# pages with no role are allowed
|
||||
if parent =="Page":
|
||||
pages_with_no_roles = frappe.db.sql("""
|
||||
select
|
||||
`tab{parent}`.name, `tab{parent}`.modified, {column}
|
||||
from `tab{parent}`
|
||||
where
|
||||
(select count(*) from `tabHas Role`
|
||||
where `tabHas Role`.parent=`tab{parent}`.`name`) = 0
|
||||
""".format(parent=parent, column=column), as_dict=1)
|
||||
if parent == "Page":
|
||||
|
||||
pages_with_no_roles = (
|
||||
frappe.qb.from_(parentTable)
|
||||
.select(parentTable.name, parentTable.modified, *columns)
|
||||
.where(subqry(no_of_roles) == 0)
|
||||
).run(as_dict=True)
|
||||
|
||||
for p in pages_with_no_roles:
|
||||
if p.name not in has_role:
|
||||
has_role[p.name] = {"modified": p.modified, "title": p.title}
|
||||
|
||||
elif parent == "Report":
|
||||
reports = frappe.get_all("Report",
|
||||
reports = frappe.get_all(
|
||||
"Report",
|
||||
fields=["name", "report_type"],
|
||||
filters={"name": ("in", has_role.keys())},
|
||||
ignore_ifnull=True
|
||||
ignore_ifnull=True,
|
||||
)
|
||||
for report in reports:
|
||||
has_role[report.name]["report_type"] = report.report_type
|
||||
|
||||
# Expire every six hours
|
||||
_cache.set_value('has_role:' + parent, has_role, frappe.session.user, 21600)
|
||||
_cache.set_value("has_role:" + parent, has_role, frappe.session.user, 21600)
|
||||
return has_role
|
||||
|
||||
def get_column(doctype):
|
||||
column = "`tabPage`.title as title"
|
||||
if doctype == "Report":
|
||||
column = "`tabReport`.`name` as title, `tabReport`.ref_doctype, `tabReport`.report_type"
|
||||
|
||||
return column
|
||||
|
||||
def load_translations(bootinfo):
|
||||
messages = frappe.get_lang_dict("boot")
|
||||
|
|
@ -220,27 +252,30 @@ def load_translations(bootinfo):
|
|||
messages[name] = frappe._(name)
|
||||
|
||||
# only untranslated
|
||||
messages = {k: v for k, v in messages.items() if k!=v}
|
||||
messages = {k: v for k, v in messages.items() if k != v}
|
||||
|
||||
bootinfo["__messages"] = messages
|
||||
|
||||
|
||||
def get_user_info():
|
||||
# get info for current user
|
||||
user_info = frappe._dict()
|
||||
add_user_info(frappe.session.user, user_info)
|
||||
|
||||
if frappe.session.user == 'Administrator' and user_info.Administrator.email:
|
||||
if frappe.session.user == "Administrator" and user_info.Administrator.email:
|
||||
user_info[user_info.Administrator.email] = user_info.Administrator
|
||||
|
||||
return user_info
|
||||
|
||||
|
||||
def get_user(bootinfo):
|
||||
"""get user info"""
|
||||
bootinfo.user = frappe.get_user().load_user()
|
||||
|
||||
|
||||
def add_home_page(bootinfo, docs):
|
||||
"""load home page"""
|
||||
if frappe.session.user=="Guest":
|
||||
if frappe.session.user == "Guest":
|
||||
return
|
||||
home_page = frappe.db.get_default("desktop:home_page")
|
||||
|
||||
|
|
@ -250,44 +285,65 @@ def add_home_page(bootinfo, docs):
|
|||
try:
|
||||
page = frappe.desk.desk_page.get(home_page)
|
||||
docs.append(page)
|
||||
bootinfo['home_page'] = page.name
|
||||
bootinfo["home_page"] = page.name
|
||||
except (frappe.DoesNotExistError, frappe.PermissionError):
|
||||
if frappe.message_log:
|
||||
frappe.message_log.pop()
|
||||
bootinfo['home_page'] = 'Workspaces'
|
||||
bootinfo["home_page"] = "Workspaces"
|
||||
|
||||
|
||||
def add_timezone_info(bootinfo):
|
||||
system = bootinfo.sysdefaults.get("time_zone")
|
||||
import frappe.utils.momentjs
|
||||
bootinfo.timezone_info = {"zones":{}, "rules":{}, "links":{}}
|
||||
|
||||
bootinfo.timezone_info = {"zones": {}, "rules": {}, "links": {}}
|
||||
frappe.utils.momentjs.update(system, bootinfo.timezone_info)
|
||||
|
||||
|
||||
def load_print(bootinfo, doclist):
|
||||
print_settings = frappe.db.get_singles_dict("Print Settings")
|
||||
print_settings.doctype = ":Print Settings"
|
||||
doclist.append(print_settings)
|
||||
load_print_css(bootinfo, print_settings)
|
||||
|
||||
|
||||
def load_print_css(bootinfo, print_settings):
|
||||
import frappe.www.printview
|
||||
bootinfo.print_css = frappe.www.printview.get_print_style(print_settings.print_style or "Redesign", for_legacy=True)
|
||||
|
||||
bootinfo.print_css = frappe.www.printview.get_print_style(
|
||||
print_settings.print_style or "Redesign", for_legacy=True
|
||||
)
|
||||
|
||||
|
||||
def get_unseen_notes():
|
||||
return frappe.db.sql('''select `name`, title, content, notify_on_every_login from `tabNote` where notify_on_login=1
|
||||
and expire_notification_on > %s and %s not in
|
||||
(select user from `tabNote Seen By` nsb
|
||||
where nsb.parent=`tabNote`.name)''', (frappe.utils.now(), frappe.session.user), as_dict=True)
|
||||
note = DocType("Note")
|
||||
nsb = DocType("Note Seen By").as_("nsb")
|
||||
|
||||
return (
|
||||
frappe.qb.from_(note)
|
||||
.select(note.name, note.title, note.content, note.notify_on_every_login)
|
||||
.where(
|
||||
(note.notify_on_every_login == 1)
|
||||
& (note.expire_notification_on > frappe.utils.now())
|
||||
& (
|
||||
subqry(frappe.qb.from_(nsb).select(nsb.user).where(nsb.parent == note.name)).notin(
|
||||
[frappe.session.user]
|
||||
)
|
||||
)
|
||||
)
|
||||
).run(as_dict=1)
|
||||
|
||||
|
||||
def get_success_action():
|
||||
return frappe.get_all("Success Action", fields=["*"])
|
||||
|
||||
|
||||
def get_link_preview_doctypes():
|
||||
from frappe.utils import cint
|
||||
|
||||
link_preview_doctypes = [d.name for d in frappe.db.get_all('DocType', {'show_preview_popup': 1})]
|
||||
customizations = frappe.get_all("Property Setter",
|
||||
fields=['doc_type', 'value'],
|
||||
filters={'property': 'show_preview_popup'}
|
||||
link_preview_doctypes = [d.name for d in frappe.db.get_all("DocType", {"show_preview_popup": 1})]
|
||||
customizations = frappe.get_all(
|
||||
"Property Setter", fields=["doc_type", "value"], filters={"property": "show_preview_popup"}
|
||||
)
|
||||
|
||||
for custom in customizations:
|
||||
|
|
@ -298,22 +354,23 @@ def get_link_preview_doctypes():
|
|||
|
||||
return link_preview_doctypes
|
||||
|
||||
|
||||
def get_additional_filters_from_hooks():
|
||||
filter_config = frappe._dict()
|
||||
filter_hooks = frappe.get_hooks('filters_config')
|
||||
filter_hooks = frappe.get_hooks("filters_config")
|
||||
for hook in filter_hooks:
|
||||
filter_config.update(frappe.get_attr(hook)())
|
||||
|
||||
return filter_config
|
||||
|
||||
|
||||
def add_layouts(bootinfo):
|
||||
# add routes for readable doctypes
|
||||
bootinfo.doctype_layouts = frappe.get_all('DocType Layout', ['name', 'route', 'document_type'])
|
||||
bootinfo.doctype_layouts = frappe.get_all("DocType Layout", ["name", "route", "document_type"])
|
||||
|
||||
|
||||
def get_desk_settings():
|
||||
role_list = frappe.get_all('Role', fields=['*'], filters=dict(
|
||||
name=['in', frappe.get_roles()]
|
||||
))
|
||||
role_list = frappe.get_all("Role", fields=["*"], filters=dict(name=["in", frappe.get_roles()]))
|
||||
desk_settings = {}
|
||||
|
||||
from frappe.core.doctype.role.role import desk_properties
|
||||
|
|
@ -324,8 +381,10 @@ def get_desk_settings():
|
|||
|
||||
return desk_settings
|
||||
|
||||
|
||||
def get_notification_settings():
|
||||
return frappe.get_cached_doc('Notification Settings', frappe.session.user)
|
||||
return frappe.get_cached_doc("Notification Settings", frappe.session.user)
|
||||
|
||||
|
||||
def get_country_codes(bootinfo):
|
||||
country_codes = get_all()
|
||||
|
|
@ -341,8 +400,10 @@ def get_link_title_doctypes():
|
|||
)
|
||||
return [d.name for d in dts + custom_dts if d]
|
||||
|
||||
|
||||
def set_time_zone(bootinfo):
|
||||
bootinfo.time_zone = {
|
||||
"system": get_time_zone(),
|
||||
"user": bootinfo.get("user_info", {}).get(frappe.session.user, {}).get("time_zone", None) or get_time_zone()
|
||||
"user": bootinfo.get("user_info", {}).get(frappe.session.user, {}).get("time_zone", None)
|
||||
or get_time_zone(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import os
|
||||
import shutil
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from distutils.spawn import find_executable
|
||||
from subprocess import getoutput
|
||||
|
|
@ -25,6 +25,7 @@ sites_path = os.path.abspath(os.getcwd())
|
|||
class AssetsNotDownloadedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AssetsDontExistError(HTTPError):
|
||||
pass
|
||||
|
||||
|
|
@ -43,7 +44,7 @@ def download_file(url, prefix):
|
|||
|
||||
|
||||
def build_missing_files():
|
||||
'''Check which files dont exist yet from the assets.json and run build for those files'''
|
||||
"""Check which files dont exist yet from the assets.json and run build for those files"""
|
||||
|
||||
missing_assets = []
|
||||
current_asset_files = []
|
||||
|
|
@ -60,7 +61,7 @@ def build_missing_files():
|
|||
assets_json = frappe.parse_json(assets_json)
|
||||
|
||||
for bundle_file, output_file in assets_json.items():
|
||||
if not output_file.startswith('/assets/frappe'):
|
||||
if not output_file.startswith("/assets/frappe"):
|
||||
continue
|
||||
|
||||
if os.path.basename(output_file) not in current_asset_files:
|
||||
|
|
@ -78,8 +79,7 @@ def build_missing_files():
|
|||
def get_assets_link(frappe_head) -> str:
|
||||
tag = getoutput(
|
||||
r"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*"
|
||||
r" refs/tags/,,' -e 's/\^{}//'"
|
||||
% frappe_head
|
||||
r" refs/tags/,,' -e 's/\^{}//'" % frappe_head
|
||||
)
|
||||
|
||||
if tag:
|
||||
|
|
@ -111,6 +111,7 @@ def fetch_assets(url, frappe_head):
|
|||
|
||||
def setup_assets(assets_archive):
|
||||
import tarfile
|
||||
|
||||
directories_created = set()
|
||||
|
||||
click.secho("\nExtracting assets...\n", fg="yellow")
|
||||
|
|
@ -221,7 +222,16 @@ def setup():
|
|||
assets_path = os.path.join(frappe.local.sites_path, "assets")
|
||||
|
||||
|
||||
def bundle(mode, apps=None, hard_link=False, make_copy=False, restore=False, verbose=False, skip_frappe=False, files=None):
|
||||
def bundle(
|
||||
mode,
|
||||
apps=None,
|
||||
hard_link=False,
|
||||
make_copy=False,
|
||||
restore=False,
|
||||
verbose=False,
|
||||
skip_frappe=False,
|
||||
files=None,
|
||||
):
|
||||
"""concat / minify js files"""
|
||||
setup()
|
||||
make_asset_dirs(hard_link=hard_link)
|
||||
|
|
@ -236,7 +246,7 @@ def bundle(mode, apps=None, hard_link=False, make_copy=False, restore=False, ver
|
|||
command += " --skip_frappe"
|
||||
|
||||
if files:
|
||||
command += " --files {files}".format(files=','.join(files))
|
||||
command += " --files {files}".format(files=",".join(files))
|
||||
|
||||
command += " --run-build-command"
|
||||
|
||||
|
|
@ -253,9 +263,7 @@ def watch(apps=None):
|
|||
if apps:
|
||||
command += " --apps {apps}".format(apps=apps)
|
||||
|
||||
live_reload = frappe.utils.cint(
|
||||
os.environ.get("LIVE_RELOAD", frappe.conf.live_reload)
|
||||
)
|
||||
live_reload = frappe.utils.cint(os.environ.get("LIVE_RELOAD", frappe.conf.live_reload))
|
||||
|
||||
if live_reload:
|
||||
command += " --live-reload"
|
||||
|
|
@ -266,8 +274,8 @@ def watch(apps=None):
|
|||
|
||||
|
||||
def check_node_executable():
|
||||
node_version = Version(subprocess.getoutput('node -v')[1:])
|
||||
warn = '⚠️ '
|
||||
node_version = Version(subprocess.getoutput("node -v")[1:])
|
||||
warn = "⚠️ "
|
||||
if node_version.major < 14:
|
||||
click.echo(f"{warn} Please update your node version to 14")
|
||||
if not find_executable("yarn"):
|
||||
|
|
@ -276,9 +284,7 @@ def check_node_executable():
|
|||
|
||||
|
||||
def get_node_env():
|
||||
node_env = {
|
||||
"NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}"
|
||||
}
|
||||
node_env = {"NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}"}
|
||||
return node_env
|
||||
|
||||
|
||||
|
|
@ -345,8 +351,7 @@ def clear_broken_symlinks():
|
|||
|
||||
|
||||
def unstrip(message: str) -> str:
|
||||
"""Pads input string on the right side until the last available column in the terminal
|
||||
"""
|
||||
"""Pads input string on the right side until the last available column in the terminal"""
|
||||
_len = len(message)
|
||||
try:
|
||||
max_str = os.get_terminal_size().columns
|
||||
|
|
@ -367,7 +372,9 @@ def make_asset_dirs(hard_link=False):
|
|||
symlinks = generate_assets_map()
|
||||
|
||||
for source, target in symlinks.items():
|
||||
start_message = unstrip(f"{'Copying assets from' if hard_link else 'Linking'} {source} to {target}")
|
||||
start_message = unstrip(
|
||||
f"{'Copying assets from' if hard_link else 'Linking'} {source} to {target}"
|
||||
)
|
||||
fail_message = unstrip(f"Cannot {'copy' if hard_link else 'link'} {source} to {target}")
|
||||
|
||||
# Used '\r' instead of '\x1b[1K\r' to print entire lines in smaller terminal sizes
|
||||
|
|
@ -404,10 +411,11 @@ def scrub_html_template(content):
|
|||
# strip comments
|
||||
content = re.sub(r"(<!--.*?-->)", "", content)
|
||||
|
||||
return content.replace("'", "\'")
|
||||
return content.replace("'", "'")
|
||||
|
||||
|
||||
def html_to_js_template(path, content):
|
||||
"""returns HTML template content as Javascript code, adding it to `frappe.templates`"""
|
||||
return """frappe.templates["{key}"] = '{content}';\n""".format(
|
||||
key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content))
|
||||
key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,33 +1,75 @@
|
|||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe, json
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.desk.notifications import clear_notifications, delete_notification_count_for
|
||||
from frappe.model.document import Document
|
||||
from frappe.desk.notifications import (delete_notification_count_for,
|
||||
clear_notifications)
|
||||
|
||||
common_default_keys = ["__default", "__global"]
|
||||
|
||||
doctype_map_keys = ('energy_point_rule_map', 'assignment_rule_map',
|
||||
'milestone_tracker_map', 'event_consumer_document_type_map')
|
||||
doctype_map_keys = (
|
||||
"energy_point_rule_map",
|
||||
"assignment_rule_map",
|
||||
"milestone_tracker_map",
|
||||
"event_consumer_document_type_map",
|
||||
)
|
||||
|
||||
bench_cache_keys = ('assets_json',)
|
||||
bench_cache_keys = ("assets_json",)
|
||||
|
||||
global_cache_keys = ("app_hooks", "installed_apps", 'all_apps',
|
||||
"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
|
||||
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")
|
||||
user_cache_keys = (
|
||||
"bootinfo",
|
||||
"user_recent",
|
||||
"roles",
|
||||
"user_doc",
|
||||
"lang",
|
||||
"defaults",
|
||||
"user_permissions",
|
||||
"home_page",
|
||||
"linked_with",
|
||||
"desktop_icons",
|
||||
"portal_menu_items",
|
||||
"user_perm_can_read",
|
||||
"has_role:Page",
|
||||
"has_role:Report",
|
||||
"desk_sidebar_items",
|
||||
)
|
||||
|
||||
doctype_cache_keys = (
|
||||
"meta",
|
||||
"form_meta",
|
||||
"table_columns",
|
||||
"last_modified",
|
||||
"linked_doctypes",
|
||||
"notifications",
|
||||
"workflow",
|
||||
"data_import_column_header_map",
|
||||
) + doctype_map_keys
|
||||
|
||||
doctype_cache_keys = ("meta", "form_meta", "table_columns", "last_modified",
|
||||
"linked_doctypes", 'notifications', 'workflow' ,
|
||||
'data_import_column_header_map') + doctype_map_keys
|
||||
|
||||
def clear_user_cache(user=None):
|
||||
cache = frappe.cache()
|
||||
|
|
@ -47,11 +89,13 @@ def clear_user_cache(user=None):
|
|||
clear_defaults_cache()
|
||||
clear_global_cache()
|
||||
|
||||
|
||||
def clear_domain_cache(user=None):
|
||||
cache = frappe.cache()
|
||||
domain_cache_keys = ('domain_restricted_doctypes', 'domain_restricted_pages')
|
||||
domain_cache_keys = ("domain_restricted_doctypes", "domain_restricted_pages")
|
||||
cache.delete_value(domain_cache_keys)
|
||||
|
||||
|
||||
def clear_global_cache():
|
||||
from frappe.website.utils import clear_website_cache
|
||||
|
||||
|
|
@ -61,21 +105,23 @@ def clear_global_cache():
|
|||
frappe.cache().delete_value(bench_cache_keys)
|
||||
frappe.setup_module_map()
|
||||
|
||||
|
||||
def clear_defaults_cache(user=None):
|
||||
if user:
|
||||
for p in ([user] + common_default_keys):
|
||||
for p in [user] + common_default_keys:
|
||||
frappe.cache().hdel("defaults", p)
|
||||
elif frappe.flags.in_install!="frappe":
|
||||
elif frappe.flags.in_install != "frappe":
|
||||
frappe.cache().delete_key("defaults")
|
||||
|
||||
|
||||
def clear_doctype_cache(doctype=None):
|
||||
clear_controller_cache(doctype)
|
||||
cache = frappe.cache()
|
||||
|
||||
if getattr(frappe.local, 'meta_cache') and (doctype in frappe.local.meta_cache):
|
||||
if getattr(frappe.local, "meta_cache") and (doctype in frappe.local.meta_cache):
|
||||
del frappe.local.meta_cache[doctype]
|
||||
|
||||
for key in ('is_table', 'doctype_modules', 'document_cache'):
|
||||
for key in ("is_table", "doctype_modules", "document_cache"):
|
||||
cache.delete_value(key)
|
||||
|
||||
frappe.local.document_cache = {}
|
||||
|
|
@ -89,8 +135,9 @@ def clear_doctype_cache(doctype=None):
|
|||
|
||||
# clear all parent doctypes
|
||||
|
||||
for dt in frappe.db.get_all('DocField', 'parent',
|
||||
dict(fieldtype=['in', frappe.model.table_fields], options=doctype)):
|
||||
for dt in frappe.db.get_all(
|
||||
"DocField", "parent", dict(fieldtype=["in", frappe.model.table_fields], options=doctype)
|
||||
):
|
||||
clear_single(dt.parent)
|
||||
|
||||
# clear all notifications
|
||||
|
|
@ -101,6 +148,7 @@ def clear_doctype_cache(doctype=None):
|
|||
for name in doctype_cache_keys:
|
||||
cache.delete_value(name)
|
||||
|
||||
|
||||
def clear_controller_cache(doctype=None):
|
||||
if not doctype:
|
||||
del frappe.controllers
|
||||
|
|
@ -110,9 +158,10 @@ def clear_controller_cache(doctype=None):
|
|||
for site_controllers in frappe.controllers.values():
|
||||
site_controllers.pop(doctype, None)
|
||||
|
||||
|
||||
def get_doctype_map(doctype, name, filters=None, order_by=None):
|
||||
cache = frappe.cache()
|
||||
cache_key = frappe.scrub(doctype) + '_map'
|
||||
cache_key = frappe.scrub(doctype) + "_map"
|
||||
doctype_map = cache.hget(cache_key, name)
|
||||
|
||||
if doctype_map is not None:
|
||||
|
|
@ -121,7 +170,7 @@ def get_doctype_map(doctype, name, filters=None, order_by=None):
|
|||
else:
|
||||
# non cached, build cache
|
||||
try:
|
||||
items = frappe.get_all(doctype, filters=filters, order_by = order_by)
|
||||
items = frappe.get_all(doctype, filters=filters, order_by=order_by)
|
||||
cache.hset(cache_key, name, json.dumps(items))
|
||||
except frappe.db.TableMissingError:
|
||||
# executed from inside patch, ignore
|
||||
|
|
@ -129,15 +178,19 @@ def get_doctype_map(doctype, name, filters=None, order_by=None):
|
|||
|
||||
return items
|
||||
|
||||
|
||||
def clear_doctype_map(doctype, name):
|
||||
frappe.cache().hdel(frappe.scrub(doctype) + '_map', name)
|
||||
frappe.cache().hdel(frappe.scrub(doctype) + "_map", name)
|
||||
|
||||
|
||||
def build_table_count_cache():
|
||||
if (frappe.flags.in_patch
|
||||
if (
|
||||
frappe.flags.in_patch
|
||||
or frappe.flags.in_install
|
||||
or frappe.flags.in_migrate
|
||||
or frappe.flags.in_import
|
||||
or frappe.flags.in_setup_wizard):
|
||||
or frappe.flags.in_setup_wizard
|
||||
):
|
||||
return
|
||||
|
||||
_cache = frappe.cache()
|
||||
|
|
@ -145,39 +198,45 @@ def build_table_count_cache():
|
|||
table_rows = frappe.qb.Field("table_rows").as_("count")
|
||||
information_schema = frappe.qb.Schema("information_schema")
|
||||
|
||||
data = (
|
||||
frappe.qb.from_(information_schema.tables).select(table_name, table_rows)
|
||||
).run(as_dict=True)
|
||||
counts = {d.get('name').replace('tab', '', 1): d.get('count', None) for d in data}
|
||||
data = (frappe.qb.from_(information_schema.tables).select(table_name, table_rows)).run(
|
||||
as_dict=True
|
||||
)
|
||||
counts = {d.get("name").replace("tab", "", 1): d.get("count", None) for d in data}
|
||||
_cache.set_value("information_schema:counts", counts)
|
||||
|
||||
return counts
|
||||
|
||||
|
||||
def build_domain_restriced_doctype_cache(*args, **kwargs):
|
||||
if (frappe.flags.in_patch
|
||||
if (
|
||||
frappe.flags.in_patch
|
||||
or frappe.flags.in_install
|
||||
or frappe.flags.in_migrate
|
||||
or frappe.flags.in_import
|
||||
or frappe.flags.in_setup_wizard):
|
||||
or frappe.flags.in_setup_wizard
|
||||
):
|
||||
return
|
||||
_cache = frappe.cache()
|
||||
active_domains = frappe.get_active_domains()
|
||||
doctypes = frappe.get_all("DocType", filters={'restrict_to_domain': ('IN', active_domains)})
|
||||
doctypes = frappe.get_all("DocType", filters={"restrict_to_domain": ("IN", active_domains)})
|
||||
doctypes = [doc.name for doc in doctypes]
|
||||
_cache.set_value("domain_restricted_doctypes", doctypes)
|
||||
|
||||
return doctypes
|
||||
|
||||
|
||||
def build_domain_restriced_page_cache(*args, **kwargs):
|
||||
if (frappe.flags.in_patch
|
||||
if (
|
||||
frappe.flags.in_patch
|
||||
or frappe.flags.in_install
|
||||
or frappe.flags.in_migrate
|
||||
or frappe.flags.in_import
|
||||
or frappe.flags.in_setup_wizard):
|
||||
or frappe.flags.in_setup_wizard
|
||||
):
|
||||
return
|
||||
_cache = frappe.cache()
|
||||
active_domains = frappe.get_active_domains()
|
||||
pages = frappe.get_all("Page", filters={'restrict_to_domain': ('IN', active_domains)})
|
||||
pages = frappe.get_all("Page", filters={"restrict_to_domain": ("IN", active_domains)})
|
||||
pages = [page.name for page in pages]
|
||||
_cache.set_value("domain_restricted_pages", pages)
|
||||
|
||||
|
|
|
|||
234
frappe/client.py
234
frappe/client.py
|
|
@ -1,32 +1,44 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import json
|
||||
import os
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
import frappe.model
|
||||
import frappe.utils
|
||||
import json, os
|
||||
from frappe.utils import get_safe_filters
|
||||
from frappe import _
|
||||
from frappe.desk.reportview import validate_args
|
||||
from frappe.model.db_query import check_parent_permission
|
||||
from frappe.utils import get_safe_filters
|
||||
|
||||
|
||||
'''
|
||||
"""
|
||||
Handle RESTful requests that are mapped to the `/api/resource` route.
|
||||
|
||||
Requests via FrappeClient are also handled here.
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_list(doctype, fields=None, filters=None, order_by=None,
|
||||
limit_start=None, limit_page_length=20, parent=None, debug=False, as_dict=True, or_filters=None):
|
||||
'''Returns a list of records by filters, fields, ordering and limit
|
||||
def get_list(
|
||||
doctype,
|
||||
fields=None,
|
||||
filters=None,
|
||||
order_by=None,
|
||||
limit_start=None,
|
||||
limit_page_length=20,
|
||||
parent=None,
|
||||
debug=False,
|
||||
as_dict=True,
|
||||
or_filters=None,
|
||||
):
|
||||
"""Returns a list of records by filters, fields, ordering and limit
|
||||
|
||||
:param doctype: DocType of the data to be queried
|
||||
:param fields: fields to be returned. Default is `name`
|
||||
:param filters: filter list by this dict
|
||||
:param order_by: Order by this fieldname
|
||||
:param limit_start: Start at this index
|
||||
:param limit_page_length: Number of records to be returned (default 20)'''
|
||||
:param limit_page_length: Number of records to be returned (default 20)"""
|
||||
if frappe.is_table(doctype):
|
||||
check_parent_permission(parent, doctype)
|
||||
|
||||
|
|
@ -40,23 +52,25 @@ def get_list(doctype, fields=None, filters=None, order_by=None,
|
|||
limit_start=limit_start,
|
||||
limit_page_length=limit_page_length,
|
||||
debug=debug,
|
||||
as_list=not as_dict
|
||||
as_list=not as_dict,
|
||||
)
|
||||
|
||||
validate_args(args)
|
||||
return frappe.get_list(**args)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_count(doctype, filters=None, debug=False, cache=False):
|
||||
return frappe.db.count(doctype, get_safe_filters(filters), debug, cache)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get(doctype, name=None, filters=None, parent=None):
|
||||
'''Returns a document by name or filters
|
||||
"""Returns a document by name or filters
|
||||
|
||||
:param doctype: DocType of the document to be returned
|
||||
:param name: return document of this `name`
|
||||
:param filters: If name is not set, filter by these values and return the first match'''
|
||||
:param filters: If name is not set, filter by these values and return the first match"""
|
||||
if frappe.is_table(doctype):
|
||||
check_parent_permission(parent, doctype)
|
||||
|
||||
|
|
@ -71,13 +85,14 @@ def get(doctype, name=None, filters=None, parent=None):
|
|||
|
||||
return frappe.get_doc(doctype, name).as_dict()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, parent=None):
|
||||
'''Returns a value form a document
|
||||
"""Returns a value form a document
|
||||
|
||||
:param doctype: DocType to be queried
|
||||
:param fieldname: Field to be returned (default `name`)
|
||||
:param filters: dict or string for identifying the record'''
|
||||
:param filters: dict or string for identifying the record"""
|
||||
if frappe.is_table(doctype):
|
||||
check_parent_permission(parent, doctype)
|
||||
|
||||
|
|
@ -102,7 +117,15 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, paren
|
|||
if frappe.get_meta(doctype).issingle:
|
||||
value = frappe.db.get_values_from_single(fields, filters, doctype, as_dict=as_dict, debug=debug)
|
||||
else:
|
||||
value = get_list(doctype, filters=filters, fields=fields, debug=debug, limit_page_length=1, parent=parent, as_dict=as_dict)
|
||||
value = get_list(
|
||||
doctype,
|
||||
filters=filters,
|
||||
fields=fields,
|
||||
debug=debug,
|
||||
limit_page_length=1,
|
||||
parent=parent,
|
||||
as_dict=as_dict,
|
||||
)
|
||||
|
||||
if as_dict:
|
||||
return value[0] if value else {}
|
||||
|
|
@ -112,6 +135,7 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, paren
|
|||
|
||||
return value[0] if len(fields) > 1 else value[0][0]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_single_value(doctype, field):
|
||||
if not frappe.has_permission(doctype):
|
||||
|
|
@ -119,14 +143,15 @@ def get_single_value(doctype, field):
|
|||
value = frappe.db.get_single_value(doctype, field)
|
||||
return value
|
||||
|
||||
@frappe.whitelist(methods=['POST', 'PUT'])
|
||||
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def set_value(doctype, name, fieldname, value=None):
|
||||
'''Set a value using get_doc, group of values
|
||||
"""Set a value using get_doc, group of values
|
||||
|
||||
:param doctype: DocType of the document
|
||||
:param name: name of the document
|
||||
:param fieldname: fieldname string or JSON / dict with key value pair
|
||||
:param value: value if fieldname is JSON / dict'''
|
||||
:param value: value if fieldname is JSON / dict"""
|
||||
|
||||
if fieldname in (frappe.model.default_fields + frappe.model.child_table_fields):
|
||||
frappe.throw(_("Cannot edit standard fields"))
|
||||
|
|
@ -137,7 +162,7 @@ def set_value(doctype, name, fieldname, value=None):
|
|||
try:
|
||||
values = json.loads(fieldname)
|
||||
except ValueError:
|
||||
values = {fieldname: ''}
|
||||
values = {fieldname: ""}
|
||||
else:
|
||||
values = {fieldname: value}
|
||||
|
||||
|
|
@ -155,11 +180,12 @@ def set_value(doctype, name, fieldname, value=None):
|
|||
|
||||
return doc.as_dict()
|
||||
|
||||
@frappe.whitelist(methods=['POST', 'PUT'])
|
||||
def insert(doc=None):
|
||||
'''Insert a document
|
||||
|
||||
:param doc: JSON or dict object to be inserted'''
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def insert(doc=None):
|
||||
"""Insert a document
|
||||
|
||||
:param doc: JSON or dict object to be inserted"""
|
||||
if isinstance(doc, str):
|
||||
doc = json.loads(doc)
|
||||
|
||||
|
|
@ -173,18 +199,19 @@ def insert(doc=None):
|
|||
doc = frappe.get_doc(doc).insert()
|
||||
return doc.as_dict()
|
||||
|
||||
@frappe.whitelist(methods=['POST', 'PUT'])
|
||||
def insert_many(docs=None):
|
||||
'''Insert multiple documents
|
||||
|
||||
:param docs: JSON or list of dict objects to be inserted in one request'''
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def insert_many(docs=None):
|
||||
"""Insert multiple documents
|
||||
|
||||
:param docs: JSON or list of dict objects to be inserted in one request"""
|
||||
if isinstance(docs, str):
|
||||
docs = json.loads(docs)
|
||||
|
||||
out = []
|
||||
|
||||
if len(docs) > 200:
|
||||
frappe.throw(_('Only 200 inserts allowed in one request'))
|
||||
frappe.throw(_("Only 200 inserts allowed in one request"))
|
||||
|
||||
for doc in docs:
|
||||
if doc.get("parenttype"):
|
||||
|
|
@ -199,11 +226,12 @@ def insert_many(docs=None):
|
|||
|
||||
return out
|
||||
|
||||
@frappe.whitelist(methods=['POST', 'PUT'])
|
||||
def save(doc):
|
||||
'''Update (save) an existing document
|
||||
|
||||
:param doc: JSON or dict object with the properties of the document to be updated'''
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def save(doc):
|
||||
"""Update (save) an existing document
|
||||
|
||||
:param doc: JSON or dict object with the properties of the document to be updated"""
|
||||
if isinstance(doc, str):
|
||||
doc = json.loads(doc)
|
||||
|
||||
|
|
@ -212,21 +240,23 @@ def save(doc):
|
|||
|
||||
return doc.as_dict()
|
||||
|
||||
@frappe.whitelist(methods=['POST', 'PUT'])
|
||||
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def rename_doc(doctype, old_name, new_name, merge=False):
|
||||
'''Rename document
|
||||
"""Rename document
|
||||
|
||||
:param doctype: DocType of the document to be renamed
|
||||
:param old_name: Current `name` of the document to be renamed
|
||||
:param new_name: New `name` to be set'''
|
||||
:param new_name: New `name` to be set"""
|
||||
new_name = frappe.rename_doc(doctype, old_name, new_name, merge=merge)
|
||||
return new_name
|
||||
|
||||
@frappe.whitelist(methods=['POST', 'PUT'])
|
||||
def submit(doc):
|
||||
'''Submit a document
|
||||
|
||||
:param doc: JSON or dict object to be submitted remotely'''
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def submit(doc):
|
||||
"""Submit a document
|
||||
|
||||
:param doc: JSON or dict object to be submitted remotely"""
|
||||
if isinstance(doc, str):
|
||||
doc = json.loads(doc)
|
||||
|
||||
|
|
@ -235,52 +265,57 @@ def submit(doc):
|
|||
|
||||
return doc.as_dict()
|
||||
|
||||
@frappe.whitelist(methods=['POST', 'PUT'])
|
||||
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def cancel(doctype, name):
|
||||
'''Cancel a document
|
||||
"""Cancel a document
|
||||
|
||||
:param doctype: DocType of the document to be cancelled
|
||||
:param name: name of the document to be cancelled'''
|
||||
:param name: name of the document to be cancelled"""
|
||||
wrapper = frappe.get_doc(doctype, name)
|
||||
wrapper.cancel()
|
||||
|
||||
return wrapper.as_dict()
|
||||
|
||||
@frappe.whitelist(methods=['DELETE', 'POST'])
|
||||
|
||||
@frappe.whitelist(methods=["DELETE", "POST"])
|
||||
def delete(doctype, name):
|
||||
'''Delete a remote document
|
||||
"""Delete a remote document
|
||||
|
||||
:param doctype: DocType of the document to be deleted
|
||||
:param name: name of the document to be deleted'''
|
||||
:param name: name of the document to be deleted"""
|
||||
frappe.delete_doc(doctype, name, ignore_missing=False)
|
||||
|
||||
@frappe.whitelist(methods=['POST', 'PUT'])
|
||||
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def set_default(key, value, parent=None):
|
||||
"""set a user default value"""
|
||||
frappe.db.set_default(key, value, parent or frappe.session.user)
|
||||
frappe.clear_cache(user=frappe.session.user)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_default(key, parent=None):
|
||||
"""set a user default value"""
|
||||
return frappe.db.get_default(key, parent)
|
||||
|
||||
|
||||
@frappe.whitelist(methods=['POST', 'PUT'])
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def make_width_property_setter(doc):
|
||||
'''Set width Property Setter
|
||||
"""Set width Property Setter
|
||||
|
||||
:param doc: Property Setter document with `width` property'''
|
||||
:param doc: Property Setter document with `width` property"""
|
||||
if isinstance(doc, str):
|
||||
doc = json.loads(doc)
|
||||
if doc["doctype"]=="Property Setter" and doc["property"]=="width":
|
||||
frappe.get_doc(doc).insert(ignore_permissions = True)
|
||||
if doc["doctype"] == "Property Setter" and doc["property"] == "width":
|
||||
frappe.get_doc(doc).insert(ignore_permissions=True)
|
||||
|
||||
@frappe.whitelist(methods=['POST', 'PUT'])
|
||||
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def bulk_update(docs):
|
||||
'''Bulk update documents
|
||||
"""Bulk update documents
|
||||
|
||||
:param docs: JSON list of documents to be updated remotely. Each document must have `docname` property'''
|
||||
:param docs: JSON list of documents to be updated remotely. Each document must have `docname` property"""
|
||||
docs = json.loads(docs)
|
||||
failed_docs = []
|
||||
for doc in docs:
|
||||
|
|
@ -290,41 +325,40 @@ def bulk_update(docs):
|
|||
existing_doc.update(doc)
|
||||
existing_doc.save()
|
||||
except Exception:
|
||||
failed_docs.append({
|
||||
'doc': doc,
|
||||
'exc': frappe.utils.get_traceback()
|
||||
})
|
||||
failed_docs.append({"doc": doc, "exc": frappe.utils.get_traceback()})
|
||||
|
||||
return {"failed_docs": failed_docs}
|
||||
|
||||
return {'failed_docs': failed_docs}
|
||||
|
||||
@frappe.whitelist()
|
||||
def has_permission(doctype, docname, perm_type="read"):
|
||||
'''Returns a JSON with data whether the document has the requested permission
|
||||
"""Returns a JSON with data whether the document has the requested permission
|
||||
|
||||
:param doctype: DocType of the document to be checked
|
||||
:param docname: `name` of the document to be checked
|
||||
:param perm_type: one of `read`, `write`, `create`, `submit`, `cancel`, `report`. Default is `read`'''
|
||||
:param perm_type: one of `read`, `write`, `create`, `submit`, `cancel`, `report`. Default is `read`"""
|
||||
# perm_type can be one of read, write, create, submit, cancel, report
|
||||
return {"has_permission": frappe.has_permission(doctype, perm_type.lower(), docname)}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_password(doctype, name, fieldname):
|
||||
'''Return a password type property. Only applicable for System Managers
|
||||
"""Return a password type property. Only applicable for System Managers
|
||||
|
||||
:param doctype: DocType of the document that holds the password
|
||||
:param name: `name` of the document that holds the password
|
||||
:param fieldname: `fieldname` of the password property
|
||||
'''
|
||||
"""
|
||||
frappe.only_for("System Manager")
|
||||
return frappe.get_doc(doctype, name).get_password(fieldname)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_js(items):
|
||||
'''Load JS code files. Will also append translations
|
||||
"""Load JS code files. Will also append translations
|
||||
and extend `frappe._messages`
|
||||
|
||||
:param items: JSON list of paths of the js files to be loaded.'''
|
||||
:param items: JSON list of paths of the js files to be loaded."""
|
||||
items = json.loads(items)
|
||||
out = []
|
||||
for src in items:
|
||||
|
|
@ -346,14 +380,25 @@ def get_js(items):
|
|||
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_time_zone():
|
||||
'''Returns default time zone'''
|
||||
"""Returns default time zone"""
|
||||
return {"time_zone": frappe.defaults.get_defaults().get("time_zone")}
|
||||
|
||||
@frappe.whitelist(methods=['POST', 'PUT'])
|
||||
def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder=None, decode_base64=False, is_private=None, docfield=None):
|
||||
'''Attach a file to Document (POST)
|
||||
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def attach_file(
|
||||
filename=None,
|
||||
filedata=None,
|
||||
doctype=None,
|
||||
docname=None,
|
||||
folder=None,
|
||||
decode_base64=False,
|
||||
is_private=None,
|
||||
docfield=None,
|
||||
):
|
||||
"""Attach a file to Document (POST)
|
||||
|
||||
:param filename: filename e.g. test-file.txt
|
||||
:param filedata: base64 encode filedata which must be urlencoded
|
||||
|
|
@ -362,7 +407,7 @@ def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder
|
|||
:param folder: Folder to add File into
|
||||
:param decode_base64: decode filedata from base64 encode, default is False
|
||||
:param is_private: Attach file as private file (1 or 0)
|
||||
:param docfield: file to attach to (optional)'''
|
||||
:param docfield: file to attach to (optional)"""
|
||||
|
||||
request_method = frappe.local.request.environ.get("REQUEST_METHOD")
|
||||
|
||||
|
|
@ -374,16 +419,19 @@ def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder
|
|||
if not doc.has_permission():
|
||||
frappe.throw(_("Not permitted"), frappe.PermissionError)
|
||||
|
||||
_file = frappe.get_doc({
|
||||
"doctype": "File",
|
||||
"file_name": filename,
|
||||
"attached_to_doctype": doctype,
|
||||
"attached_to_name": docname,
|
||||
"attached_to_field": docfield,
|
||||
"folder": folder,
|
||||
"is_private": is_private,
|
||||
"content": filedata,
|
||||
"decode": decode_base64})
|
||||
_file = frappe.get_doc(
|
||||
{
|
||||
"doctype": "File",
|
||||
"file_name": filename,
|
||||
"attached_to_doctype": doctype,
|
||||
"attached_to_name": docname,
|
||||
"attached_to_field": docfield,
|
||||
"folder": folder,
|
||||
"is_private": is_private,
|
||||
"content": filedata,
|
||||
"decode": decode_base64,
|
||||
}
|
||||
)
|
||||
_file.save()
|
||||
|
||||
if docfield and doctype:
|
||||
|
|
@ -392,22 +440,23 @@ def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder
|
|||
|
||||
return _file.as_dict()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_hooks(hook, app_name=None):
|
||||
return frappe.get_hooks(hook, app_name)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_document_amended(doctype, docname):
|
||||
if frappe.permissions.has_permission(doctype):
|
||||
try:
|
||||
return frappe.db.exists(doctype, {
|
||||
'amended_from': docname
|
||||
})
|
||||
return frappe.db.exists(doctype, {"amended_from": docname})
|
||||
except frappe.db.InternalError:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def validate_link(doctype: str, docname: str, fields=None):
|
||||
if not isinstance(doctype, str):
|
||||
|
|
@ -417,13 +466,11 @@ def validate_link(doctype: str, docname: str, fields=None):
|
|||
frappe.throw(_("Document Name must be a string"))
|
||||
|
||||
if doctype != "DocType" and not (
|
||||
frappe.has_permission(doctype, "select")
|
||||
or frappe.has_permission(doctype, "read")
|
||||
frappe.has_permission(doctype, "select") or frappe.has_permission(doctype, "read")
|
||||
):
|
||||
frappe.throw(
|
||||
_("You do not have Read or Select Permissions for {}")
|
||||
.format(frappe.bold(doctype)),
|
||||
frappe.PermissionError
|
||||
_("You do not have Read or Select Permissions for {}").format(frappe.bold(doctype)),
|
||||
frappe.PermissionError,
|
||||
)
|
||||
|
||||
values = frappe._dict()
|
||||
|
|
@ -438,14 +485,11 @@ def validate_link(doctype: str, docname: str, fields=None):
|
|||
except frappe.PermissionError:
|
||||
frappe.clear_last_message()
|
||||
frappe.msgprint(
|
||||
_("You need {0} permission to fetch values from {1} {2}")
|
||||
.format(
|
||||
frappe.bold(_("Read")),
|
||||
frappe.bold(doctype),
|
||||
frappe.bold(docname)
|
||||
_("You need {0} permission to fetch values from {1} {2}").format(
|
||||
frappe.bold(_("Read")), frappe.bold(doctype), frappe.bold(docname)
|
||||
),
|
||||
title=_("Cannot Fetch Values"),
|
||||
indicator="orange"
|
||||
indicator="orange",
|
||||
)
|
||||
|
||||
return values
|
||||
|
|
|
|||
|
|
@ -1,23 +1,26 @@
|
|||
# Copyright (c) 2015, Web Notes Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import sys
|
||||
import click
|
||||
import cProfile
|
||||
import pstats
|
||||
import frappe
|
||||
import frappe.utils
|
||||
import subprocess # nosec
|
||||
import subprocess # nosec
|
||||
import sys
|
||||
from functools import wraps
|
||||
from io import StringIO
|
||||
from os import environ
|
||||
|
||||
import click
|
||||
|
||||
import frappe
|
||||
import frappe.utils
|
||||
|
||||
click.disable_unicode_literals_warning = True
|
||||
|
||||
|
||||
def pass_context(f):
|
||||
@wraps(f)
|
||||
def _func(ctx, *args, **kwargs):
|
||||
profile = ctx.obj['profile']
|
||||
profile = ctx.obj["profile"]
|
||||
if profile:
|
||||
pr = cProfile.Profile()
|
||||
pr.enable()
|
||||
|
|
@ -25,18 +28,17 @@ def pass_context(f):
|
|||
try:
|
||||
ret = f(frappe._dict(ctx.obj), *args, **kwargs)
|
||||
except frappe.exceptions.SiteNotSpecifiedError as e:
|
||||
click.secho(str(e), fg='yellow')
|
||||
click.secho(str(e), fg="yellow")
|
||||
sys.exit(1)
|
||||
except frappe.exceptions.IncorrectSitePath:
|
||||
site = ctx.obj.get("sites", "")[0]
|
||||
click.secho(f'Site {site} does not exist!', fg='yellow')
|
||||
click.secho(f"Site {site} does not exist!", fg="yellow")
|
||||
sys.exit(1)
|
||||
|
||||
if profile:
|
||||
pr.disable()
|
||||
s = StringIO()
|
||||
ps = pstats.Stats(pr, stream=s)\
|
||||
.sort_stats('cumtime', 'tottime', 'ncalls')
|
||||
ps = pstats.Stats(pr, stream=s).sort_stats("cumtime", "tottime", "ncalls")
|
||||
ps.print_stats()
|
||||
|
||||
# print the top-100
|
||||
|
|
@ -47,6 +49,7 @@ def pass_context(f):
|
|||
|
||||
return click.pass_context(_func)
|
||||
|
||||
|
||||
def get_site(context, raise_err=True):
|
||||
try:
|
||||
site = context.sites[0]
|
||||
|
|
@ -56,17 +59,19 @@ def get_site(context, raise_err=True):
|
|||
raise frappe.SiteNotSpecifiedError
|
||||
return None
|
||||
|
||||
|
||||
def popen(command, *args, **kwargs):
|
||||
output = kwargs.get('output', True)
|
||||
cwd = kwargs.get('cwd')
|
||||
shell = kwargs.get('shell', True)
|
||||
raise_err = kwargs.get('raise_err')
|
||||
env = kwargs.get('env')
|
||||
output = kwargs.get("output", True)
|
||||
cwd = kwargs.get("cwd")
|
||||
shell = kwargs.get("shell", True)
|
||||
raise_err = kwargs.get("raise_err")
|
||||
env = kwargs.get("env")
|
||||
if env:
|
||||
env = dict(environ, **env)
|
||||
|
||||
def set_low_prio():
|
||||
import psutil
|
||||
|
||||
if psutil.LINUX:
|
||||
psutil.Process().nice(19)
|
||||
psutil.Process().ionice(psutil.IOPRIO_CLASS_IDLE)
|
||||
|
|
@ -77,13 +82,14 @@ def popen(command, *args, **kwargs):
|
|||
psutil.Process().nice(19)
|
||||
# ionice not supported
|
||||
|
||||
proc = subprocess.Popen(command,
|
||||
proc = subprocess.Popen(
|
||||
command,
|
||||
stdout=None if output else subprocess.PIPE,
|
||||
stderr=None if output else subprocess.PIPE,
|
||||
shell=shell,
|
||||
cwd=cwd,
|
||||
preexec_fn=set_low_prio,
|
||||
env=env
|
||||
env=env,
|
||||
)
|
||||
|
||||
return_ = proc.wait()
|
||||
|
|
@ -93,26 +99,22 @@ def popen(command, *args, **kwargs):
|
|||
|
||||
return return_
|
||||
|
||||
|
||||
def call_command(cmd, context):
|
||||
return click.Context(cmd, obj=context).forward(cmd)
|
||||
|
||||
|
||||
def get_commands():
|
||||
# prevent circular imports
|
||||
from .redis_utils import commands as redis_commands
|
||||
from .scheduler import commands as scheduler_commands
|
||||
from .site import commands as site_commands
|
||||
from .translate import commands as translate_commands
|
||||
from .utils import commands as utils_commands
|
||||
from .redis_utils import commands as redis_commands
|
||||
|
||||
clickable_link = (
|
||||
"\x1b]8;;https://frappeframework.com/docs\afrappeframework.com\x1b]8;;\a"
|
||||
)
|
||||
clickable_link = "\x1b]8;;https://frappeframework.com/docs\afrappeframework.com\x1b]8;;\a"
|
||||
all_commands = (
|
||||
scheduler_commands
|
||||
+ site_commands
|
||||
+ translate_commands
|
||||
+ utils_commands
|
||||
+ redis_commands
|
||||
scheduler_commands + site_commands + translate_commands + utils_commands + redis_commands
|
||||
)
|
||||
|
||||
for command in all_commands:
|
||||
|
|
|
|||
|
|
@ -3,51 +3,71 @@ import os
|
|||
import click
|
||||
|
||||
import frappe
|
||||
from frappe.utils.redis_queue import RedisQueue
|
||||
from frappe.installer import update_site_config
|
||||
from frappe.utils.redis_queue import RedisQueue
|
||||
|
||||
@click.command('create-rq-users')
|
||||
@click.option('--set-admin-password', is_flag=True, default=False, help='Set new Redis admin(default user) password')
|
||||
@click.option('--use-rq-auth', is_flag=True, default=False, help='Enable Redis authentication for sites')
|
||||
|
||||
@click.command("create-rq-users")
|
||||
@click.option(
|
||||
"--set-admin-password",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Set new Redis admin(default user) password",
|
||||
)
|
||||
@click.option(
|
||||
"--use-rq-auth", is_flag=True, default=False, help="Enable Redis authentication for sites"
|
||||
)
|
||||
def create_rq_users(set_admin_password=False, use_rq_auth=False):
|
||||
"""Create Redis Queue users and add to acl and app configs.
|
||||
|
||||
acl config file will be used by redis server while starting the server
|
||||
and app config is used by app while connecting to redis server.
|
||||
"""
|
||||
acl_file_path = os.path.abspath('../config/redis_queue.acl')
|
||||
acl_file_path = os.path.abspath("../config/redis_queue.acl")
|
||||
|
||||
with frappe.init_site():
|
||||
acl_list, user_credentials = RedisQueue.gen_acl_list(
|
||||
set_admin_password=set_admin_password)
|
||||
acl_list, user_credentials = RedisQueue.gen_acl_list(set_admin_password=set_admin_password)
|
||||
|
||||
with open(acl_file_path, 'w') as f:
|
||||
f.writelines([acl+'\n' for acl in acl_list])
|
||||
with open(acl_file_path, "w") as f:
|
||||
f.writelines([acl + "\n" for acl in acl_list])
|
||||
|
||||
sites_path = os.getcwd()
|
||||
common_site_config_path = os.path.join(sites_path, 'common_site_config.json')
|
||||
update_site_config("rq_username", user_credentials['bench'][0], validate=False,
|
||||
site_config_path=common_site_config_path)
|
||||
update_site_config("rq_password", user_credentials['bench'][1], validate=False,
|
||||
site_config_path=common_site_config_path)
|
||||
update_site_config("use_rq_auth", use_rq_auth, validate=False,
|
||||
site_config_path=common_site_config_path)
|
||||
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')
|
||||
click.secho(
|
||||
"* ACL and site configs are updated with new user credentials. "
|
||||
"Please restart Redis Queue server to enable namespaces.",
|
||||
fg="green",
|
||||
)
|
||||
|
||||
if set_admin_password:
|
||||
env_key = 'RQ_ADMIN_PASWORD'
|
||||
click.secho('* Redis admin password is successfully set up. '
|
||||
'Include below line in .bashrc file for system to use',
|
||||
fg='green')
|
||||
env_key = "RQ_ADMIN_PASWORD"
|
||||
click.secho(
|
||||
"* Redis admin password is successfully set up. "
|
||||
"Include below line in .bashrc file for system to use",
|
||||
fg="green",
|
||||
)
|
||||
click.secho(f"`export {env_key}={user_credentials['default'][1]}`")
|
||||
click.secho('NOTE: Please save the admin password as you '
|
||||
'can not access redis server without the password',
|
||||
fg='yellow')
|
||||
click.secho(
|
||||
"NOTE: Please save the admin password as you "
|
||||
"can not access redis server without the password",
|
||||
fg="yellow",
|
||||
)
|
||||
|
||||
|
||||
commands = [
|
||||
create_rq_users
|
||||
]
|
||||
commands = [create_rq_users]
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
import click
|
||||
import sys
|
||||
|
||||
import click
|
||||
|
||||
import frappe
|
||||
from frappe.utils import cint
|
||||
from frappe.commands import pass_context, get_site
|
||||
from frappe.commands import get_site, pass_context
|
||||
from frappe.exceptions import SiteNotSpecifiedError
|
||||
from frappe.utils import cint
|
||||
|
||||
|
||||
def _is_scheduler_enabled():
|
||||
enable_scheduler = False
|
||||
try:
|
||||
frappe.connect()
|
||||
enable_scheduler = cint(frappe.db.get_single_value("System Settings", "enable_scheduler")) and True or False
|
||||
enable_scheduler = (
|
||||
cint(frappe.db.get_single_value("System Settings", "enable_scheduler")) and True or False
|
||||
)
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
|
|
@ -44,11 +49,12 @@ def trigger_scheduler_event(context, event):
|
|||
sys.exit(exit_code)
|
||||
|
||||
|
||||
@click.command('enable-scheduler')
|
||||
@click.command("enable-scheduler")
|
||||
@pass_context
|
||||
def enable_scheduler(context):
|
||||
"Enable scheduler"
|
||||
import frappe.utils.scheduler
|
||||
|
||||
for site in context.sites:
|
||||
try:
|
||||
frappe.init(site=site)
|
||||
|
|
@ -61,11 +67,13 @@ def enable_scheduler(context):
|
|||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('disable-scheduler')
|
||||
|
||||
@click.command("disable-scheduler")
|
||||
@pass_context
|
||||
def disable_scheduler(context):
|
||||
"Disable scheduler"
|
||||
import frappe.utils.scheduler
|
||||
|
||||
for site in context.sites:
|
||||
try:
|
||||
frappe.init(site=site)
|
||||
|
|
@ -79,13 +87,13 @@ def disable_scheduler(context):
|
|||
raise SiteNotSpecifiedError
|
||||
|
||||
|
||||
@click.command('scheduler')
|
||||
@click.option('--site', help='site name')
|
||||
@click.argument('state', type=click.Choice(['pause', 'resume', 'disable', 'enable']))
|
||||
@click.command("scheduler")
|
||||
@click.option("--site", help="site name")
|
||||
@click.argument("state", type=click.Choice(["pause", "resume", "disable", "enable"]))
|
||||
@pass_context
|
||||
def scheduler(context, state, site=None):
|
||||
from frappe.installer import update_site_config
|
||||
import frappe.utils.scheduler
|
||||
from frappe.installer import update_site_config
|
||||
|
||||
if not site:
|
||||
site = get_site(context)
|
||||
|
|
@ -93,58 +101,64 @@ def scheduler(context, state, site=None):
|
|||
try:
|
||||
frappe.init(site=site)
|
||||
|
||||
if state == 'pause':
|
||||
update_site_config('pause_scheduler', 1)
|
||||
elif state == 'resume':
|
||||
update_site_config('pause_scheduler', 0)
|
||||
elif state == 'disable':
|
||||
if state == "pause":
|
||||
update_site_config("pause_scheduler", 1)
|
||||
elif state == "resume":
|
||||
update_site_config("pause_scheduler", 0)
|
||||
elif state == "disable":
|
||||
frappe.connect()
|
||||
frappe.utils.scheduler.disable_scheduler()
|
||||
frappe.db.commit()
|
||||
elif state == 'enable':
|
||||
elif state == "enable":
|
||||
frappe.connect()
|
||||
frappe.utils.scheduler.enable_scheduler()
|
||||
frappe.db.commit()
|
||||
|
||||
print('Scheduler {0}d for site {1}'.format(state, site))
|
||||
print("Scheduler {0}d for site {1}".format(state, site))
|
||||
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
|
||||
@click.command('set-maintenance-mode')
|
||||
@click.option('--site', help='site name')
|
||||
@click.argument('state', type=click.Choice(['on', 'off']))
|
||||
@click.command("set-maintenance-mode")
|
||||
@click.option("--site", help="site name")
|
||||
@click.argument("state", type=click.Choice(["on", "off"]))
|
||||
@pass_context
|
||||
def set_maintenance_mode(context, state, site=None):
|
||||
from frappe.installer import update_site_config
|
||||
|
||||
if not site:
|
||||
site = get_site(context)
|
||||
|
||||
try:
|
||||
frappe.init(site=site)
|
||||
update_site_config('maintenance_mode', 1 if (state == 'on') else 0)
|
||||
update_site_config("maintenance_mode", 1 if (state == "on") else 0)
|
||||
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
|
||||
@click.command('doctor') #Passing context always gets a site and if there is no use site it breaks
|
||||
@click.option('--site', help='site name')
|
||||
@click.command(
|
||||
"doctor"
|
||||
) # Passing context always gets a site and if there is no use site it breaks
|
||||
@click.option("--site", help="site name")
|
||||
@pass_context
|
||||
def doctor(context, site=None):
|
||||
"Get diagnostic info about background workers"
|
||||
from frappe.utils.doctor import doctor as _doctor
|
||||
|
||||
if not site:
|
||||
site = get_site(context, raise_err=False)
|
||||
return _doctor(site=site)
|
||||
|
||||
@click.command('show-pending-jobs')
|
||||
@click.option('--site', help='site name')
|
||||
|
||||
@click.command("show-pending-jobs")
|
||||
@click.option("--site", help="site name")
|
||||
@pass_context
|
||||
def show_pending_jobs(context, site=None):
|
||||
"Get diagnostic info about background jobs"
|
||||
from frappe.utils.doctor import pending_jobs as _pending_jobs
|
||||
|
||||
if not site:
|
||||
site = get_site(context)
|
||||
|
||||
|
|
@ -153,35 +167,45 @@ def show_pending_jobs(context, site=None):
|
|||
|
||||
return pending_jobs
|
||||
|
||||
@click.command('purge-jobs')
|
||||
@click.option('--site', help='site name')
|
||||
@click.option('--queue', default=None, help='one of "low", "default", "high')
|
||||
@click.option('--event', default=None, help='one of "all", "weekly", "monthly", "hourly", "daily", "weekly_long", "daily_long"')
|
||||
|
||||
@click.command("purge-jobs")
|
||||
@click.option("--site", help="site name")
|
||||
@click.option("--queue", default=None, help='one of "low", "default", "high')
|
||||
@click.option(
|
||||
"--event",
|
||||
default=None,
|
||||
help='one of "all", "weekly", "monthly", "hourly", "daily", "weekly_long", "daily_long"',
|
||||
)
|
||||
def purge_jobs(site=None, queue=None, event=None):
|
||||
"Purge any pending periodic tasks, if event option is not given, it will purge everything for the site"
|
||||
from frappe.utils.doctor import purge_pending_jobs
|
||||
frappe.init(site or '')
|
||||
|
||||
frappe.init(site or "")
|
||||
count = purge_pending_jobs(event=event, site=site, queue=queue)
|
||||
print("Purged {} jobs".format(count))
|
||||
|
||||
@click.command('schedule')
|
||||
|
||||
@click.command("schedule")
|
||||
def start_scheduler():
|
||||
from frappe.utils.scheduler import start_scheduler
|
||||
|
||||
start_scheduler()
|
||||
|
||||
@click.command('worker')
|
||||
@click.option('--queue', type=str)
|
||||
@click.option('--quiet', is_flag = True, default = False, help = 'Hide Log Outputs')
|
||||
@click.option('-u', '--rq-username', default=None, help='Redis ACL user')
|
||||
@click.option('-p', '--rq-password', default=None, help='Redis ACL user password')
|
||||
def start_worker(queue, quiet = False, rq_username=None, rq_password=None):
|
||||
"""Site is used to find redis credentals.
|
||||
"""
|
||||
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')
|
||||
@click.command("worker")
|
||||
@click.option("--queue", type=str)
|
||||
@click.option("--quiet", is_flag=True, default=False, help="Hide Log Outputs")
|
||||
@click.option("-u", "--rq-username", default=None, help="Redis ACL user")
|
||||
@click.option("-p", "--rq-password", default=None, help="Redis ACL user password")
|
||||
def start_worker(queue, quiet=False, rq_username=None, rq_password=None):
|
||||
"""Site is used to find redis credentals."""
|
||||
from frappe.utils.background_jobs import start_worker
|
||||
|
||||
start_worker(queue, quiet=quiet, rq_username=rq_username, rq_password=rq_password)
|
||||
|
||||
|
||||
@click.command("ready-for-migration")
|
||||
@click.option("--site", help="site name")
|
||||
@pass_context
|
||||
def ready_for_migration(context, site=None):
|
||||
from frappe.utils.doctor import get_pending_jobs
|
||||
|
|
@ -194,16 +218,17 @@ def ready_for_migration(context, site=None):
|
|||
pending_jobs = get_pending_jobs(site=site)
|
||||
|
||||
if pending_jobs:
|
||||
print('NOT READY for migration: site {0} has pending background jobs'.format(site))
|
||||
print("NOT READY for migration: site {0} has pending background jobs".format(site))
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
print('READY for migration: site {0} does not have any background jobs'.format(site))
|
||||
print("READY for migration: site {0} does not have any background jobs".format(site))
|
||||
return 0
|
||||
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
|
||||
commands = [
|
||||
disable_scheduler,
|
||||
doctor,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,13 +1,16 @@
|
|||
import click
|
||||
from frappe.commands import pass_context, get_site
|
||||
|
||||
from frappe.commands import get_site, pass_context
|
||||
from frappe.exceptions import SiteNotSpecifiedError
|
||||
|
||||
|
||||
# translation
|
||||
@click.command('build-message-files')
|
||||
@click.command("build-message-files")
|
||||
@pass_context
|
||||
def build_message_files(context):
|
||||
"Build message files for translation"
|
||||
import frappe.translate
|
||||
|
||||
for site in context.sites:
|
||||
try:
|
||||
frappe.init(site=site)
|
||||
|
|
@ -18,32 +21,41 @@ def build_message_files(context):
|
|||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('new-language') #, help="Create lang-code.csv for given app")
|
||||
|
||||
@click.command("new-language") # , help="Create lang-code.csv for given app")
|
||||
@pass_context
|
||||
@click.argument('lang_code') #, help="Language code eg. en")
|
||||
@click.argument('app') #, help="App name eg. frappe")
|
||||
@click.argument("lang_code") # , help="Language code eg. en")
|
||||
@click.argument("app") # , help="App name eg. frappe")
|
||||
def new_language(context, lang_code, app):
|
||||
"""Create lang-code.csv for given app"""
|
||||
import frappe.translate
|
||||
|
||||
if not context['sites']:
|
||||
raise Exception('--site is required')
|
||||
if not context["sites"]:
|
||||
raise Exception("--site is required")
|
||||
|
||||
# init site
|
||||
frappe.connect(site=context['sites'][0])
|
||||
frappe.connect(site=context["sites"][0])
|
||||
frappe.translate.write_translations_file(app, lang_code)
|
||||
|
||||
print("File created at ./apps/{app}/{app}/translations/{lang_code}.csv".format(app=app, lang_code=lang_code))
|
||||
print("You will need to add the language in frappe/geo/languages.json, if you haven't done it already.")
|
||||
print(
|
||||
"File created at ./apps/{app}/{app}/translations/{lang_code}.csv".format(
|
||||
app=app, lang_code=lang_code
|
||||
)
|
||||
)
|
||||
print(
|
||||
"You will need to add the language in frappe/geo/languages.json, if you haven't done it already."
|
||||
)
|
||||
|
||||
@click.command('get-untranslated')
|
||||
@click.argument('lang')
|
||||
@click.argument('untranslated_file')
|
||||
@click.option('--all', default=False, is_flag=True, help='Get all message strings')
|
||||
|
||||
@click.command("get-untranslated")
|
||||
@click.argument("lang")
|
||||
@click.argument("untranslated_file")
|
||||
@click.option("--all", default=False, is_flag=True, help="Get all message strings")
|
||||
@pass_context
|
||||
def get_untranslated(context, lang, untranslated_file, all=None):
|
||||
"Get untranslated strings for language"
|
||||
import frappe.translate
|
||||
|
||||
site = get_site(context)
|
||||
try:
|
||||
frappe.init(site=site)
|
||||
|
|
@ -52,14 +64,16 @@ def get_untranslated(context, lang, untranslated_file, all=None):
|
|||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
@click.command('update-translations')
|
||||
@click.argument('lang')
|
||||
@click.argument('untranslated_file')
|
||||
@click.argument('translated-file')
|
||||
|
||||
@click.command("update-translations")
|
||||
@click.argument("lang")
|
||||
@click.argument("untranslated_file")
|
||||
@click.argument("translated-file")
|
||||
@pass_context
|
||||
def update_translations(context, lang, untranslated_file, translated_file):
|
||||
"Update translated strings"
|
||||
import frappe.translate
|
||||
|
||||
site = get_site(context)
|
||||
try:
|
||||
frappe.init(site=site)
|
||||
|
|
@ -68,13 +82,15 @@ def update_translations(context, lang, untranslated_file, translated_file):
|
|||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
@click.command('import-translations')
|
||||
@click.argument('lang')
|
||||
@click.argument('path')
|
||||
|
||||
@click.command("import-translations")
|
||||
@click.argument("lang")
|
||||
@click.argument("path")
|
||||
@pass_context
|
||||
def import_translations(context, lang, path):
|
||||
"Update translated strings"
|
||||
import frappe.translate
|
||||
|
||||
site = get_site(context)
|
||||
try:
|
||||
frappe.init(site=site)
|
||||
|
|
@ -83,6 +99,7 @@ def import_translations(context, lang, path):
|
|||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
|
||||
commands = [
|
||||
build_message_files,
|
||||
get_untranslated,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,14 +1,20 @@
|
|||
import frappe
|
||||
from frappe import _
|
||||
from frappe.desk.moduleview import (get_data, get_onboard_items, config_exists, get_module_link_items_from_list)
|
||||
from frappe.desk.moduleview import (
|
||||
config_exists,
|
||||
get_data,
|
||||
get_module_link_items_from_list,
|
||||
get_onboard_items,
|
||||
)
|
||||
|
||||
|
||||
def get_modules_from_all_apps_for_user(user=None):
|
||||
if not user:
|
||||
user = frappe.session.user
|
||||
|
||||
all_modules = get_modules_from_all_apps()
|
||||
global_blocked_modules = frappe.get_doc('User', 'Administrator').get_blocked_modules()
|
||||
user_blocked_modules = frappe.get_doc('User', user).get_blocked_modules()
|
||||
global_blocked_modules = frappe.get_doc("User", "Administrator").get_blocked_modules()
|
||||
user_blocked_modules = frappe.get_doc("User", user).get_blocked_modules()
|
||||
blocked_modules = global_blocked_modules + user_blocked_modules
|
||||
allowed_modules_list = [m for m in all_modules if m.get("module_name") not in blocked_modules]
|
||||
|
||||
|
|
@ -22,31 +28,31 @@ def get_modules_from_all_apps_for_user(user=None):
|
|||
module["onboard_present"] = 1
|
||||
|
||||
# Set defaults links
|
||||
module["links"] = get_onboard_items(module["app"], frappe.scrub(module_name))[:5]
|
||||
module["links"] = get_onboard_items(module["app"], frappe.scrub(module_name))[:5]
|
||||
|
||||
return allowed_modules_list
|
||||
|
||||
|
||||
def get_modules_from_all_apps():
|
||||
modules_list = []
|
||||
for app in frappe.get_installed_apps():
|
||||
modules_list += get_modules_from_app(app)
|
||||
return modules_list
|
||||
|
||||
|
||||
def get_modules_from_app(app):
|
||||
return frappe.get_all('Module Def',
|
||||
filters={'app_name': app},
|
||||
fields=['module_name', 'app_name as app']
|
||||
return frappe.get_all(
|
||||
"Module Def", filters={"app_name": app}, fields=["module_name", "app_name as app"]
|
||||
)
|
||||
|
||||
|
||||
def get_all_empty_tables_by_module():
|
||||
table_rows = frappe.qb.Field("table_rows")
|
||||
table_name = frappe.qb.Field("table_name")
|
||||
information_schema = frappe.qb.Schema("information_schema")
|
||||
|
||||
empty_tables = (
|
||||
frappe.qb.from_(information_schema.tables)
|
||||
.select(table_name)
|
||||
.where(table_rows == 0)
|
||||
frappe.qb.from_(information_schema.tables).select(table_name).where(table_rows == 0)
|
||||
).run()
|
||||
|
||||
empty_tables = {r[0] for r in empty_tables}
|
||||
|
|
@ -62,8 +68,10 @@ def get_all_empty_tables_by_module():
|
|||
empty_tables_by_module[module] = [doctype]
|
||||
return empty_tables_by_module
|
||||
|
||||
|
||||
def is_domain(module):
|
||||
return module.get("category") == "Domains"
|
||||
|
||||
|
||||
def is_module(module):
|
||||
return module.get("type") == "module"
|
||||
return module.get("type") == "module"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
|
||||
from frappe import _
|
||||
import functools
|
||||
import re
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
|
||||
def load_address_and_contact(doc, key=None):
|
||||
"""Loads address list and contact list in `__onload`"""
|
||||
from frappe.contacts.doctype.address.address import get_address_display, get_condensed_address
|
||||
|
|
@ -18,15 +19,18 @@ def load_address_and_contact(doc, key=None):
|
|||
]
|
||||
address_list = frappe.get_list("Address", filters=filters, fields=["*"])
|
||||
|
||||
address_list = [a.update({"display": get_address_display(a)})
|
||||
for a in address_list]
|
||||
address_list = [a.update({"display": get_address_display(a)}) for a in address_list]
|
||||
|
||||
address_list = sorted(address_list,
|
||||
key = functools.cmp_to_key(lambda a, b:
|
||||
(int(a.is_primary_address - b.is_primary_address)) or
|
||||
(1 if a.modified - b.modified else 0)), reverse=True)
|
||||
address_list = sorted(
|
||||
address_list,
|
||||
key=functools.cmp_to_key(
|
||||
lambda a, b: (int(a.is_primary_address - b.is_primary_address))
|
||||
or (1 if a.modified - b.modified else 0)
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
doc.set_onload('addr_list', address_list)
|
||||
doc.set_onload("addr_list", address_list)
|
||||
|
||||
contact_list = []
|
||||
filters = [
|
||||
|
|
@ -37,29 +41,38 @@ def load_address_and_contact(doc, key=None):
|
|||
contact_list = frappe.get_list("Contact", filters=filters, fields=["*"])
|
||||
|
||||
for contact in contact_list:
|
||||
contact["email_ids"] = frappe.get_all("Contact Email", filters={
|
||||
"parenttype": "Contact",
|
||||
"parent": contact.name,
|
||||
"is_primary": 0
|
||||
}, fields=["email_id"])
|
||||
contact["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["phone_nos"] = frappe.get_all(
|
||||
"Contact Phone",
|
||||
filters={
|
||||
"parenttype": "Contact",
|
||||
"parent": contact.name,
|
||||
"is_primary_phone": 0,
|
||||
"is_primary_mobile_no": 0
|
||||
}, fields=["phone"])
|
||||
"is_primary_mobile_no": 0,
|
||||
},
|
||||
fields=["phone"],
|
||||
)
|
||||
|
||||
if contact.address:
|
||||
address = frappe.get_doc("Address", contact.address)
|
||||
contact["address"] = get_condensed_address(address)
|
||||
|
||||
contact_list = sorted(contact_list,
|
||||
key = functools.cmp_to_key(lambda a, b:
|
||||
(int(a.is_primary_contact - b.is_primary_contact)) or
|
||||
(1 if a.modified - b.modified else 0)), reverse=True)
|
||||
contact_list = sorted(
|
||||
contact_list,
|
||||
key=functools.cmp_to_key(
|
||||
lambda a, b: (int(a.is_primary_contact - b.is_primary_contact))
|
||||
or (1 if a.modified - b.modified else 0)
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
doc.set_onload("contact_list", contact_list)
|
||||
|
||||
doc.set_onload('contact_list', contact_list)
|
||||
|
||||
def has_permission(doc, ptype, user):
|
||||
links = get_permitted_and_not_permitted_links(doc.doctype)
|
||||
|
|
@ -69,7 +82,7 @@ def has_permission(doc, ptype, user):
|
|||
|
||||
# True if any one is True or all are empty
|
||||
names = []
|
||||
for df in (links.get("permitted_links") + links.get("not_permitted_links")):
|
||||
for df in links.get("permitted_links") + links.get("not_permitted_links"):
|
||||
doctype = df.options
|
||||
name = doc.get(df.fieldname)
|
||||
names.append(name)
|
||||
|
|
@ -81,12 +94,15 @@ def has_permission(doc, ptype, user):
|
|||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_permission_query_conditions_for_contact(user):
|
||||
return get_permission_query_conditions("Contact")
|
||||
|
||||
|
||||
def get_permission_query_conditions_for_address(user):
|
||||
return get_permission_query_conditions("Address")
|
||||
|
||||
|
||||
def get_permission_query_conditions(doctype):
|
||||
links = get_permitted_and_not_permitted_links(doctype)
|
||||
|
||||
|
|
@ -100,7 +116,9 @@ def get_permission_query_conditions(doctype):
|
|||
# when everything is not permitted
|
||||
for df in links.get("not_permitted_links"):
|
||||
# like ifnull(customer, '')='' and ifnull(supplier, '')=''
|
||||
conditions.append("ifnull(`tab{doctype}`.`{fieldname}`, '')=''".format(doctype=doctype, fieldname=df.fieldname))
|
||||
conditions.append(
|
||||
"ifnull(`tab{doctype}`.`{fieldname}`, '')=''".format(doctype=doctype, fieldname=df.fieldname)
|
||||
)
|
||||
|
||||
return "( " + " and ".join(conditions) + " )"
|
||||
|
||||
|
|
@ -109,10 +127,13 @@ def get_permission_query_conditions(doctype):
|
|||
|
||||
for df in links.get("permitted_links"):
|
||||
# like ifnull(customer, '')!='' or ifnull(supplier, '')!=''
|
||||
conditions.append("ifnull(`tab{doctype}`.`{fieldname}`, '')!=''".format(doctype=doctype, fieldname=df.fieldname))
|
||||
conditions.append(
|
||||
"ifnull(`tab{doctype}`.`{fieldname}`, '')!=''".format(doctype=doctype, fieldname=df.fieldname)
|
||||
)
|
||||
|
||||
return "( " + " or ".join(conditions) + " )"
|
||||
|
||||
|
||||
def get_permitted_and_not_permitted_links(doctype):
|
||||
permitted_links = []
|
||||
not_permitted_links = []
|
||||
|
|
@ -129,40 +150,40 @@ def get_permitted_and_not_permitted_links(doctype):
|
|||
else:
|
||||
not_permitted_links.append(df)
|
||||
|
||||
return {
|
||||
"permitted_links": permitted_links,
|
||||
"not_permitted_links": not_permitted_links
|
||||
}
|
||||
return {"permitted_links": permitted_links, "not_permitted_links": not_permitted_links}
|
||||
|
||||
|
||||
def delete_contact_and_address(doctype, docname):
|
||||
for parenttype in ('Contact', 'Address'):
|
||||
items = frappe.db.sql_list("""select parent from `tabDynamic Link`
|
||||
for parenttype in ("Contact", "Address"):
|
||||
items = frappe.db.sql_list(
|
||||
"""select parent from `tabDynamic Link`
|
||||
where parenttype=%s and link_doctype=%s and link_name=%s""",
|
||||
(parenttype, doctype, docname))
|
||||
(parenttype, doctype, docname),
|
||||
)
|
||||
|
||||
for name in items:
|
||||
doc = frappe.get_doc(parenttype, name)
|
||||
if len(doc.links)==1:
|
||||
if len(doc.links) == 1:
|
||||
doc.delete()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, filters):
|
||||
if not txt: txt = ""
|
||||
if not txt:
|
||||
txt = ""
|
||||
|
||||
doctypes = frappe.db.get_all("DocField", filters=filters, fields=["parent"],
|
||||
distinct=True, as_list=True)
|
||||
doctypes = frappe.db.get_all(
|
||||
"DocField", filters=filters, fields=["parent"], distinct=True, as_list=True
|
||||
)
|
||||
|
||||
doctypes = tuple(d for d in doctypes if re.search(txt+".*", _(d[0]), re.IGNORECASE))
|
||||
doctypes = tuple(d for d in doctypes if re.search(txt + ".*", _(d[0]), re.IGNORECASE))
|
||||
|
||||
filters.update({
|
||||
"dt": ("not in", [d[0] for d in doctypes])
|
||||
})
|
||||
filters.update({"dt": ("not in", [d[0] for d in doctypes])})
|
||||
|
||||
_doctypes = frappe.db.get_all("Custom Field", filters=filters, fields=["dt"],
|
||||
as_list=True)
|
||||
_doctypes = frappe.db.get_all("Custom Field", filters=filters, fields=["dt"], as_list=True)
|
||||
|
||||
_doctypes = tuple([d for d in _doctypes if re.search(txt+".*", _(d[0]), re.IGNORECASE)])
|
||||
_doctypes = tuple([d for d in _doctypes if re.search(txt + ".*", _(d[0]), re.IGNORECASE)])
|
||||
|
||||
all_doctypes = [d[0] for d in doctypes + _doctypes]
|
||||
allowed_doctypes = frappe.permissions.get_doctypes_with_read()
|
||||
|
|
@ -172,6 +193,7 @@ def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, fil
|
|||
|
||||
return valid_doctypes
|
||||
|
||||
|
||||
def set_link_title(doc):
|
||||
if not doc.links:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -2,16 +2,15 @@
|
|||
# Copyright (c) 2015, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
|
||||
from frappe import throw, _
|
||||
from frappe.utils import cstr
|
||||
|
||||
from frappe.model.document import Document
|
||||
from jinja2 import TemplateSyntaxError
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links
|
||||
|
||||
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.naming import make_autoname
|
||||
from frappe.utils import cstr
|
||||
|
||||
|
||||
class Address(Document):
|
||||
|
|
@ -24,10 +23,11 @@ class Address(Document):
|
|||
self.address_title = self.links[0].link_name
|
||||
|
||||
if self.address_title:
|
||||
self.name = (cstr(self.address_title).strip() + "-" + cstr(_(self.address_type)).strip())
|
||||
self.name = cstr(self.address_title).strip() + "-" + cstr(_(self.address_type)).strip()
|
||||
if frappe.db.exists("Address", self.name):
|
||||
self.name = make_autoname(cstr(self.address_title).strip() + "-" +
|
||||
cstr(self.address_type).strip() + "-.#")
|
||||
self.name = make_autoname(
|
||||
cstr(self.address_title).strip() + "-" + cstr(self.address_type).strip() + "-.#"
|
||||
)
|
||||
else:
|
||||
throw(_("Address Title is mandatory."))
|
||||
|
||||
|
|
@ -42,15 +42,15 @@ class Address(Document):
|
|||
if not self.links:
|
||||
contact_name = frappe.db.get_value("Contact", {"email_id": self.owner})
|
||||
if contact_name:
|
||||
contact = frappe.get_cached_doc('Contact', contact_name)
|
||||
contact = frappe.get_cached_doc("Contact", contact_name)
|
||||
for link in contact.links:
|
||||
self.append('links', dict(link_doctype=link.link_doctype, link_name=link.link_name))
|
||||
self.append("links", dict(link_doctype=link.link_doctype, link_name=link.link_name))
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def validate_preferred_address(self):
|
||||
preferred_fields = ['is_primary_address', 'is_shipping_address']
|
||||
preferred_fields = ["is_primary_address", "is_shipping_address"]
|
||||
|
||||
for field in preferred_fields:
|
||||
if self.get(field):
|
||||
|
|
@ -76,9 +76,11 @@ class Address(Document):
|
|||
|
||||
return False
|
||||
|
||||
def get_preferred_address(doctype, name, preferred_key='is_primary_address'):
|
||||
if preferred_key in ['is_shipping_address', 'is_primary_address']:
|
||||
address = frappe.db.sql(""" SELECT
|
||||
|
||||
def get_preferred_address(doctype, name, preferred_key="is_primary_address"):
|
||||
if preferred_key in ["is_shipping_address", "is_primary_address"]:
|
||||
address = frappe.db.sql(
|
||||
""" SELECT
|
||||
addr.name
|
||||
FROM
|
||||
`tabAddress` addr, `tabDynamic Link` dl
|
||||
|
|
@ -86,27 +88,37 @@ def get_preferred_address(doctype, name, preferred_key='is_primary_address'):
|
|||
dl.parent = addr.name and dl.link_doctype = %s and
|
||||
dl.link_name = %s and ifnull(addr.disabled, 0) = 0 and
|
||||
%s = %s
|
||||
""" % ('%s', '%s', preferred_key, '%s'), (doctype, name, 1), as_dict=1)
|
||||
"""
|
||||
% ("%s", "%s", preferred_key, "%s"),
|
||||
(doctype, name, 1),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if address:
|
||||
return address[0].name
|
||||
|
||||
return
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_default_address(doctype, name, sort_key='is_primary_address'):
|
||||
'''Returns default Address name for the given doctype, name'''
|
||||
if sort_key not in ['is_shipping_address', 'is_primary_address']:
|
||||
def get_default_address(doctype, name, sort_key="is_primary_address"):
|
||||
"""Returns default Address name for the given doctype, name"""
|
||||
if sort_key not in ["is_shipping_address", "is_primary_address"]:
|
||||
return None
|
||||
|
||||
out = frappe.db.sql(""" SELECT
|
||||
out = frappe.db.sql(
|
||||
""" SELECT
|
||||
addr.name, addr.%s
|
||||
FROM
|
||||
`tabAddress` addr, `tabDynamic Link` dl
|
||||
WHERE
|
||||
dl.parent = addr.name and dl.link_doctype = %s and
|
||||
dl.link_name = %s and ifnull(addr.disabled, 0) = 0
|
||||
""" %(sort_key, '%s', '%s'), (doctype, name), as_dict=True)
|
||||
"""
|
||||
% (sort_key, "%s", "%s"),
|
||||
(doctype, name),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if out:
|
||||
for contact in out:
|
||||
|
|
@ -150,84 +162,96 @@ def get_territory_from_address(address):
|
|||
|
||||
return territory
|
||||
|
||||
|
||||
def get_list_context(context=None):
|
||||
return {
|
||||
"title": _("Addresses"),
|
||||
"get_list": get_address_list,
|
||||
"row_template": "templates/includes/address_row.html",
|
||||
'no_breadcrumbs': True,
|
||||
"no_breadcrumbs": True,
|
||||
}
|
||||
|
||||
def get_address_list(doctype, txt, filters, limit_start, limit_page_length = 20, order_by = None):
|
||||
|
||||
def get_address_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by=None):
|
||||
from frappe.www.list import get_list
|
||||
|
||||
user = frappe.session.user
|
||||
ignore_permissions = True
|
||||
|
||||
if not filters: filters = []
|
||||
if not filters:
|
||||
filters = []
|
||||
filters.append(("Address", "owner", "=", user))
|
||||
|
||||
return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions)
|
||||
return get_list(
|
||||
doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions
|
||||
)
|
||||
|
||||
|
||||
def has_website_permission(doc, ptype, user, verbose=False):
|
||||
"""Returns true if there is a related lead or contact related to this document"""
|
||||
contact_name = frappe.db.get_value("Contact", {"email_id": frappe.session.user})
|
||||
|
||||
if contact_name:
|
||||
contact = frappe.get_doc('Contact', contact_name)
|
||||
contact = frappe.get_doc("Contact", contact_name)
|
||||
return contact.has_common_link(doc)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_address_templates(address):
|
||||
result = frappe.db.get_value("Address Template", \
|
||||
{"country": address.get("country")}, ["name", "template"])
|
||||
result = frappe.db.get_value(
|
||||
"Address Template", {"country": address.get("country")}, ["name", "template"]
|
||||
)
|
||||
|
||||
if not result:
|
||||
result = frappe.db.get_value("Address Template", \
|
||||
{"is_default": 1}, ["name", "template"])
|
||||
result = frappe.db.get_value("Address Template", {"is_default": 1}, ["name", "template"])
|
||||
|
||||
if not result:
|
||||
frappe.throw(_("No default Address Template found. Please create a new one from Setup > Printing and Branding > Address Template."))
|
||||
frappe.throw(
|
||||
_(
|
||||
"No default Address Template found. Please create a new one from Setup > Printing and Branding > Address Template."
|
||||
)
|
||||
)
|
||||
else:
|
||||
return result
|
||||
|
||||
|
||||
def get_company_address(company):
|
||||
ret = frappe._dict()
|
||||
ret.company_address = get_default_address('Company', company)
|
||||
ret.company_address = get_default_address("Company", company)
|
||||
ret.company_address_display = get_address_display(ret.company_address)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def address_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
from frappe.desk.reportview import get_match_cond
|
||||
|
||||
link_doctype = filters.pop('link_doctype')
|
||||
link_name = filters.pop('link_name')
|
||||
link_doctype = filters.pop("link_doctype")
|
||||
link_name = filters.pop("link_name")
|
||||
|
||||
condition = ""
|
||||
meta = frappe.get_meta("Address")
|
||||
for fieldname, value in filters.items():
|
||||
if meta.get_field(fieldname) or fieldname in frappe.db.DEFAULT_COLUMNS:
|
||||
condition += " and {field}={value}".format(
|
||||
field=fieldname,
|
||||
value=frappe.db.escape(value))
|
||||
condition += " and {field}={value}".format(field=fieldname, value=frappe.db.escape(value))
|
||||
|
||||
searchfields = meta.get_search_fields()
|
||||
|
||||
if searchfield and (meta.get_field(searchfield)\
|
||||
or searchfield in frappe.db.DEFAULT_COLUMNS):
|
||||
if searchfield and (meta.get_field(searchfield) or searchfield in frappe.db.DEFAULT_COLUMNS):
|
||||
searchfields.append(searchfield)
|
||||
|
||||
search_condition = ''
|
||||
search_condition = ""
|
||||
for field in searchfields:
|
||||
if search_condition == '':
|
||||
search_condition += '`tabAddress`.`{field}` like %(txt)s'.format(field=field)
|
||||
if search_condition == "":
|
||||
search_condition += "`tabAddress`.`{field}` like %(txt)s".format(field=field)
|
||||
else:
|
||||
search_condition += ' or `tabAddress`.`{field}` like %(txt)s'.format(field=field)
|
||||
search_condition += " or `tabAddress`.`{field}` like %(txt)s".format(field=field)
|
||||
|
||||
return frappe.db.sql("""select
|
||||
return frappe.db.sql(
|
||||
"""select
|
||||
`tabAddress`.name, `tabAddress`.city, `tabAddress`.country
|
||||
from
|
||||
`tabAddress`, `tabDynamic Link`
|
||||
|
|
@ -245,19 +269,24 @@ def address_query(doctype, txt, searchfield, start, page_len, filters):
|
|||
limit %(start)s, %(page_len)s """.format(
|
||||
mcond=get_match_cond(doctype),
|
||||
key=searchfield,
|
||||
search_condition = search_condition,
|
||||
condition=condition or ""), {
|
||||
'txt': '%' + txt + '%',
|
||||
'_txt': txt.replace("%", ""),
|
||||
'start': start,
|
||||
'page_len': page_len,
|
||||
'link_name': link_name,
|
||||
'link_doctype': link_doctype
|
||||
})
|
||||
search_condition=search_condition,
|
||||
condition=condition or "",
|
||||
),
|
||||
{
|
||||
"txt": "%" + txt + "%",
|
||||
"_txt": txt.replace("%", ""),
|
||||
"start": start,
|
||||
"page_len": page_len,
|
||||
"link_name": link_name,
|
||||
"link_doctype": link_doctype,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def get_condensed_address(doc):
|
||||
fields = ["address_title", "address_line1", "address_line2", "city", "county", "state", "country"]
|
||||
return ", ".join(doc.get(d) for d in fields if doc.get(d))
|
||||
|
||||
|
||||
def update_preferred_address(address, field):
|
||||
frappe.db.set_value('Address', address, field, 0)
|
||||
frappe.db.set_value("Address", address, field, 0)
|
||||
|
|
|
|||
|
|
@ -1,31 +1,32 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import frappe, unittest
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
|
||||
|
||||
class TestAddress(unittest.TestCase):
|
||||
def test_template_works(self):
|
||||
if not frappe.db.exists('Address Template', 'India'):
|
||||
frappe.get_doc({
|
||||
"doctype": "Address Template",
|
||||
"country": 'India',
|
||||
"is_default": 1
|
||||
}).insert()
|
||||
if not frappe.db.exists("Address Template", "India"):
|
||||
frappe.get_doc({"doctype": "Address Template", "country": "India", "is_default": 1}).insert()
|
||||
|
||||
if not frappe.db.exists('Address', '_Test Address-Office'):
|
||||
frappe.get_doc({
|
||||
"address_line1": "_Test Address Line 1",
|
||||
"address_title": "_Test Address",
|
||||
"address_type": "Office",
|
||||
"city": "_Test City",
|
||||
"state": "Test State",
|
||||
"country": "India",
|
||||
"doctype": "Address",
|
||||
"is_primary_address": 1,
|
||||
"phone": "+91 0000000000"
|
||||
}).insert()
|
||||
if not frappe.db.exists("Address", "_Test Address-Office"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"address_line1": "_Test Address Line 1",
|
||||
"address_title": "_Test Address",
|
||||
"address_type": "Office",
|
||||
"city": "_Test City",
|
||||
"state": "Test State",
|
||||
"country": "India",
|
||||
"doctype": "Address",
|
||||
"is_primary_address": 1,
|
||||
"phone": "+91 0000000000",
|
||||
}
|
||||
).insert()
|
||||
|
||||
address = frappe.get_list("Address")[0].name
|
||||
display = get_address_display(frappe.get_doc("Address", address).as_dict())
|
||||
self.assertTrue(display)
|
||||
self.assertTrue(display)
|
||||
|
|
|
|||
|
|
@ -3,21 +3,24 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
from frappe.utils.jinja import validate_template
|
||||
from frappe import _
|
||||
|
||||
|
||||
class AddressTemplate(Document):
|
||||
def validate(self):
|
||||
if not self.template:
|
||||
self.template = get_default_address_template()
|
||||
|
||||
self.defaults = frappe.db.get_values("Address Template", {"is_default":1, "name":("!=", self.name)})
|
||||
self.defaults = frappe.db.get_values(
|
||||
"Address Template", {"is_default": 1, "name": ("!=", self.name)}
|
||||
)
|
||||
if not self.is_default:
|
||||
if not self.defaults:
|
||||
self.is_default = 1
|
||||
if cint(frappe.db.get_single_value('System Settings', 'setup_complete')):
|
||||
if cint(frappe.db.get_single_value("System Settings", "setup_complete")):
|
||||
frappe.msgprint(_("Setting this Address Template as default as there is no other default"))
|
||||
|
||||
validate_template(self.template)
|
||||
|
|
@ -31,14 +34,23 @@ class AddressTemplate(Document):
|
|||
if self.is_default:
|
||||
frappe.throw(_("Default Address Template cannot be deleted"))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_default_address_template():
|
||||
'''Get default address template (translated)'''
|
||||
return '''{{ address_line1 }}<br>{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}\
|
||||
"""Get default address template (translated)"""
|
||||
return (
|
||||
"""{{ address_line1 }}<br>{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}\
|
||||
{{ city }}<br>
|
||||
{% if state %}{{ state }}<br>{% endif -%}
|
||||
{% if pincode %}{{ pincode }}<br>{% endif -%}
|
||||
{{ country }}<br>
|
||||
{% if phone %}'''+_('Phone')+''': {{ phone }}<br>{% endif -%}
|
||||
{% if fax %}'''+_('Fax')+''': {{ fax }}<br>{% endif -%}
|
||||
{% if email_id %}'''+_('Email')+''': {{ email_id }}<br>{% endif -%}'''
|
||||
{% if phone %}"""
|
||||
+ _("Phone")
|
||||
+ """: {{ phone }}<br>{% endif -%}
|
||||
{% if fax %}"""
|
||||
+ _("Fax")
|
||||
+ """: {{ fax }}<br>{% endif -%}
|
||||
{% if email_id %}"""
|
||||
+ _("Email")
|
||||
+ """: {{ email_id }}<br>{% endif -%}"""
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import frappe, unittest
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
class TestAddressTemplate(unittest.TestCase):
|
||||
def setUp(self):
|
||||
|
|
@ -27,17 +30,12 @@ class TestAddressTemplate(unittest.TestCase):
|
|||
def make_default_address_template(self):
|
||||
template = """{{ address_line1 }}<br>{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}{{ city }}<br>{% if state %}{{ state }}<br>{% endif -%}{% if pincode %}{{ pincode }}<br>{% endif -%}{{ country }}<br>{% if phone %}Phone: {{ phone }}<br>{% endif -%}{% if fax %}Fax: {{ fax }}<br>{% endif -%}{% if email_id %}Email: {{ email_id }}<br>{% endif -%}"""
|
||||
|
||||
if not frappe.db.exists('Address Template', 'India'):
|
||||
frappe.get_doc({
|
||||
"doctype": "Address Template",
|
||||
"country": 'India',
|
||||
"is_default": 1,
|
||||
"template": template
|
||||
}).insert()
|
||||
if not frappe.db.exists("Address Template", "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", "Brazil"):
|
||||
frappe.get_doc(
|
||||
{"doctype": "Address Template", "country": "Brazil", "template": template}
|
||||
).insert()
|
||||
|
|
|
|||
|
|
@ -1,26 +1,27 @@
|
|||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import frappe
|
||||
from frappe.utils import cstr, has_gravatar
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links
|
||||
from frappe.model.naming import append_number_if_name_exists
|
||||
from frappe.contacts.address_and_contact import set_link_title
|
||||
from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.naming import append_number_if_name_exists
|
||||
from frappe.utils import cstr, has_gravatar
|
||||
|
||||
|
||||
class Contact(Document):
|
||||
def autoname(self):
|
||||
# concat first and last name
|
||||
self.name = " ".join(filter(None,
|
||||
[cstr(self.get(f)).strip() for f in ["first_name", "last_name"]]))
|
||||
self.name = " ".join(
|
||||
filter(None, [cstr(self.get(f)).strip() for f in ["first_name", "last_name"]])
|
||||
)
|
||||
|
||||
if frappe.db.exists("Contact", self.name):
|
||||
self.name = append_number_if_name_exists('Contact', self.name)
|
||||
self.name = append_number_if_name_exists("Contact", self.name)
|
||||
|
||||
# concat party name if reqd
|
||||
for link in self.links:
|
||||
self.name = self.name + '-' + link.link_name.strip()
|
||||
self.name = self.name + "-" + link.link_name.strip()
|
||||
break
|
||||
|
||||
def validate(self):
|
||||
|
|
@ -45,7 +46,7 @@ class Contact(Document):
|
|||
self.user = frappe.db.get_value("User", {"email": self.email_id})
|
||||
|
||||
def get_link_for(self, link_doctype):
|
||||
'''Return the link name, if exists for the given link DocType'''
|
||||
"""Return the link name, if exists for the given link DocType"""
|
||||
for link in self.links:
|
||||
if link.link_doctype == link_doctype:
|
||||
return link.link_name
|
||||
|
|
@ -65,21 +66,21 @@ class Contact(Document):
|
|||
|
||||
def add_email(self, email_id, is_primary=0, autosave=False):
|
||||
if not frappe.db.exists("Contact Email", {"email_id": email_id, "parent": self.name}):
|
||||
self.append("email_ids", {
|
||||
"email_id": email_id,
|
||||
"is_primary": is_primary
|
||||
})
|
||||
self.append("email_ids", {"email_id": email_id, "is_primary": is_primary})
|
||||
|
||||
if autosave:
|
||||
self.save(ignore_permissions=True)
|
||||
|
||||
def add_phone(self, phone, is_primary_phone=0, is_primary_mobile_no=0, autosave=False):
|
||||
if not frappe.db.exists("Contact Phone", {"phone": phone, "parent": self.name}):
|
||||
self.append("phone_nos", {
|
||||
"phone": phone,
|
||||
"is_primary_phone": is_primary_phone,
|
||||
"is_primary_mobile_no": is_primary_mobile_no
|
||||
})
|
||||
self.append(
|
||||
"phone_nos",
|
||||
{
|
||||
"phone": phone,
|
||||
"is_primary_phone": is_primary_phone,
|
||||
"is_primary_mobile_no": is_primary_mobile_no,
|
||||
},
|
||||
)
|
||||
|
||||
if autosave:
|
||||
self.save(ignore_permissions=True)
|
||||
|
|
@ -113,7 +114,9 @@ class Contact(Document):
|
|||
is_primary = [phone.phone for phone in self.phone_nos if phone.get(field_name)]
|
||||
|
||||
if len(is_primary) > 1:
|
||||
frappe.throw(_("Only one {0} can be set as primary.").format(frappe.bold(frappe.unscrub(fieldname))))
|
||||
frappe.throw(
|
||||
_("Only one {0} can be set as primary.").format(frappe.bold(frappe.unscrub(fieldname)))
|
||||
)
|
||||
|
||||
primary_number_exists = False
|
||||
for d in self.phone_nos:
|
||||
|
|
@ -125,9 +128,11 @@ class Contact(Document):
|
|||
if not primary_number_exists:
|
||||
setattr(self, fieldname, "")
|
||||
|
||||
|
||||
def get_default_contact(doctype, name):
|
||||
'''Returns default contact for the given doctype, name'''
|
||||
out = frappe.db.sql('''select parent,
|
||||
"""Returns default contact for the given doctype, name"""
|
||||
out = frappe.db.sql(
|
||||
'''select parent,
|
||||
IFNULL((select is_primary_contact from tabContact c where c.name = dl.parent), 0)
|
||||
as is_primary_contact
|
||||
from
|
||||
|
|
@ -135,7 +140,10 @@ def get_default_contact(doctype, name):
|
|||
where
|
||||
dl.link_doctype=%s and
|
||||
dl.link_name=%s and
|
||||
dl.parenttype = "Contact"''', (doctype, name), as_dict=True)
|
||||
dl.parenttype = "Contact"''',
|
||||
(doctype, name),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if out:
|
||||
for contact in out:
|
||||
|
|
@ -145,6 +153,7 @@ def get_default_contact(doctype, name):
|
|||
else:
|
||||
return None
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def invite_user(contact):
|
||||
contact = frappe.get_doc("Contact", contact)
|
||||
|
|
@ -153,34 +162,39 @@ def invite_user(contact):
|
|||
frappe.throw(_("Please set Email Address"))
|
||||
|
||||
if contact.has_permission("write"):
|
||||
user = frappe.get_doc({
|
||||
"doctype": "User",
|
||||
"first_name": contact.first_name,
|
||||
"last_name": contact.last_name,
|
||||
"email": contact.email_id,
|
||||
"user_type": "Website User",
|
||||
"send_welcome_email": 1
|
||||
}).insert(ignore_permissions = True)
|
||||
user = frappe.get_doc(
|
||||
{
|
||||
"doctype": "User",
|
||||
"first_name": contact.first_name,
|
||||
"last_name": contact.last_name,
|
||||
"email": contact.email_id,
|
||||
"user_type": "Website User",
|
||||
"send_welcome_email": 1,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
return user.name
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_contact_details(contact):
|
||||
contact = frappe.get_doc("Contact", contact)
|
||||
out = {
|
||||
"contact_person": contact.get("name"),
|
||||
"contact_display": " ".join(filter(None,
|
||||
[contact.get("salutation"), contact.get("first_name"), contact.get("last_name")])),
|
||||
"contact_display": " ".join(
|
||||
filter(None, [contact.get("salutation"), contact.get("first_name"), contact.get("last_name")])
|
||||
),
|
||||
"contact_email": contact.get("email_id"),
|
||||
"contact_mobile": contact.get("mobile_no"),
|
||||
"contact_phone": contact.get("phone"),
|
||||
"contact_designation": contact.get("designation"),
|
||||
"contact_department": contact.get("department")
|
||||
"contact_department": contact.get("department"),
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def update_contact(doc, method):
|
||||
'''Update contact when user is updated, if contact is found. Called via hooks'''
|
||||
"""Update contact when user is updated, if contact is found. Called via hooks"""
|
||||
contact_name = frappe.db.get_value("Contact", {"email_id": doc.name})
|
||||
if contact_name:
|
||||
contact = frappe.get_doc("Contact", contact_name)
|
||||
|
|
@ -190,19 +204,23 @@ def update_contact(doc, method):
|
|||
contact.flags.ignore_mandatory = True
|
||||
contact.save(ignore_permissions=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def contact_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
from frappe.desk.reportview import get_match_cond
|
||||
|
||||
if not frappe.get_meta("Contact").get_field(searchfield)\
|
||||
and searchfield not in frappe.db.DEFAULT_COLUMNS:
|
||||
if (
|
||||
not frappe.get_meta("Contact").get_field(searchfield)
|
||||
and searchfield not in frappe.db.DEFAULT_COLUMNS
|
||||
):
|
||||
return []
|
||||
|
||||
link_doctype = filters.pop('link_doctype')
|
||||
link_name = filters.pop('link_name')
|
||||
link_doctype = filters.pop("link_doctype")
|
||||
link_name = filters.pop("link_name")
|
||||
|
||||
return frappe.db.sql("""select
|
||||
return frappe.db.sql(
|
||||
"""select
|
||||
`tabContact`.name, `tabContact`.first_name, `tabContact`.last_name
|
||||
from
|
||||
`tabContact`, `tabDynamic Link`
|
||||
|
|
@ -216,68 +234,90 @@ def contact_query(doctype, txt, searchfield, start, page_len, filters):
|
|||
order by
|
||||
if(locate(%(_txt)s, `tabContact`.name), locate(%(_txt)s, `tabContact`.name), 99999),
|
||||
`tabContact`.idx desc, `tabContact`.name
|
||||
limit %(start)s, %(page_len)s """.format(mcond=get_match_cond(doctype), key=searchfield), {
|
||||
'txt': '%' + txt + '%',
|
||||
'_txt': txt.replace("%", ""),
|
||||
'start': start,
|
||||
'page_len': page_len,
|
||||
'link_name': link_name,
|
||||
'link_doctype': link_doctype
|
||||
})
|
||||
limit %(start)s, %(page_len)s """.format(
|
||||
mcond=get_match_cond(doctype), key=searchfield
|
||||
),
|
||||
{
|
||||
"txt": "%" + txt + "%",
|
||||
"_txt": txt.replace("%", ""),
|
||||
"start": start,
|
||||
"page_len": page_len,
|
||||
"link_name": link_name,
|
||||
"link_doctype": link_doctype,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def address_query(links):
|
||||
import json
|
||||
|
||||
links = [{"link_doctype": d.get("link_doctype"), "link_name": d.get("link_name")} for d in json.loads(links)]
|
||||
links = [
|
||||
{"link_doctype": d.get("link_doctype"), "link_name": d.get("link_name")}
|
||||
for d in json.loads(links)
|
||||
]
|
||||
result = []
|
||||
|
||||
for link in links:
|
||||
if not frappe.has_permission(doctype=link.get("link_doctype"), ptype="read", doc=link.get("link_name")):
|
||||
if not frappe.has_permission(
|
||||
doctype=link.get("link_doctype"), ptype="read", doc=link.get("link_name")
|
||||
):
|
||||
continue
|
||||
|
||||
res = frappe.db.sql("""
|
||||
res = frappe.db.sql(
|
||||
"""
|
||||
SELECT `tabAddress`.name
|
||||
FROM `tabAddress`, `tabDynamic Link`
|
||||
WHERE `tabDynamic Link`.parenttype='Address'
|
||||
AND `tabDynamic Link`.parent=`tabAddress`.name
|
||||
AND `tabDynamic Link`.link_doctype = %(link_doctype)s
|
||||
AND `tabDynamic Link`.link_name = %(link_name)s
|
||||
""", {
|
||||
"link_doctype": link.get("link_doctype"),
|
||||
"link_name": link.get("link_name"),
|
||||
}, as_dict=True)
|
||||
""",
|
||||
{
|
||||
"link_doctype": link.get("link_doctype"),
|
||||
"link_name": link.get("link_name"),
|
||||
},
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
result.extend([l.name for l in res])
|
||||
|
||||
return result
|
||||
|
||||
def get_contact_with_phone_number(number):
|
||||
if not number: return
|
||||
|
||||
contacts = frappe.get_all('Contact Phone', filters=[
|
||||
['phone', 'like', '%{0}'.format(number)]
|
||||
], fields=["parent"], limit=1)
|
||||
def get_contact_with_phone_number(number):
|
||||
if not number:
|
||||
return
|
||||
|
||||
contacts = frappe.get_all(
|
||||
"Contact Phone", filters=[["phone", "like", "%{0}".format(number)]], fields=["parent"], limit=1
|
||||
)
|
||||
|
||||
return contacts[0].parent if contacts else None
|
||||
|
||||
|
||||
def get_contact_name(email_id):
|
||||
contact = frappe.get_all("Contact Email", filters={"email_id": email_id}, fields=["parent"], limit=1)
|
||||
contact = frappe.get_all(
|
||||
"Contact Email", filters={"email_id": email_id}, fields=["parent"], limit=1
|
||||
)
|
||||
return contact[0].parent if contact else None
|
||||
|
||||
|
||||
def get_contacts_linking_to(doctype, docname, fields=None):
|
||||
"""Return a list of contacts containing a link to the given document."""
|
||||
return frappe.get_list('Contact', fields=fields, filters=[
|
||||
['Dynamic Link', 'link_doctype', '=', doctype],
|
||||
['Dynamic Link', 'link_name', '=', docname]
|
||||
])
|
||||
return frappe.get_list(
|
||||
"Contact",
|
||||
fields=fields,
|
||||
filters=[
|
||||
["Dynamic Link", "link_doctype", "=", doctype],
|
||||
["Dynamic Link", "link_name", "=", docname],
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def get_contacts_linked_from(doctype, docname, fields=None):
|
||||
"""Return a list of contacts that are contained in (linked from) the given document."""
|
||||
link_fields = frappe.get_meta(doctype).get('fields', {
|
||||
'fieldtype': 'Link',
|
||||
'options': 'Contact'
|
||||
})
|
||||
link_fields = frappe.get_meta(doctype).get("fields", {"fieldtype": "Link", "options": "Contact"})
|
||||
if not link_fields:
|
||||
return []
|
||||
|
||||
|
|
@ -285,6 +325,4 @@ def get_contacts_linked_from(doctype, docname, fields=None):
|
|||
if not contact_names:
|
||||
return []
|
||||
|
||||
return frappe.get_list('Contact', fields=fields, filters={
|
||||
'name': ('in', contact_names)
|
||||
})
|
||||
return frappe.get_list("Contact", fields=fields, filters={"name": ("in", contact_names)})
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import frappe
|
||||
import unittest
|
||||
|
||||
test_dependencies = ['Contact', 'Salutation']
|
||||
import frappe
|
||||
|
||||
test_dependencies = ["Contact", "Salutation"]
|
||||
|
||||
|
||||
class TestContact(unittest.TestCase):
|
||||
|
||||
def test_check_default_email(self):
|
||||
emails = [
|
||||
{"email": "test1@example.com", "is_primary": 0},
|
||||
|
|
@ -32,13 +33,11 @@ class TestContact(unittest.TestCase):
|
|||
self.assertEqual(contact.phone, "+91 0000000002")
|
||||
self.assertEqual(contact.mobile_no, "+91 0000000003")
|
||||
|
||||
|
||||
def create_contact(name, salutation, emails=None, phones=None, save=True):
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Contact",
|
||||
"first_name": name,
|
||||
"status": "Open",
|
||||
"salutation": salutation
|
||||
})
|
||||
doc = frappe.get_doc(
|
||||
{"doctype": "Contact", "first_name": name, "status": "Open", "salutation": salutation}
|
||||
)
|
||||
|
||||
if emails:
|
||||
for d in emails:
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@
|
|||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ContactEmail(Document):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@
|
|||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ContactPhone(Document):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -4,5 +4,6 @@
|
|||
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class Gender(Document):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@
|
|||
# License: MIT. See LICENSE
|
||||
import unittest
|
||||
|
||||
|
||||
class TestGender(unittest.TestCase):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -4,5 +4,6 @@
|
|||
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class Salutation(Document):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@
|
|||
# License: MIT. See LICENSE
|
||||
import unittest
|
||||
|
||||
|
||||
class TestSalutation(unittest.TestCase):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -4,17 +4,37 @@ import frappe
|
|||
from frappe import _
|
||||
|
||||
field_map = {
|
||||
"Contact": ["first_name", "last_name", "address", "phone", "mobile_no", "email_id", "is_primary_contact"],
|
||||
"Address": ["address_line1", "address_line2", "city", "state", "pincode", "country", "is_primary_address"]
|
||||
"Contact": [
|
||||
"first_name",
|
||||
"last_name",
|
||||
"address",
|
||||
"phone",
|
||||
"mobile_no",
|
||||
"email_id",
|
||||
"is_primary_contact",
|
||||
],
|
||||
"Address": [
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"city",
|
||||
"state",
|
||||
"pincode",
|
||||
"country",
|
||||
"is_primary_address",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
columns, data = get_columns(filters), get_data(filters)
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
return [
|
||||
"{reference_doctype}:Link/{reference_doctype}".format(reference_doctype=filters.get("reference_doctype")),
|
||||
"{reference_doctype}:Link/{reference_doctype}".format(
|
||||
reference_doctype=filters.get("reference_doctype")
|
||||
),
|
||||
"Address Line 1",
|
||||
"Address Line 2",
|
||||
"City",
|
||||
|
|
@ -27,9 +47,10 @@ def get_columns(filters):
|
|||
"Address",
|
||||
"Phone",
|
||||
"Email Id",
|
||||
"Is Primary Contact:Check"
|
||||
"Is Primary Contact:Check",
|
||||
]
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
data = []
|
||||
reference_doctype = filters.get("reference_doctype")
|
||||
|
|
@ -37,6 +58,7 @@ def get_data(filters):
|
|||
|
||||
return get_reference_addresses_and_contact(reference_doctype, reference_name)
|
||||
|
||||
|
||||
def get_reference_addresses_and_contact(reference_doctype, reference_name):
|
||||
data = []
|
||||
filters = None
|
||||
|
|
@ -48,16 +70,22 @@ def get_reference_addresses_and_contact(reference_doctype, reference_name):
|
|||
if reference_name:
|
||||
filters = {"name": reference_name}
|
||||
|
||||
reference_list = [d[0] for d in frappe.get_list(reference_doctype, filters=filters, fields=["name"], as_list=True)]
|
||||
reference_list = [
|
||||
d[0] for d in frappe.get_list(reference_doctype, filters=filters, fields=["name"], as_list=True)
|
||||
]
|
||||
|
||||
for d in reference_list:
|
||||
reference_details.setdefault(d, frappe._dict())
|
||||
reference_details = get_reference_details(reference_doctype, "Address", reference_list, reference_details)
|
||||
reference_details = get_reference_details(reference_doctype, "Contact", reference_list, reference_details)
|
||||
reference_details = get_reference_details(
|
||||
reference_doctype, "Address", reference_list, reference_details
|
||||
)
|
||||
reference_details = get_reference_details(
|
||||
reference_doctype, "Contact", reference_list, reference_details
|
||||
)
|
||||
|
||||
for reference_name, details in reference_details.items():
|
||||
addresses = details.get("address", [])
|
||||
contacts = details.get("contact", [])
|
||||
contacts = details.get("contact", [])
|
||||
if not any([addresses, contacts]):
|
||||
result = [reference_name]
|
||||
result.extend(add_blank_columns_for("Address"))
|
||||
|
|
@ -78,10 +106,11 @@ def get_reference_addresses_and_contact(reference_doctype, reference_name):
|
|||
|
||||
return data
|
||||
|
||||
|
||||
def get_reference_details(reference_doctype, doctype, reference_list, reference_details):
|
||||
filters = [
|
||||
filters = [
|
||||
["Dynamic Link", "link_doctype", "=", reference_doctype],
|
||||
["Dynamic Link", "link_name", "in", reference_list]
|
||||
["Dynamic Link", "link_name", "in", reference_list],
|
||||
]
|
||||
fields = ["`tabDynamic Link`.link_name"] + field_map.get(doctype, [])
|
||||
|
||||
|
|
@ -97,5 +126,6 @@ def get_reference_details(reference_doctype, doctype, reference_list, reference_
|
|||
reference_details[reference_list[0]][frappe.scrub(doctype)] = temp_records
|
||||
return reference_details
|
||||
|
||||
|
||||
def add_blank_columns_for(doctype):
|
||||
return ["" for field in field_map.get(doctype, [])]
|
||||
|
|
|
|||
|
|
@ -1,95 +1,87 @@
|
|||
import unittest
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
import unittest
|
||||
|
||||
from frappe.contacts.report.addresses_and_contacts.addresses_and_contacts import get_data
|
||||
|
||||
|
||||
def get_custom_linked_doctype():
|
||||
if bool(frappe.get_all("DocType", filters={'name':'Test Custom Doctype'})):
|
||||
if bool(frappe.get_all("DocType", filters={"name": "Test Custom Doctype"})):
|
||||
return
|
||||
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "DocType",
|
||||
"module": "Core",
|
||||
"custom": 1,
|
||||
"fields": [{
|
||||
"label": "Test Field",
|
||||
"fieldname": "test_field",
|
||||
"fieldtype": "Data"
|
||||
},
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"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",
|
||||
})
|
||||
"doctype": "DocType",
|
||||
"module": "Core",
|
||||
"custom": 1,
|
||||
"fields": [
|
||||
{"label": "Test Field", "fieldname": "test_field", "fieldtype": "Data"},
|
||||
{"label": "Contact HTML", "fieldname": "contact_html", "fieldtype": "HTML"},
|
||||
{"label": "Address HTML", "fieldname": "address_html", "fieldtype": "HTML"},
|
||||
],
|
||||
"permissions": [{"role": "System Manager", "read": 1}],
|
||||
"name": "Test Custom Doctype",
|
||||
}
|
||||
)
|
||||
doc.insert()
|
||||
|
||||
|
||||
def get_custom_doc_for_address_and_contacts():
|
||||
get_custom_linked_doctype()
|
||||
linked_doc = frappe.get_doc({
|
||||
"doctype": "Test Custom Doctype",
|
||||
"test_field": "Hello",
|
||||
}).insert()
|
||||
linked_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Test Custom Doctype",
|
||||
"test_field": "Hello",
|
||||
}
|
||||
).insert()
|
||||
return linked_doc
|
||||
|
||||
|
||||
def create_linked_address(link_list):
|
||||
if frappe.flags.test_address_created:
|
||||
return
|
||||
|
||||
address = frappe.get_doc({
|
||||
"doctype": "Address",
|
||||
"address_title": "_Test Address",
|
||||
"address_type": "Billing",
|
||||
"address_line1": "test address line 1",
|
||||
"address_line2": "test address line 2",
|
||||
"city": "Milan",
|
||||
"country": "Italy"
|
||||
})
|
||||
address = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Address",
|
||||
"address_title": "_Test Address",
|
||||
"address_type": "Billing",
|
||||
"address_line1": "test address line 1",
|
||||
"address_line2": "test address line 2",
|
||||
"city": "Milan",
|
||||
"country": "Italy",
|
||||
}
|
||||
)
|
||||
|
||||
for name in link_list:
|
||||
address.append("links",{
|
||||
'link_doctype': 'Test Custom Doctype',
|
||||
'link_name': name
|
||||
})
|
||||
address.append("links", {"link_doctype": "Test Custom Doctype", "link_name": name})
|
||||
|
||||
address.insert()
|
||||
frappe.flags.test_address_created = True
|
||||
|
||||
return address.name
|
||||
|
||||
|
||||
def create_linked_contact(link_list, address):
|
||||
if frappe.flags.test_contact_created:
|
||||
return
|
||||
|
||||
contact = frappe.get_doc({
|
||||
"doctype": "Contact",
|
||||
"salutation": "Mr",
|
||||
"first_name": "_Test First Name",
|
||||
"last_name": "_Test Last Name",
|
||||
"is_primary_contact": 1,
|
||||
"address": address,
|
||||
"status": "Open"
|
||||
})
|
||||
contact = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Contact",
|
||||
"salutation": "Mr",
|
||||
"first_name": "_Test First Name",
|
||||
"last_name": "_Test Last Name",
|
||||
"is_primary_contact": 1,
|
||||
"address": address,
|
||||
"status": "Open",
|
||||
}
|
||||
)
|
||||
contact.add_email("test_contact@example.com", is_primary=True)
|
||||
contact.add_phone("+91 0000000000", is_primary_phone=True)
|
||||
|
||||
for name in link_list:
|
||||
contact.append("links",{
|
||||
'link_doctype': 'Test Custom Doctype',
|
||||
'link_name': name
|
||||
})
|
||||
contact.append("links", {"link_doctype": "Test Custom Doctype", "link_name": name})
|
||||
|
||||
contact.insert(ignore_permissions=True)
|
||||
frappe.flags.test_contact_created = True
|
||||
|
|
@ -103,7 +95,23 @@ class TestAddressesAndContacts(unittest.TestCase):
|
|||
create_linked_contact(links_list, d)
|
||||
report_data = get_data({"reference_doctype": "Test Custom Doctype"})
|
||||
for idx, link in enumerate(links_list):
|
||||
test_item = [link, 'test address line 1', 'test address line 2', 'Milan', None, None, 'Italy', 0, '_Test First Name', '_Test Last Name', '_Test Address-Billing', '+91 0000000000', '', 'test_contact@example.com', 1]
|
||||
test_item = [
|
||||
link,
|
||||
"test address line 1",
|
||||
"test address line 2",
|
||||
"Milan",
|
||||
None,
|
||||
None,
|
||||
"Italy",
|
||||
0,
|
||||
"_Test First Name",
|
||||
"_Test Last Name",
|
||||
"_Test Address-Billing",
|
||||
"+91 0000000000",
|
||||
"",
|
||||
"test_contact@example.com",
|
||||
1,
|
||||
]
|
||||
self.assertListEqual(test_item, report_data[idx])
|
||||
|
||||
def tearDown(self):
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
# Copyright (c) 2021, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
import frappe
|
||||
from frappe.utils import cstr
|
||||
from tenacity import retry, retry_if_exception_type, stop_after_attempt
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cstr
|
||||
|
||||
|
||||
class AccessLog(Document):
|
||||
|
|
@ -22,14 +23,19 @@ def make_access_log(
|
|||
columns=None,
|
||||
):
|
||||
_make_access_log(
|
||||
doctype, document, method, file_type, report_name, filters, page, columns,
|
||||
doctype,
|
||||
document,
|
||||
method,
|
||||
file_type,
|
||||
report_name,
|
||||
filters,
|
||||
page,
|
||||
columns,
|
||||
)
|
||||
|
||||
|
||||
@frappe.write_only()
|
||||
@retry(
|
||||
stop=stop_after_attempt(3), retry=retry_if_exception_type(frappe.DuplicateEntryError)
|
||||
)
|
||||
@retry(stop=stop_after_attempt(3), retry=retry_if_exception_type(frappe.DuplicateEntryError))
|
||||
def _make_access_log(
|
||||
doctype=None,
|
||||
document=None,
|
||||
|
|
@ -43,18 +49,20 @@ def _make_access_log(
|
|||
user = frappe.session.user
|
||||
in_request = frappe.request and frappe.request.method == "GET"
|
||||
|
||||
frappe.get_doc({
|
||||
"doctype": "Access Log",
|
||||
"user": user,
|
||||
"export_from": doctype,
|
||||
"reference_document": document,
|
||||
"file_type": file_type,
|
||||
"report_name": report_name,
|
||||
"page": page,
|
||||
"method": method,
|
||||
"filters": cstr(filters) or None,
|
||||
"columns": columns,
|
||||
}).db_insert()
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Access Log",
|
||||
"user": user,
|
||||
"export_from": doctype,
|
||||
"reference_document": document,
|
||||
"file_type": file_type,
|
||||
"report_name": report_name,
|
||||
"page": page,
|
||||
"method": method,
|
||||
"filters": cstr(filters) or None,
|
||||
"columns": columns,
|
||||
}
|
||||
).db_insert()
|
||||
|
||||
# `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview`
|
||||
# dont commit in test mode. It must be tempting to put this block along with the in_request in the
|
||||
|
|
|
|||
|
|
@ -2,20 +2,21 @@
|
|||
# Copyright (c) 2019, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
# imports - standard imports
|
||||
import unittest
|
||||
import base64
|
||||
import os
|
||||
|
||||
# imports - standard imports
|
||||
import unittest
|
||||
|
||||
# imports - third party imports
|
||||
import requests
|
||||
|
||||
# imports - module imports
|
||||
import frappe
|
||||
from frappe.core.doctype.access_log.access_log import make_access_log
|
||||
from frappe.utils import cstr, get_site_url
|
||||
from frappe.core.doctype.data_import.data_import import export_csv
|
||||
from frappe.core.doctype.user.user import generate_keys
|
||||
|
||||
# imports - third party imports
|
||||
import requests
|
||||
from frappe.utils import cstr, get_site_url
|
||||
|
||||
|
||||
class TestAccessLog(unittest.TestCase):
|
||||
|
|
@ -23,8 +24,9 @@ class TestAccessLog(unittest.TestCase):
|
|||
# generate keys for current user to send requests for the following tests
|
||||
generate_keys(frappe.session.user)
|
||||
frappe.db.commit()
|
||||
generated_secret = frappe.utils.password.get_decrypted_password("User",
|
||||
frappe.session.user, fieldname='api_secret')
|
||||
generated_secret = frappe.utils.password.get_decrypted_password(
|
||||
"User", frappe.session.user, fieldname="api_secret"
|
||||
)
|
||||
api_key = frappe.db.get_value("User", "Administrator", "api_key")
|
||||
self.header = {"Authorization": "token {}:{}".format(api_key, generated_secret)}
|
||||
|
||||
|
|
@ -101,54 +103,55 @@ class TestAccessLog(unittest.TestCase):
|
|||
"party": [],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"cost_center": [],
|
||||
"project": []
|
||||
"project": [],
|
||||
}
|
||||
|
||||
self.test_doctype = 'File'
|
||||
self.test_document = 'Test Document'
|
||||
self.test_report_name = 'General Ledger'
|
||||
self.test_file_type = 'CSV'
|
||||
self.test_method = 'Test Method'
|
||||
self.file_name = frappe.utils.random_string(10) + '.txt'
|
||||
self.test_doctype = "File"
|
||||
self.test_document = "Test Document"
|
||||
self.test_report_name = "General Ledger"
|
||||
self.test_file_type = "CSV"
|
||||
self.test_method = "Test Method"
|
||||
self.file_name = frappe.utils.random_string(10) + ".txt"
|
||||
self.test_content = frappe.utils.random_string(1024)
|
||||
|
||||
|
||||
def test_make_full_access_log(self):
|
||||
self.maxDiff = None
|
||||
|
||||
# test if all fields maintain data: html page and filters are converted?
|
||||
make_access_log(doctype=self.test_doctype,
|
||||
make_access_log(
|
||||
doctype=self.test_doctype,
|
||||
document=self.test_document,
|
||||
report_name=self.test_report_name,
|
||||
page=self.test_html_template,
|
||||
file_type=self.test_file_type,
|
||||
method=self.test_method,
|
||||
filters=self.test_filters)
|
||||
filters=self.test_filters,
|
||||
)
|
||||
|
||||
last_doc = frappe.get_last_doc('Access Log')
|
||||
last_doc = frappe.get_last_doc("Access Log")
|
||||
self.assertEqual(last_doc.filters, cstr(self.test_filters))
|
||||
self.assertEqual(self.test_doctype, last_doc.export_from)
|
||||
self.assertEqual(self.test_document, last_doc.reference_document)
|
||||
|
||||
|
||||
def test_make_export_log(self):
|
||||
# export data and delete temp file generated on disk
|
||||
export_csv(self.test_doctype, self.file_name)
|
||||
os.remove(self.file_name)
|
||||
|
||||
# test if the exported data is logged
|
||||
last_doc = frappe.get_last_doc('Access Log')
|
||||
last_doc = frappe.get_last_doc("Access Log")
|
||||
self.assertEqual(self.test_doctype, last_doc.export_from)
|
||||
|
||||
|
||||
def test_private_file_download(self):
|
||||
# create new private file
|
||||
new_private_file = frappe.get_doc({
|
||||
'doctype': self.test_doctype,
|
||||
'file_name': self.file_name,
|
||||
'content': base64.b64encode(self.test_content.encode('utf-8')),
|
||||
'is_private': 1,
|
||||
})
|
||||
new_private_file = frappe.get_doc(
|
||||
{
|
||||
"doctype": self.test_doctype,
|
||||
"file_name": self.file_name,
|
||||
"content": base64.b64encode(self.test_content.encode("utf-8")),
|
||||
"is_private": 1,
|
||||
}
|
||||
)
|
||||
new_private_file.insert()
|
||||
|
||||
# access the created file
|
||||
|
|
@ -156,7 +159,7 @@ class TestAccessLog(unittest.TestCase):
|
|||
|
||||
try:
|
||||
request = requests.post(private_file_link, headers=self.header)
|
||||
last_doc = frappe.get_last_doc('Access Log')
|
||||
last_doc = frappe.get_last_doc("Access Log")
|
||||
|
||||
if request.ok:
|
||||
# check for the access log of downloaded file
|
||||
|
|
@ -169,6 +172,5 @@ class TestAccessLog(unittest.TestCase):
|
|||
# cleanup
|
||||
new_private_file.delete()
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -26,20 +26,25 @@ class ActivityLog(Document):
|
|||
if self.reference_doctype and self.reference_name:
|
||||
self.status = "Linked"
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
"""Add indexes in `tabActivity Log`"""
|
||||
frappe.db.add_index("Activity Log", ["reference_doctype", "reference_name"])
|
||||
frappe.db.add_index("Activity Log", ["timeline_doctype", "timeline_name"])
|
||||
frappe.db.add_index("Activity Log", ["link_doctype", "link_name"])
|
||||
|
||||
|
||||
def add_authentication_log(subject, user, operation="Login", status="Success"):
|
||||
frappe.get_doc({
|
||||
"doctype": "Activity Log",
|
||||
"user": user,
|
||||
"status": status,
|
||||
"subject": subject,
|
||||
"operation": operation,
|
||||
}).insert(ignore_permissions=True, ignore_links=True)
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Activity Log",
|
||||
"user": user,
|
||||
"status": status,
|
||||
"subject": subject,
|
||||
"operation": operation,
|
||||
}
|
||||
).insert(ignore_permissions=True, ignore_links=True)
|
||||
|
||||
|
||||
def clear_activity_logs(days=None):
|
||||
"""clear 90 day old authentication logs or configured in log settings"""
|
||||
|
|
@ -47,6 +52,4 @@ def clear_activity_logs(days=None):
|
|||
if not days:
|
||||
days = 90
|
||||
doctype = DocType("Activity Log")
|
||||
frappe.db.delete(doctype, filters=(
|
||||
doctype.creation < (Now() - Interval(days=days))
|
||||
))
|
||||
frappe.db.delete(doctype, filters=(doctype.creation < (Now() - Interval(days=days))))
|
||||
|
|
|
|||
|
|
@ -3,15 +3,16 @@
|
|||
|
||||
import frappe
|
||||
import frappe.permissions
|
||||
from frappe.utils import get_fullname
|
||||
from frappe import _
|
||||
from frappe.core.doctype.activity_log.activity_log import add_authentication_log
|
||||
from frappe.utils import get_fullname
|
||||
|
||||
|
||||
def update_feed(doc, method=None):
|
||||
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import:
|
||||
return
|
||||
|
||||
if doc._action!="save" or doc.flags.ignore_feed:
|
||||
if doc._action != "save" or doc.flags.ignore_feed:
|
||||
return
|
||||
|
||||
if doc.doctype == "Activity Log" or doc.meta.issingle:
|
||||
|
|
@ -29,65 +30,75 @@ def update_feed(doc, method=None):
|
|||
name = feed.name or doc.name
|
||||
|
||||
# delete earlier feed
|
||||
frappe.db.delete("Activity Log", {
|
||||
"reference_doctype": doctype,
|
||||
"reference_name": name,
|
||||
"link_doctype": feed.link_doctype
|
||||
})
|
||||
frappe.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.get_doc({
|
||||
"doctype": "Activity Log",
|
||||
"reference_doctype": doctype,
|
||||
"reference_name": name,
|
||||
"subject": feed.subject,
|
||||
"full_name": get_fullname(doc.owner),
|
||||
"reference_owner": frappe.db.get_value(doctype, name, "owner"),
|
||||
"link_doctype": feed.link_doctype,
|
||||
"link_name": feed.link_name
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
def login_feed(login_manager):
|
||||
if login_manager.user != "Guest":
|
||||
subject = _("{0} logged in").format(get_fullname(login_manager.user))
|
||||
add_authentication_log(subject, login_manager.user)
|
||||
|
||||
|
||||
def logout_feed(user, reason):
|
||||
if user and user != "Guest":
|
||||
subject = _("{0} logged out: {1}").format(get_fullname(user), frappe.bold(reason))
|
||||
add_authentication_log(subject, user, operation="Logout")
|
||||
|
||||
def get_feed_match_conditions(user=None, doctype='Comment'):
|
||||
if not user: user = frappe.session.user
|
||||
|
||||
conditions = ['`tab{doctype}`.owner={user} or `tab{doctype}`.reference_owner={user}'.format(
|
||||
user = frappe.db.escape(user),
|
||||
doctype = doctype
|
||||
)]
|
||||
def get_feed_match_conditions(user=None, doctype="Comment"):
|
||||
if not user:
|
||||
user = frappe.session.user
|
||||
|
||||
conditions = [
|
||||
"`tab{doctype}`.owner={user} or `tab{doctype}`.reference_owner={user}".format(
|
||||
user=frappe.db.escape(user), doctype=doctype
|
||||
)
|
||||
]
|
||||
|
||||
user_permissions = frappe.permissions.get_user_permissions(user)
|
||||
can_read = frappe.get_user().get_can_read()
|
||||
|
||||
can_read_doctypes = ["'{}'".format(dt) for dt in
|
||||
list(set(can_read) - set(list(user_permissions)))]
|
||||
can_read_doctypes = [
|
||||
"'{}'".format(dt) for dt in list(set(can_read) - set(list(user_permissions)))
|
||||
]
|
||||
|
||||
if can_read_doctypes:
|
||||
conditions += ["""(`tab{doctype}`.reference_doctype is null
|
||||
conditions += [
|
||||
"""(`tab{doctype}`.reference_doctype is null
|
||||
or `tab{doctype}`.reference_doctype = ''
|
||||
or `tab{doctype}`.reference_doctype
|
||||
in ({values}))""".format(
|
||||
doctype = doctype,
|
||||
values =", ".join(can_read_doctypes)
|
||||
)]
|
||||
doctype=doctype, values=", ".join(can_read_doctypes)
|
||||
)
|
||||
]
|
||||
|
||||
if user_permissions:
|
||||
can_read_docs = []
|
||||
for dt, obj in user_permissions.items():
|
||||
for n in obj:
|
||||
can_read_docs.append('{}|{}'.format(frappe.db.escape(dt), frappe.db.escape(n.get('doc', ''))))
|
||||
can_read_docs.append("{}|{}".format(frappe.db.escape(dt), frappe.db.escape(n.get("doc", ""))))
|
||||
|
||||
if can_read_docs:
|
||||
conditions.append("concat_ws('|', `tab{doctype}`.reference_doctype, `tab{doctype}`.reference_name) in ({values})".format(
|
||||
doctype = doctype,
|
||||
values = ", ".join(can_read_docs)))
|
||||
conditions.append(
|
||||
"concat_ws('|', `tab{doctype}`.reference_doctype, `tab{doctype}`.reference_name) in ({values})".format(
|
||||
doctype=doctype, values=", ".join(can_read_docs)
|
||||
)
|
||||
)
|
||||
|
||||
return "(" + " or ".join(conditions) + ")"
|
||||
return "(" + " or ".join(conditions) + ")"
|
||||
|
|
|
|||
|
|
@ -1,77 +1,74 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import frappe
|
||||
import unittest
|
||||
import time
|
||||
from frappe.auth import LoginManager, CookieManager
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.auth import CookieManager, LoginManager
|
||||
|
||||
|
||||
class TestActivityLog(unittest.TestCase):
|
||||
def test_activity_log(self):
|
||||
|
||||
# test user login log
|
||||
frappe.local.form_dict = frappe._dict({
|
||||
'cmd': 'login',
|
||||
'sid': 'Guest',
|
||||
'pwd': 'admin',
|
||||
'usr': 'Administrator'
|
||||
})
|
||||
frappe.local.form_dict = frappe._dict(
|
||||
{"cmd": "login", "sid": "Guest", "pwd": "admin", "usr": "Administrator"}
|
||||
)
|
||||
|
||||
frappe.local.cookie_manager = CookieManager()
|
||||
frappe.local.login_manager = LoginManager()
|
||||
|
||||
auth_log = self.get_auth_log()
|
||||
self.assertEqual(auth_log.status, 'Success')
|
||||
self.assertEqual(auth_log.status, "Success")
|
||||
|
||||
# test user logout log
|
||||
frappe.local.login_manager.logout()
|
||||
auth_log = self.get_auth_log(operation='Logout')
|
||||
self.assertEqual(auth_log.status, 'Success')
|
||||
auth_log = self.get_auth_log(operation="Logout")
|
||||
self.assertEqual(auth_log.status, "Success")
|
||||
|
||||
# test invalid login
|
||||
frappe.form_dict.update({ 'pwd': 'password' })
|
||||
frappe.form_dict.update({"pwd": "password"})
|
||||
self.assertRaises(frappe.AuthenticationError, LoginManager)
|
||||
auth_log = self.get_auth_log()
|
||||
self.assertEqual(auth_log.status, 'Failed')
|
||||
self.assertEqual(auth_log.status, "Failed")
|
||||
|
||||
frappe.local.form_dict = frappe._dict()
|
||||
|
||||
def get_auth_log(self, operation='Login'):
|
||||
names = frappe.db.get_all('Activity Log', filters={
|
||||
'user': 'Administrator',
|
||||
'operation': operation,
|
||||
}, order_by='`creation` DESC')
|
||||
def get_auth_log(self, operation="Login"):
|
||||
names = frappe.db.get_all(
|
||||
"Activity Log",
|
||||
filters={
|
||||
"user": "Administrator",
|
||||
"operation": operation,
|
||||
},
|
||||
order_by="`creation` DESC",
|
||||
)
|
||||
|
||||
name = names[0]
|
||||
auth_log = frappe.get_doc('Activity Log', name)
|
||||
auth_log = frappe.get_doc("Activity Log", name)
|
||||
return auth_log
|
||||
|
||||
def test_brute_security(self):
|
||||
update_system_settings({
|
||||
'allow_consecutive_login_attempts': 3,
|
||||
'allow_login_after_fail': 5
|
||||
})
|
||||
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.form_dict = frappe._dict(
|
||||
{"cmd": "login", "sid": "Guest", "pwd": "admin", "usr": "Administrator"}
|
||||
)
|
||||
|
||||
frappe.local.cookie_manager = CookieManager()
|
||||
frappe.local.login_manager = LoginManager()
|
||||
|
||||
auth_log = self.get_auth_log()
|
||||
self.assertEqual(auth_log.status, 'Success')
|
||||
self.assertEqual(auth_log.status, "Success")
|
||||
|
||||
# test user logout log
|
||||
frappe.local.login_manager.logout()
|
||||
auth_log = self.get_auth_log(operation='Logout')
|
||||
self.assertEqual(auth_log.status, 'Success')
|
||||
auth_log = self.get_auth_log(operation="Logout")
|
||||
self.assertEqual(auth_log.status, "Success")
|
||||
|
||||
# test invalid login
|
||||
frappe.form_dict.update({ 'pwd': 'password' })
|
||||
frappe.form_dict.update({"pwd": "password"})
|
||||
self.assertRaises(frappe.AuthenticationError, LoginManager)
|
||||
self.assertRaises(frappe.AuthenticationError, LoginManager)
|
||||
self.assertRaises(frappe.AuthenticationError, LoginManager)
|
||||
|
|
@ -85,8 +82,9 @@ class TestActivityLog(unittest.TestCase):
|
|||
|
||||
frappe.local.form_dict = frappe._dict()
|
||||
|
||||
|
||||
def update_system_settings(args):
|
||||
doc = frappe.get_doc('System Settings')
|
||||
doc = frappe.get_doc("System Settings")
|
||||
doc.update(args)
|
||||
doc.flags.ignore_mandatory = 1
|
||||
doc.save()
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class BlockModule(Document):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -1,22 +1,27 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
import json
|
||||
from frappe.model.document import Document
|
||||
from frappe.core.doctype.user.user import extract_mentions
|
||||
from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification,\
|
||||
get_title, get_title_html
|
||||
from frappe.database.schema import add_column
|
||||
from frappe.desk.doctype.notification_log.notification_log import (
|
||||
enqueue_create_notification,
|
||||
get_title,
|
||||
get_title_html,
|
||||
)
|
||||
from frappe.exceptions import ImplicitCommitError
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_fullname
|
||||
from frappe.website.utils import clear_cache
|
||||
from frappe.database.schema import add_column
|
||||
from frappe.exceptions import ImplicitCommitError
|
||||
|
||||
|
||||
class Comment(Document):
|
||||
def after_insert(self):
|
||||
self.notify_mentions()
|
||||
self.notify_change('add')
|
||||
self.notify_change("add")
|
||||
|
||||
def validate(self):
|
||||
if not self.comment_email:
|
||||
|
|
@ -26,34 +31,35 @@ class Comment(Document):
|
|||
def on_update(self):
|
||||
update_comment_in_doc(self)
|
||||
if self.is_new():
|
||||
self.notify_change('update')
|
||||
self.notify_change("update")
|
||||
|
||||
def on_trash(self):
|
||||
self.remove_comment_from_cache()
|
||||
self.notify_change('delete')
|
||||
self.notify_change("delete")
|
||||
|
||||
def notify_change(self, action):
|
||||
key_map = {
|
||||
'Like': 'like_logs',
|
||||
'Assigned': 'assignment_logs',
|
||||
'Assignment Completed': 'assignment_logs',
|
||||
'Comment': 'comments',
|
||||
'Attachment': 'attachment_logs',
|
||||
'Attachment Removed': 'attachment_logs',
|
||||
"Like": "like_logs",
|
||||
"Assigned": "assignment_logs",
|
||||
"Assignment Completed": "assignment_logs",
|
||||
"Comment": "comments",
|
||||
"Attachment": "attachment_logs",
|
||||
"Attachment Removed": "attachment_logs",
|
||||
}
|
||||
key = key_map.get(self.comment_type)
|
||||
if not key: return
|
||||
if not key:
|
||||
return
|
||||
|
||||
frappe.publish_realtime('update_docinfo_for_{}_{}'.format(self.reference_doctype, self.reference_name), {
|
||||
'doc': self.as_dict(),
|
||||
'key': key,
|
||||
'action': action
|
||||
}, after_commit=True)
|
||||
frappe.publish_realtime(
|
||||
"update_docinfo_for_{}_{}".format(self.reference_doctype, self.reference_name),
|
||||
{"doc": self.as_dict(), "key": key, "action": action},
|
||||
after_commit=True,
|
||||
)
|
||||
|
||||
def remove_comment_from_cache(self):
|
||||
_comments = get_comments_from_parent(self)
|
||||
for c in _comments:
|
||||
if c.get("name")==self.name:
|
||||
if c.get("name") == self.name:
|
||||
_comments.remove(c)
|
||||
|
||||
update_comments_in_parent(self.reference_doctype, self.reference_name, _comments)
|
||||
|
|
@ -68,19 +74,26 @@ class Comment(Document):
|
|||
sender_fullname = get_fullname(frappe.session.user)
|
||||
title = get_title(self.reference_doctype, self.reference_name)
|
||||
|
||||
recipients = [frappe.db.get_value("User", {"enabled": 1, "name": name, "user_type": "System User", "allowed_in_mentions": 1}, "email")
|
||||
for name in mentions]
|
||||
recipients = [
|
||||
frappe.db.get_value(
|
||||
"User",
|
||||
{"enabled": 1, "name": name, "user_type": "System User", "allowed_in_mentions": 1},
|
||||
"email",
|
||||
)
|
||||
for name in mentions
|
||||
]
|
||||
|
||||
notification_message = _('''{0} mentioned you in a comment in {1} {2}''')\
|
||||
.format(frappe.bold(sender_fullname), frappe.bold(self.reference_doctype), get_title_html(title))
|
||||
notification_message = _("""{0} mentioned you in a comment in {1} {2}""").format(
|
||||
frappe.bold(sender_fullname), frappe.bold(self.reference_doctype), get_title_html(title)
|
||||
)
|
||||
|
||||
notification_doc = {
|
||||
'type': 'Mention',
|
||||
'document_type': self.reference_doctype,
|
||||
'document_name': self.reference_name,
|
||||
'subject': notification_message,
|
||||
'from_user': frappe.session.user,
|
||||
'email_content': self.content
|
||||
"type": "Mention",
|
||||
"document_type": self.reference_doctype,
|
||||
"document_name": self.reference_name,
|
||||
"subject": notification_message,
|
||||
"from_user": frappe.session.user,
|
||||
"email_content": self.content,
|
||||
}
|
||||
|
||||
enqueue_create_notification(recipients, notification_doc)
|
||||
|
|
@ -99,45 +112,46 @@ def update_comment_in_doc(doc):
|
|||
|
||||
`_comments` format
|
||||
|
||||
{
|
||||
"comment": [String],
|
||||
"by": [user],
|
||||
"name": [Comment Document name]
|
||||
}"""
|
||||
{
|
||||
"comment": [String],
|
||||
"by": [user],
|
||||
"name": [Comment Document name]
|
||||
}"""
|
||||
|
||||
# only comments get updates, not likes, assignments etc.
|
||||
if doc.doctype == 'Comment' and doc.comment_type != 'Comment':
|
||||
if doc.doctype == "Comment" and doc.comment_type != "Comment":
|
||||
return
|
||||
|
||||
def get_truncated(content):
|
||||
return (content[:97] + '...') if len(content) > 100 else content
|
||||
return (content[:97] + "...") if len(content) > 100 else content
|
||||
|
||||
if doc.reference_doctype and doc.reference_name and doc.content:
|
||||
_comments = get_comments_from_parent(doc)
|
||||
|
||||
updated = False
|
||||
for c in _comments:
|
||||
if c.get("name")==doc.name:
|
||||
if c.get("name") == doc.name:
|
||||
c["comment"] = get_truncated(doc.content)
|
||||
updated = True
|
||||
|
||||
if not updated:
|
||||
_comments.append({
|
||||
"comment": get_truncated(doc.content),
|
||||
|
||||
# "comment_email" for Comment and "sender" for Communication
|
||||
"by": getattr(doc, 'comment_email', None) or getattr(doc, 'sender', None) or doc.owner,
|
||||
"name": doc.name
|
||||
})
|
||||
_comments.append(
|
||||
{
|
||||
"comment": get_truncated(doc.content),
|
||||
# "comment_email" for Comment and "sender" for Communication
|
||||
"by": getattr(doc, "comment_email", None) or getattr(doc, "sender", None) or doc.owner,
|
||||
"name": doc.name,
|
||||
}
|
||||
)
|
||||
|
||||
update_comments_in_parent(doc.reference_doctype, doc.reference_name, _comments)
|
||||
|
||||
|
||||
def get_comments_from_parent(doc):
|
||||
'''
|
||||
"""
|
||||
get the list of comments cached in the document record in the column
|
||||
`_comments`
|
||||
'''
|
||||
"""
|
||||
try:
|
||||
_comments = frappe.db.get_value(doc.reference_doctype, doc.reference_name, "_comments") or "[]"
|
||||
|
||||
|
|
@ -153,23 +167,32 @@ def get_comments_from_parent(doc):
|
|||
except ValueError:
|
||||
return []
|
||||
|
||||
|
||||
def update_comments_in_parent(reference_doctype, reference_name, _comments):
|
||||
"""Updates `_comments` property in parent Document with given dict.
|
||||
|
||||
:param _comments: Dict of comments."""
|
||||
if not reference_doctype or not reference_name or frappe.db.get_value("DocType", reference_doctype, "issingle") or frappe.db.get_value("DocType", reference_doctype, "is_virtual"):
|
||||
if (
|
||||
not reference_doctype
|
||||
or not reference_name
|
||||
or frappe.db.get_value("DocType", reference_doctype, "issingle")
|
||||
or frappe.db.get_value("DocType", reference_doctype, "is_virtual")
|
||||
):
|
||||
return
|
||||
|
||||
try:
|
||||
# use sql, so that we do not mess with the timestamp
|
||||
frappe.db.sql("""update `tab{0}` set `_comments`=%s where name=%s""".format(reference_doctype), # nosec
|
||||
(json.dumps(_comments[-100:]), reference_name))
|
||||
frappe.db.sql(
|
||||
"""update `tab{0}` set `_comments`=%s where name=%s""".format(reference_doctype), # nosec
|
||||
(json.dumps(_comments[-100:]), reference_name),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
if frappe.db.is_column_missing(e) and getattr(frappe.local, 'request', None):
|
||||
if frappe.db.is_column_missing(e) and getattr(frappe.local, "request", None):
|
||||
# missing column and in request, add column and update after commit
|
||||
frappe.local._comments = (getattr(frappe.local, "_comments", [])
|
||||
+ [(reference_doctype, reference_name, _comments)])
|
||||
frappe.local._comments = getattr(frappe.local, "_comments", []) + [
|
||||
(reference_doctype, reference_name, _comments)
|
||||
]
|
||||
|
||||
elif frappe.db.is_data_too_long(e):
|
||||
raise frappe.DataTooLongException
|
||||
|
|
@ -183,6 +206,7 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments):
|
|||
if getattr(reference_doc, "route", None):
|
||||
clear_cache(reference_doc.route)
|
||||
|
||||
|
||||
def update_comments_in_parent_after_request():
|
||||
"""update _comments in parent if _comments column is missing"""
|
||||
if hasattr(frappe.local, "_comments"):
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import frappe, json
|
||||
import json
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
class TestComment(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
frappe.form_dict.comment = None
|
||||
|
|
@ -15,75 +18,88 @@ class TestComment(unittest.TestCase):
|
|||
frappe.local.request_ip = None
|
||||
|
||||
def test_comment_creation(self):
|
||||
test_doc = frappe.get_doc(dict(doctype = 'ToDo', description = 'test'))
|
||||
test_doc = frappe.get_doc(dict(doctype="ToDo", description="test"))
|
||||
test_doc.insert()
|
||||
comment = test_doc.add_comment('Comment', 'test comment')
|
||||
comment = test_doc.add_comment("Comment", "test comment")
|
||||
|
||||
test_doc.reload()
|
||||
|
||||
# check if updated in _comments cache
|
||||
comments = json.loads(test_doc.get('_comments'))
|
||||
self.assertEqual(comments[0].get('name'), comment.name)
|
||||
self.assertEqual(comments[0].get('comment'), comment.content)
|
||||
comments = json.loads(test_doc.get("_comments"))
|
||||
self.assertEqual(comments[0].get("name"), comment.name)
|
||||
self.assertEqual(comments[0].get("comment"), comment.content)
|
||||
|
||||
# check document creation
|
||||
comment_1 = frappe.get_all('Comment', fields = ['*'], filters = dict(
|
||||
reference_doctype = test_doc.doctype,
|
||||
reference_name = test_doc.name
|
||||
))[0]
|
||||
comment_1 = frappe.get_all(
|
||||
"Comment",
|
||||
fields=["*"],
|
||||
filters=dict(reference_doctype=test_doc.doctype, reference_name=test_doc.name),
|
||||
)[0]
|
||||
|
||||
self.assertEqual(comment_1.content, 'test comment')
|
||||
self.assertEqual(comment_1.content, "test comment")
|
||||
|
||||
# test via blog
|
||||
def test_public_comment(self):
|
||||
from frappe.website.doctype.blog_post.test_blog_post import make_test_blog
|
||||
|
||||
test_blog = make_test_blog()
|
||||
|
||||
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})
|
||||
|
||||
from frappe.templates.includes.comments.comments import add_comment
|
||||
|
||||
frappe.form_dict.comment = 'Good comment with 10 chars'
|
||||
frappe.form_dict.comment_email = 'test@test.com'
|
||||
frappe.form_dict.comment_by = 'Good Tester'
|
||||
frappe.form_dict.reference_doctype = 'Blog Post'
|
||||
frappe.form_dict.comment = "Good comment with 10 chars"
|
||||
frappe.form_dict.comment_email = "test@test.com"
|
||||
frappe.form_dict.comment_by = "Good Tester"
|
||||
frappe.form_dict.reference_doctype = "Blog Post"
|
||||
frappe.form_dict.reference_name = test_blog.name
|
||||
frappe.form_dict.route = test_blog.route
|
||||
frappe.local.request_ip = '127.0.0.1'
|
||||
frappe.local.request_ip = "127.0.0.1"
|
||||
|
||||
add_comment()
|
||||
|
||||
self.assertEqual(frappe.get_all('Comment', fields = ['*'], filters = dict(
|
||||
reference_doctype = test_blog.doctype,
|
||||
reference_name = test_blog.name
|
||||
))[0].published, 1)
|
||||
self.assertEqual(
|
||||
frappe.get_all(
|
||||
"Comment",
|
||||
fields=["*"],
|
||||
filters=dict(reference_doctype=test_blog.doctype, reference_name=test_blog.name),
|
||||
)[0].published,
|
||||
1,
|
||||
)
|
||||
|
||||
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})
|
||||
|
||||
frappe.form_dict.comment = 'pleez vizits my site http://mysite.com'
|
||||
frappe.form_dict.comment_by = 'bad commentor'
|
||||
frappe.form_dict.comment = "pleez vizits my site http://mysite.com"
|
||||
frappe.form_dict.comment_by = "bad commentor"
|
||||
|
||||
add_comment()
|
||||
|
||||
self.assertEqual(len(frappe.get_all('Comment', fields = ['*'], filters = dict(
|
||||
reference_doctype = test_blog.doctype,
|
||||
reference_name = test_blog.name
|
||||
))), 0)
|
||||
self.assertEqual(
|
||||
len(
|
||||
frappe.get_all(
|
||||
"Comment",
|
||||
fields=["*"],
|
||||
filters=dict(reference_doctype=test_blog.doctype, reference_name=test_blog.name),
|
||||
)
|
||||
),
|
||||
0,
|
||||
)
|
||||
|
||||
# test for filtering html and css injection elements
|
||||
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})
|
||||
|
||||
frappe.form_dict.comment = '<script>alert(1)</script>Comment'
|
||||
frappe.form_dict.comment_by = 'hacker'
|
||||
frappe.form_dict.comment = "<script>alert(1)</script>Comment"
|
||||
frappe.form_dict.comment_by = "hacker"
|
||||
|
||||
add_comment()
|
||||
|
||||
self.assertEqual(frappe.get_all('Comment', fields = ['content'], filters = dict(
|
||||
reference_doctype = test_blog.doctype,
|
||||
reference_name = test_blog.name
|
||||
))[0]['content'], 'Comment')
|
||||
self.assertEqual(
|
||||
frappe.get_all(
|
||||
"Comment",
|
||||
fields=["content"],
|
||||
filters=dict(reference_doctype=test_blog.doctype, reference_name=test_blog.name),
|
||||
)[0]["content"],
|
||||
"Comment",
|
||||
)
|
||||
|
||||
test_blog.delete()
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
|
|
|
|||
|
|
@ -2,49 +2,67 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
from collections import Counter
|
||||
from email.utils import getaddresses
|
||||
from typing import List
|
||||
from urllib.parse import unquote
|
||||
|
||||
from parse import compile
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import validate_email_address, strip_html, cstr, time_diff_in_seconds
|
||||
from frappe.automation.doctype.assignment_rule.assignment_rule import (
|
||||
apply as apply_assignment_rule,
|
||||
)
|
||||
from frappe.contacts.doctype.contact.contact import get_contact_name
|
||||
from frappe.core.doctype.comment.comment import update_comment_in_doc
|
||||
from frappe.core.doctype.communication.email import validate_email
|
||||
from frappe.core.doctype.communication.mixins import CommunicationEmailMixin
|
||||
from frappe.core.utils import get_parent_doc
|
||||
from frappe.utils import parse_addr, split_emails
|
||||
from frappe.core.doctype.comment.comment import update_comment_in_doc
|
||||
from email.utils import getaddresses
|
||||
from urllib.parse import unquote
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import (
|
||||
cstr,
|
||||
parse_addr,
|
||||
split_emails,
|
||||
strip_html,
|
||||
time_diff_in_seconds,
|
||||
validate_email_address,
|
||||
)
|
||||
from frappe.utils.user import is_system_user
|
||||
from frappe.contacts.doctype.contact.contact import get_contact_name
|
||||
from frappe.automation.doctype.assignment_rule.assignment_rule import apply as apply_assignment_rule
|
||||
from parse import compile
|
||||
|
||||
exclude_from_linked_with = True
|
||||
|
||||
|
||||
class Communication(Document, CommunicationEmailMixin):
|
||||
"""Communication represents an external communication like Email.
|
||||
"""
|
||||
"""Communication represents an external communication like Email."""
|
||||
|
||||
no_feed_on_delete = True
|
||||
DOCTYPE = 'Communication'
|
||||
DOCTYPE = "Communication"
|
||||
|
||||
def onload(self):
|
||||
"""create email flag queue"""
|
||||
if self.communication_type == "Communication" and self.communication_medium == "Email" \
|
||||
and self.sent_or_received == "Received" and self.uid and self.uid != -1:
|
||||
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})
|
||||
email_flag_queue = frappe.db.get_value(
|
||||
"Email Flag Queue", {"communication": self.name, "is_completed": 0}
|
||||
)
|
||||
if email_flag_queue:
|
||||
return
|
||||
|
||||
frappe.get_doc({
|
||||
"doctype": "Email Flag Queue",
|
||||
"action": "Read",
|
||||
"communication": self.name,
|
||||
"uid": self.uid,
|
||||
"email_account": self.email_account
|
||||
}).insert(ignore_permissions=True)
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Email Flag Queue",
|
||||
"action": "Read",
|
||||
"communication": self.name,
|
||||
"uid": self.uid,
|
||||
"email_account": self.email_account,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
|
||||
def validate(self):
|
||||
|
|
@ -74,25 +92,33 @@ class Communication(Document, CommunicationEmailMixin):
|
|||
def validate_reference(self):
|
||||
if self.reference_doctype and self.reference_name:
|
||||
if not self.reference_owner:
|
||||
self.reference_owner = frappe.db.get_value(self.reference_doctype, self.reference_name, "owner")
|
||||
self.reference_owner = frappe.db.get_value(
|
||||
self.reference_doctype, self.reference_name, "owner"
|
||||
)
|
||||
|
||||
# prevent communication against a child table
|
||||
if frappe.get_meta(self.reference_doctype).istable:
|
||||
frappe.throw(_("Cannot create a {0} against a child document: {1}")
|
||||
.format(_(self.communication_type), _(self.reference_doctype)))
|
||||
frappe.throw(
|
||||
_("Cannot create a {0} against a child document: {1}").format(
|
||||
_(self.communication_type), _(self.reference_doctype)
|
||||
)
|
||||
)
|
||||
|
||||
# Prevent circular linking of Communication DocTypes
|
||||
if self.reference_doctype == "Communication":
|
||||
circular_linking = False
|
||||
doc = get_parent_doc(self)
|
||||
while doc.reference_doctype == "Communication":
|
||||
if get_parent_doc(doc).name==self.name:
|
||||
if get_parent_doc(doc).name == self.name:
|
||||
circular_linking = True
|
||||
break
|
||||
doc = get_parent_doc(doc)
|
||||
|
||||
if circular_linking:
|
||||
frappe.throw(_("Please make sure the Reference Communication Docs are not circularly linked."), frappe.CircularLinkingError)
|
||||
frappe.throw(
|
||||
_("Please make sure the Reference Communication Docs are not circularly linked."),
|
||||
frappe.CircularLinkingError,
|
||||
)
|
||||
|
||||
def after_insert(self):
|
||||
if not (self.reference_doctype and self.reference_name):
|
||||
|
|
@ -102,21 +128,21 @@ class Communication(Document, CommunicationEmailMixin):
|
|||
frappe.db.set_value("Communication", self.reference_name, "status", "Replied")
|
||||
|
||||
if self.communication_type == "Communication":
|
||||
self.notify_change('add')
|
||||
self.notify_change("add")
|
||||
|
||||
elif self.communication_type in ("Chat", "Notification"):
|
||||
if self.reference_name == frappe.session.user:
|
||||
message = self.as_dict()
|
||||
message['broadcast'] = True
|
||||
frappe.publish_realtime('new_message', message, after_commit=True)
|
||||
message["broadcast"] = True
|
||||
frappe.publish_realtime("new_message", message, after_commit=True)
|
||||
else:
|
||||
# reference_name contains the user who is addressed in the messages' page comment
|
||||
frappe.publish_realtime('new_message', self.as_dict(),
|
||||
user=self.reference_name, after_commit=True)
|
||||
frappe.publish_realtime(
|
||||
"new_message", self.as_dict(), user=self.reference_name, after_commit=True
|
||||
)
|
||||
|
||||
def set_signature_in_email_content(self):
|
||||
"""Set sender's User.email_signature or default outgoing's EmailAccount.signature to the email
|
||||
"""
|
||||
"""Set sender's User.email_signature or default outgoing's EmailAccount.signature to the email"""
|
||||
if not self.content:
|
||||
return
|
||||
|
||||
|
|
@ -128,11 +154,15 @@ class Communication(Document, CommunicationEmailMixin):
|
|||
|
||||
email_body = email_body[0]
|
||||
|
||||
user_email_signature = frappe.db.get_value(
|
||||
"User",
|
||||
self.sender,
|
||||
"email_signature",
|
||||
) if self.sender else None
|
||||
user_email_signature = (
|
||||
frappe.db.get_value(
|
||||
"User",
|
||||
self.sender,
|
||||
"email_signature",
|
||||
)
|
||||
if self.sender
|
||||
else None
|
||||
)
|
||||
|
||||
signature = user_email_signature or frappe.db.get_value(
|
||||
"Email Account",
|
||||
|
|
@ -157,19 +187,19 @@ class Communication(Document, CommunicationEmailMixin):
|
|||
# comments count for the list view
|
||||
update_comment_in_doc(self)
|
||||
|
||||
if self.comment_type != 'Updated':
|
||||
if self.comment_type != "Updated":
|
||||
update_parent_document_on_communication(self)
|
||||
|
||||
def on_trash(self):
|
||||
if self.communication_type == "Communication":
|
||||
self.notify_change('delete')
|
||||
self.notify_change("delete")
|
||||
|
||||
@property
|
||||
def sender_mailid(self):
|
||||
return parse_addr(self.sender)[1] if self.sender else ""
|
||||
|
||||
@staticmethod
|
||||
def _get_emails_list(emails=None, exclude_displayname = False):
|
||||
def _get_emails_list(emails=None, exclude_displayname=False):
|
||||
"""Returns list of emails from given email string.
|
||||
|
||||
* Removes duplicate mailids
|
||||
|
|
@ -180,35 +210,32 @@ class Communication(Document, CommunicationEmailMixin):
|
|||
return [email.lower() for email in set([parse_addr(email)[1] for email in emails]) if email]
|
||||
return [email.lower() for email in set(emails) if email]
|
||||
|
||||
def to_list(self, exclude_displayname = True):
|
||||
"""Returns to list.
|
||||
"""
|
||||
def to_list(self, exclude_displayname=True):
|
||||
"""Returns to list."""
|
||||
return self._get_emails_list(self.recipients, exclude_displayname=exclude_displayname)
|
||||
|
||||
def cc_list(self, exclude_displayname = True):
|
||||
"""Returns cc list.
|
||||
"""
|
||||
def cc_list(self, exclude_displayname=True):
|
||||
"""Returns cc list."""
|
||||
return self._get_emails_list(self.cc, exclude_displayname=exclude_displayname)
|
||||
|
||||
def bcc_list(self, exclude_displayname = True):
|
||||
"""Returns bcc list.
|
||||
"""
|
||||
def bcc_list(self, exclude_displayname=True):
|
||||
"""Returns bcc list."""
|
||||
return self._get_emails_list(self.bcc, exclude_displayname=exclude_displayname)
|
||||
|
||||
def get_attachments(self):
|
||||
attachments = frappe.get_all(
|
||||
"File",
|
||||
fields=["name", "file_name", "file_url", "is_private"],
|
||||
filters = {"attached_to_name": self.name, "attached_to_doctype": self.DOCTYPE}
|
||||
filters={"attached_to_name": self.name, "attached_to_doctype": self.DOCTYPE},
|
||||
)
|
||||
return attachments
|
||||
|
||||
def notify_change(self, action):
|
||||
frappe.publish_realtime('update_docinfo_for_{}_{}'.format(self.reference_doctype, self.reference_name), {
|
||||
'doc': self.as_dict(),
|
||||
'key': 'communications',
|
||||
'action': action
|
||||
}, after_commit=True)
|
||||
frappe.publish_realtime(
|
||||
"update_docinfo_for_{}_{}".format(self.reference_doctype, self.reference_name),
|
||||
{"doc": self.as_dict(), "key": "communications", "action": action},
|
||||
after_commit=True,
|
||||
)
|
||||
|
||||
def set_status(self):
|
||||
if not self.is_new():
|
||||
|
|
@ -216,15 +243,19 @@ class Communication(Document, CommunicationEmailMixin):
|
|||
|
||||
if self.reference_doctype and self.reference_name:
|
||||
self.status = "Linked"
|
||||
elif self.communication_type=="Communication":
|
||||
elif self.communication_type == "Communication":
|
||||
self.status = "Open"
|
||||
else:
|
||||
self.status = "Closed"
|
||||
|
||||
# set email status to spam
|
||||
email_rule = frappe.db.get_value("Email Rule", { "email_id": self.sender, "is_spam":1 })
|
||||
if self.communication_type == "Communication" and self.communication_medium == "Email" \
|
||||
and self.sent_or_received == "Sent" and email_rule:
|
||||
email_rule = frappe.db.get_value("Email Rule", {"email_id": self.sender, "is_spam": 1})
|
||||
if (
|
||||
self.communication_type == "Communication"
|
||||
and self.communication_medium == "Email"
|
||||
and self.sent_or_received == "Sent"
|
||||
and email_rule
|
||||
):
|
||||
|
||||
self.email_status = "Spam"
|
||||
|
||||
|
|
@ -254,7 +285,7 @@ class Communication(Document, CommunicationEmailMixin):
|
|||
self.sender_full_name = self.sender
|
||||
self.sender = None
|
||||
else:
|
||||
if self.sent_or_received=='Sent':
|
||||
if self.sent_or_received == "Sent":
|
||||
validate_email_address(self.sender, throw=True)
|
||||
sender_name, sender_email = parse_addr(self.sender)
|
||||
if sender_name == sender_email:
|
||||
|
|
@ -264,40 +295,41 @@ class Communication(Document, CommunicationEmailMixin):
|
|||
self.sender_full_name = sender_name
|
||||
|
||||
if not self.sender_full_name:
|
||||
self.sender_full_name = frappe.db.get_value('User', self.sender, 'full_name')
|
||||
self.sender_full_name = frappe.db.get_value("User", self.sender, "full_name")
|
||||
|
||||
if not self.sender_full_name:
|
||||
first_name, last_name = frappe.db.get_value('Contact',
|
||||
filters={'email_id': sender_email},
|
||||
fieldname=['first_name', 'last_name']
|
||||
first_name, last_name = frappe.db.get_value(
|
||||
"Contact", filters={"email_id": sender_email}, fieldname=["first_name", "last_name"]
|
||||
) or [None, None]
|
||||
self.sender_full_name = (first_name or '') + (last_name or '')
|
||||
self.sender_full_name = (first_name or "") + (last_name or "")
|
||||
|
||||
if not self.sender_full_name:
|
||||
self.sender_full_name = sender_email
|
||||
|
||||
def set_delivery_status(self, commit=False):
|
||||
'''Look into the status of Email Queue linked to this Communication and set the Delivery Status of this Communication'''
|
||||
"""Look into the status of Email Queue linked to this Communication and set the Delivery Status of this Communication"""
|
||||
delivery_status = None
|
||||
status_counts = Counter(frappe.get_all("Email Queue", pluck="status", filters={"communication": self.name}))
|
||||
status_counts = Counter(
|
||||
frappe.get_all("Email Queue", pluck="status", filters={"communication": self.name})
|
||||
)
|
||||
if self.sent_or_received == "Received":
|
||||
return
|
||||
|
||||
if status_counts.get('Not Sent') or status_counts.get('Sending'):
|
||||
delivery_status = 'Sending'
|
||||
if status_counts.get("Not Sent") or status_counts.get("Sending"):
|
||||
delivery_status = "Sending"
|
||||
|
||||
elif status_counts.get('Error'):
|
||||
delivery_status = 'Error'
|
||||
elif status_counts.get("Error"):
|
||||
delivery_status = "Error"
|
||||
|
||||
elif status_counts.get('Expired'):
|
||||
delivery_status = 'Expired'
|
||||
elif status_counts.get("Expired"):
|
||||
delivery_status = "Expired"
|
||||
|
||||
elif status_counts.get('Sent'):
|
||||
delivery_status = 'Sent'
|
||||
elif status_counts.get("Sent"):
|
||||
delivery_status = "Sent"
|
||||
|
||||
if delivery_status:
|
||||
self.db_set('delivery_status', delivery_status)
|
||||
self.notify_change('update')
|
||||
self.db_set("delivery_status", delivery_status)
|
||||
self.notify_change("update")
|
||||
|
||||
# for list views and forms
|
||||
self.notify_update()
|
||||
|
|
@ -311,13 +343,17 @@ class Communication(Document, CommunicationEmailMixin):
|
|||
# Timeline Links
|
||||
def set_timeline_links(self):
|
||||
contacts = []
|
||||
create_contact_enabled = self.email_account and frappe.db.get_value("Email Account", self.email_account, "create_contact")
|
||||
contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc], auto_create_contact=create_contact_enabled)
|
||||
create_contact_enabled = self.email_account and frappe.db.get_value(
|
||||
"Email Account", self.email_account, "create_contact"
|
||||
)
|
||||
contacts = get_contacts(
|
||||
[self.sender, self.recipients, self.cc, self.bcc], auto_create_contact=create_contact_enabled
|
||||
)
|
||||
|
||||
for contact_name in contacts:
|
||||
self.add_link('Contact', contact_name)
|
||||
self.add_link("Contact", contact_name)
|
||||
|
||||
#link contact's dynamic links to communication
|
||||
# link contact's dynamic links to communication
|
||||
add_contact_links_to_communication(self, contact_name)
|
||||
|
||||
def deduplicate_timeline_links(self):
|
||||
|
|
@ -332,17 +368,12 @@ class Communication(Document, CommunicationEmailMixin):
|
|||
duplicate = True
|
||||
|
||||
if duplicate:
|
||||
del self.timeline_links[:] # make it python 2 compatible as list.clear() is python 3 only
|
||||
del self.timeline_links[:] # make it python 2 compatible as list.clear() is python 3 only
|
||||
for l in links:
|
||||
self.add_link(link_doctype=l[0], link_name=l[1])
|
||||
|
||||
def add_link(self, link_doctype, link_name, autosave=False):
|
||||
self.append("timeline_links",
|
||||
{
|
||||
"link_doctype": link_doctype,
|
||||
"link_name": link_name
|
||||
}
|
||||
)
|
||||
self.append("timeline_links", {"link_doctype": link_doctype, "link_name": link_name})
|
||||
|
||||
if autosave:
|
||||
self.save(ignore_permissions=True)
|
||||
|
|
@ -358,13 +389,15 @@ class Communication(Document, CommunicationEmailMixin):
|
|||
if autosave:
|
||||
self.save(ignore_permissions=ignore_permissions)
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
"""Add indexes in `tabCommunication`"""
|
||||
frappe.db.add_index("Communication", ["reference_doctype", "reference_name"])
|
||||
frappe.db.add_index("Communication", ["status", "communication_type"])
|
||||
|
||||
|
||||
def has_permission(doc, ptype, user):
|
||||
if ptype=="read":
|
||||
if ptype == "read":
|
||||
if doc.reference_doctype == "Communication" and doc.reference_name == doc.name:
|
||||
return
|
||||
|
||||
|
|
@ -372,24 +405,28 @@ def has_permission(doc, ptype, user):
|
|||
if frappe.has_permission(doc.reference_doctype, ptype="read", doc=doc.reference_name):
|
||||
return True
|
||||
|
||||
|
||||
def get_permission_query_conditions_for_communication(user):
|
||||
if not user: user = frappe.session.user
|
||||
if not user:
|
||||
user = frappe.session.user
|
||||
|
||||
roles = frappe.get_roles(user)
|
||||
|
||||
if "Super Email User" in roles or "System Manager" in roles:
|
||||
return None
|
||||
else:
|
||||
accounts = frappe.get_all("User Email", filters={ "parent": user },
|
||||
fields=["email_account"],
|
||||
distinct=True, order_by="idx")
|
||||
accounts = frappe.get_all(
|
||||
"User Email", filters={"parent": user}, fields=["email_account"], distinct=True, order_by="idx"
|
||||
)
|
||||
|
||||
if not accounts:
|
||||
return """`tabCommunication`.communication_medium!='Email'"""
|
||||
|
||||
email_accounts = [ '"%s"'%account.get("email_account") for account in accounts ]
|
||||
return """`tabCommunication`.email_account in ({email_accounts})"""\
|
||||
.format(email_accounts=','.join(email_accounts))
|
||||
email_accounts = ['"%s"' % account.get("email_account") for account in accounts]
|
||||
return """`tabCommunication`.email_account in ({email_accounts})""".format(
|
||||
email_accounts=",".join(email_accounts)
|
||||
)
|
||||
|
||||
|
||||
def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[str]:
|
||||
email_addrs = get_emails(email_strings)
|
||||
|
|
@ -403,12 +440,12 @@ def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[st
|
|||
first_name = frappe.unscrub(email_parts[0])
|
||||
|
||||
try:
|
||||
contact_name = '{0}-{1}'.format(first_name, email_parts[1]) if first_name == 'Contact' else first_name
|
||||
contact = frappe.get_doc({
|
||||
"doctype": "Contact",
|
||||
"first_name": contact_name,
|
||||
"name": contact_name
|
||||
})
|
||||
contact_name = (
|
||||
"{0}-{1}".format(first_name, email_parts[1]) if first_name == "Contact" else first_name
|
||||
)
|
||||
contact = frappe.get_doc(
|
||||
{"doctype": "Contact", "first_name": contact_name, "name": contact_name}
|
||||
)
|
||||
contact.add_email(email_id=email, is_primary=True)
|
||||
contact.insert(ignore_permissions=True)
|
||||
contact_name = contact.name
|
||||
|
|
@ -421,6 +458,7 @@ def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[st
|
|||
|
||||
return contacts
|
||||
|
||||
|
||||
def get_emails(email_strings: List[str]) -> List[str]:
|
||||
email_addrs = []
|
||||
|
||||
|
|
@ -432,22 +470,25 @@ def get_emails(email_strings: List[str]) -> List[str]:
|
|||
|
||||
return email_addrs
|
||||
|
||||
|
||||
def add_contact_links_to_communication(communication, contact_name):
|
||||
contact_links = frappe.get_all("Dynamic Link", filters={
|
||||
"parenttype": "Contact",
|
||||
"parent": contact_name
|
||||
}, fields=["link_doctype", "link_name"])
|
||||
contact_links = frappe.get_all(
|
||||
"Dynamic Link",
|
||||
filters={"parenttype": "Contact", "parent": contact_name},
|
||||
fields=["link_doctype", "link_name"],
|
||||
)
|
||||
|
||||
if contact_links:
|
||||
for contact_link in contact_links:
|
||||
communication.add_link(contact_link.link_doctype, contact_link.link_name)
|
||||
|
||||
|
||||
def parse_email(communication, email_strings):
|
||||
"""
|
||||
Parse email to add timeline links.
|
||||
When automatic email linking is enabled, an email from email_strings can contain
|
||||
a doctype and docname ie in the format `admin+doctype+docname@example.com`,
|
||||
the email is parsed and doctype and docname is extracted and timeline link is added.
|
||||
Parse email to add timeline links.
|
||||
When automatic email linking is enabled, an email from email_strings can contain
|
||||
a doctype and docname ie in the format `admin+doctype+docname@example.com`,
|
||||
the email is parsed and doctype and docname is extracted and timeline link is added.
|
||||
"""
|
||||
if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}):
|
||||
return
|
||||
|
|
@ -469,10 +510,11 @@ def parse_email(communication, email_strings):
|
|||
if doctype and docname and frappe.db.exists(doctype, docname):
|
||||
communication.add_link(doctype, docname)
|
||||
|
||||
|
||||
def get_email_without_link(email):
|
||||
"""
|
||||
returns email address without doctype links
|
||||
returns admin@example.com for email admin+doctype+docname@example.com
|
||||
returns email address without doctype links
|
||||
returns admin@example.com for email admin+doctype+docname@example.com
|
||||
"""
|
||||
if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}):
|
||||
return email
|
||||
|
|
@ -486,6 +528,7 @@ def get_email_without_link(email):
|
|||
|
||||
return "{0}@{1}".format(email_id, email_host)
|
||||
|
||||
|
||||
def update_parent_document_on_communication(doc):
|
||||
"""Update mins_to_first_communication of parent document based on who is replying."""
|
||||
|
||||
|
|
@ -516,6 +559,7 @@ def update_parent_document_on_communication(doc):
|
|||
parent.run_method("notify_communication", doc)
|
||||
parent.notify_update()
|
||||
|
||||
|
||||
def update_first_response_time(parent, communication):
|
||||
if parent.meta.has_field("first_response_time") and not parent.get("first_response_time"):
|
||||
if is_system_user(communication.sender):
|
||||
|
|
@ -526,25 +570,29 @@ def update_first_response_time(parent, communication):
|
|||
first_response_time = round(time_diff_in_seconds(first_responded_on, parent.creation), 2)
|
||||
parent.db_set("first_response_time", first_response_time)
|
||||
|
||||
|
||||
def set_avg_response_time(parent, communication):
|
||||
if parent.meta.has_field("avg_response_time") and communication.sent_or_received == "Sent":
|
||||
# avg response time for all the responses
|
||||
communications = frappe.get_list("Communication", filters={
|
||||
"reference_doctype": parent.doctype,
|
||||
"reference_name": parent.name
|
||||
},
|
||||
communications = frappe.get_list(
|
||||
"Communication",
|
||||
filters={"reference_doctype": parent.doctype, "reference_name": parent.name},
|
||||
fields=["sent_or_received", "name", "creation"],
|
||||
order_by="creation"
|
||||
order_by="creation",
|
||||
)
|
||||
|
||||
if len(communications):
|
||||
response_times = []
|
||||
for i in range(len(communications)):
|
||||
if communications[i].sent_or_received == "Sent" and communications[i-1].sent_or_received == "Received":
|
||||
response_time = round(time_diff_in_seconds(communications[i].creation, communications[i-1].creation), 2)
|
||||
if (
|
||||
communications[i].sent_or_received == "Sent"
|
||||
and communications[i - 1].sent_or_received == "Received"
|
||||
):
|
||||
response_time = round(
|
||||
time_diff_in_seconds(communications[i].creation, communications[i - 1].creation), 2
|
||||
)
|
||||
if response_time > 0:
|
||||
response_times.append(response_time)
|
||||
if response_times:
|
||||
avg_response_time = sum(response_times) / len(response_times)
|
||||
parent.db_set("avg_response_time", avg_response_time)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,17 +8,25 @@ import frappe
|
|||
import frappe.email.smtp
|
||||
from frappe import _
|
||||
from frappe.email.email_body import get_message_id
|
||||
from frappe.utils import (cint, get_datetime, get_formatted_email,
|
||||
list_to_str, split_emails, validate_email_address)
|
||||
from frappe.utils import (
|
||||
cint,
|
||||
get_datetime,
|
||||
get_formatted_email,
|
||||
list_to_str,
|
||||
split_emails,
|
||||
validate_email_address,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.core.doctype.communication.communication import Communication
|
||||
|
||||
|
||||
OUTGOING_EMAIL_ACCOUNT_MISSING = _("""
|
||||
OUTGOING_EMAIL_ACCOUNT_MISSING = _(
|
||||
"""
|
||||
Unable to send mail because of a missing email account.
|
||||
Please setup default Email Account from Setup > Email > Email Account
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -64,16 +72,15 @@ def make(
|
|||
"""
|
||||
if kwargs:
|
||||
from frappe.utils.commands import warn
|
||||
|
||||
warn(
|
||||
f"Options {kwargs} used in frappe.core.doctype.communication.email.make "
|
||||
"are deprecated or unsupported",
|
||||
category=DeprecationWarning
|
||||
category=DeprecationWarning,
|
||||
)
|
||||
|
||||
if doctype and name and not frappe.has_permission(doctype=doctype, ptype="email", doc=name):
|
||||
raise frappe.PermissionError(
|
||||
f"You are not allowed to send emails related to: {doctype} {name}"
|
||||
)
|
||||
raise frappe.PermissionError(f"You are not allowed to send emails related to: {doctype} {name}")
|
||||
|
||||
return _make(
|
||||
doctype=doctype,
|
||||
|
|
@ -123,33 +130,34 @@ def _make(
|
|||
communication_type=None,
|
||||
add_signature=True,
|
||||
) -> Dict[str, str]:
|
||||
"""Internal method to make a new communication that ignores Permission checks.
|
||||
"""
|
||||
"""Internal method to make a new communication that ignores Permission checks."""
|
||||
|
||||
sender = sender or get_formatted_email(frappe.session.user)
|
||||
recipients = list_to_str(recipients) if isinstance(recipients, list) else recipients
|
||||
cc = list_to_str(cc) if isinstance(cc, list) else cc
|
||||
bcc = list_to_str(bcc) if isinstance(bcc, list) else bcc
|
||||
|
||||
comm: "Communication" = frappe.get_doc({
|
||||
"doctype":"Communication",
|
||||
"subject": subject,
|
||||
"content": content,
|
||||
"sender": sender,
|
||||
"sender_full_name":sender_full_name,
|
||||
"recipients": recipients,
|
||||
"cc": cc or None,
|
||||
"bcc": bcc or None,
|
||||
"communication_medium": communication_medium,
|
||||
"sent_or_received": sent_or_received,
|
||||
"reference_doctype": doctype,
|
||||
"reference_name": name,
|
||||
"email_template": email_template,
|
||||
"message_id":get_message_id().strip(" <>"),
|
||||
"read_receipt":read_receipt,
|
||||
"has_attachment": 1 if attachments else 0,
|
||||
"communication_type": communication_type,
|
||||
})
|
||||
comm: "Communication" = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Communication",
|
||||
"subject": subject,
|
||||
"content": content,
|
||||
"sender": sender,
|
||||
"sender_full_name": sender_full_name,
|
||||
"recipients": recipients,
|
||||
"cc": cc or None,
|
||||
"bcc": bcc or None,
|
||||
"communication_medium": communication_medium,
|
||||
"sent_or_received": sent_or_received,
|
||||
"reference_doctype": doctype,
|
||||
"reference_name": name,
|
||||
"email_template": email_template,
|
||||
"message_id": get_message_id().strip(" <>"),
|
||||
"read_receipt": read_receipt,
|
||||
"has_attachment": 1 if attachments else 0,
|
||||
"communication_type": communication_type,
|
||||
}
|
||||
)
|
||||
comm.flags.skip_add_signature = not add_signature
|
||||
comm.insert(ignore_permissions=True)
|
||||
|
||||
|
|
@ -161,9 +169,7 @@ def _make(
|
|||
|
||||
if cint(send_email):
|
||||
if not comm.get_outgoing_email_account():
|
||||
frappe.throw(
|
||||
msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError
|
||||
)
|
||||
frappe.throw(msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError)
|
||||
|
||||
comm.send_email(
|
||||
print_html=print_html,
|
||||
|
|
@ -179,7 +185,10 @@ def _make(
|
|||
|
||||
def validate_email(doc: "Communication") -> None:
|
||||
"""Validate Email Addresses of Recipients and CC"""
|
||||
if not (doc.communication_type=="Communication" and doc.communication_medium == "Email") or doc.flags.in_receive:
|
||||
if (
|
||||
not (doc.communication_type == "Communication" and doc.communication_medium == "Email")
|
||||
or doc.flags.in_receive
|
||||
):
|
||||
return
|
||||
|
||||
# validate recipients
|
||||
|
|
@ -193,36 +202,45 @@ def validate_email(doc: "Communication") -> None:
|
|||
for email in split_emails(doc.bcc):
|
||||
validate_email_address(email, throw=True)
|
||||
|
||||
|
||||
def set_incoming_outgoing_accounts(doc):
|
||||
from frappe.email.doctype.email_account.email_account import EmailAccount
|
||||
|
||||
incoming_email_account = EmailAccount.find_incoming(
|
||||
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype)
|
||||
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype
|
||||
)
|
||||
doc.incoming_email_account = incoming_email_account.email_id if incoming_email_account else None
|
||||
|
||||
doc.outgoing_email_account = EmailAccount.find_outgoing(
|
||||
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype)
|
||||
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype
|
||||
)
|
||||
|
||||
if doc.sent_or_received == "Sent":
|
||||
doc.db_set("email_account", doc.outgoing_email_account.name)
|
||||
|
||||
|
||||
def add_attachments(name, attachments):
|
||||
'''Add attachments to the given Communication'''
|
||||
"""Add attachments to the given Communication"""
|
||||
# loop through attachments
|
||||
for a in attachments:
|
||||
if isinstance(a, str):
|
||||
attach = frappe.db.get_value("File", {"name":a},
|
||||
["file_name", "file_url", "is_private"], as_dict=1)
|
||||
attach = frappe.db.get_value(
|
||||
"File", {"name": a}, ["file_name", "file_url", "is_private"], as_dict=1
|
||||
)
|
||||
# save attachments to new doc
|
||||
_file = frappe.get_doc({
|
||||
"doctype": "File",
|
||||
"file_url": attach.file_url,
|
||||
"attached_to_doctype": "Communication",
|
||||
"attached_to_name": name,
|
||||
"folder": "Home/Attachments",
|
||||
"is_private": attach.is_private
|
||||
})
|
||||
_file = frappe.get_doc(
|
||||
{
|
||||
"doctype": "File",
|
||||
"file_url": attach.file_url,
|
||||
"attached_to_doctype": "Communication",
|
||||
"attached_to_name": name,
|
||||
"folder": "Home/Attachments",
|
||||
"is_private": attach.is_private,
|
||||
}
|
||||
)
|
||||
_file.save(ignore_permissions=True)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True, methods=("GET",))
|
||||
def mark_email_as_seen(name: str = None):
|
||||
try:
|
||||
|
|
@ -233,33 +251,31 @@ def mark_email_as_seen(name: str = None):
|
|||
frappe.log_error(frappe.get_traceback())
|
||||
|
||||
finally:
|
||||
frappe.response.update({
|
||||
"type": "binary",
|
||||
"filename": "imaginary_pixel.png",
|
||||
"filecontent": (
|
||||
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00"
|
||||
b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\r"
|
||||
b"IDATx\x9cc\xf8\xff\xff?\x03\x00\x08\xfc\x02\xfe\xa7\x9a\xa0"
|
||||
b"\xa0\x00\x00\x00\x00IEND\xaeB`\x82"
|
||||
)
|
||||
})
|
||||
frappe.response.update(
|
||||
{
|
||||
"type": "binary",
|
||||
"filename": "imaginary_pixel.png",
|
||||
"filecontent": (
|
||||
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00"
|
||||
b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\r"
|
||||
b"IDATx\x9cc\xf8\xff\xff?\x03\x00\x08\xfc\x02\xfe\xa7\x9a\xa0"
|
||||
b"\xa0\x00\x00\x00\x00IEND\xaeB`\x82"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def update_communication_as_read(name):
|
||||
if not name or not isinstance(name, str):
|
||||
return
|
||||
|
||||
communication = frappe.db.get_value(
|
||||
"Communication",
|
||||
name,
|
||||
"read_by_recipient",
|
||||
as_dict=True
|
||||
)
|
||||
communication = frappe.db.get_value("Communication", name, "read_by_recipient", as_dict=True)
|
||||
|
||||
if not communication or communication.read_by_recipient:
|
||||
return
|
||||
|
||||
frappe.db.set_value("Communication", name, {
|
||||
"read_by_recipient": 1,
|
||||
"delivery_status": "Read",
|
||||
"read_by_recipient_on": get_datetime()
|
||||
})
|
||||
frappe.db.set_value(
|
||||
"Communication",
|
||||
name,
|
||||
{"read_by_recipient": 1, "delivery_status": "Read", "read_by_recipient_on": get_datetime()},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,33 +1,34 @@
|
|||
from typing import List
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.core.utils import get_parent_doc
|
||||
from frappe.utils import parse_addr, get_formatted_email, get_url
|
||||
from frappe.email.doctype.email_account.email_account import EmailAccount
|
||||
from frappe.desk.doctype.todo.todo import ToDo
|
||||
from frappe.email.doctype.email_account.email_account import EmailAccount
|
||||
from frappe.utils import get_formatted_email, get_url, parse_addr
|
||||
|
||||
|
||||
class CommunicationEmailMixin:
|
||||
"""Mixin class to handle communication mails.
|
||||
"""
|
||||
"""Mixin class to handle communication mails."""
|
||||
|
||||
def is_email_communication(self):
|
||||
return self.communication_type=="Communication" and self.communication_medium == "Email"
|
||||
return self.communication_type == "Communication" and self.communication_medium == "Email"
|
||||
|
||||
def get_owner(self):
|
||||
"""Get owner of the communication docs parent.
|
||||
"""
|
||||
"""Get owner of the communication docs parent."""
|
||||
parent_doc = get_parent_doc(self)
|
||||
return parent_doc.owner if parent_doc else None
|
||||
|
||||
def get_all_email_addresses(self, exclude_displayname=False):
|
||||
"""Get all Email addresses mentioned in the doc along with display name.
|
||||
"""
|
||||
return self.to_list(exclude_displayname=exclude_displayname) + \
|
||||
self.cc_list(exclude_displayname=exclude_displayname) + \
|
||||
self.bcc_list(exclude_displayname=exclude_displayname)
|
||||
"""Get all Email addresses mentioned in the doc along with display name."""
|
||||
return (
|
||||
self.to_list(exclude_displayname=exclude_displayname)
|
||||
+ self.cc_list(exclude_displayname=exclude_displayname)
|
||||
+ self.bcc_list(exclude_displayname=exclude_displayname)
|
||||
)
|
||||
|
||||
def get_email_with_displayname(self, email_address):
|
||||
"""Returns email address after adding displayname.
|
||||
"""
|
||||
"""Returns email address after adding displayname."""
|
||||
display_name, email = parse_addr(email_address)
|
||||
if display_name and display_name != email:
|
||||
return email_address
|
||||
|
|
@ -37,26 +38,24 @@ class CommunicationEmailMixin:
|
|||
return email_map.get(email, email)
|
||||
|
||||
def mail_recipients(self, is_inbound_mail_communcation=False):
|
||||
"""Build to(recipient) list to send an email.
|
||||
"""
|
||||
"""Build to(recipient) list to send an email."""
|
||||
# Incase of inbound mail, recipients already received the mail, no need to send again.
|
||||
if is_inbound_mail_communcation:
|
||||
return []
|
||||
|
||||
if hasattr(self, '_final_recipients'):
|
||||
if hasattr(self, "_final_recipients"):
|
||||
return self._final_recipients
|
||||
|
||||
to = self.to_list()
|
||||
self._final_recipients = list(filter(lambda id: id != 'Administrator', to))
|
||||
self._final_recipients = list(filter(lambda id: id != "Administrator", to))
|
||||
return self._final_recipients
|
||||
|
||||
def get_mail_recipients_with_displayname(self, is_inbound_mail_communcation=False):
|
||||
"""Build to(recipient) list to send an email including displayname in email.
|
||||
"""
|
||||
"""Build to(recipient) list to send an email including displayname in email."""
|
||||
to_list = self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation)
|
||||
return [self.get_email_with_displayname(email) for email in to_list]
|
||||
|
||||
def mail_cc(self, is_inbound_mail_communcation=False, include_sender = False):
|
||||
def mail_cc(self, is_inbound_mail_communcation=False, include_sender=False):
|
||||
"""Build cc list to send an email.
|
||||
|
||||
* if email copy is requested by sender, then add sender to CC.
|
||||
|
|
@ -67,7 +66,7 @@ class CommunicationEmailMixin:
|
|||
|
||||
* FixMe: Removed adding TODO owners to cc list. Check if that is needed.
|
||||
"""
|
||||
if hasattr(self, '_final_cc'):
|
||||
if hasattr(self, "_final_cc"):
|
||||
return self._final_cc
|
||||
|
||||
cc = self.cc_list()
|
||||
|
|
@ -88,11 +87,13 @@ class CommunicationEmailMixin:
|
|||
if is_inbound_mail_communcation:
|
||||
cc = cc - set(self.cc_list() + self.to_list())
|
||||
|
||||
self._final_cc = list(filter(lambda id: id != 'Administrator', cc))
|
||||
self._final_cc = list(filter(lambda id: id != "Administrator", cc))
|
||||
return self._final_cc
|
||||
|
||||
def get_mail_cc_with_displayname(self, is_inbound_mail_communcation=False, include_sender = False):
|
||||
cc_list = self.mail_cc(is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender = include_sender)
|
||||
def get_mail_cc_with_displayname(self, is_inbound_mail_communcation=False, include_sender=False):
|
||||
cc_list = self.mail_cc(
|
||||
is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=include_sender
|
||||
)
|
||||
return [self.get_email_with_displayname(email) for email in cc_list]
|
||||
|
||||
def mail_bcc(self, is_inbound_mail_communcation=False):
|
||||
|
|
@ -102,7 +103,7 @@ class CommunicationEmailMixin:
|
|||
* User must be enabled in the system
|
||||
* remove_administrator_from_email_list
|
||||
"""
|
||||
if hasattr(self, '_final_bcc'):
|
||||
if hasattr(self, "_final_bcc"):
|
||||
return self._final_bcc
|
||||
|
||||
bcc = set(self.bcc_list())
|
||||
|
|
@ -116,7 +117,7 @@ class CommunicationEmailMixin:
|
|||
if is_inbound_mail_communcation:
|
||||
bcc = bcc - set(self.bcc_list() + self.to_list())
|
||||
|
||||
self._final_bcc = list(filter(lambda id: id != 'Administrator', bcc))
|
||||
self._final_bcc = list(filter(lambda id: id != "Administrator", bcc))
|
||||
return self._final_bcc
|
||||
|
||||
def get_mail_bcc_with_displayname(self, is_inbound_mail_communcation=False):
|
||||
|
|
@ -145,22 +146,23 @@ class CommunicationEmailMixin:
|
|||
|
||||
def get_attach_link(self, print_format):
|
||||
"""Returns public link for the attachment via `templates/emails/print_link.html`."""
|
||||
return frappe.get_template("templates/emails/print_link.html").render({
|
||||
"url": get_url(),
|
||||
"doctype": self.reference_doctype,
|
||||
"name": self.reference_name,
|
||||
"print_format": print_format,
|
||||
"key": get_parent_doc(self).get_signature()
|
||||
})
|
||||
return frappe.get_template("templates/emails/print_link.html").render(
|
||||
{
|
||||
"url": get_url(),
|
||||
"doctype": self.reference_doctype,
|
||||
"name": self.reference_name,
|
||||
"print_format": print_format,
|
||||
"key": get_parent_doc(self).get_signature(),
|
||||
}
|
||||
)
|
||||
|
||||
def get_outgoing_email_account(self):
|
||||
if not hasattr(self, '_outgoing_email_account'):
|
||||
if not hasattr(self, "_outgoing_email_account"):
|
||||
if self.email_account:
|
||||
self._outgoing_email_account = EmailAccount.find(self.email_account)
|
||||
else:
|
||||
self._outgoing_email_account = EmailAccount.find_outgoing(
|
||||
match_by_email=self.sender_mailid,
|
||||
match_by_doctype=self.reference_doctype
|
||||
match_by_email=self.sender_mailid, match_by_doctype=self.reference_doctype
|
||||
)
|
||||
|
||||
if self.sent_or_received == "Sent" and self._outgoing_email_account:
|
||||
|
|
@ -169,10 +171,9 @@ class CommunicationEmailMixin:
|
|||
return self._outgoing_email_account
|
||||
|
||||
def get_incoming_email_account(self):
|
||||
if not hasattr(self, '_incoming_email_account'):
|
||||
if not hasattr(self, "_incoming_email_account"):
|
||||
self._incoming_email_account = EmailAccount.find_incoming(
|
||||
match_by_email=self.sender_mailid,
|
||||
match_by_doctype=self.reference_doctype
|
||||
match_by_email=self.sender_mailid, match_by_doctype=self.reference_doctype
|
||||
)
|
||||
return self._incoming_email_account
|
||||
|
||||
|
|
@ -180,12 +181,17 @@ class CommunicationEmailMixin:
|
|||
final_attachments = []
|
||||
|
||||
if print_format or print_html:
|
||||
d = {'print_format': print_format, 'html': print_html, 'print_format_attachment': 1,
|
||||
'doctype': self.reference_doctype, 'name': self.reference_name}
|
||||
d = {
|
||||
"print_format": print_format,
|
||||
"html": print_html,
|
||||
"print_format_attachment": 1,
|
||||
"doctype": self.reference_doctype,
|
||||
"name": self.reference_name,
|
||||
}
|
||||
final_attachments.append(d)
|
||||
|
||||
for a in self.get_attachments() or []:
|
||||
final_attachments.append({"fid": a['name']})
|
||||
final_attachments.append({"fid": a["name"]})
|
||||
|
||||
return final_attachments
|
||||
|
||||
|
|
@ -193,48 +199,57 @@ class CommunicationEmailMixin:
|
|||
email_account = self.get_outgoing_email_account()
|
||||
if email_account and email_account.send_unsubscribe_message:
|
||||
return _("Leave this conversation")
|
||||
return ''
|
||||
return ""
|
||||
|
||||
def exclude_emails_list(self, is_inbound_mail_communcation=False, include_sender=False) -> List:
|
||||
"""List of mail id's excluded while sending mail.
|
||||
"""
|
||||
"""List of mail id's excluded while sending mail."""
|
||||
all_ids = self.get_all_email_addresses(exclude_displayname=True)
|
||||
|
||||
final_ids = (
|
||||
self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation)
|
||||
+ self.mail_bcc(is_inbound_mail_communcation=is_inbound_mail_communcation)
|
||||
+ self.mail_cc(is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=include_sender)
|
||||
+ self.mail_cc(
|
||||
is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=include_sender
|
||||
)
|
||||
)
|
||||
|
||||
return list(set(all_ids) - set(final_ids))
|
||||
|
||||
def get_assignees(self):
|
||||
"""Get owners of the reference document.
|
||||
"""
|
||||
filters = {'status': 'Open', 'reference_name': self.reference_name,
|
||||
'reference_type': self.reference_doctype}
|
||||
"""Get owners of the reference document."""
|
||||
filters = {
|
||||
"status": "Open",
|
||||
"reference_name": self.reference_name,
|
||||
"reference_type": self.reference_doctype,
|
||||
}
|
||||
return ToDo.get_owners(filters)
|
||||
|
||||
@staticmethod
|
||||
def filter_thread_notification_disbled_users(emails):
|
||||
"""Filter users based on notifications for email threads setting is disabled.
|
||||
"""
|
||||
"""Filter users based on notifications for email threads setting is disabled."""
|
||||
if not emails:
|
||||
return []
|
||||
|
||||
return frappe.get_all("User", pluck="email", filters={"email": ["in", emails], "thread_notify": 0})
|
||||
return frappe.get_all(
|
||||
"User", pluck="email", filters={"email": ["in", emails], "thread_notify": 0}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def filter_disabled_users(emails):
|
||||
"""
|
||||
"""
|
||||
""" """
|
||||
if not emails:
|
||||
return []
|
||||
|
||||
return frappe.get_all("User", pluck="email", filters={"email": ["in", emails], "enabled": 0})
|
||||
|
||||
def sendmail_input_dict(self, print_html=None, print_format=None,
|
||||
send_me_a_copy=None, print_letterhead=None, is_inbound_mail_communcation=None):
|
||||
def sendmail_input_dict(
|
||||
self,
|
||||
print_html=None,
|
||||
print_format=None,
|
||||
send_me_a_copy=None,
|
||||
print_letterhead=None,
|
||||
is_inbound_mail_communcation=None,
|
||||
):
|
||||
|
||||
outgoing_email_account = self.get_outgoing_email_account()
|
||||
if not outgoing_email_account:
|
||||
|
|
@ -244,8 +259,7 @@ class CommunicationEmailMixin:
|
|||
is_inbound_mail_communcation=is_inbound_mail_communcation
|
||||
)
|
||||
cc = self.get_mail_cc_with_displayname(
|
||||
is_inbound_mail_communcation=is_inbound_mail_communcation,
|
||||
include_sender = send_me_a_copy
|
||||
is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=send_me_a_copy
|
||||
)
|
||||
bcc = self.get_mail_bcc_with_displayname(
|
||||
is_inbound_mail_communcation=is_inbound_mail_communcation
|
||||
|
|
@ -273,18 +287,24 @@ class CommunicationEmailMixin:
|
|||
"delayed": True,
|
||||
"communication": self.name,
|
||||
"read_receipt": self.read_receipt,
|
||||
"is_notification": (self.sent_or_received =="Received" and True) or False,
|
||||
"print_letterhead": print_letterhead
|
||||
"is_notification": (self.sent_or_received == "Received" and True) or False,
|
||||
"print_letterhead": print_letterhead,
|
||||
}
|
||||
|
||||
def send_email(self, print_html=None, print_format=None,
|
||||
send_me_a_copy=None, print_letterhead=None, is_inbound_mail_communcation=None):
|
||||
def send_email(
|
||||
self,
|
||||
print_html=None,
|
||||
print_format=None,
|
||||
send_me_a_copy=None,
|
||||
print_letterhead=None,
|
||||
is_inbound_mail_communcation=None,
|
||||
):
|
||||
input_dict = self.sendmail_input_dict(
|
||||
print_html=print_html,
|
||||
print_format=print_format,
|
||||
send_me_a_copy=send_me_a_copy,
|
||||
print_letterhead=print_letterhead,
|
||||
is_inbound_mail_communcation=is_inbound_mail_communcation
|
||||
is_inbound_mail_communcation=is_inbound_mail_communcation,
|
||||
)
|
||||
|
||||
if input_dict:
|
||||
|
|
|
|||
|
|
@ -7,20 +7,30 @@ import frappe
|
|||
from frappe.core.doctype.communication.communication import get_emails
|
||||
from frappe.email.doctype.email_queue.email_queue import EmailQueue
|
||||
|
||||
test_records = frappe.get_test_records('Communication')
|
||||
test_records = frappe.get_test_records("Communication")
|
||||
|
||||
|
||||
class TestCommunication(unittest.TestCase):
|
||||
|
||||
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>"]
|
||||
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"]
|
||||
invalid_email_list = [
|
||||
"[invalid!email]",
|
||||
"invalid-email",
|
||||
"tes2",
|
||||
"e",
|
||||
"rrrrrrrr",
|
||||
"manas",
|
||||
"[[[sample]]]",
|
||||
"[invalid!email].com",
|
||||
]
|
||||
|
||||
for x in valid_email_list:
|
||||
self.assertTrue(frappe.utils.parse_addr(x)[1])
|
||||
|
|
@ -29,15 +39,25 @@ class TestCommunication(unittest.TestCase):
|
|||
self.assertFalse(frappe.utils.parse_addr(x)[0])
|
||||
|
||||
def test_name(self):
|
||||
valid_email_list = ["Full Name <full@example.com>",
|
||||
'"Full Name with quotes and <weird@chars.com>" <weird@example.com>',
|
||||
"Surname, Name <name.surname@domain.com>",
|
||||
"Purchase@ABC <purchase@abc.com>", "xyz@abc2.com <xyz@abc.com>",
|
||||
"Name [something else] <name@domain.com>"]
|
||||
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"]
|
||||
invalid_email_list = [
|
||||
"[invalid!email]",
|
||||
"invalid-email",
|
||||
"tes2",
|
||||
"e",
|
||||
"rrrrrrrr",
|
||||
"manas",
|
||||
"[[[sample]]]",
|
||||
"[invalid!email].com",
|
||||
]
|
||||
|
||||
for x in valid_email_list:
|
||||
self.assertTrue(frappe.utils.parse_addr(x)[0])
|
||||
|
|
@ -46,27 +66,33 @@ class TestCommunication(unittest.TestCase):
|
|||
self.assertFalse(frappe.utils.parse_addr(x)[0])
|
||||
|
||||
def test_circular_linking(self):
|
||||
a = frappe.get_doc({
|
||||
"doctype": "Communication",
|
||||
"communication_type": "Communication",
|
||||
"content": "This was created to test circular linking: Communication A",
|
||||
}).insert(ignore_permissions=True)
|
||||
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)
|
||||
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)
|
||||
c = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Communication",
|
||||
"communication_type": "Communication",
|
||||
"content": "This was created to test circular linking: Communication C",
|
||||
"reference_doctype": "Communication",
|
||||
"reference_name": b.name,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
a = frappe.get_doc("Communication", a.name)
|
||||
a.reference_doctype = "Communication"
|
||||
|
|
@ -77,20 +103,24 @@ class TestCommunication(unittest.TestCase):
|
|||
def test_deduplication_timeline_links(self):
|
||||
frappe.delete_doc_if_exists("Note", "deduplication timeline links")
|
||||
|
||||
note = frappe.get_doc({
|
||||
"doctype": "Note",
|
||||
"title": "deduplication timeline links",
|
||||
"content": "deduplication timeline links"
|
||||
}).insert(ignore_permissions=True)
|
||||
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)
|
||||
comm = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Communication",
|
||||
"communication_type": "Communication",
|
||||
"content": "Deduplication of Links",
|
||||
"communication_medium": "Email",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
#adding same link twice
|
||||
# adding same link twice
|
||||
comm.add_link(link_doctype="Note", link_name=note.name, autosave=True)
|
||||
comm.add_link(link_doctype="Note", link_name=note.name, autosave=True)
|
||||
|
||||
|
|
@ -99,35 +129,43 @@ class TestCommunication(unittest.TestCase):
|
|||
self.assertNotEqual(2, len(comm.timeline_links))
|
||||
|
||||
def test_contacts_attached(self):
|
||||
contact_sender = frappe.get_doc({
|
||||
"doctype": "Contact",
|
||||
"first_name": "contact_sender",
|
||||
})
|
||||
contact_sender = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Contact",
|
||||
"first_name": "contact_sender",
|
||||
}
|
||||
)
|
||||
contact_sender.add_email("comm_sender@example.com")
|
||||
contact_sender.insert(ignore_permissions=True)
|
||||
|
||||
contact_recipient = frappe.get_doc({
|
||||
"doctype": "Contact",
|
||||
"first_name": "contact_recipient",
|
||||
})
|
||||
contact_recipient = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Contact",
|
||||
"first_name": "contact_recipient",
|
||||
}
|
||||
)
|
||||
contact_recipient.add_email("comm_recipient@example.com")
|
||||
contact_recipient.insert(ignore_permissions=True)
|
||||
|
||||
contact_cc = frappe.get_doc({
|
||||
"doctype": "Contact",
|
||||
"first_name": "contact_cc",
|
||||
})
|
||||
contact_cc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Contact",
|
||||
"first_name": "contact_cc",
|
||||
}
|
||||
)
|
||||
contact_cc.add_email("comm_cc@example.com")
|
||||
contact_cc.insert(ignore_permissions=True)
|
||||
|
||||
comm = frappe.get_doc({
|
||||
"doctype": "Communication",
|
||||
"communication_medium": "Email",
|
||||
"subject": "Contacts Attached Test",
|
||||
"sender": "comm_sender@example.com",
|
||||
"recipients": "comm_recipient@example.com",
|
||||
"cc": "comm_cc@example.com"
|
||||
}).insert(ignore_permissions=True)
|
||||
comm = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Communication",
|
||||
"communication_medium": "Email",
|
||||
"subject": "Contacts Attached Test",
|
||||
"sender": "comm_sender@example.com",
|
||||
"recipients": "comm_recipient@example.com",
|
||||
"cc": "comm_cc@example.com",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
comm = frappe.get_doc("Communication", comm.name)
|
||||
|
||||
|
|
@ -144,27 +182,29 @@ class TestCommunication(unittest.TestCase):
|
|||
|
||||
frappe.delete_doc_if_exists("Note", "get communication data")
|
||||
|
||||
note = frappe.get_doc({
|
||||
"doctype": "Note",
|
||||
"title": "get communication data",
|
||||
"content": "get communication data"
|
||||
}).insert(ignore_permissions=True)
|
||||
note = frappe.get_doc(
|
||||
{"doctype": "Note", "title": "get communication data", "content": "get communication data"}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
comm_note_1 = frappe.get_doc({
|
||||
"doctype": "Communication",
|
||||
"communication_type": "Communication",
|
||||
"content": "Test Get Communication Data 1",
|
||||
"communication_medium": "Email"
|
||||
}).insert(ignore_permissions=True)
|
||||
comm_note_1 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Communication",
|
||||
"communication_type": "Communication",
|
||||
"content": "Test Get Communication Data 1",
|
||||
"communication_medium": "Email",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
comm_note_1.add_link(link_doctype="Note", link_name=note.name, autosave=True)
|
||||
|
||||
comm_note_2 = frappe.get_doc({
|
||||
"doctype": "Communication",
|
||||
"communication_type": "Communication",
|
||||
"content": "Test Get Communication Data 2",
|
||||
"communication_medium": "Email"
|
||||
}).insert(ignore_permissions=True)
|
||||
comm_note_2 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Communication",
|
||||
"communication_type": "Communication",
|
||||
"content": "Test Get Communication Data 2",
|
||||
"communication_medium": "Email",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
comm_note_2.add_link(link_doctype="Note", link_name=note.name, autosave=True)
|
||||
|
||||
|
|
@ -182,19 +222,23 @@ class TestCommunication(unittest.TestCase):
|
|||
|
||||
create_email_account()
|
||||
|
||||
note = frappe.get_doc({
|
||||
"doctype": "Note",
|
||||
"title": "test document link in email",
|
||||
"content": "test document link in email"
|
||||
}).insert(ignore_permissions=True)
|
||||
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)
|
||||
comm = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Communication",
|
||||
"communication_medium": "Email",
|
||||
"subject": "Document Link in Email",
|
||||
"sender": "comm_sender@example.com",
|
||||
"recipients": "comm_recipient+{0}+{1}@example.com".format(quote("Note"), quote(note.name)),
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
doc_links = []
|
||||
for timeline_link in comm.timeline_links:
|
||||
|
|
@ -205,9 +249,9 @@ class TestCommunication(unittest.TestCase):
|
|||
def test_parse_emails(self):
|
||||
emails = get_emails(
|
||||
[
|
||||
'comm_recipient+DocType+DocName@example.com',
|
||||
"comm_recipient+DocType+DocName@example.com",
|
||||
'"First, LastName" <first.lastname@email.com>',
|
||||
'test@user.com'
|
||||
"test@user.com",
|
||||
]
|
||||
)
|
||||
|
||||
|
|
@ -215,99 +259,108 @@ class TestCommunication(unittest.TestCase):
|
|||
self.assertEqual(emails[1], "first.lastname@email.com")
|
||||
self.assertEqual(emails[2], "test@user.com")
|
||||
|
||||
|
||||
class TestCommunicationEmailMixin(unittest.TestCase):
|
||||
def new_communication(self, recipients=None, cc=None, bcc=None):
|
||||
recipients = ', '.join(recipients or [])
|
||||
cc = ', '.join(cc or [])
|
||||
bcc = ', '.join(bcc or [])
|
||||
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)
|
||||
comm = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Communication",
|
||||
"communication_type": "Communication",
|
||||
"communication_medium": "Email",
|
||||
"content": "Test content",
|
||||
"recipients": recipients,
|
||||
"cc": cc,
|
||||
"bcc": bcc,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
return comm
|
||||
|
||||
def new_user(self, email, **user_data):
|
||||
user_data.setdefault('first_name', 'first_name')
|
||||
user = frappe.new_doc('User')
|
||||
user_data.setdefault("first_name", "first_name")
|
||||
user = frappe.new_doc("User")
|
||||
user.email = email
|
||||
user.update(user_data)
|
||||
user.insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||
return user
|
||||
|
||||
def test_recipients(self):
|
||||
to_list = ['to@test.com', 'receiver <to+1@test.com>', 'to@test.com']
|
||||
comm = self.new_communication(recipients = to_list)
|
||||
to_list = ["to@test.com", "receiver <to+1@test.com>", "to@test.com"]
|
||||
comm = self.new_communication(recipients=to_list)
|
||||
res = comm.get_mail_recipients_with_displayname()
|
||||
self.assertCountEqual(res, ['to@test.com', 'receiver <to+1@test.com>'])
|
||||
self.assertCountEqual(res, ["to@test.com", "receiver <to+1@test.com>"])
|
||||
comm.delete()
|
||||
|
||||
def test_cc(self):
|
||||
to_list = ['to@test.com']
|
||||
cc_list = ['cc+1@test.com', 'cc <cc+2@test.com>', 'to@test.com']
|
||||
user = self.new_user(email='cc+1@test.com', thread_notify=0)
|
||||
to_list = ["to@test.com"]
|
||||
cc_list = ["cc+1@test.com", "cc <cc+2@test.com>", "to@test.com"]
|
||||
user = self.new_user(email="cc+1@test.com", thread_notify=0)
|
||||
comm = self.new_communication(recipients=to_list, cc=cc_list)
|
||||
res = comm.get_mail_cc_with_displayname()
|
||||
self.assertCountEqual(res, ['cc <cc+2@test.com>'])
|
||||
self.assertCountEqual(res, ["cc <cc+2@test.com>"])
|
||||
user.delete()
|
||||
comm.delete()
|
||||
|
||||
def test_bcc(self):
|
||||
bcc_list = ['bcc+1@test.com', 'cc <bcc+2@test.com>', ]
|
||||
user = self.new_user(email='bcc+2@test.com', enabled=0)
|
||||
bcc_list = [
|
||||
"bcc+1@test.com",
|
||||
"cc <bcc+2@test.com>",
|
||||
]
|
||||
user = self.new_user(email="bcc+2@test.com", enabled=0)
|
||||
comm = self.new_communication(bcc=bcc_list)
|
||||
res = comm.get_mail_bcc_with_displayname()
|
||||
self.assertCountEqual(res, ['bcc+1@test.com'])
|
||||
self.assertCountEqual(res, ["bcc+1@test.com"])
|
||||
user.delete()
|
||||
comm.delete()
|
||||
|
||||
def test_sendmail(self):
|
||||
to_list = ['to <to@test.com>']
|
||||
cc_list = ['cc <cc+1@test.com>', 'cc <cc+2@test.com>']
|
||||
to_list = ["to <to@test.com>"]
|
||||
cc_list = ["cc <cc+1@test.com>", "cc <cc+2@test.com>"]
|
||||
|
||||
comm = self.new_communication(recipients=to_list, cc=cc_list)
|
||||
comm.send_email()
|
||||
doc = EmailQueue.find_one_by_filters(communication=comm.name)
|
||||
mail_receivers = [each.recipient for each in doc.recipients]
|
||||
self.assertIsNotNone(doc)
|
||||
self.assertCountEqual(to_list+cc_list, mail_receivers)
|
||||
self.assertCountEqual(to_list + cc_list, mail_receivers)
|
||||
doc.delete()
|
||||
comm.delete()
|
||||
|
||||
|
||||
def create_email_account():
|
||||
frappe.delete_doc_if_exists("Email Account", "_Test Comm Account 1")
|
||||
|
||||
frappe.flags.mute_emails = False
|
||||
frappe.flags.sent_mail = None
|
||||
|
||||
email_account = frappe.get_doc({
|
||||
"is_default": 1,
|
||||
"is_global": 1,
|
||||
"doctype": "Email Account",
|
||||
"domain":"example.com",
|
||||
"append_to": "ToDo",
|
||||
"email_account_name": "_Test Comm Account 1",
|
||||
"enable_outgoing": 1,
|
||||
"smtp_server": "test.example.com",
|
||||
"email_id": "test_comm@example.com",
|
||||
"password": "password",
|
||||
"add_signature": 1,
|
||||
"signature": "\nBest Wishes\nTest Signature",
|
||||
"enable_auto_reply": 1,
|
||||
"auto_reply_message": "",
|
||||
"enable_incoming": 1,
|
||||
"notify_if_unreplied": 1,
|
||||
"unreplied_for_mins": 20,
|
||||
"send_notification_to": "test_comm@example.com",
|
||||
"pop3_server": "pop.test.example.com",
|
||||
"imap_folder": [{"folder_name": "INBOX", "append_to": "ToDo"}],
|
||||
"no_remaining":"0",
|
||||
"enable_automatic_linking": 1
|
||||
}).insert(ignore_permissions=True)
|
||||
email_account = frappe.get_doc(
|
||||
{
|
||||
"is_default": 1,
|
||||
"is_global": 1,
|
||||
"doctype": "Email Account",
|
||||
"domain": "example.com",
|
||||
"append_to": "ToDo",
|
||||
"email_account_name": "_Test Comm Account 1",
|
||||
"enable_outgoing": 1,
|
||||
"smtp_server": "test.example.com",
|
||||
"email_id": "test_comm@example.com",
|
||||
"password": "password",
|
||||
"add_signature": 1,
|
||||
"signature": "\nBest Wishes\nTest Signature",
|
||||
"enable_auto_reply": 1,
|
||||
"auto_reply_message": "",
|
||||
"enable_incoming": 1,
|
||||
"notify_if_unreplied": 1,
|
||||
"unreplied_for_mins": 20,
|
||||
"send_notification_to": "test_comm@example.com",
|
||||
"pop3_server": "pop.test.example.com",
|
||||
"imap_folder": [{"folder_name": "INBOX", "append_to": "ToDo"}],
|
||||
"no_remaining": "0",
|
||||
"enable_automatic_linking": 1,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
return email_account
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class CommunicationLink(Document):
|
||||
pass
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Communication Link", ["link_doctype", "link_name"])
|
||||
frappe.db.add_index("Communication Link", ["link_doctype", "link_name"])
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class CustomDocPerm(Document):
|
||||
def on_update(self):
|
||||
frappe.clear_cache(doctype = self.parent)
|
||||
frappe.clear_cache(doctype=self.parent)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import frappe
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
# test_records = frappe.get_test_records('Custom DocPerm')
|
||||
|
||||
|
||||
class TestCustomDocPerm(unittest.TestCase):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -5,16 +5,18 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class CustomRole(Document):
|
||||
def validate(self):
|
||||
if self.report and not self.ref_doctype:
|
||||
self.ref_doctype = frappe.db.get_value('Report', self.report, 'ref_doctype')
|
||||
self.ref_doctype = frappe.db.get_value("Report", self.report, "ref_doctype")
|
||||
|
||||
|
||||
def get_custom_allowed_roles(field, name):
|
||||
allowed_roles = []
|
||||
custom_role = frappe.db.get_value('Custom Role', {field: name}, 'name')
|
||||
custom_role = frappe.db.get_value("Custom Role", {field: name}, "name")
|
||||
if custom_role:
|
||||
custom_role_doc = frappe.get_doc('Custom Role', custom_role)
|
||||
custom_role_doc = frappe.get_doc("Custom Role", custom_role)
|
||||
allowed_roles = [d.role for d in custom_role_doc.roles]
|
||||
|
||||
return allowed_roles
|
||||
return allowed_roles
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import frappe
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
# test_records = frappe.get_test_records('Custom Role')
|
||||
|
||||
|
||||
class TestCustomRole(unittest.TestCase):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -4,5 +4,6 @@
|
|||
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class DataExport(Document):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -1,47 +1,78 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
import frappe.permissions
|
||||
import re, csv, os
|
||||
from frappe.utils.csvutils import UnicodeWriter
|
||||
from frappe.utils import cstr, formatdate, format_datetime, parse_json, cint, format_duration
|
||||
from frappe.core.doctype.access_log.access_log import make_access_log
|
||||
import csv
|
||||
import os
|
||||
import re
|
||||
|
||||
import frappe
|
||||
import frappe.permissions
|
||||
from frappe import _
|
||||
from frappe.core.doctype.access_log.access_log import make_access_log
|
||||
from frappe.utils import cint, cstr, format_datetime, format_duration, formatdate, parse_json
|
||||
from frappe.utils.csvutils import UnicodeWriter
|
||||
|
||||
reflags = {"I": re.I, "L": re.L, "M": re.M, "U": re.U, "S": re.S, "X": re.X, "D": re.DEBUG}
|
||||
|
||||
reflags = {
|
||||
"I":re.I,
|
||||
"L":re.L,
|
||||
"M":re.M,
|
||||
"U":re.U,
|
||||
"S":re.S,
|
||||
"X":re.X,
|
||||
"D": re.DEBUG
|
||||
}
|
||||
|
||||
def get_data_keys():
|
||||
return frappe._dict({
|
||||
"data_separator": _('Start entering data below this line'),
|
||||
"main_table": _("Table") + ":",
|
||||
"parent_table": _("Parent Table") + ":",
|
||||
"columns": _("Column Name") + ":",
|
||||
"doctype": _("DocType") + ":"
|
||||
})
|
||||
return frappe._dict(
|
||||
{
|
||||
"data_separator": _("Start entering data below this line"),
|
||||
"main_table": _("Table") + ":",
|
||||
"parent_table": _("Parent Table") + ":",
|
||||
"columns": _("Column Name") + ":",
|
||||
"doctype": _("DocType") + ":",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def export_data(doctype=None, parent_doctype=None, all_doctypes=True, with_data=False,
|
||||
select_columns=None, file_type='CSV', template=False, filters=None):
|
||||
def export_data(
|
||||
doctype=None,
|
||||
parent_doctype=None,
|
||||
all_doctypes=True,
|
||||
with_data=False,
|
||||
select_columns=None,
|
||||
file_type="CSV",
|
||||
template=False,
|
||||
filters=None,
|
||||
):
|
||||
_doctype = doctype
|
||||
if isinstance(_doctype, list):
|
||||
_doctype = _doctype[0]
|
||||
make_access_log(doctype=_doctype, file_type=file_type, columns=select_columns, filters=filters, method=parent_doctype)
|
||||
exporter = DataExporter(doctype=doctype, parent_doctype=parent_doctype, all_doctypes=all_doctypes, with_data=with_data,
|
||||
select_columns=select_columns, file_type=file_type, template=template, filters=filters)
|
||||
make_access_log(
|
||||
doctype=_doctype,
|
||||
file_type=file_type,
|
||||
columns=select_columns,
|
||||
filters=filters,
|
||||
method=parent_doctype,
|
||||
)
|
||||
exporter = DataExporter(
|
||||
doctype=doctype,
|
||||
parent_doctype=parent_doctype,
|
||||
all_doctypes=all_doctypes,
|
||||
with_data=with_data,
|
||||
select_columns=select_columns,
|
||||
file_type=file_type,
|
||||
template=template,
|
||||
filters=filters,
|
||||
)
|
||||
exporter.build_response()
|
||||
|
||||
|
||||
class DataExporter:
|
||||
def __init__(self, doctype=None, parent_doctype=None, all_doctypes=True, with_data=False,
|
||||
select_columns=None, file_type='CSV', template=False, filters=None):
|
||||
def __init__(
|
||||
self,
|
||||
doctype=None,
|
||||
parent_doctype=None,
|
||||
all_doctypes=True,
|
||||
with_data=False,
|
||||
select_columns=None,
|
||||
file_type="CSV",
|
||||
template=False,
|
||||
filters=None,
|
||||
):
|
||||
self.doctype = doctype
|
||||
self.parent_doctype = parent_doctype
|
||||
self.all_doctypes = all_doctypes
|
||||
|
|
@ -81,18 +112,18 @@ class DataExporter:
|
|||
|
||||
def build_response(self):
|
||||
self.writer = UnicodeWriter()
|
||||
self.name_field = 'parent' if self.parent_doctype != self.doctype else 'name'
|
||||
self.name_field = "parent" if self.parent_doctype != self.doctype else "name"
|
||||
|
||||
if self.template:
|
||||
self.add_main_header()
|
||||
|
||||
self.writer.writerow([''])
|
||||
self.writer.writerow([""])
|
||||
self.tablerow = [self.data_keys.doctype]
|
||||
self.labelrow = [_("Column Labels:")]
|
||||
self.fieldrow = [self.data_keys.columns]
|
||||
self.mandatoryrow = [_("Mandatory:")]
|
||||
self.typerow = [_('Type:')]
|
||||
self.inforow = [_('Info:')]
|
||||
self.typerow = [_("Type:")]
|
||||
self.inforow = [_("Info:")]
|
||||
self.columns = []
|
||||
|
||||
self.build_field_columns(self.doctype)
|
||||
|
|
@ -100,74 +131,99 @@ class DataExporter:
|
|||
if self.all_doctypes:
|
||||
for d in self.child_doctypes:
|
||||
self.append_empty_field_column()
|
||||
if (self.select_columns and self.select_columns.get(d['doctype'], None)) or not self.select_columns:
|
||||
if (
|
||||
self.select_columns and self.select_columns.get(d["doctype"], None)
|
||||
) or not self.select_columns:
|
||||
# if atleast one column is selected for this doctype
|
||||
self.build_field_columns(d['doctype'], d['parentfield'])
|
||||
self.build_field_columns(d["doctype"], d["parentfield"])
|
||||
|
||||
self.add_field_headings()
|
||||
self.add_data()
|
||||
if self.with_data and not self.data:
|
||||
frappe.respond_as_web_page(_('No Data'), _('There is no data to be exported'), indicator_color='orange')
|
||||
frappe.respond_as_web_page(
|
||||
_("No Data"), _("There is no data to be exported"), indicator_color="orange"
|
||||
)
|
||||
|
||||
if self.file_type == 'Excel':
|
||||
if self.file_type == "Excel":
|
||||
self.build_response_as_excel()
|
||||
else:
|
||||
# write out response as a type csv
|
||||
frappe.response['result'] = cstr(self.writer.getvalue())
|
||||
frappe.response['type'] = 'csv'
|
||||
frappe.response['doctype'] = self.doctype
|
||||
frappe.response["result"] = cstr(self.writer.getvalue())
|
||||
frappe.response["type"] = "csv"
|
||||
frappe.response["doctype"] = self.doctype
|
||||
|
||||
def add_main_header(self):
|
||||
self.writer.writerow([_('Data Import Template')])
|
||||
self.writer.writerow([_("Data Import Template")])
|
||||
self.writer.writerow([self.data_keys.main_table, self.doctype])
|
||||
|
||||
if self.parent_doctype != self.doctype:
|
||||
self.writer.writerow([self.data_keys.parent_table, self.parent_doctype])
|
||||
else:
|
||||
self.writer.writerow([''])
|
||||
self.writer.writerow([""])
|
||||
|
||||
self.writer.writerow([''])
|
||||
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([_("Notes:")])
|
||||
self.writer.writerow([_("Please do not change the template headings.")])
|
||||
self.writer.writerow([_("First data column must be blank.")])
|
||||
self.writer.writerow(
|
||||
[_('If you are uploading new records, leave the "name" (ID) column blank.')]
|
||||
)
|
||||
self.writer.writerow(
|
||||
[_('If you are uploading new records, "Naming Series" becomes mandatory, if present.')]
|
||||
)
|
||||
self.writer.writerow(
|
||||
[
|
||||
_(
|
||||
"Only mandatory fields are necessary for new records. You can delete non-mandatory columns if you wish."
|
||||
)
|
||||
]
|
||||
)
|
||||
self.writer.writerow([_("For updating, you can update only selective columns.")])
|
||||
self.writer.writerow(
|
||||
[_("You can only upload upto 5000 records in one go. (may be less in some cases)")]
|
||||
)
|
||||
if self.name_field == "parent":
|
||||
self.writer.writerow([_('"Parent" signifies the parent table in which this row must be added')])
|
||||
self.writer.writerow([_('If you are updating, please select "Overwrite" else existing rows will not be deleted.')])
|
||||
self.writer.writerow(
|
||||
[_('If you are updating, please select "Overwrite" else existing rows will not be deleted.')]
|
||||
)
|
||||
|
||||
def build_field_columns(self, dt, parentfield=None):
|
||||
meta = frappe.get_meta(dt)
|
||||
|
||||
# build list of valid docfields
|
||||
tablecolumns = []
|
||||
table_name = 'tab' + dt
|
||||
table_name = "tab" + dt
|
||||
for f in frappe.db.get_table_columns_description(table_name):
|
||||
field = meta.get_field(f.name)
|
||||
if field and ((self.select_columns and f.name in self.select_columns[dt]) or not self.select_columns):
|
||||
if field and (
|
||||
(self.select_columns and f.name in self.select_columns[dt]) or not self.select_columns
|
||||
):
|
||||
tablecolumns.append(field)
|
||||
|
||||
tablecolumns.sort(key = lambda a: int(a.idx))
|
||||
tablecolumns.sort(key=lambda a: int(a.idx))
|
||||
|
||||
_column_start_end = frappe._dict(start=0)
|
||||
|
||||
if dt==self.doctype:
|
||||
if (meta.get('autoname') and meta.get('autoname').lower()=='prompt') or (self.with_data):
|
||||
if dt == self.doctype:
|
||||
if (meta.get("autoname") and meta.get("autoname").lower() == "prompt") or (self.with_data):
|
||||
self._append_name_column()
|
||||
|
||||
# if importing only child table for new record, add parent field
|
||||
if meta.get('istable') and not self.with_data:
|
||||
self.append_field_column(frappe._dict({
|
||||
"fieldname": "parent",
|
||||
"parent": "",
|
||||
"label": "Parent",
|
||||
"fieldtype": "Data",
|
||||
"reqd": 1,
|
||||
"info": _("Parent is the name of the document to which the data will get added to.")
|
||||
}), True)
|
||||
if meta.get("istable") and not self.with_data:
|
||||
self.append_field_column(
|
||||
frappe._dict(
|
||||
{
|
||||
"fieldname": "parent",
|
||||
"parent": "",
|
||||
"label": "Parent",
|
||||
"fieldtype": "Data",
|
||||
"reqd": 1,
|
||||
"info": _("Parent is the name of the document to which the data will get added to."),
|
||||
}
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
_column_start_end = frappe._dict(start=0)
|
||||
else:
|
||||
|
|
@ -184,7 +240,7 @@ class DataExporter:
|
|||
self.append_field_column(docfield, False)
|
||||
|
||||
# if there is one column, add a blank column (?)
|
||||
if len(self.columns)-_column_start_end.start == 1:
|
||||
if len(self.columns) - _column_start_end.start == 1:
|
||||
self.append_empty_field_column()
|
||||
|
||||
# append DocType name
|
||||
|
|
@ -204,18 +260,21 @@ class DataExporter:
|
|||
return
|
||||
if not for_mandatory and docfield.reqd:
|
||||
return
|
||||
if docfield.fieldname in ('parenttype', 'trash_reason'):
|
||||
if docfield.fieldname in ("parenttype", "trash_reason"):
|
||||
return
|
||||
if docfield.hidden:
|
||||
return
|
||||
if self.select_columns and docfield.fieldname not in self.select_columns.get(docfield.parent, []) \
|
||||
and docfield.fieldname!="name":
|
||||
if (
|
||||
self.select_columns
|
||||
and docfield.fieldname not in self.select_columns.get(docfield.parent, [])
|
||||
and docfield.fieldname != "name"
|
||||
):
|
||||
return
|
||||
|
||||
self.tablerow.append("")
|
||||
self.fieldrow.append(docfield.fieldname)
|
||||
self.labelrow.append(_(docfield.label))
|
||||
self.mandatoryrow.append(docfield.reqd and 'Yes' or 'No')
|
||||
self.mandatoryrow.append(docfield.reqd and "Yes" or "No")
|
||||
self.typerow.append(docfield.fieldtype)
|
||||
self.inforow.append(self.getinforow(docfield))
|
||||
self.columns.append(docfield.fieldname)
|
||||
|
|
@ -232,15 +291,15 @@ class DataExporter:
|
|||
@staticmethod
|
||||
def getinforow(docfield):
|
||||
"""make info comment for options, links etc."""
|
||||
if docfield.fieldtype == 'Select':
|
||||
if docfield.fieldtype == "Select":
|
||||
if not docfield.options:
|
||||
return ''
|
||||
return ""
|
||||
else:
|
||||
return _("One of") + ': %s' % ', '.join(filter(None, docfield.options.split('\n')))
|
||||
elif docfield.fieldtype == 'Link':
|
||||
return 'Valid %s' % docfield.options
|
||||
elif docfield.fieldtype == 'Int':
|
||||
return 'Integer'
|
||||
return _("One of") + ": %s" % ", ".join(filter(None, docfield.options.split("\n")))
|
||||
elif docfield.fieldtype == "Link":
|
||||
return "Valid %s" % docfield.options
|
||||
elif docfield.fieldtype == "Int":
|
||||
return "Integer"
|
||||
elif docfield.fieldtype == "Check":
|
||||
return "0 or 1"
|
||||
elif docfield.fieldtype in ["Date", "Datetime"]:
|
||||
|
|
@ -248,7 +307,7 @@ class DataExporter:
|
|||
elif hasattr(docfield, "info"):
|
||||
return docfield.info
|
||||
else:
|
||||
return ''
|
||||
return ""
|
||||
|
||||
def add_field_headings(self):
|
||||
self.writer.writerow(self.tablerow)
|
||||
|
|
@ -262,6 +321,7 @@ class DataExporter:
|
|||
|
||||
def add_data(self):
|
||||
from frappe.query_builder import DocType
|
||||
|
||||
if self.template and not self.with_data:
|
||||
return
|
||||
|
||||
|
|
@ -270,26 +330,28 @@ class DataExporter:
|
|||
# sort nested set doctypes by `lft asc`
|
||||
order_by = None
|
||||
table_columns = frappe.db.get_table_columns(self.parent_doctype)
|
||||
if 'lft' in table_columns and 'rgt' in table_columns:
|
||||
order_by = '`tab{doctype}`.`lft` asc'.format(doctype=self.parent_doctype)
|
||||
if "lft" in table_columns and "rgt" in table_columns:
|
||||
order_by = "`tab{doctype}`.`lft` asc".format(doctype=self.parent_doctype)
|
||||
# get permitted data only
|
||||
self.data = frappe.get_list(self.doctype, fields=["*"], filters=self.filters, limit_page_length=None, order_by=order_by)
|
||||
self.data = frappe.get_list(
|
||||
self.doctype, fields=["*"], filters=self.filters, limit_page_length=None, order_by=order_by
|
||||
)
|
||||
|
||||
for doc in self.data:
|
||||
op = self.docs_to_export.get("op")
|
||||
names = self.docs_to_export.get("name")
|
||||
|
||||
if names and op:
|
||||
if op == '=' and doc.name not in names:
|
||||
if op == "=" and doc.name not in names:
|
||||
continue
|
||||
elif op == '!=' and doc.name in names:
|
||||
elif op == "!=" and doc.name in names:
|
||||
continue
|
||||
elif names:
|
||||
try:
|
||||
sflags = self.docs_to_export.get("flags", "I,U").upper()
|
||||
flags = 0
|
||||
for a in re.split(r'\W+', sflags):
|
||||
flags = flags | reflags.get(a,0)
|
||||
for a in re.split(r"\W+", sflags):
|
||||
flags = flags | reflags.get(a, 0)
|
||||
|
||||
c = re.compile(names, flags)
|
||||
m = c.match(doc.name)
|
||||
|
|
@ -315,7 +377,7 @@ class DataExporter:
|
|||
.orderby(child_doctype_table.idx)
|
||||
)
|
||||
for ci, child in enumerate(data_row.run(as_dict=True)):
|
||||
self.add_data_row(rows, c['doctype'], c['parentfield'], child, ci)
|
||||
self.add_data_row(rows, c["doctype"], c["parentfield"], child, ci)
|
||||
|
||||
for row in rows:
|
||||
self.writer.writerow(row)
|
||||
|
|
@ -333,7 +395,7 @@ class DataExporter:
|
|||
_column_start_end = self.column_start_end.get((dt, parentfield))
|
||||
|
||||
if _column_start_end:
|
||||
for i, c in enumerate(self.columns[_column_start_end.start:_column_start_end.end]):
|
||||
for i, c in enumerate(self.columns[_column_start_end.start : _column_start_end.end]):
|
||||
df = meta.get_field(c)
|
||||
fieldtype = df.fieldtype if df else "Data"
|
||||
value = d.get(c, "")
|
||||
|
|
@ -349,27 +411,33 @@ class DataExporter:
|
|||
|
||||
def build_response_as_excel(self):
|
||||
filename = frappe.generate_hash("", 10)
|
||||
with open(filename, 'wb') as f:
|
||||
f.write(cstr(self.writer.getvalue()).encode('utf-8'))
|
||||
with open(filename, "wb") as f:
|
||||
f.write(cstr(self.writer.getvalue()).encode("utf-8"))
|
||||
f = open(filename)
|
||||
reader = csv.reader(f)
|
||||
|
||||
from frappe.utils.xlsxutils import make_xlsx
|
||||
xlsx_file = make_xlsx(reader, "Data Import Template" if self.template else 'Data Export')
|
||||
|
||||
xlsx_file = make_xlsx(reader, "Data Import Template" if self.template else "Data Export")
|
||||
|
||||
f.close()
|
||||
os.remove(filename)
|
||||
|
||||
# write out response as a xlsx type
|
||||
frappe.response['filename'] = self.doctype + '.xlsx'
|
||||
frappe.response['filecontent'] = xlsx_file.getvalue()
|
||||
frappe.response['type'] = 'binary'
|
||||
frappe.response["filename"] = self.doctype + ".xlsx"
|
||||
frappe.response["filecontent"] = xlsx_file.getvalue()
|
||||
frappe.response["type"] = "binary"
|
||||
|
||||
def _append_name_column(self, dt=None):
|
||||
self.append_field_column(frappe._dict({
|
||||
"fieldname": "name" if dt else self.name_field,
|
||||
"parent": dt or "",
|
||||
"label": "ID",
|
||||
"fieldtype": "Data",
|
||||
"reqd": 1,
|
||||
}), True)
|
||||
self.append_field_column(
|
||||
frappe._dict(
|
||||
{
|
||||
"fieldname": "name" if dt else self.name_field,
|
||||
"parent": dt or "",
|
||||
"label": "ID",
|
||||
"fieldtype": "Data",
|
||||
"reqd": 1,
|
||||
}
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,13 +2,15 @@
|
|||
# Copyright (c) 2019, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.core.doctype.data_export.exporter import DataExporter
|
||||
|
||||
|
||||
class TestDataExporter(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.doctype_name = 'Test DocType for Export Tool'
|
||||
self.doc_name = 'Test Data for Export Tool'
|
||||
self.doctype_name = "Test DocType for Export Tool"
|
||||
self.doc_name = "Test Data for Export Tool"
|
||||
self.create_doctype_if_not_exists(doctype_name=self.doctype_name)
|
||||
self.create_test_data()
|
||||
|
||||
|
|
@ -17,42 +19,49 @@ class TestDataExporter(unittest.TestCase):
|
|||
Helper Function for setting up doctypes
|
||||
"""
|
||||
if force:
|
||||
frappe.delete_doc_if_exists('DocType', doctype_name)
|
||||
frappe.delete_doc_if_exists('DocType', 'Child 1 of ' + doctype_name)
|
||||
frappe.delete_doc_if_exists("DocType", doctype_name)
|
||||
frappe.delete_doc_if_exists("DocType", "Child 1 of " + doctype_name)
|
||||
|
||||
if frappe.db.exists('DocType', doctype_name):
|
||||
if frappe.db.exists("DocType", doctype_name):
|
||||
return
|
||||
|
||||
# Child Table 1
|
||||
table_1_name = 'Child 1 of ' + doctype_name
|
||||
frappe.get_doc({
|
||||
'doctype': 'DocType',
|
||||
'name': table_1_name,
|
||||
'module': 'Custom',
|
||||
'custom': 1,
|
||||
'istable': 1,
|
||||
'fields': [
|
||||
{'label': 'Child Title', 'fieldname': 'child_title', 'reqd': 1, 'fieldtype': 'Data'},
|
||||
{'label': 'Child Number', 'fieldname': 'child_number', 'fieldtype': 'Int'},
|
||||
]
|
||||
}).insert()
|
||||
table_1_name = "Child 1 of " + doctype_name
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"name": table_1_name,
|
||||
"module": "Custom",
|
||||
"custom": 1,
|
||||
"istable": 1,
|
||||
"fields": [
|
||||
{"label": "Child Title", "fieldname": "child_title", "reqd": 1, "fieldtype": "Data"},
|
||||
{"label": "Child Number", "fieldname": "child_number", "fieldtype": "Int"},
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# Main Table
|
||||
frappe.get_doc({
|
||||
'doctype': 'DocType',
|
||||
'name': doctype_name,
|
||||
'module': 'Custom',
|
||||
'custom': 1,
|
||||
'autoname': 'field:title',
|
||||
'fields': [
|
||||
{'label': 'Title', 'fieldname': 'title', 'reqd': 1, 'fieldtype': 'Data'},
|
||||
{'label': 'Number', 'fieldname': 'number', 'fieldtype': 'Int'},
|
||||
{'label': 'Table Field 1', 'fieldname': 'table_field_1', 'fieldtype': 'Table', 'options': table_1_name},
|
||||
],
|
||||
'permissions': [
|
||||
{'role': 'System Manager'}
|
||||
]
|
||||
}).insert()
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"name": doctype_name,
|
||||
"module": "Custom",
|
||||
"custom": 1,
|
||||
"autoname": "field:title",
|
||||
"fields": [
|
||||
{"label": "Title", "fieldname": "title", "reqd": 1, "fieldtype": "Data"},
|
||||
{"label": "Number", "fieldname": "number", "fieldtype": "Int"},
|
||||
{
|
||||
"label": "Table Field 1",
|
||||
"fieldname": "table_field_1",
|
||||
"fieldtype": "Table",
|
||||
"options": table_1_name,
|
||||
},
|
||||
],
|
||||
"permissions": [{"role": "System Manager"}],
|
||||
}
|
||||
).insert()
|
||||
|
||||
def create_test_data(self, force=False):
|
||||
"""
|
||||
|
|
@ -69,37 +78,38 @@ class TestDataExporter(unittest.TestCase):
|
|||
table_field_1=[
|
||||
{"child_title": "Child Title 1", "child_number": "50"},
|
||||
{"child_title": "Child Title 2", "child_number": "51"},
|
||||
]
|
||||
],
|
||||
).insert()
|
||||
else:
|
||||
self.doc = frappe.get_doc(self.doctype_name, self.doc_name)
|
||||
|
||||
def test_export_content(self):
|
||||
exp = DataExporter(doctype=self.doctype_name, file_type='CSV')
|
||||
exp = DataExporter(doctype=self.doctype_name, file_type="CSV")
|
||||
exp.build_response()
|
||||
|
||||
self.assertEqual(frappe.response['type'],'csv')
|
||||
self.assertEqual(frappe.response['doctype'], self.doctype_name)
|
||||
self.assertTrue(frappe.response['result'])
|
||||
self.assertIn('Child Title 1\",50',frappe.response['result'])
|
||||
self.assertIn('Child Title 2\",51',frappe.response['result'])
|
||||
self.assertEqual(frappe.response["type"], "csv")
|
||||
self.assertEqual(frappe.response["doctype"], self.doctype_name)
|
||||
self.assertTrue(frappe.response["result"])
|
||||
self.assertIn('Child Title 1",50', frappe.response["result"])
|
||||
self.assertIn('Child Title 2",51', frappe.response["result"])
|
||||
|
||||
def test_export_type(self):
|
||||
for type in ['csv', 'Excel']:
|
||||
for type in ["csv", "Excel"]:
|
||||
with self.subTest(type=type):
|
||||
exp = DataExporter(doctype=self.doctype_name, file_type=type)
|
||||
exp.build_response()
|
||||
|
||||
self.assertEqual(frappe.response['doctype'], self.doctype_name)
|
||||
self.assertTrue(frappe.response['result'])
|
||||
self.assertEqual(frappe.response["doctype"], self.doctype_name)
|
||||
self.assertTrue(frappe.response["result"])
|
||||
|
||||
if type == 'csv':
|
||||
self.assertEqual(frappe.response['type'],'csv')
|
||||
elif type == 'Excel':
|
||||
self.assertEqual(frappe.response['type'],'binary')
|
||||
self.assertEqual(frappe.response['filename'], self.doctype_name+'.xlsx') # 'Test DocType for Export Tool.xlsx')
|
||||
self.assertTrue(frappe.response['filecontent'])
|
||||
if type == "csv":
|
||||
self.assertEqual(frappe.response["type"], "csv")
|
||||
elif type == "Excel":
|
||||
self.assertEqual(frappe.response["type"], "binary")
|
||||
self.assertEqual(
|
||||
frappe.response["filename"], self.doctype_name + ".xlsx"
|
||||
) # 'Test DocType for Export Tool.xlsx')
|
||||
self.assertTrue(frappe.response["filecontent"])
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -64,9 +64,7 @@ class DataImport(Document):
|
|||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
if is_scheduler_inactive() and not frappe.flags.in_test:
|
||||
frappe.throw(
|
||||
_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive")
|
||||
)
|
||||
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
|
||||
|
||||
enqueued_jobs = [d.get("job_name") for d in get_info()]
|
||||
|
||||
|
|
@ -100,6 +98,7 @@ def get_preview_from_template(data_import, import_file=None, google_sheets_url=N
|
|||
import_file, google_sheets_url
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def form_start_import(data_import):
|
||||
return frappe.get_doc("Data Import", data_import).start_import()
|
||||
|
|
@ -127,11 +126,11 @@ def download_template(
|
|||
):
|
||||
"""
|
||||
Download template from Exporter
|
||||
:param doctype: Document Type
|
||||
:param export_fields=None: Fields to export as dict {'Sales Invoice': ['name', 'customer'], 'Sales Invoice Item': ['item_code']}
|
||||
:param export_records=None: One of 'all', 'by_filter', 'blank_template'
|
||||
:param export_filters: Filter dict
|
||||
:param file_type: File type to export into
|
||||
:param doctype: Document Type
|
||||
:param export_fields=None: Fields to export as dict {'Sales Invoice': ['name', 'customer'], 'Sales Invoice Item': ['item_code']}
|
||||
:param export_records=None: One of 'all', 'by_filter', 'blank_template'
|
||||
:param export_filters: Filter dict
|
||||
:param file_type: File type to export into
|
||||
"""
|
||||
|
||||
export_fields = frappe.parse_json(export_fields)
|
||||
|
|
@ -154,34 +153,38 @@ def download_errored_template(data_import_name):
|
|||
data_import = frappe.get_doc("Data Import", data_import_name)
|
||||
data_import.export_errored_rows()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_import_log(data_import_name):
|
||||
data_import = frappe.get_doc("Data Import", data_import_name)
|
||||
data_import.download_import_log()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_import_status(data_import_name):
|
||||
import_status = {}
|
||||
|
||||
logs = frappe.get_all('Data Import Log', fields=['count(*) as count', 'success'],
|
||||
filters={'data_import': data_import_name},
|
||||
group_by='success')
|
||||
logs = frappe.get_all(
|
||||
"Data Import Log",
|
||||
fields=["count(*) as count", "success"],
|
||||
filters={"data_import": data_import_name},
|
||||
group_by="success",
|
||||
)
|
||||
|
||||
total_payload_count = frappe.db.get_value('Data Import', data_import_name, 'payload_count')
|
||||
total_payload_count = frappe.db.get_value("Data Import", data_import_name, "payload_count")
|
||||
|
||||
for log in logs:
|
||||
if log.get('success'):
|
||||
import_status['success'] = log.get('count')
|
||||
if log.get("success"):
|
||||
import_status["success"] = log.get("count")
|
||||
else:
|
||||
import_status['failed'] = log.get('count')
|
||||
import_status["failed"] = log.get("count")
|
||||
|
||||
import_status['total_records'] = total_payload_count
|
||||
import_status["total_records"] = total_payload_count
|
||||
|
||||
return import_status
|
||||
|
||||
def import_file(
|
||||
doctype, file_path, import_type, submit_after_import=False, console=False
|
||||
):
|
||||
|
||||
def import_file(doctype, file_path, import_type, submit_after_import=False, console=False):
|
||||
"""
|
||||
Import documents in from CSV or XLSX using data import.
|
||||
|
||||
|
|
@ -198,9 +201,7 @@ def import_file(
|
|||
"Insert New Records" if import_type.lower() == "insert" else "Update Existing Records"
|
||||
)
|
||||
|
||||
i = Importer(
|
||||
doctype=doctype, file_path=file_path, data_import=data_import, console=console
|
||||
)
|
||||
i = Importer(doctype=doctype, file_path=file_path, data_import=data_import, console=console)
|
||||
i.import_data()
|
||||
|
||||
|
||||
|
|
@ -214,11 +215,7 @@ def import_doc(path, pre_process=None):
|
|||
if f.endswith(".json"):
|
||||
frappe.flags.mute_emails = True
|
||||
import_file_by_path(
|
||||
f,
|
||||
data_import=True,
|
||||
force=True,
|
||||
pre_process=pre_process,
|
||||
reset_permissions=True
|
||||
f, data_import=True, force=True, pre_process=pre_process, reset_permissions=True
|
||||
)
|
||||
frappe.flags.mute_emails = False
|
||||
frappe.db.commit()
|
||||
|
|
@ -226,9 +223,7 @@ def import_doc(path, pre_process=None):
|
|||
raise NotImplementedError("Only .json files can be imported")
|
||||
|
||||
|
||||
def export_json(
|
||||
doctype, path, filters=None, or_filters=None, name=None, order_by="creation asc"
|
||||
):
|
||||
def export_json(doctype, path, filters=None, or_filters=None, name=None, order_by="creation asc"):
|
||||
def post_process(out):
|
||||
# Note on Tree DocTypes:
|
||||
# The tree structure is maintained in the database via the fields "lft"
|
||||
|
|
|
|||
|
|
@ -6,11 +6,8 @@ import typing
|
|||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model import (
|
||||
display_fieldtypes,
|
||||
no_value_fields,
|
||||
table_fields as table_fieldtypes,
|
||||
)
|
||||
from frappe.model import display_fieldtypes, no_value_fields
|
||||
from frappe.model import table_fields as table_fieldtypes
|
||||
from frappe.utils import flt, format_duration, groupby_metric
|
||||
from frappe.utils.csvutils import build_csv_response
|
||||
from frappe.utils.xlsxutils import build_xlsx_response
|
||||
|
|
@ -28,11 +25,11 @@ class Exporter:
|
|||
):
|
||||
"""
|
||||
Exports records of a DocType for use with Importer
|
||||
:param doctype: Document Type to export
|
||||
:param export_fields=None: One of 'All', 'Mandatory' or {'DocType': ['field1', 'field2'], 'Child DocType': ['childfield1']}
|
||||
:param export_data=False: Whether to export data as well
|
||||
:param export_filters=None: The filters (dict or list) which is used to query the records
|
||||
:param file_type: One of 'Excel' or 'CSV'
|
||||
:param doctype: Document Type to export
|
||||
:param export_fields=None: One of 'All', 'Mandatory' or {'DocType': ['field1', 'field2'], 'Child DocType': ['childfield1']}
|
||||
:param export_data=False: Whether to export data as well
|
||||
:param export_filters=None: The filters (dict or list) which is used to query the records
|
||||
:param file_type: One of 'Excel' or 'CSV'
|
||||
"""
|
||||
self.doctype = doctype
|
||||
self.meta = frappe.get_meta(doctype)
|
||||
|
|
@ -168,9 +165,7 @@ class Exporter:
|
|||
else:
|
||||
order_by = "`tab{0}`.`creation` DESC".format(self.doctype)
|
||||
|
||||
parent_fields = [
|
||||
format_column_name(df) for df in self.fields if df.parent == self.doctype
|
||||
]
|
||||
parent_fields = [format_column_name(df) for df in self.fields if df.parent == self.doctype]
|
||||
parent_data = frappe.db.get_list(
|
||||
self.doctype,
|
||||
filters=filters,
|
||||
|
|
@ -188,9 +183,7 @@ class Exporter:
|
|||
child_table_df = self.meta.get_field(key)
|
||||
child_table_doctype = child_table_df.options
|
||||
child_fields = ["name", "idx", "parent", "parentfield"] + list(
|
||||
set(
|
||||
[format_column_name(df) for df in self.fields if df.parent == child_table_doctype]
|
||||
)
|
||||
set([format_column_name(df) for df in self.fields if df.parent == child_table_doctype])
|
||||
)
|
||||
data = frappe.db.get_all(
|
||||
child_table_doctype,
|
||||
|
|
@ -261,4 +254,4 @@ class Exporter:
|
|||
build_xlsx_response(self.get_csv_array_for_export(), _(self.doctype))
|
||||
|
||||
def group_children_data_by_parent(self, children_data: typing.Dict[str, list]):
|
||||
return groupby_metric(children_data, key='parent')
|
||||
return groupby_metric(children_data, key="parent")
|
||||
|
|
|
|||
|
|
@ -1,21 +1,23 @@
|
|||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import os
|
||||
import io
|
||||
import frappe
|
||||
import timeit
|
||||
import json
|
||||
from datetime import datetime, date
|
||||
import os
|
||||
import timeit
|
||||
from datetime import date, datetime
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint, flt, update_progress_bar, cstr, duration_to_seconds
|
||||
from frappe.utils.csvutils import read_csv_content, get_csv_content_from_google_sheets
|
||||
from frappe.utils.xlsxutils import (
|
||||
read_xlsx_file_from_attached_file,
|
||||
read_xls_file_from_attached_file,
|
||||
)
|
||||
from frappe.model import no_value_fields, table_fields as table_fieldtypes
|
||||
from frappe.core.doctype.version.version import get_diff
|
||||
from frappe.model import no_value_fields
|
||||
from frappe.model import table_fields as table_fieldtypes
|
||||
from frappe.utils import cint, cstr, duration_to_seconds, flt, update_progress_bar
|
||||
from frappe.utils.csvutils import get_csv_content_from_google_sheets, read_csv_content
|
||||
from frappe.utils.xlsxutils import (
|
||||
read_xls_file_from_attached_file,
|
||||
read_xlsx_file_from_attached_file,
|
||||
)
|
||||
|
||||
INVALID_VALUES = ("", None)
|
||||
MAX_ROWS_IN_PREVIEW = 10
|
||||
|
|
@ -24,9 +26,7 @@ UPDATE = "Update Existing Records"
|
|||
|
||||
|
||||
class Importer:
|
||||
def __init__(
|
||||
self, doctype, data_import=None, file_path=None, import_type=None, console=False
|
||||
):
|
||||
def __init__(self, doctype, data_import=None, file_path=None, import_type=None, console=False):
|
||||
self.doctype = doctype
|
||||
self.console = console
|
||||
|
||||
|
|
@ -49,9 +49,13 @@ class Importer:
|
|||
def get_data_for_import_preview(self):
|
||||
out = self.import_file.get_data_for_import_preview()
|
||||
|
||||
out.import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success"],
|
||||
out.import_log = frappe.db.get_all(
|
||||
"Data Import Log",
|
||||
fields=["row_indexes", "success"],
|
||||
filters={"data_import": self.data_import.name},
|
||||
order_by="log_index", limit=10)
|
||||
order_by="log_index",
|
||||
limit=10,
|
||||
)
|
||||
|
||||
return out
|
||||
|
||||
|
|
@ -84,14 +88,23 @@ class Importer:
|
|||
return
|
||||
|
||||
# setup import log
|
||||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "log_index"],
|
||||
filters={"data_import": self.data_import.name},
|
||||
order_by="log_index") or []
|
||||
import_log = (
|
||||
frappe.db.get_all(
|
||||
"Data Import Log",
|
||||
fields=["row_indexes", "success", "log_index"],
|
||||
filters={"data_import": self.data_import.name},
|
||||
order_by="log_index",
|
||||
)
|
||||
or []
|
||||
)
|
||||
|
||||
log_index = 0
|
||||
|
||||
# Do not remove rows in case of retry after an error or pending data import
|
||||
if self.data_import.status == "Partial Success" and len(import_log) >= self.data_import.payload_count:
|
||||
if (
|
||||
self.data_import.status == "Partial Success"
|
||||
and len(import_log) >= self.data_import.payload_count
|
||||
):
|
||||
# remove previous failures from import log only in case of retry after partial success
|
||||
import_log = [log for log in import_log if log.get("success")]
|
||||
|
||||
|
|
@ -108,9 +121,7 @@ class Importer:
|
|||
total_payload_count = len(payloads)
|
||||
batch_size = frappe.conf.data_import_batch_size or 1000
|
||||
|
||||
for batch_index, batched_payloads in enumerate(
|
||||
frappe.utils.create_batch(payloads, batch_size)
|
||||
):
|
||||
for batch_index, batched_payloads in enumerate(frappe.utils.create_batch(payloads, batch_size)):
|
||||
for i, payload in enumerate(batched_payloads):
|
||||
doc = payload.doc
|
||||
row_indexes = [row.row_number for row in payload.rows]
|
||||
|
|
@ -156,11 +167,11 @@ class Importer:
|
|||
},
|
||||
)
|
||||
|
||||
create_import_log(self.data_import.name, log_index, {
|
||||
'success': True,
|
||||
'docname': doc.name,
|
||||
'row_indexes': row_indexes
|
||||
})
|
||||
create_import_log(
|
||||
self.data_import.name,
|
||||
log_index,
|
||||
{"success": True, "docname": doc.name, "row_indexes": row_indexes},
|
||||
)
|
||||
|
||||
log_index += 1
|
||||
|
||||
|
|
@ -177,19 +188,29 @@ class Importer:
|
|||
# rollback if exception
|
||||
frappe.db.rollback()
|
||||
|
||||
create_import_log(self.data_import.name, log_index, {
|
||||
'success': False,
|
||||
'exception': frappe.get_traceback(),
|
||||
'messages': messages,
|
||||
'row_indexes': row_indexes
|
||||
})
|
||||
create_import_log(
|
||||
self.data_import.name,
|
||||
log_index,
|
||||
{
|
||||
"success": False,
|
||||
"exception": frappe.get_traceback(),
|
||||
"messages": messages,
|
||||
"row_indexes": row_indexes,
|
||||
},
|
||||
)
|
||||
|
||||
log_index += 1
|
||||
|
||||
# Logs are db inserted directly so will have to be fetched again
|
||||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "log_index"],
|
||||
filters={"data_import": self.data_import.name},
|
||||
order_by="log_index") or []
|
||||
import_log = (
|
||||
frappe.db.get_all(
|
||||
"Data Import Log",
|
||||
fields=["row_indexes", "success", "log_index"],
|
||||
filters={"data_import": self.data_import.name},
|
||||
order_by="log_index",
|
||||
)
|
||||
or []
|
||||
)
|
||||
|
||||
# set status
|
||||
failures = [log for log in import_log if not log.get("success")]
|
||||
|
|
@ -274,9 +295,15 @@ class Importer:
|
|||
if not self.data_import:
|
||||
return
|
||||
|
||||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success"],
|
||||
filters={"data_import": self.data_import.name},
|
||||
order_by="log_index") or []
|
||||
import_log = (
|
||||
frappe.db.get_all(
|
||||
"Data Import Log",
|
||||
fields=["row_indexes", "success"],
|
||||
filters={"data_import": self.data_import.name},
|
||||
order_by="log_index",
|
||||
)
|
||||
or []
|
||||
)
|
||||
|
||||
failures = [log for log in import_log if not log.get("success")]
|
||||
row_indexes = []
|
||||
|
|
@ -299,9 +326,12 @@ class Importer:
|
|||
if not self.data_import:
|
||||
return
|
||||
|
||||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "messages", "exception", "docname"],
|
||||
import_log = frappe.db.get_all(
|
||||
"Data Import Log",
|
||||
fields=["row_indexes", "success", "messages", "exception", "docname"],
|
||||
filters={"data_import": self.data_import.name},
|
||||
order_by="log_index")
|
||||
order_by="log_index",
|
||||
)
|
||||
|
||||
header_row = ["Row Numbers", "Status", "Message", "Exception"]
|
||||
|
||||
|
|
@ -309,10 +339,13 @@ class Importer:
|
|||
|
||||
for log in import_log:
|
||||
row_number = json.loads(log.get("row_indexes"))[0]
|
||||
status = "Success" if log.get('success') else "Failure"
|
||||
message = "Successfully Imported {0}".format(log.get('docname')) if log.get('success') else \
|
||||
log.get("messages")
|
||||
exception = frappe.utils.cstr(log.get("exception", ''))
|
||||
status = "Success" if log.get("success") else "Failure"
|
||||
message = (
|
||||
"Successfully Imported {0}".format(log.get("docname"))
|
||||
if log.get("success")
|
||||
else log.get("messages")
|
||||
)
|
||||
exception = frappe.utils.cstr(log.get("exception", ""))
|
||||
rows += [[row_number, status, message, exception]]
|
||||
|
||||
build_csv_response(rows, self.doctype)
|
||||
|
|
@ -324,9 +357,7 @@ class Importer:
|
|||
if successful_records:
|
||||
print()
|
||||
print(
|
||||
"Successfully imported {0} records out of {1}".format(
|
||||
len(successful_records), len(import_log)
|
||||
)
|
||||
"Successfully imported {0} records out of {1}".format(len(successful_records), len(import_log))
|
||||
)
|
||||
|
||||
if failed_records:
|
||||
|
|
@ -363,9 +394,7 @@ class Importer:
|
|||
class ImportFile:
|
||||
def __init__(self, doctype, file, template_options=None, import_type=None):
|
||||
self.doctype = doctype
|
||||
self.template_options = template_options or frappe._dict(
|
||||
column_to_field_map=frappe._dict()
|
||||
)
|
||||
self.template_options = template_options or frappe._dict(column_to_field_map=frappe._dict())
|
||||
self.column_to_field_map = self.template_options.column_to_field_map
|
||||
self.import_type = import_type
|
||||
self.warnings = []
|
||||
|
|
@ -556,9 +585,7 @@ class ImportFile:
|
|||
def read_content(self, content, extension):
|
||||
error_title = _("Template Error")
|
||||
if extension not in ("csv", "xlsx", "xls"):
|
||||
frappe.throw(
|
||||
_("Import template should be of type .csv, .xlsx or .xls"), title=error_title
|
||||
)
|
||||
frappe.throw(_("Import template should be of type .csv, .xlsx or .xls"), title=error_title)
|
||||
|
||||
if extension == "csv":
|
||||
data = read_csv_content(content)
|
||||
|
|
@ -587,12 +614,13 @@ class Row:
|
|||
if len_row != len_columns:
|
||||
less_than_columns = len_row < len_columns
|
||||
message = (
|
||||
"Row has less values than columns"
|
||||
if less_than_columns
|
||||
else "Row has more values than columns"
|
||||
"Row has less values than columns" if less_than_columns else "Row has more values than columns"
|
||||
)
|
||||
self.warnings.append(
|
||||
{"row": self.row_number, "message": message,}
|
||||
{
|
||||
"row": self.row_number,
|
||||
"message": message,
|
||||
}
|
||||
)
|
||||
|
||||
def parse_doc(self, doctype, parent_doc=None, table_df=None):
|
||||
|
|
@ -662,18 +690,24 @@ class Row:
|
|||
options_string = ", ".join(frappe.bold(d) for d in select_options)
|
||||
msg = _("Value must be one of {0}").format(options_string)
|
||||
self.warnings.append(
|
||||
{"row": self.row_number, "field": df_as_json(df), "message": msg,}
|
||||
{
|
||||
"row": self.row_number,
|
||||
"field": df_as_json(df),
|
||||
"message": msg,
|
||||
}
|
||||
)
|
||||
return
|
||||
|
||||
elif df.fieldtype == "Link":
|
||||
exists = self.link_exists(value, df)
|
||||
if not exists:
|
||||
msg = _("Value {0} missing for {1}").format(
|
||||
frappe.bold(value), frappe.bold(df.options)
|
||||
)
|
||||
msg = _("Value {0} missing for {1}").format(frappe.bold(value), frappe.bold(df.options))
|
||||
self.warnings.append(
|
||||
{"row": self.row_number, "field": df_as_json(df), "message": msg,}
|
||||
{
|
||||
"row": self.row_number,
|
||||
"field": df_as_json(df),
|
||||
"message": msg,
|
||||
}
|
||||
)
|
||||
return
|
||||
elif df.fieldtype in ["Date", "Datetime"]:
|
||||
|
|
@ -693,6 +727,7 @@ class Row:
|
|||
return
|
||||
elif df.fieldtype == "Duration":
|
||||
import re
|
||||
|
||||
is_valid_duration = re.match(r"^(?:(\d+d)?((^|\s)\d+h)?((^|\s)\d+m)?((^|\s)\d+s)?)$", value)
|
||||
if not is_valid_duration:
|
||||
self.warnings.append(
|
||||
|
|
@ -702,7 +737,7 @@ class Row:
|
|||
"field": df_as_json(df),
|
||||
"message": _("Value {0} must be in the valid duration format: d h m s").format(
|
||||
frappe.bold(value)
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -789,9 +824,7 @@ class Header(Row):
|
|||
else:
|
||||
doctypes.append((col.df.parent, col.df.child_table_df))
|
||||
|
||||
self.doctypes = sorted(
|
||||
list(set(doctypes)), key=lambda x: -1 if x[0] == self.doctype else 1
|
||||
)
|
||||
self.doctypes = sorted(list(set(doctypes)), key=lambda x: -1 if x[0] == self.doctype else 1)
|
||||
|
||||
def get_column_indexes(self, doctype, tablefield=None):
|
||||
def is_table_field(df):
|
||||
|
|
@ -802,10 +835,7 @@ class Header(Row):
|
|||
return [
|
||||
col.index
|
||||
for col in self.columns
|
||||
if not col.skip_import
|
||||
and col.df
|
||||
and col.df.parent == doctype
|
||||
and is_table_field(col.df)
|
||||
if not col.skip_import and col.df and col.df.parent == doctype and is_table_field(col.df)
|
||||
]
|
||||
|
||||
def get_columns(self, indexes):
|
||||
|
|
@ -893,9 +923,7 @@ class Column:
|
|||
self.warnings.append(
|
||||
{
|
||||
"col": column_number,
|
||||
"message": _("Cannot match column {0} with any field").format(
|
||||
frappe.bold(header_title)
|
||||
),
|
||||
"message": _("Cannot match column {0} with any field").format(frappe.bold(header_title)),
|
||||
"type": "info",
|
||||
}
|
||||
)
|
||||
|
|
@ -958,9 +986,7 @@ class Column:
|
|||
if self.df.fieldtype == "Link":
|
||||
# find all values that dont exist
|
||||
values = list({cstr(v) for v in self.column_values[1:] if v})
|
||||
exists = [
|
||||
d.name for d in frappe.db.get_all(self.df.options, filters={"name": ("in", values)})
|
||||
]
|
||||
exists = [d.name for d in frappe.db.get_all(self.df.options, filters={"name": ("in", values)})]
|
||||
not_exists = list(set(values) - set(exists))
|
||||
if not_exists:
|
||||
missing_values = ", ".join(not_exists)
|
||||
|
|
@ -968,9 +994,7 @@ class Column:
|
|||
{
|
||||
"col": self.column_number,
|
||||
"message": (
|
||||
"The following values do not exist for {}: {}".format(
|
||||
self.df.options, missing_values
|
||||
)
|
||||
"The following values do not exist for {}: {}".format(self.df.options, missing_values)
|
||||
),
|
||||
"type": "warning",
|
||||
}
|
||||
|
|
@ -983,7 +1007,9 @@ class Column:
|
|||
self.warnings.append(
|
||||
{
|
||||
"col": self.column_number,
|
||||
"message": _("Date format could not be determined from the values in this column. Defaulting to yyyy-mm-dd."),
|
||||
"message": _(
|
||||
"Date format could not be determined from the values in this column. Defaulting to yyyy-mm-dd."
|
||||
),
|
||||
"type": "info",
|
||||
}
|
||||
)
|
||||
|
|
@ -1027,12 +1053,12 @@ def build_fields_dict_for_column_matching(parent_doctype):
|
|||
Build a dict with various keys to match with column headers and value as docfield
|
||||
The keys can be label or fieldname
|
||||
{
|
||||
'Customer': df1,
|
||||
'customer': df1,
|
||||
'Due Date': df2,
|
||||
'due_date': df2,
|
||||
'Item Code (Sales Invoice Item)': df3,
|
||||
'Sales Invoice Item:item_code': df3,
|
||||
'Customer': df1,
|
||||
'customer': df1,
|
||||
'Due Date': df2,
|
||||
'due_date': df2,
|
||||
'Item Code (Sales Invoice Item)': df3,
|
||||
'Sales Invoice Item:item_code': df3,
|
||||
}
|
||||
"""
|
||||
|
||||
|
|
@ -1062,9 +1088,7 @@ def build_fields_dict_for_column_matching(parent_doctype):
|
|||
out = {}
|
||||
|
||||
# doctypes and fieldname if it is a child doctype
|
||||
doctypes = [(parent_doctype, None)] + [
|
||||
(df.options, df) for df in parent_meta.get_table_fields()
|
||||
]
|
||||
doctypes = [(parent_doctype, None)] + [(df.options, df) for df in parent_meta.get_table_fields()]
|
||||
|
||||
for doctype, table_df in doctypes:
|
||||
translated_table_label = _(table_df.label) if table_df else None
|
||||
|
|
@ -1082,15 +1106,15 @@ def build_fields_dict_for_column_matching(parent_doctype):
|
|||
|
||||
if doctype == parent_doctype:
|
||||
name_headers = (
|
||||
"name", # fieldname
|
||||
"ID", # label
|
||||
_("ID"), # translated label
|
||||
"name", # fieldname
|
||||
"ID", # label
|
||||
_("ID"), # translated label
|
||||
)
|
||||
else:
|
||||
name_headers = (
|
||||
"{0}.name".format(table_df.fieldname), # fieldname
|
||||
"ID ({0})".format(table_df.label), # label
|
||||
"{0} ({1})".format(_("ID"), translated_table_label), # translated label
|
||||
"{0}.name".format(table_df.fieldname), # fieldname
|
||||
"ID ({0})".format(table_df.label), # label
|
||||
"{0} ({1})".format(_("ID"), translated_table_label), # translated label
|
||||
)
|
||||
|
||||
name_df.is_child_table_field = True
|
||||
|
|
@ -1122,7 +1146,7 @@ def build_fields_dict_for_column_matching(parent_doctype):
|
|||
for header in (
|
||||
df.fieldname,
|
||||
f"{label} ({df.fieldname})",
|
||||
f"{translated_label} ({df.fieldname})"
|
||||
f"{translated_label} ({df.fieldname})",
|
||||
):
|
||||
out[header] = df
|
||||
|
||||
|
|
@ -1155,9 +1179,8 @@ def build_fields_dict_for_column_matching(parent_doctype):
|
|||
autoname_field = get_autoname_field(parent_doctype)
|
||||
if autoname_field:
|
||||
for header in (
|
||||
"ID ({})".format(autoname_field.label), # label
|
||||
"{0} ({1})".format(_("ID"), _(autoname_field.label)), # translated label
|
||||
|
||||
"ID ({})".format(autoname_field.label), # label
|
||||
"{0} ({1})".format(_("ID"), _(autoname_field.label)), # translated label
|
||||
# ID field should also map to the autoname field
|
||||
"ID",
|
||||
_("ID"),
|
||||
|
|
@ -1205,10 +1228,7 @@ def get_item_at_index(_list, i, default=None):
|
|||
|
||||
def get_user_format(date_format):
|
||||
return (
|
||||
date_format.replace("%Y", "yyyy")
|
||||
.replace("%y", "yy")
|
||||
.replace("%m", "mm")
|
||||
.replace("%d", "dd")
|
||||
date_format.replace("%Y", "yyyy").replace("%y", "yy").replace("%m", "mm").replace("%d", "dd")
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1226,16 +1246,17 @@ def df_as_json(df):
|
|||
def get_select_options(df):
|
||||
return [d for d in (df.options or "").split("\n") if d]
|
||||
|
||||
|
||||
def create_import_log(data_import, log_index, log_details):
|
||||
frappe.get_doc({
|
||||
'doctype': 'Data Import Log',
|
||||
'log_index': log_index,
|
||||
'success': log_details.get('success'),
|
||||
'data_import': data_import,
|
||||
'row_indexes': json.dumps(log_details.get('row_indexes')),
|
||||
'docname': log_details.get('docname'),
|
||||
'messages': json.dumps(log_details.get('messages', '[]')),
|
||||
'exception': log_details.get('exception')
|
||||
}).db_insert()
|
||||
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Data Import Log",
|
||||
"log_index": log_index,
|
||||
"success": log_details.get("success"),
|
||||
"data_import": data_import,
|
||||
"row_indexes": json.dumps(log_details.get("row_indexes")),
|
||||
"docname": log_details.get("docname"),
|
||||
"messages": json.dumps(log_details.get("messages", "[]")),
|
||||
"exception": log_details.get("exception"),
|
||||
}
|
||||
).db_insert()
|
||||
|
|
|
|||
|
|
@ -4,5 +4,6 @@
|
|||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class TestDataImport(unittest.TestCase):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
# Copyright (c) 2019, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.core.doctype.data_import.exporter import Exporter
|
||||
from frappe.core.doctype.data_import.test_importer import (
|
||||
create_doctype_if_not_exists,
|
||||
)
|
||||
from frappe.core.doctype.data_import.test_importer import create_doctype_if_not_exists
|
||||
|
||||
doctype_name = "DocType for Export"
|
||||
|
||||
doctype_name = 'DocType for Export'
|
||||
|
||||
class TestExporter(unittest.TestCase):
|
||||
def setUp(self):
|
||||
|
|
@ -93,10 +93,10 @@ class TestExporter(unittest.TestCase):
|
|||
doctype_name,
|
||||
export_fields={doctype_name: ["title", "description"]},
|
||||
export_data=True,
|
||||
file_type="CSV"
|
||||
file_type="CSV",
|
||||
)
|
||||
e.build_response()
|
||||
|
||||
self.assertTrue(frappe.response['result'])
|
||||
self.assertEqual(frappe.response['doctype'], doctype_name)
|
||||
self.assertEqual(frappe.response['type'], "csv")
|
||||
self.assertTrue(frappe.response["result"])
|
||||
self.assertEqual(frappe.response["doctype"], doctype_name)
|
||||
self.assertEqual(frappe.response["type"], "csv")
|
||||
|
|
|
|||
|
|
@ -2,53 +2,57 @@
|
|||
# Copyright (c) 2019, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.core.doctype.data_import.importer import Importer
|
||||
from frappe.tests.test_query_builder import db_type_is, run_only_if
|
||||
from frappe.utils import getdate, format_duration
|
||||
from frappe.utils import format_duration, getdate
|
||||
|
||||
doctype_name = "DocType for Import"
|
||||
|
||||
doctype_name = 'DocType for Import'
|
||||
|
||||
class TestImporter(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
create_doctype_if_not_exists(doctype_name,)
|
||||
create_doctype_if_not_exists(
|
||||
doctype_name,
|
||||
)
|
||||
|
||||
def test_data_import_from_file(self):
|
||||
import_file = get_import_file('sample_import_file')
|
||||
import_file = get_import_file("sample_import_file")
|
||||
data_import = self.get_importer(doctype_name, import_file)
|
||||
data_import.start_import()
|
||||
|
||||
doc1 = frappe.get_doc(doctype_name, 'Test')
|
||||
doc2 = frappe.get_doc(doctype_name, 'Test 2')
|
||||
doc3 = frappe.get_doc(doctype_name, 'Test 3')
|
||||
doc1 = frappe.get_doc(doctype_name, "Test")
|
||||
doc2 = frappe.get_doc(doctype_name, "Test 2")
|
||||
doc3 = frappe.get_doc(doctype_name, "Test 3")
|
||||
|
||||
self.assertEqual(doc1.description, 'test description')
|
||||
self.assertEqual(doc1.description, "test description")
|
||||
self.assertEqual(doc1.number, 1)
|
||||
self.assertEqual(format_duration(doc1.duration), '3h')
|
||||
self.assertEqual(format_duration(doc1.duration), "3h")
|
||||
|
||||
self.assertEqual(doc1.table_field_1[0].child_title, 'child title')
|
||||
self.assertEqual(doc1.table_field_1[0].child_description, 'child description')
|
||||
self.assertEqual(doc1.table_field_1[0].child_title, "child title")
|
||||
self.assertEqual(doc1.table_field_1[0].child_description, "child description")
|
||||
|
||||
self.assertEqual(doc1.table_field_1[1].child_title, 'child title 2')
|
||||
self.assertEqual(doc1.table_field_1[1].child_description, 'child description 2')
|
||||
self.assertEqual(doc1.table_field_1[1].child_title, "child title 2")
|
||||
self.assertEqual(doc1.table_field_1[1].child_description, "child description 2")
|
||||
|
||||
self.assertEqual(doc1.table_field_2[1].child_2_title, 'title child')
|
||||
self.assertEqual(doc1.table_field_2[1].child_2_date, getdate('2019-10-30'))
|
||||
self.assertEqual(doc1.table_field_2[1].child_2_title, "title child")
|
||||
self.assertEqual(doc1.table_field_2[1].child_2_date, getdate("2019-10-30"))
|
||||
self.assertEqual(doc1.table_field_2[1].child_2_another_number, 5)
|
||||
|
||||
self.assertEqual(doc1.table_field_1_again[0].child_title, 'child title again')
|
||||
self.assertEqual(doc1.table_field_1_again[1].child_title, 'child title again 2')
|
||||
self.assertEqual(doc1.table_field_1_again[1].child_date, getdate('2021-09-22'))
|
||||
self.assertEqual(doc1.table_field_1_again[0].child_title, "child title again")
|
||||
self.assertEqual(doc1.table_field_1_again[1].child_title, "child title again 2")
|
||||
self.assertEqual(doc1.table_field_1_again[1].child_date, getdate("2021-09-22"))
|
||||
|
||||
self.assertEqual(doc2.description, 'test description 2')
|
||||
self.assertEqual(format_duration(doc2.duration), '4d 3h')
|
||||
self.assertEqual(doc2.description, "test description 2")
|
||||
self.assertEqual(format_duration(doc2.duration), "4d 3h")
|
||||
|
||||
self.assertEqual(doc3.another_number, 5)
|
||||
self.assertEqual(format_duration(doc3.duration), '5d 5h 45m')
|
||||
self.assertEqual(format_duration(doc3.duration), "5d 5h 45m")
|
||||
|
||||
def test_data_import_preview(self):
|
||||
import_file = get_import_file('sample_import_file')
|
||||
import_file = get_import_file("sample_import_file")
|
||||
data_import = self.get_importer(doctype_name, import_file)
|
||||
preview = data_import.get_preview_from_template()
|
||||
|
||||
|
|
@ -58,35 +62,49 @@ class TestImporter(unittest.TestCase):
|
|||
# ignored on postgres because myisam doesn't exist on pg
|
||||
@run_only_if(db_type_is.MARIADB)
|
||||
def test_data_import_without_mandatory_values(self):
|
||||
import_file = get_import_file('sample_import_file_without_mandatory')
|
||||
import_file = get_import_file("sample_import_file_without_mandatory")
|
||||
data_import = self.get_importer(doctype_name, import_file)
|
||||
frappe.local.message_log = []
|
||||
data_import.start_import()
|
||||
data_import.reload()
|
||||
|
||||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "messages", "exception", "docname"],
|
||||
import_log = frappe.db.get_all(
|
||||
"Data Import Log",
|
||||
fields=["row_indexes", "success", "messages", "exception", "docname"],
|
||||
filters={"data_import": data_import.name},
|
||||
order_by="log_index")
|
||||
order_by="log_index",
|
||||
)
|
||||
|
||||
self.assertEqual(frappe.parse_json(import_log[0]['row_indexes']), [2,3])
|
||||
expected_error = "Error: <strong>Child 1 of DocType for Import</strong> Row #1: Value missing for: Child Title"
|
||||
self.assertEqual(frappe.parse_json(frappe.parse_json(import_log[0]['messages'])[0])['message'], expected_error)
|
||||
expected_error = "Error: <strong>Child 1 of DocType for Import</strong> Row #2: Value missing for: Child Title"
|
||||
self.assertEqual(frappe.parse_json(frappe.parse_json(import_log[0]['messages'])[1])['message'], expected_error)
|
||||
self.assertEqual(frappe.parse_json(import_log[0]["row_indexes"]), [2, 3])
|
||||
expected_error = (
|
||||
"Error: <strong>Child 1 of DocType for Import</strong> Row #1: Value missing for: Child Title"
|
||||
)
|
||||
self.assertEqual(
|
||||
frappe.parse_json(frappe.parse_json(import_log[0]["messages"])[0])["message"], expected_error
|
||||
)
|
||||
expected_error = (
|
||||
"Error: <strong>Child 1 of DocType for Import</strong> Row #2: Value missing for: Child Title"
|
||||
)
|
||||
self.assertEqual(
|
||||
frappe.parse_json(frappe.parse_json(import_log[0]["messages"])[1])["message"], expected_error
|
||||
)
|
||||
|
||||
self.assertEqual(frappe.parse_json(import_log[1]['row_indexes']), [4])
|
||||
self.assertEqual(frappe.parse_json(frappe.parse_json(import_log[1]['messages'])[0])['message'], "Title is required")
|
||||
self.assertEqual(frappe.parse_json(import_log[1]["row_indexes"]), [4])
|
||||
self.assertEqual(
|
||||
frappe.parse_json(frappe.parse_json(import_log[1]["messages"])[0])["message"],
|
||||
"Title is required",
|
||||
)
|
||||
|
||||
def test_data_import_update(self):
|
||||
existing_doc = frappe.get_doc(
|
||||
doctype=doctype_name,
|
||||
title=frappe.generate_hash(doctype_name, 8),
|
||||
table_field_1=[{'child_title': 'child title to update'}]
|
||||
table_field_1=[{"child_title": "child title to update"}],
|
||||
)
|
||||
existing_doc.save()
|
||||
frappe.db.commit()
|
||||
|
||||
import_file = get_import_file('sample_import_file_for_update')
|
||||
import_file = get_import_file("sample_import_file_for_update")
|
||||
data_import = self.get_importer(doctype_name, import_file, update=True)
|
||||
i = Importer(data_import.reference_doctype, data_import=data_import)
|
||||
|
||||
|
|
@ -104,15 +122,15 @@ class TestImporter(unittest.TestCase):
|
|||
|
||||
updated_doc = frappe.get_doc(doctype_name, existing_doc.name)
|
||||
self.assertEqual(existing_doc.title, updated_doc.title)
|
||||
self.assertEqual(updated_doc.description, 'test description')
|
||||
self.assertEqual(updated_doc.table_field_1[0].child_title, 'child title')
|
||||
self.assertEqual(updated_doc.description, "test description")
|
||||
self.assertEqual(updated_doc.table_field_1[0].child_title, "child title")
|
||||
self.assertEqual(updated_doc.table_field_1[0].name, existing_doc.table_field_1[0].name)
|
||||
self.assertEqual(updated_doc.table_field_1[0].child_description, 'child description')
|
||||
self.assertEqual(updated_doc.table_field_1_again[0].child_title, 'child title again')
|
||||
self.assertEqual(updated_doc.table_field_1[0].child_description, "child description")
|
||||
self.assertEqual(updated_doc.table_field_1_again[0].child_title, "child title again")
|
||||
|
||||
def get_importer(self, doctype, import_file, update=False):
|
||||
data_import = frappe.new_doc('Data Import')
|
||||
data_import.import_type = 'Insert New Records' if not update else 'Update Existing Records'
|
||||
data_import = frappe.new_doc("Data Import")
|
||||
data_import.import_type = "Insert New Records" if not update else "Update Existing Records"
|
||||
data_import.reference_doctype = doctype
|
||||
data_import.import_file = import_file.file_url
|
||||
data_import.insert()
|
||||
|
|
@ -121,88 +139,109 @@ class TestImporter(unittest.TestCase):
|
|||
|
||||
return data_import
|
||||
|
||||
|
||||
def create_doctype_if_not_exists(doctype_name, force=False):
|
||||
if force:
|
||||
frappe.delete_doc_if_exists('DocType', doctype_name)
|
||||
frappe.delete_doc_if_exists('DocType', 'Child 1 of ' + doctype_name)
|
||||
frappe.delete_doc_if_exists('DocType', 'Child 2 of ' + doctype_name)
|
||||
frappe.delete_doc_if_exists("DocType", doctype_name)
|
||||
frappe.delete_doc_if_exists("DocType", "Child 1 of " + doctype_name)
|
||||
frappe.delete_doc_if_exists("DocType", "Child 2 of " + doctype_name)
|
||||
|
||||
if frappe.db.exists('DocType', doctype_name):
|
||||
if frappe.db.exists("DocType", doctype_name):
|
||||
return
|
||||
|
||||
# Child Table 1
|
||||
table_1_name = 'Child 1 of ' + doctype_name
|
||||
frappe.get_doc({
|
||||
'doctype': 'DocType',
|
||||
'name': table_1_name,
|
||||
'module': 'Custom',
|
||||
'custom': 1,
|
||||
'istable': 1,
|
||||
'fields': [
|
||||
{'label': 'Child Title', 'fieldname': 'child_title', 'reqd': 1, 'fieldtype': 'Data'},
|
||||
{'label': 'Child Description', 'fieldname': 'child_description', 'fieldtype': 'Small Text'},
|
||||
{'label': 'Child Date', 'fieldname': 'child_date', 'fieldtype': 'Date'},
|
||||
{'label': 'Child Number', 'fieldname': 'child_number', 'fieldtype': 'Int'},
|
||||
{'label': 'Child Number', 'fieldname': 'child_another_number', 'fieldtype': 'Int'},
|
||||
]
|
||||
}).insert()
|
||||
table_1_name = "Child 1 of " + doctype_name
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"name": table_1_name,
|
||||
"module": "Custom",
|
||||
"custom": 1,
|
||||
"istable": 1,
|
||||
"fields": [
|
||||
{"label": "Child Title", "fieldname": "child_title", "reqd": 1, "fieldtype": "Data"},
|
||||
{"label": "Child Description", "fieldname": "child_description", "fieldtype": "Small Text"},
|
||||
{"label": "Child Date", "fieldname": "child_date", "fieldtype": "Date"},
|
||||
{"label": "Child Number", "fieldname": "child_number", "fieldtype": "Int"},
|
||||
{"label": "Child Number", "fieldname": "child_another_number", "fieldtype": "Int"},
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# Child Table 2
|
||||
table_2_name = 'Child 2 of ' + doctype_name
|
||||
frappe.get_doc({
|
||||
'doctype': 'DocType',
|
||||
'name': table_2_name,
|
||||
'module': 'Custom',
|
||||
'custom': 1,
|
||||
'istable': 1,
|
||||
'fields': [
|
||||
{'label': 'Child 2 Title', 'fieldname': 'child_2_title', 'reqd': 1, 'fieldtype': 'Data'},
|
||||
{'label': 'Child 2 Description', 'fieldname': 'child_2_description', 'fieldtype': 'Small Text'},
|
||||
{'label': 'Child 2 Date', 'fieldname': 'child_2_date', 'fieldtype': 'Date'},
|
||||
{'label': 'Child 2 Number', 'fieldname': 'child_2_number', 'fieldtype': 'Int'},
|
||||
{'label': 'Child 2 Number', 'fieldname': 'child_2_another_number', 'fieldtype': 'Int'},
|
||||
]
|
||||
}).insert()
|
||||
table_2_name = "Child 2 of " + doctype_name
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"name": table_2_name,
|
||||
"module": "Custom",
|
||||
"custom": 1,
|
||||
"istable": 1,
|
||||
"fields": [
|
||||
{"label": "Child 2 Title", "fieldname": "child_2_title", "reqd": 1, "fieldtype": "Data"},
|
||||
{
|
||||
"label": "Child 2 Description",
|
||||
"fieldname": "child_2_description",
|
||||
"fieldtype": "Small Text",
|
||||
},
|
||||
{"label": "Child 2 Date", "fieldname": "child_2_date", "fieldtype": "Date"},
|
||||
{"label": "Child 2 Number", "fieldname": "child_2_number", "fieldtype": "Int"},
|
||||
{"label": "Child 2 Number", "fieldname": "child_2_another_number", "fieldtype": "Int"},
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# Main Table
|
||||
frappe.get_doc({
|
||||
'doctype': 'DocType',
|
||||
'name': doctype_name,
|
||||
'module': 'Custom',
|
||||
'custom': 1,
|
||||
'autoname': 'field:title',
|
||||
'fields': [
|
||||
{'label': 'Title', 'fieldname': 'title', 'reqd': 1, 'fieldtype': 'Data'},
|
||||
{'label': 'Description', 'fieldname': 'description', 'fieldtype': 'Small Text'},
|
||||
{'label': 'Date', 'fieldname': 'date', 'fieldtype': 'Date'},
|
||||
{'label': 'Duration', 'fieldname': 'duration', 'fieldtype': 'Duration'},
|
||||
{'label': 'Number', 'fieldname': 'number', 'fieldtype': 'Int'},
|
||||
{'label': 'Number', 'fieldname': 'another_number', 'fieldtype': 'Int'},
|
||||
{'label': 'Table Field 1', 'fieldname': 'table_field_1', 'fieldtype': 'Table', 'options': table_1_name},
|
||||
{'label': 'Table Field 2', 'fieldname': 'table_field_2', 'fieldtype': 'Table', 'options': table_2_name},
|
||||
{'label': 'Table Field 1 Again', 'fieldname': 'table_field_1_again', 'fieldtype': 'Table', 'options': table_1_name},
|
||||
],
|
||||
'permissions': [
|
||||
{'role': 'System Manager'}
|
||||
]
|
||||
}).insert()
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"name": doctype_name,
|
||||
"module": "Custom",
|
||||
"custom": 1,
|
||||
"autoname": "field:title",
|
||||
"fields": [
|
||||
{"label": "Title", "fieldname": "title", "reqd": 1, "fieldtype": "Data"},
|
||||
{"label": "Description", "fieldname": "description", "fieldtype": "Small Text"},
|
||||
{"label": "Date", "fieldname": "date", "fieldtype": "Date"},
|
||||
{"label": "Duration", "fieldname": "duration", "fieldtype": "Duration"},
|
||||
{"label": "Number", "fieldname": "number", "fieldtype": "Int"},
|
||||
{"label": "Number", "fieldname": "another_number", "fieldtype": "Int"},
|
||||
{
|
||||
"label": "Table Field 1",
|
||||
"fieldname": "table_field_1",
|
||||
"fieldtype": "Table",
|
||||
"options": table_1_name,
|
||||
},
|
||||
{
|
||||
"label": "Table Field 2",
|
||||
"fieldname": "table_field_2",
|
||||
"fieldtype": "Table",
|
||||
"options": table_2_name,
|
||||
},
|
||||
{
|
||||
"label": "Table Field 1 Again",
|
||||
"fieldname": "table_field_1_again",
|
||||
"fieldtype": "Table",
|
||||
"options": table_1_name,
|
||||
},
|
||||
],
|
||||
"permissions": [{"role": "System Manager"}],
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
def get_import_file(csv_file_name, force=False):
|
||||
file_name = csv_file_name + '.csv'
|
||||
_file = frappe.db.exists('File', {'file_name': file_name})
|
||||
file_name = csv_file_name + ".csv"
|
||||
_file = frappe.db.exists("File", {"file_name": file_name})
|
||||
if force and _file:
|
||||
frappe.delete_doc_if_exists('File', _file)
|
||||
frappe.delete_doc_if_exists("File", _file)
|
||||
|
||||
if frappe.db.exists('File', {'file_name': file_name}):
|
||||
f = frappe.get_doc('File', {'file_name': file_name})
|
||||
if frappe.db.exists("File", {"file_name": file_name}):
|
||||
f = frappe.get_doc("File", {"file_name": file_name})
|
||||
else:
|
||||
full_path = get_csv_file_path(file_name)
|
||||
f = frappe.get_doc(
|
||||
doctype='File',
|
||||
content=frappe.read_file(full_path),
|
||||
file_name=file_name,
|
||||
is_private=1
|
||||
doctype="File", content=frappe.read_file(full_path), file_name=file_name, is_private=1
|
||||
)
|
||||
f.save(ignore_permissions=True)
|
||||
|
||||
|
|
@ -210,4 +249,4 @@ def get_import_file(csv_file_name, force=False):
|
|||
|
||||
|
||||
def get_csv_file_path(file_name):
|
||||
return frappe.get_app_path('frappe', 'core', 'doctype', 'data_import', 'fixtures', file_name)
|
||||
return frappe.get_app_path("frappe", "core", "doctype", "data_import", "fixtures", file_name)
|
||||
|
|
|
|||
|
|
@ -4,5 +4,6 @@
|
|||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class DataImportLog(Document):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -4,5 +4,6 @@
|
|||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class TestDataImportLog(unittest.TestCase):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
|
|
|
|||
|
|
@ -2,19 +2,24 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class DefaultValue(Document):
|
||||
pass
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
"""Create indexes for `tabDefaultValue` on `(parent, defkey)`"""
|
||||
frappe.db.commit()
|
||||
frappe.db.add_index(doctype='DefaultValue',
|
||||
fields=['parent', 'defkey'],
|
||||
index_name='defaultvalue_parent_defkey_index')
|
||||
frappe.db.add_index(
|
||||
doctype="DefaultValue",
|
||||
fields=["parent", "defkey"],
|
||||
index_name="defaultvalue_parent_defkey_index",
|
||||
)
|
||||
|
||||
frappe.db.add_index(doctype='DefaultValue',
|
||||
fields=['parent', 'parenttype'],
|
||||
index_name='defaultvalue_parent_parenttype_index')
|
||||
frappe.db.add_index(
|
||||
doctype="DefaultValue",
|
||||
fields=["parent", "parenttype"],
|
||||
index_name="defaultvalue_parent_parenttype_index",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@
|
|||
# Copyright (c) 2015, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.desk.doctype.bulk_update.bulk_update import show_progress
|
||||
from frappe.model.document import Document
|
||||
from frappe import _
|
||||
|
||||
|
||||
class DeletedDocument(Document):
|
||||
|
|
@ -15,7 +16,7 @@ class DeletedDocument(Document):
|
|||
|
||||
@frappe.whitelist()
|
||||
def restore(name, alert=True):
|
||||
deleted = frappe.get_doc('Deleted Document', name)
|
||||
deleted = frappe.get_doc("Deleted Document", name)
|
||||
|
||||
if deleted.restored:
|
||||
frappe.throw(_("Document {0} Already Restored").format(name), exc=frappe.DocumentAlreadyRestored)
|
||||
|
|
@ -29,20 +30,20 @@ def restore(name, alert=True):
|
|||
doc.docstatus = 0
|
||||
doc.insert()
|
||||
|
||||
doc.add_comment('Edit', _('restored {0} as {1}').format(deleted.deleted_name, doc.name))
|
||||
doc.add_comment("Edit", _("restored {0} as {1}").format(deleted.deleted_name, doc.name))
|
||||
|
||||
deleted.new_name = doc.name
|
||||
deleted.restored = 1
|
||||
deleted.db_update()
|
||||
|
||||
if alert:
|
||||
frappe.msgprint(_('Document Restored'))
|
||||
frappe.msgprint(_("Document Restored"))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def bulk_restore(docnames):
|
||||
docnames = frappe.parse_json(docnames)
|
||||
message = _('Restoring Deleted Document')
|
||||
message = _("Restoring Deleted Document")
|
||||
restored, invalid, failed = [], [], []
|
||||
|
||||
for i, d in enumerate(docnames):
|
||||
|
|
@ -61,8 +62,4 @@ def bulk_restore(docnames):
|
|||
failed.append(d)
|
||||
frappe.db.rollback()
|
||||
|
||||
return {
|
||||
"restored": restored,
|
||||
"invalid": invalid,
|
||||
"failed": failed
|
||||
}
|
||||
return {"restored": restored, "invalid": invalid, "failed": failed}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import frappe
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
# test_records = frappe.get_test_records('Deleted Document')
|
||||
|
||||
|
||||
class TestDeletedDocument(unittest.TestCase):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
|
|
|
|||
|
|
@ -4,28 +4,28 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class DocField(Document):
|
||||
def get_link_doctype(self):
|
||||
'''Returns the Link doctype for the docfield (if applicable)
|
||||
"""Returns the Link doctype for the docfield (if applicable)
|
||||
if fieldtype is Link: Returns "options"
|
||||
if fieldtype is Table MultiSelect: Returns "options" of the Link field in the Child Table
|
||||
'''
|
||||
if self.fieldtype == 'Link':
|
||||
"""
|
||||
if self.fieldtype == "Link":
|
||||
return self.options
|
||||
|
||||
if self.fieldtype == 'Table MultiSelect':
|
||||
if self.fieldtype == "Table MultiSelect":
|
||||
table_doctype = self.options
|
||||
|
||||
link_doctype = frappe.db.get_value('DocField', {
|
||||
'fieldtype': 'Link',
|
||||
'parenttype': 'DocType',
|
||||
'parent': table_doctype,
|
||||
'in_list_view': 1
|
||||
}, 'options')
|
||||
link_doctype = frappe.db.get_value(
|
||||
"DocField",
|
||||
{"fieldtype": "Link", "parenttype": "DocType", "parent": table_doctype, "in_list_view": 1},
|
||||
"options",
|
||||
)
|
||||
|
||||
return link_doctype
|
||||
|
||||
def get_select_options(self):
|
||||
if self.fieldtype == 'Select':
|
||||
options = self.options or ''
|
||||
return [d for d in options.split('\n') if d]
|
||||
if self.fieldtype == "Select":
|
||||
options = self.options or ""
|
||||
return [d for d in options.split("\n") if d]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class DocPerm(Document):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe import _
|
||||
from frappe.utils import get_fullname, cint
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, get_fullname
|
||||
|
||||
exclude_from_linked_with = True
|
||||
|
||||
|
||||
class DocShare(Document):
|
||||
no_feed_on_delete = True
|
||||
|
||||
|
|
@ -36,15 +37,21 @@ class DocShare(Document):
|
|||
frappe.throw(_("User is mandatory for Share"), frappe.MandatoryError)
|
||||
|
||||
def check_share_permission(self):
|
||||
if (not self.flags.ignore_share_permission and
|
||||
not frappe.has_permission(self.share_doctype, "share", self.get_doc())):
|
||||
if not self.flags.ignore_share_permission and not frappe.has_permission(
|
||||
self.share_doctype, "share", self.get_doc()
|
||||
):
|
||||
|
||||
frappe.throw(_('You need to have "Share" permission'), frappe.PermissionError)
|
||||
|
||||
def check_is_submittable(self):
|
||||
if self.submit and not cint(frappe.db.get_value("DocType", self.share_doctype, "is_submittable")):
|
||||
frappe.throw(_("Cannot share {0} with submit permission as the doctype {1} is not submittable").format(
|
||||
frappe.bold(self.share_name), frappe.bold(self.share_doctype)))
|
||||
if self.submit and not cint(
|
||||
frappe.db.get_value("DocType", self.share_doctype, "is_submittable")
|
||||
):
|
||||
frappe.throw(
|
||||
_("Cannot share {0} with submit permission as the doctype {1} is not submittable").format(
|
||||
frappe.bold(self.share_name), frappe.bold(self.share_doctype)
|
||||
)
|
||||
)
|
||||
|
||||
def after_insert(self):
|
||||
doc = self.get_doc()
|
||||
|
|
@ -53,14 +60,21 @@ class DocShare(Document):
|
|||
if self.everyone:
|
||||
doc.add_comment("Shared", _("{0} shared this document with everyone").format(owner))
|
||||
else:
|
||||
doc.add_comment("Shared", _("{0} shared this document with {1}").format(owner, get_fullname(self.user)))
|
||||
doc.add_comment(
|
||||
"Shared", _("{0} shared this document with {1}").format(owner, get_fullname(self.user))
|
||||
)
|
||||
|
||||
def on_trash(self):
|
||||
if not self.flags.ignore_share_permission:
|
||||
self.check_share_permission()
|
||||
|
||||
self.get_doc().add_comment("Unshared",
|
||||
_("{0} un-shared this document with {1}").format(get_fullname(self.owner), get_fullname(self.user)))
|
||||
self.get_doc().add_comment(
|
||||
"Unshared",
|
||||
_("{0} un-shared this document with {1}").format(
|
||||
get_fullname(self.owner), get_fullname(self.user)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
"""Add index in `tabDocShare` for `(user, share_doctype)`"""
|
||||
|
|
|
|||
|
|
@ -1,20 +1,26 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
import frappe.share
|
||||
import unittest
|
||||
from frappe.automation.doctype.auto_repeat.test_auto_repeat import create_submittable_doctype
|
||||
|
||||
test_dependencies = ['User']
|
||||
test_dependencies = ["User"]
|
||||
|
||||
|
||||
class TestDocShare(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.user = "test@example.com"
|
||||
self.event = frappe.get_doc({"doctype": "Event",
|
||||
"subject": "test share event",
|
||||
"starts_on": "2015-01-01 10:00:00",
|
||||
"event_type": "Private"}).insert()
|
||||
self.event = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Event",
|
||||
"subject": "test share event",
|
||||
"starts_on": "2015-01-01 10:00:00",
|
||||
"event_type": "Private",
|
||||
}
|
||||
).insert()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.set_user("Administrator")
|
||||
|
|
@ -98,7 +104,9 @@ class TestDocShare(unittest.TestCase):
|
|||
doctype = "Test DocShare with Submit"
|
||||
create_submittable_doctype(doctype, submit_perms=0)
|
||||
|
||||
submittable_doc = frappe.get_doc(dict(doctype=doctype, test="test docshare with submit")).insert()
|
||||
submittable_doc = frappe.get_doc(
|
||||
dict(doctype=doctype, test="test docshare with submit")
|
||||
).insert()
|
||||
|
||||
frappe.set_user(self.user)
|
||||
self.assertFalse(frappe.has_permission(doctype, "submit", user=self.user))
|
||||
|
|
@ -107,10 +115,14 @@ class TestDocShare(unittest.TestCase):
|
|||
frappe.share.add(doctype, submittable_doc.name, self.user, submit=1)
|
||||
|
||||
frappe.set_user(self.user)
|
||||
self.assertTrue(frappe.has_permission(doctype, "submit", doc=submittable_doc.name, user=self.user))
|
||||
self.assertTrue(
|
||||
frappe.has_permission(doctype, "submit", doc=submittable_doc.name, user=self.user)
|
||||
)
|
||||
|
||||
# test cascade
|
||||
self.assertTrue(frappe.has_permission(doctype, "read", doc=submittable_doc.name, user=self.user))
|
||||
self.assertTrue(frappe.has_permission(doctype, "write", doc=submittable_doc.name, user=self.user))
|
||||
self.assertTrue(
|
||||
frappe.has_permission(doctype, "write", doc=submittable_doc.name, user=self.user)
|
||||
)
|
||||
|
||||
frappe.share.remove(doctype, submittable_doc.name, self.user)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,8 @@
|
|||
import frappe
|
||||
from frappe.desk.utils import slug
|
||||
|
||||
|
||||
def execute():
|
||||
for doctype in frappe.get_all('DocType', ['name', 'route'], dict(istable=0)):
|
||||
if not doctype.route:
|
||||
frappe.db.set_value('DocType', doctype.name, 'route', slug(doctype.name), update_modified = False)
|
||||
for doctype in frappe.get_all("DocType", ["name", "route"], dict(istable=0)):
|
||||
if not doctype.route:
|
||||
frappe.db.set_value("DocType", doctype.name, "route", slug(doctype.name), update_modified=False)
|
||||
|
|
|
|||
|
|
@ -1,21 +1,24 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import frappe
|
||||
import unittest
|
||||
from frappe.core.doctype.doctype.doctype import (UniqueFieldnameError,
|
||||
IllegalMandatoryError,
|
||||
DoctypeLinkError,
|
||||
WrongOptionsDoctypeLinkError,
|
||||
HiddenAndMandatoryWithoutDefaultError,
|
||||
|
||||
import frappe
|
||||
from frappe.core.doctype.doctype.doctype import (
|
||||
CannotIndexedError,
|
||||
DoctypeLinkError,
|
||||
HiddenAndMandatoryWithoutDefaultError,
|
||||
IllegalMandatoryError,
|
||||
InvalidFieldNameError,
|
||||
validate_links_table_fieldnames)
|
||||
UniqueFieldnameError,
|
||||
WrongOptionsDoctypeLinkError,
|
||||
validate_links_table_fieldnames,
|
||||
)
|
||||
|
||||
# test_records = frappe.get_test_records('DocType')
|
||||
|
||||
class TestDocType(unittest.TestCase):
|
||||
|
||||
class TestDocType(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
|
|
@ -23,7 +26,10 @@ class TestDocType(unittest.TestCase):
|
|||
self.assertRaises(frappe.NameError, new_doctype("_Some DocType").insert)
|
||||
self.assertRaises(frappe.NameError, new_doctype("8Some DocType").insert)
|
||||
self.assertRaises(frappe.NameError, new_doctype("Some (DocType)").insert)
|
||||
self.assertRaises(frappe.NameError, new_doctype("Some Doctype with a name whose length is more than 61 characters").insert)
|
||||
self.assertRaises(
|
||||
frappe.NameError,
|
||||
new_doctype("Some Doctype with a name whose length is more than 61 characters").insert,
|
||||
)
|
||||
for name in ("Some DocType", "Some_DocType", "Some-DocType"):
|
||||
if frappe.db.exists("DocType", name):
|
||||
frappe.delete_doc("DocType", name)
|
||||
|
|
@ -86,19 +92,33 @@ class TestDocType(unittest.TestCase):
|
|||
def test_all_depends_on_fields_conditions(self):
|
||||
import re
|
||||
|
||||
docfields = frappe.get_all("DocField",
|
||||
or_filters={
|
||||
"ifnull(depends_on, '')": ("!=", ''),
|
||||
"ifnull(collapsible_depends_on, '')": ("!=", ''),
|
||||
"ifnull(mandatory_depends_on, '')": ("!=", ''),
|
||||
"ifnull(read_only_depends_on, '')": ("!=", '')
|
||||
docfields = frappe.get_all(
|
||||
"DocField",
|
||||
or_filters={
|
||||
"ifnull(depends_on, '')": ("!=", ""),
|
||||
"ifnull(collapsible_depends_on, '')": ("!=", ""),
|
||||
"ifnull(mandatory_depends_on, '')": ("!=", ""),
|
||||
"ifnull(read_only_depends_on, '')": ("!=", ""),
|
||||
},
|
||||
fields=["parent", "depends_on", "collapsible_depends_on", "mandatory_depends_on",\
|
||||
"read_only_depends_on", "fieldname", "fieldtype"])
|
||||
fields=[
|
||||
"parent",
|
||||
"depends_on",
|
||||
"collapsible_depends_on",
|
||||
"mandatory_depends_on",
|
||||
"read_only_depends_on",
|
||||
"fieldname",
|
||||
"fieldtype",
|
||||
],
|
||||
)
|
||||
|
||||
pattern = r'[\w\.:_]+\s*={1}\s*[\w\.@\'"]+'
|
||||
for field in docfields:
|
||||
for depends_on in ["depends_on", "collapsible_depends_on", "mandatory_depends_on", "read_only_depends_on"]:
|
||||
for depends_on in [
|
||||
"depends_on",
|
||||
"collapsible_depends_on",
|
||||
"mandatory_depends_on",
|
||||
"read_only_depends_on",
|
||||
]:
|
||||
condition = field.get(depends_on)
|
||||
if condition:
|
||||
self.assertFalse(re.match(pattern, condition))
|
||||
|
|
@ -108,18 +128,18 @@ class TestDocType(unittest.TestCase):
|
|||
valid_data_field_options = frappe.model.data_field_options + ("",)
|
||||
invalid_data_field_options = ("Invalid Option 1", frappe.utils.random_string(5))
|
||||
|
||||
for field_option in (valid_data_field_options + invalid_data_field_options):
|
||||
test_doctype = frappe.get_doc({
|
||||
"doctype": "DocType",
|
||||
"name": doctype_name,
|
||||
"module": "Core",
|
||||
"custom": 1,
|
||||
"fields": [{
|
||||
"fieldname": "{0}_field".format(field_option),
|
||||
"fieldtype": "Data",
|
||||
"options": field_option
|
||||
}]
|
||||
})
|
||||
for field_option in valid_data_field_options + invalid_data_field_options:
|
||||
test_doctype = frappe.get_doc(
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"name": doctype_name,
|
||||
"module": "Core",
|
||||
"custom": 1,
|
||||
"fields": [
|
||||
{"fieldname": "{0}_field".format(field_option), "fieldtype": "Data", "options": field_option}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
if field_option in invalid_data_field_options:
|
||||
# assert that only data options in frappe.model.data_field_options are valid
|
||||
|
|
@ -130,45 +150,29 @@ class TestDocType(unittest.TestCase):
|
|||
test_doctype.delete()
|
||||
|
||||
def test_sync_field_order(self):
|
||||
from frappe.modules.import_file import get_file_path
|
||||
import os
|
||||
|
||||
from frappe.modules.import_file import get_file_path
|
||||
|
||||
# create test doctype
|
||||
test_doctype = frappe.get_doc({
|
||||
"doctype": "DocType",
|
||||
"module": "Core",
|
||||
"fields": [
|
||||
{
|
||||
"label": "Field 1",
|
||||
"fieldname": "field_1",
|
||||
"fieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"label": "Field 2",
|
||||
"fieldname": "field_2",
|
||||
"fieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"label": "Field 3",
|
||||
"fieldname": "field_3",
|
||||
"fieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"label": "Field 4",
|
||||
"fieldname": "field_4",
|
||||
"fieldtype": "Data"
|
||||
}
|
||||
],
|
||||
"permissions": [{
|
||||
"role": "System Manager",
|
||||
"read": 1
|
||||
}],
|
||||
"name": "Test Field Order DocType",
|
||||
"__islocal": 1
|
||||
})
|
||||
test_doctype = frappe.get_doc(
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"module": "Core",
|
||||
"fields": [
|
||||
{"label": "Field 1", "fieldname": "field_1", "fieldtype": "Data"},
|
||||
{"label": "Field 2", "fieldname": "field_2", "fieldtype": "Data"},
|
||||
{"label": "Field 3", "fieldname": "field_3", "fieldtype": "Data"},
|
||||
{"label": "Field 4", "fieldname": "field_4", "fieldtype": "Data"},
|
||||
],
|
||||
"permissions": [{"role": "System Manager", "read": 1}],
|
||||
"name": "Test Field Order DocType",
|
||||
"__islocal": 1,
|
||||
}
|
||||
)
|
||||
|
||||
path = get_file_path(test_doctype.module, test_doctype.doctype, test_doctype.name)
|
||||
initial_fields_order = ['field_1', 'field_2', 'field_3', 'field_4']
|
||||
initial_fields_order = ["field_1", "field_2", "field_3", "field_4"]
|
||||
|
||||
frappe.delete_doc_if_exists("DocType", "Test Field Order DocType")
|
||||
if os.path.isfile(path):
|
||||
|
|
@ -181,14 +185,18 @@ class TestDocType(unittest.TestCase):
|
|||
# assert that field_order list is being created with the default order
|
||||
test_doctype_json = frappe.get_file_json(path)
|
||||
self.assertTrue(test_doctype_json.get("field_order"))
|
||||
self.assertEqual(len(test_doctype_json['fields']), len(test_doctype_json['field_order']))
|
||||
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], test_doctype_json['field_order'])
|
||||
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], initial_fields_order)
|
||||
self.assertListEqual(test_doctype_json['field_order'], initial_fields_order)
|
||||
self.assertEqual(len(test_doctype_json["fields"]), len(test_doctype_json["field_order"]))
|
||||
self.assertListEqual(
|
||||
[f["fieldname"] for f in test_doctype_json["fields"]], test_doctype_json["field_order"]
|
||||
)
|
||||
self.assertListEqual(
|
||||
[f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order
|
||||
)
|
||||
self.assertListEqual(test_doctype_json["field_order"], initial_fields_order)
|
||||
|
||||
# remove field_order to test reload_doc/sync/migrate is backwards compatible without field_order
|
||||
del test_doctype_json['field_order']
|
||||
with open(path, 'w+') as txtfile:
|
||||
del test_doctype_json["field_order"]
|
||||
with open(path, "w+") as txtfile:
|
||||
txtfile.write(frappe.as_json(test_doctype_json))
|
||||
|
||||
# assert that field_order is actually removed from the json file
|
||||
|
|
@ -203,10 +211,14 @@ class TestDocType(unittest.TestCase):
|
|||
test_doctype.save()
|
||||
test_doctype_json = frappe.get_file_json(path)
|
||||
self.assertTrue(test_doctype_json.get("field_order"))
|
||||
self.assertEqual(len(test_doctype_json['fields']), len(test_doctype_json['field_order']))
|
||||
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], test_doctype_json['field_order'])
|
||||
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], initial_fields_order)
|
||||
self.assertListEqual(test_doctype_json['field_order'], initial_fields_order)
|
||||
self.assertEqual(len(test_doctype_json["fields"]), len(test_doctype_json["field_order"]))
|
||||
self.assertListEqual(
|
||||
[f["fieldname"] for f in test_doctype_json["fields"]], test_doctype_json["field_order"]
|
||||
)
|
||||
self.assertListEqual(
|
||||
[f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order
|
||||
)
|
||||
self.assertListEqual(test_doctype_json["field_order"], initial_fields_order)
|
||||
|
||||
# reorder fields: swap row 1 and 3
|
||||
test_doctype.fields[0], test_doctype.fields[2] = test_doctype.fields[2], test_doctype.fields[0]
|
||||
|
|
@ -216,25 +228,30 @@ class TestDocType(unittest.TestCase):
|
|||
# assert that reordering fields only affects `field_order` rather than `fields` attr
|
||||
test_doctype.save()
|
||||
test_doctype_json = frappe.get_file_json(path)
|
||||
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], initial_fields_order)
|
||||
self.assertListEqual(test_doctype_json['field_order'], ['field_3', 'field_2', 'field_1', 'field_4'])
|
||||
self.assertListEqual(
|
||||
[f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order
|
||||
)
|
||||
self.assertListEqual(
|
||||
test_doctype_json["field_order"], ["field_3", "field_2", "field_1", "field_4"]
|
||||
)
|
||||
|
||||
# reorder `field_order` in the json file: swap row 2 and 4
|
||||
test_doctype_json['field_order'][1], test_doctype_json['field_order'][3] = test_doctype_json['field_order'][3], test_doctype_json['field_order'][1]
|
||||
with open(path, 'w+') as txtfile:
|
||||
test_doctype_json["field_order"][1], test_doctype_json["field_order"][3] = (
|
||||
test_doctype_json["field_order"][3],
|
||||
test_doctype_json["field_order"][1],
|
||||
)
|
||||
with open(path, "w+") as txtfile:
|
||||
txtfile.write(frappe.as_json(test_doctype_json))
|
||||
|
||||
# assert that reordering `field_order` from json file is reflected in DocType upon migrate/sync
|
||||
frappe.reload_doctype(test_doctype.name, force=True)
|
||||
test_doctype.reload()
|
||||
self.assertListEqual([f.fieldname for f in test_doctype.fields], ['field_3', 'field_4', 'field_1', 'field_2'])
|
||||
self.assertListEqual(
|
||||
[f.fieldname for f in test_doctype.fields], ["field_3", "field_4", "field_1", "field_2"]
|
||||
)
|
||||
|
||||
# insert row in the middle and remove first row (field 3)
|
||||
test_doctype.append("fields", {
|
||||
"label": "Field 5",
|
||||
"fieldname": "field_5",
|
||||
"fieldtype": "Data"
|
||||
})
|
||||
test_doctype.append("fields", {"label": "Field 5", "fieldname": "field_5", "fieldtype": "Data"})
|
||||
test_doctype.fields[4], test_doctype.fields[3] = test_doctype.fields[3], test_doctype.fields[4]
|
||||
test_doctype.fields[3], test_doctype.fields[2] = test_doctype.fields[2], test_doctype.fields[3]
|
||||
test_doctype.remove(test_doctype.fields[0])
|
||||
|
|
@ -243,115 +260,121 @@ class TestDocType(unittest.TestCase):
|
|||
|
||||
test_doctype.save()
|
||||
test_doctype_json = frappe.get_file_json(path)
|
||||
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], ['field_1', 'field_2', 'field_4', 'field_5'])
|
||||
self.assertListEqual(test_doctype_json['field_order'], ['field_4', 'field_5', 'field_1', 'field_2'])
|
||||
self.assertListEqual(
|
||||
[f["fieldname"] for f in test_doctype_json["fields"]],
|
||||
["field_1", "field_2", "field_4", "field_5"],
|
||||
)
|
||||
self.assertListEqual(
|
||||
test_doctype_json["field_order"], ["field_4", "field_5", "field_1", "field_2"]
|
||||
)
|
||||
except:
|
||||
raise
|
||||
finally:
|
||||
frappe.flags.allow_doctype_export = 0
|
||||
|
||||
def test_unique_field_name_for_two_fields(self):
|
||||
doc = new_doctype('Test Unique Field')
|
||||
field_1 = doc.append('fields', {})
|
||||
field_1.fieldname = 'some_fieldname_1'
|
||||
field_1.fieldtype = 'Data'
|
||||
doc = new_doctype("Test Unique Field")
|
||||
field_1 = doc.append("fields", {})
|
||||
field_1.fieldname = "some_fieldname_1"
|
||||
field_1.fieldtype = "Data"
|
||||
|
||||
field_2 = doc.append('fields', {})
|
||||
field_2.fieldname = 'some_fieldname_1'
|
||||
field_2.fieldtype = 'Data'
|
||||
field_2 = doc.append("fields", {})
|
||||
field_2.fieldname = "some_fieldname_1"
|
||||
field_2.fieldtype = "Data"
|
||||
|
||||
self.assertRaises(UniqueFieldnameError, doc.insert)
|
||||
|
||||
def test_fieldname_is_not_name(self):
|
||||
doc = new_doctype('Test Name Field')
|
||||
field_1 = doc.append('fields', {})
|
||||
field_1.label = 'Name'
|
||||
field_1.fieldtype = 'Data'
|
||||
doc = new_doctype("Test Name Field")
|
||||
field_1 = doc.append("fields", {})
|
||||
field_1.label = "Name"
|
||||
field_1.fieldtype = "Data"
|
||||
doc.insert()
|
||||
self.assertEqual(doc.fields[1].fieldname, "name1")
|
||||
doc.fields[1].fieldname = 'name'
|
||||
doc.fields[1].fieldname = "name"
|
||||
self.assertRaises(InvalidFieldNameError, doc.save)
|
||||
|
||||
def test_illegal_mandatory_validation(self):
|
||||
doc = new_doctype('Test Illegal mandatory')
|
||||
field_1 = doc.append('fields', {})
|
||||
field_1.fieldname = 'some_fieldname_1'
|
||||
field_1.fieldtype = 'Section Break'
|
||||
doc = new_doctype("Test Illegal mandatory")
|
||||
field_1 = doc.append("fields", {})
|
||||
field_1.fieldname = "some_fieldname_1"
|
||||
field_1.fieldtype = "Section Break"
|
||||
field_1.reqd = 1
|
||||
|
||||
self.assertRaises(IllegalMandatoryError, doc.insert)
|
||||
|
||||
def test_link_with_wrong_and_no_options(self):
|
||||
doc = new_doctype('Test link')
|
||||
field_1 = doc.append('fields', {})
|
||||
field_1.fieldname = 'some_fieldname_1'
|
||||
field_1.fieldtype = 'Link'
|
||||
doc = new_doctype("Test link")
|
||||
field_1 = doc.append("fields", {})
|
||||
field_1.fieldname = "some_fieldname_1"
|
||||
field_1.fieldtype = "Link"
|
||||
|
||||
self.assertRaises(DoctypeLinkError, doc.insert)
|
||||
|
||||
field_1.options = 'wrongdoctype'
|
||||
field_1.options = "wrongdoctype"
|
||||
|
||||
self.assertRaises(WrongOptionsDoctypeLinkError, doc.insert)
|
||||
|
||||
def test_hidden_and_mandatory_without_default(self):
|
||||
doc = new_doctype('Test hidden and mandatory')
|
||||
field_1 = doc.append('fields', {})
|
||||
field_1.fieldname = 'some_fieldname_1'
|
||||
field_1.fieldtype = 'Data'
|
||||
doc = new_doctype("Test hidden and mandatory")
|
||||
field_1 = doc.append("fields", {})
|
||||
field_1.fieldname = "some_fieldname_1"
|
||||
field_1.fieldtype = "Data"
|
||||
field_1.reqd = 1
|
||||
field_1.hidden = 1
|
||||
|
||||
self.assertRaises(HiddenAndMandatoryWithoutDefaultError, doc.insert)
|
||||
|
||||
def test_field_can_not_be_indexed_validation(self):
|
||||
doc = new_doctype('Test index')
|
||||
field_1 = doc.append('fields', {})
|
||||
field_1.fieldname = 'some_fieldname_1'
|
||||
field_1.fieldtype = 'Long Text'
|
||||
doc = new_doctype("Test index")
|
||||
field_1 = doc.append("fields", {})
|
||||
field_1.fieldname = "some_fieldname_1"
|
||||
field_1.fieldtype = "Long Text"
|
||||
field_1.search_index = 1
|
||||
|
||||
self.assertRaises(CannotIndexedError, doc.insert)
|
||||
|
||||
def test_cancel_link_doctype(self):
|
||||
import json
|
||||
from frappe.desk.form.linked_with import get_submitted_linked_docs, cancel_all_linked_docs
|
||||
|
||||
#create doctype
|
||||
link_doc = new_doctype('Test Linked Doctype')
|
||||
from frappe.desk.form.linked_with import cancel_all_linked_docs, get_submitted_linked_docs
|
||||
|
||||
# create doctype
|
||||
link_doc = new_doctype("Test Linked Doctype")
|
||||
link_doc.is_submittable = 1
|
||||
for data in link_doc.get('permissions'):
|
||||
for data in link_doc.get("permissions"):
|
||||
data.submit = 1
|
||||
data.cancel = 1
|
||||
link_doc.insert()
|
||||
|
||||
doc = new_doctype('Test Doctype')
|
||||
doc = new_doctype("Test Doctype")
|
||||
doc.is_submittable = 1
|
||||
field_2 = doc.append('fields', {})
|
||||
field_2.label = 'Test Linked Doctype'
|
||||
field_2.fieldname = 'test_linked_doctype'
|
||||
field_2.fieldtype = 'Link'
|
||||
field_2.options = 'Test Linked Doctype'
|
||||
for data in link_doc.get('permissions'):
|
||||
field_2 = doc.append("fields", {})
|
||||
field_2.label = "Test Linked Doctype"
|
||||
field_2.fieldname = "test_linked_doctype"
|
||||
field_2.fieldtype = "Link"
|
||||
field_2.options = "Test Linked Doctype"
|
||||
for data in link_doc.get("permissions"):
|
||||
data.submit = 1
|
||||
data.cancel = 1
|
||||
doc.insert()
|
||||
|
||||
# create doctype data
|
||||
data_link_doc = frappe.new_doc('Test Linked Doctype')
|
||||
data_link_doc.some_fieldname = 'Data1'
|
||||
data_link_doc = frappe.new_doc("Test Linked Doctype")
|
||||
data_link_doc.some_fieldname = "Data1"
|
||||
data_link_doc.insert()
|
||||
data_link_doc.save()
|
||||
data_link_doc.submit()
|
||||
|
||||
data_doc = frappe.new_doc('Test Doctype')
|
||||
data_doc.some_fieldname = 'Data1'
|
||||
data_doc = frappe.new_doc("Test Doctype")
|
||||
data_doc.some_fieldname = "Data1"
|
||||
data_doc.test_linked_doctype = data_link_doc.name
|
||||
data_doc.insert()
|
||||
data_doc.save()
|
||||
data_doc.submit()
|
||||
|
||||
docs = get_submitted_linked_docs(link_doc.name, data_link_doc.name)
|
||||
dump_docs = json.dumps(docs.get('docs'))
|
||||
dump_docs = json.dumps(docs.get("docs"))
|
||||
cancel_all_linked_docs(dump_docs)
|
||||
data_link_doc.cancel()
|
||||
data_doc.load_from_db()
|
||||
|
|
@ -369,69 +392,70 @@ class TestDocType(unittest.TestCase):
|
|||
|
||||
def test_ignore_cancelation_of_linked_doctype_during_cancel(self):
|
||||
import json
|
||||
from frappe.desk.form.linked_with import get_submitted_linked_docs, cancel_all_linked_docs
|
||||
|
||||
#create linked doctype
|
||||
link_doc = new_doctype('Test Linked Doctype 1')
|
||||
from frappe.desk.form.linked_with import cancel_all_linked_docs, get_submitted_linked_docs
|
||||
|
||||
# create linked doctype
|
||||
link_doc = new_doctype("Test Linked Doctype 1")
|
||||
link_doc.is_submittable = 1
|
||||
for data in link_doc.get('permissions'):
|
||||
for data in link_doc.get("permissions"):
|
||||
data.submit = 1
|
||||
data.cancel = 1
|
||||
link_doc.insert()
|
||||
|
||||
#create first parent doctype
|
||||
test_doc_1 = new_doctype('Test Doctype 1')
|
||||
# create first parent doctype
|
||||
test_doc_1 = new_doctype("Test Doctype 1")
|
||||
test_doc_1.is_submittable = 1
|
||||
|
||||
field_2 = test_doc_1.append('fields', {})
|
||||
field_2.label = 'Test Linked Doctype 1'
|
||||
field_2.fieldname = 'test_linked_doctype_a'
|
||||
field_2.fieldtype = 'Link'
|
||||
field_2.options = 'Test Linked Doctype 1'
|
||||
field_2 = test_doc_1.append("fields", {})
|
||||
field_2.label = "Test Linked Doctype 1"
|
||||
field_2.fieldname = "test_linked_doctype_a"
|
||||
field_2.fieldtype = "Link"
|
||||
field_2.options = "Test Linked Doctype 1"
|
||||
|
||||
for data in test_doc_1.get('permissions'):
|
||||
for data in test_doc_1.get("permissions"):
|
||||
data.submit = 1
|
||||
data.cancel = 1
|
||||
test_doc_1.insert()
|
||||
|
||||
#crete second parent doctype
|
||||
doc = new_doctype('Test Doctype 2')
|
||||
# crete second parent doctype
|
||||
doc = new_doctype("Test Doctype 2")
|
||||
doc.is_submittable = 1
|
||||
|
||||
field_2 = doc.append('fields', {})
|
||||
field_2.label = 'Test Linked Doctype 1'
|
||||
field_2.fieldname = 'test_linked_doctype_a'
|
||||
field_2.fieldtype = 'Link'
|
||||
field_2.options = 'Test Linked Doctype 1'
|
||||
field_2 = doc.append("fields", {})
|
||||
field_2.label = "Test Linked Doctype 1"
|
||||
field_2.fieldname = "test_linked_doctype_a"
|
||||
field_2.fieldtype = "Link"
|
||||
field_2.options = "Test Linked Doctype 1"
|
||||
|
||||
for data in link_doc.get('permissions'):
|
||||
for data in link_doc.get("permissions"):
|
||||
data.submit = 1
|
||||
data.cancel = 1
|
||||
doc.insert()
|
||||
|
||||
# create doctype data
|
||||
data_link_doc_1 = frappe.new_doc('Test Linked Doctype 1')
|
||||
data_link_doc_1.some_fieldname = 'Data1'
|
||||
data_link_doc_1 = frappe.new_doc("Test Linked Doctype 1")
|
||||
data_link_doc_1.some_fieldname = "Data1"
|
||||
data_link_doc_1.insert()
|
||||
data_link_doc_1.save()
|
||||
data_link_doc_1.submit()
|
||||
|
||||
data_doc_2 = frappe.new_doc('Test Doctype 1')
|
||||
data_doc_2.some_fieldname = 'Data1'
|
||||
data_doc_2 = frappe.new_doc("Test Doctype 1")
|
||||
data_doc_2.some_fieldname = "Data1"
|
||||
data_doc_2.test_linked_doctype_a = data_link_doc_1.name
|
||||
data_doc_2.insert()
|
||||
data_doc_2.save()
|
||||
data_doc_2.submit()
|
||||
|
||||
data_doc = frappe.new_doc('Test Doctype 2')
|
||||
data_doc.some_fieldname = 'Data1'
|
||||
data_doc = frappe.new_doc("Test Doctype 2")
|
||||
data_doc.some_fieldname = "Data1"
|
||||
data_doc.test_linked_doctype_a = data_link_doc_1.name
|
||||
data_doc.insert()
|
||||
data_doc.save()
|
||||
data_doc.submit()
|
||||
|
||||
docs = get_submitted_linked_docs(link_doc.name, data_link_doc_1.name)
|
||||
dump_docs = json.dumps(docs.get('docs'))
|
||||
dump_docs = json.dumps(docs.get("docs"))
|
||||
|
||||
cancel_all_linked_docs(dump_docs, ignore_doctypes_on_cancel_all=["Test Doctype 2"])
|
||||
|
||||
|
|
@ -442,10 +466,10 @@ class TestDocType(unittest.TestCase):
|
|||
data_doc_2.load_from_db()
|
||||
self.assertEqual(data_link_doc_1.docstatus, 2)
|
||||
|
||||
#linked doc is canceled
|
||||
# linked doc is canceled
|
||||
self.assertEqual(data_doc_2.docstatus, 2)
|
||||
|
||||
#ignored doctype 2 during cancel
|
||||
# ignored doctype 2 during cancel
|
||||
self.assertEqual(data_doc.docstatus, 1)
|
||||
|
||||
# delete doctype record
|
||||
|
|
@ -464,42 +488,35 @@ class TestDocType(unittest.TestCase):
|
|||
doc = new_doctype("Test Links Table Validation")
|
||||
|
||||
# check valid data
|
||||
doc.append("links", {
|
||||
'link_doctype': "User",
|
||||
'link_fieldname': "first_name"
|
||||
})
|
||||
validate_links_table_fieldnames(doc) # no error
|
||||
doc.links = [] # reset links table
|
||||
doc.append("links", {"link_doctype": "User", "link_fieldname": "first_name"})
|
||||
validate_links_table_fieldnames(doc) # no error
|
||||
doc.links = [] # reset links table
|
||||
|
||||
# check invalid doctype
|
||||
doc.append("links", {
|
||||
'link_doctype': "User2",
|
||||
'link_fieldname': "first_name"
|
||||
})
|
||||
doc.append("links", {"link_doctype": "User2", "link_fieldname": "first_name"})
|
||||
self.assertRaises(frappe.DoesNotExistError, validate_links_table_fieldnames, doc)
|
||||
doc.links = [] # reset links table
|
||||
doc.links = [] # reset links table
|
||||
|
||||
# check invalid fieldname
|
||||
doc.append("links", {
|
||||
'link_doctype': "User",
|
||||
'link_fieldname': "a_field_that_does_not_exists"
|
||||
})
|
||||
doc.append("links", {"link_doctype": "User", "link_fieldname": "a_field_that_does_not_exists"})
|
||||
|
||||
self.assertRaises(InvalidFieldNameError, validate_links_table_fieldnames, doc)
|
||||
|
||||
def test_create_virtual_doctype(self):
|
||||
"""Test virtual DOcTYpe."""
|
||||
virtual_doc = new_doctype('Test Virtual Doctype')
|
||||
virtual_doc = new_doctype("Test Virtual Doctype")
|
||||
virtual_doc.is_virtual = 1
|
||||
virtual_doc.insert()
|
||||
virtual_doc.save()
|
||||
doc = frappe.get_doc("DocType", "Test Virtual Doctype")
|
||||
|
||||
self.assertEqual(doc.is_virtual, 1)
|
||||
self.assertFalse(frappe.db.table_exists('Test Virtual Doctype'))
|
||||
self.assertFalse(frappe.db.table_exists("Test Virtual Doctype"))
|
||||
|
||||
def test_default_fieldname(self):
|
||||
fields = [{"label": "title", "fieldname": "title", "fieldtype": "Data", "default": "{some_fieldname}"}]
|
||||
fields = [
|
||||
{"label": "title", "fieldname": "title", "fieldtype": "Data", "default": "{some_fieldname}"}
|
||||
]
|
||||
dt = new_doctype("DT with default field", fields=fields)
|
||||
dt.insert()
|
||||
|
||||
|
|
@ -521,28 +538,34 @@ class TestDocType(unittest.TestCase):
|
|||
dt.delete(ignore_permissions=True)
|
||||
|
||||
|
||||
def new_doctype(name, unique=0, depends_on='', fields=None, autoincremented=False):
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "DocType",
|
||||
"module": "Core",
|
||||
"custom": 1,
|
||||
"fields": [{
|
||||
"label": "Some Field",
|
||||
"fieldname": "some_fieldname",
|
||||
"fieldtype": "Data",
|
||||
"unique": unique,
|
||||
"depends_on": depends_on,
|
||||
}],
|
||||
"permissions": [{
|
||||
"role": "System Manager",
|
||||
"read": 1,
|
||||
}],
|
||||
"name": name,
|
||||
"autoname": "autoincrement" if autoincremented else ""
|
||||
})
|
||||
def new_doctype(name, unique=0, depends_on="", fields=None, autoincremented=False):
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"module": "Core",
|
||||
"custom": 1,
|
||||
"fields": [
|
||||
{
|
||||
"label": "Some Field",
|
||||
"fieldname": "some_fieldname",
|
||||
"fieldtype": "Data",
|
||||
"unique": unique,
|
||||
"depends_on": depends_on,
|
||||
}
|
||||
],
|
||||
"permissions": [
|
||||
{
|
||||
"role": "System Manager",
|
||||
"read": 1,
|
||||
}
|
||||
],
|
||||
"name": name,
|
||||
"autoname": "autoincrement" if autoincremented else "",
|
||||
}
|
||||
)
|
||||
|
||||
if fields:
|
||||
for f in fields:
|
||||
doc.append('fields', f)
|
||||
doc.append("fields", f)
|
||||
|
||||
return doc
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@
|
|||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class DocTypeAction(Document):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@
|
|||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class DocTypeLink(Document):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -4,5 +4,6 @@
|
|||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class DocTypeState(Document):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.data import evaluate_filters
|
||||
from frappe.model.naming import parse_naming_series
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.naming import parse_naming_series
|
||||
from frappe.utils.data import evaluate_filters
|
||||
|
||||
|
||||
class DocumentNamingRule(Document):
|
||||
def validate(self):
|
||||
|
|
@ -17,23 +18,30 @@ class DocumentNamingRule(Document):
|
|||
docfields = [x.fieldname for x in frappe.get_meta(self.document_type).fields]
|
||||
for condition in self.conditions:
|
||||
if condition.field not in docfields:
|
||||
frappe.throw(_("{0} is not a field of doctype {1}").format(frappe.bold(condition.field), frappe.bold(self.document_type)))
|
||||
frappe.throw(
|
||||
_("{0} is not a field of doctype {1}").format(
|
||||
frappe.bold(condition.field), frappe.bold(self.document_type)
|
||||
)
|
||||
)
|
||||
|
||||
def apply(self, doc):
|
||||
'''
|
||||
"""
|
||||
Apply naming rules for the given document. Will set `name` if the rule is matched.
|
||||
'''
|
||||
"""
|
||||
if self.conditions:
|
||||
if not evaluate_filters(doc, [(self.document_type, d.field, d.condition, d.value) for d in self.conditions]):
|
||||
if not evaluate_filters(
|
||||
doc, [(self.document_type, d.field, d.condition, d.value) for d in self.conditions]
|
||||
):
|
||||
return
|
||||
|
||||
counter = frappe.db.get_value(self.doctype, self.name, 'counter', for_update=True) or 0
|
||||
counter = frappe.db.get_value(self.doctype, self.name, "counter", for_update=True) or 0
|
||||
naming_series = parse_naming_series(self.prefix, doc=doc)
|
||||
|
||||
doc.name = naming_series + ('%0'+str(self.prefix_digits)+'d') % (counter + 1)
|
||||
frappe.db.set_value(self.doctype, self.name, 'counter', counter + 1)
|
||||
doc.name = naming_series + ("%0" + str(self.prefix_digits) + "d") % (counter + 1)
|
||||
frappe.db.set_value(self.doctype, self.name, "counter", counter + 1)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_current(name, new_counter):
|
||||
frappe.only_for('System Manager')
|
||||
frappe.db.set_value('Document Naming Rule', name, 'counter', new_counter)
|
||||
frappe.only_for("System Manager")
|
||||
frappe.db.set_value("Document Naming Rule", name, "counter", new_counter)
|
||||
|
|
|
|||
|
|
@ -1,79 +1,68 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import frappe
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
class TestDocumentNamingRule(unittest.TestCase):
|
||||
def test_naming_rule_by_series(self):
|
||||
naming_rule = frappe.get_doc(dict(
|
||||
doctype = 'Document Naming Rule',
|
||||
document_type = 'ToDo',
|
||||
prefix = 'test-todo-',
|
||||
prefix_digits = 5
|
||||
)).insert()
|
||||
naming_rule = frappe.get_doc(
|
||||
dict(doctype="Document Naming Rule", document_type="ToDo", prefix="test-todo-", prefix_digits=5)
|
||||
).insert()
|
||||
|
||||
todo = frappe.get_doc(dict(
|
||||
doctype = 'ToDo',
|
||||
description = 'Is this my name ' + frappe.generate_hash()
|
||||
)).insert()
|
||||
todo = frappe.get_doc(
|
||||
dict(doctype="ToDo", description="Is this my name " + frappe.generate_hash())
|
||||
).insert()
|
||||
|
||||
self.assertEqual(todo.name, 'test-todo-00001')
|
||||
self.assertEqual(todo.name, "test-todo-00001")
|
||||
|
||||
naming_rule.delete()
|
||||
todo.delete()
|
||||
|
||||
def test_naming_rule_by_condition(self):
|
||||
naming_rule = frappe.get_doc(dict(
|
||||
doctype = 'Document Naming Rule',
|
||||
document_type = 'ToDo',
|
||||
prefix = 'test-high-',
|
||||
prefix_digits = 5,
|
||||
priority = 10,
|
||||
conditions = [dict(
|
||||
field = 'priority',
|
||||
condition = '=',
|
||||
value = 'High'
|
||||
)]
|
||||
)).insert()
|
||||
naming_rule = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Document Naming Rule",
|
||||
document_type="ToDo",
|
||||
prefix="test-high-",
|
||||
prefix_digits=5,
|
||||
priority=10,
|
||||
conditions=[dict(field="priority", condition="=", value="High")],
|
||||
)
|
||||
).insert()
|
||||
|
||||
# another rule
|
||||
naming_rule_1 = frappe.copy_doc(naming_rule)
|
||||
naming_rule_1.prefix = 'test-medium-'
|
||||
naming_rule_1.conditions[0].value = 'Medium'
|
||||
naming_rule_1.prefix = "test-medium-"
|
||||
naming_rule_1.conditions[0].value = "Medium"
|
||||
naming_rule_1.insert()
|
||||
|
||||
# default rule with low priority - should not get applied for rules
|
||||
# with higher priority
|
||||
naming_rule_2 = frappe.copy_doc(naming_rule)
|
||||
naming_rule_2.prefix = 'test-low-'
|
||||
naming_rule_2.prefix = "test-low-"
|
||||
naming_rule_2.priority = 0
|
||||
naming_rule_2.conditions = []
|
||||
naming_rule_2.insert()
|
||||
|
||||
todo = frappe.get_doc(
|
||||
dict(doctype="ToDo", priority="High", description="Is this my name " + frappe.generate_hash())
|
||||
).insert()
|
||||
|
||||
todo = frappe.get_doc(dict(
|
||||
doctype = 'ToDo',
|
||||
priority = 'High',
|
||||
description = 'Is this my name ' + frappe.generate_hash()
|
||||
)).insert()
|
||||
todo_1 = frappe.get_doc(
|
||||
dict(doctype="ToDo", priority="Medium", description="Is this my name " + frappe.generate_hash())
|
||||
).insert()
|
||||
|
||||
todo_1 = frappe.get_doc(dict(
|
||||
doctype = 'ToDo',
|
||||
priority = 'Medium',
|
||||
description = 'Is this my name ' + frappe.generate_hash()
|
||||
)).insert()
|
||||
|
||||
todo_2 = frappe.get_doc(dict(
|
||||
doctype = 'ToDo',
|
||||
priority = 'Low',
|
||||
description = 'Is this my name ' + frappe.generate_hash()
|
||||
)).insert()
|
||||
todo_2 = frappe.get_doc(
|
||||
dict(doctype="ToDo", priority="Low", description="Is this my name " + frappe.generate_hash())
|
||||
).insert()
|
||||
|
||||
try:
|
||||
self.assertEqual(todo.name, 'test-high-00001')
|
||||
self.assertEqual(todo_1.name, 'test-medium-00001')
|
||||
self.assertEqual(todo_2.name, 'test-low-00001')
|
||||
self.assertEqual(todo.name, "test-high-00001")
|
||||
self.assertEqual(todo_1.name, "test-medium-00001")
|
||||
self.assertEqual(todo_2.name, "test-low-00001")
|
||||
finally:
|
||||
naming_rule.delete()
|
||||
naming_rule_1.delete()
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@
|
|||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class DocumentNamingRuleCondition(Document):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -4,5 +4,6 @@
|
|||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class TestDocumentNamingRuleCondition(unittest.TestCase):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -3,16 +3,17 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
|
||||
from frappe.model.document import Document
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class Domain(Document):
|
||||
'''Domain documents are created automatically when DocTypes
|
||||
"""Domain documents are created automatically when DocTypes
|
||||
with "Restricted" domains are imported during
|
||||
installation or migration'''
|
||||
installation or migration"""
|
||||
|
||||
def setup_domain(self):
|
||||
'''Setup domain icons, permissions, custom fields etc.'''
|
||||
"""Setup domain icons, permissions, custom fields etc."""
|
||||
self.setup_data()
|
||||
self.setup_roles()
|
||||
self.setup_properties()
|
||||
|
|
@ -31,20 +32,20 @@ class Domain(Document):
|
|||
frappe.get_attr(self.data.on_setup)()
|
||||
|
||||
def remove_domain(self):
|
||||
'''Unset domain settings'''
|
||||
"""Unset domain settings"""
|
||||
self.setup_data()
|
||||
|
||||
if self.data.restricted_roles:
|
||||
for role_name in self.data.restricted_roles:
|
||||
if frappe.db.exists('Role', role_name):
|
||||
role = frappe.get_doc('Role', role_name)
|
||||
if frappe.db.exists("Role", role_name):
|
||||
role = frappe.get_doc("Role", role_name)
|
||||
role.disabled = 1
|
||||
role.save()
|
||||
|
||||
self.remove_custom_field()
|
||||
|
||||
def remove_custom_field(self):
|
||||
'''Remove custom_fields when disabling domain'''
|
||||
"""Remove custom_fields when disabling domain"""
|
||||
if self.data.custom_fields:
|
||||
for doctype in self.data.custom_fields:
|
||||
custom_fields = self.data.custom_fields[doctype]
|
||||
|
|
@ -54,47 +55,48 @@ class Domain(Document):
|
|||
custom_fields = [custom_fields]
|
||||
|
||||
for custom_field_detail in custom_fields:
|
||||
custom_field_name = frappe.db.get_value('Custom Field',
|
||||
dict(dt=doctype, fieldname=custom_field_detail.get('fieldname')))
|
||||
custom_field_name = frappe.db.get_value(
|
||||
"Custom Field", dict(dt=doctype, fieldname=custom_field_detail.get("fieldname"))
|
||||
)
|
||||
if custom_field_name:
|
||||
frappe.delete_doc('Custom Field', custom_field_name)
|
||||
frappe.delete_doc("Custom Field", custom_field_name)
|
||||
|
||||
def setup_roles(self):
|
||||
'''Enable roles that are restricted to this domain'''
|
||||
"""Enable roles that are restricted to this domain"""
|
||||
if self.data.restricted_roles:
|
||||
user = frappe.get_doc("User", frappe.session.user)
|
||||
for role_name in self.data.restricted_roles:
|
||||
user.append("roles", {"role": role_name})
|
||||
if not frappe.db.get_value('Role', role_name):
|
||||
frappe.get_doc(dict(doctype='Role', role_name=role_name)).insert()
|
||||
if not frappe.db.get_value("Role", role_name):
|
||||
frappe.get_doc(dict(doctype="Role", role_name=role_name)).insert()
|
||||
continue
|
||||
|
||||
role = frappe.get_doc('Role', role_name)
|
||||
role = frappe.get_doc("Role", role_name)
|
||||
role.disabled = 0
|
||||
role.save()
|
||||
user.save()
|
||||
|
||||
def setup_data(self, domain=None):
|
||||
'''Load domain info via hooks'''
|
||||
"""Load domain info via hooks"""
|
||||
self.data = frappe.get_domain_data(self.name)
|
||||
|
||||
def get_domain_data(self, module):
|
||||
return frappe.get_attr(frappe.get_hooks('domains')[self.name] + '.data')
|
||||
return frappe.get_attr(frappe.get_hooks("domains")[self.name] + ".data")
|
||||
|
||||
def set_default_portal_role(self):
|
||||
'''Set default portal role based on domain'''
|
||||
if self.data.get('default_portal_role'):
|
||||
frappe.db.set_value('Portal Settings', None, 'default_role',
|
||||
self.data.get('default_portal_role'))
|
||||
"""Set default portal role based on domain"""
|
||||
if self.data.get("default_portal_role"):
|
||||
frappe.db.set_value(
|
||||
"Portal Settings", None, "default_role", self.data.get("default_portal_role")
|
||||
)
|
||||
|
||||
def setup_properties(self):
|
||||
if self.data.properties:
|
||||
for args in self.data.properties:
|
||||
frappe.make_property_setter(args)
|
||||
|
||||
|
||||
def set_values(self):
|
||||
'''set values based on `data.set_value`'''
|
||||
"""set values based on `data.set_value`"""
|
||||
if self.data.set_value:
|
||||
for args in self.data.set_value:
|
||||
frappe.reload_doctype(args[0])
|
||||
|
|
@ -103,19 +105,27 @@ class Domain(Document):
|
|||
doc.save()
|
||||
|
||||
def setup_sidebar_items(self):
|
||||
'''Enable / disable sidebar items'''
|
||||
"""Enable / disable sidebar items"""
|
||||
if self.data.allow_sidebar_items:
|
||||
# disable all
|
||||
frappe.db.sql('update `tabPortal Menu Item` set enabled=0')
|
||||
frappe.db.sql("update `tabPortal Menu Item` set enabled=0")
|
||||
|
||||
# enable
|
||||
frappe.db.sql('''update `tabPortal Menu Item` set enabled=1
|
||||
where route in ({0})'''.format(', '.join('"{0}"'.format(d) for d in self.data.allow_sidebar_items)))
|
||||
frappe.db.sql(
|
||||
"""update `tabPortal Menu Item` set enabled=1
|
||||
where route in ({0})""".format(
|
||||
", ".join('"{0}"'.format(d) for d in self.data.allow_sidebar_items)
|
||||
)
|
||||
)
|
||||
|
||||
if self.data.remove_sidebar_items:
|
||||
# disable all
|
||||
frappe.db.sql('update `tabPortal Menu Item` set enabled=1')
|
||||
frappe.db.sql("update `tabPortal Menu Item` set enabled=1")
|
||||
|
||||
# enable
|
||||
frappe.db.sql('''update `tabPortal Menu Item` set enabled=0
|
||||
where route in ({0})'''.format(', '.join('"{0}"'.format(d) for d in self.data.remove_sidebar_items)))
|
||||
frappe.db.sql(
|
||||
"""update `tabPortal Menu Item` set enabled=0
|
||||
where route in ({0})""".format(
|
||||
", ".join('"{0}"'.format(d) for d in self.data.remove_sidebar_items)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import frappe
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
class TestDomain(unittest.TestCase):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class DomainSettings(Document):
|
||||
def set_active_domains(self, domains):
|
||||
active_domains = [d.domain for d in self.active_domains]
|
||||
added = False
|
||||
for d in domains:
|
||||
if not d in active_domains:
|
||||
self.append('active_domains', dict(domain=d))
|
||||
self.append("active_domains", dict(domain=d))
|
||||
added = True
|
||||
|
||||
if added:
|
||||
|
|
@ -22,49 +23,52 @@ class DomainSettings(Document):
|
|||
# set the flag to update the the desktop icons of all domains
|
||||
if i >= 1:
|
||||
frappe.flags.keep_desktop_icons = True
|
||||
domain = frappe.get_doc('Domain', d.domain)
|
||||
domain = frappe.get_doc("Domain", d.domain)
|
||||
domain.setup_domain()
|
||||
|
||||
self.restrict_roles_and_modules()
|
||||
frappe.clear_cache()
|
||||
|
||||
def restrict_roles_and_modules(self):
|
||||
'''Disable all restricted roles and set `restrict_to_domain` property in Module Def'''
|
||||
"""Disable all restricted roles and set `restrict_to_domain` property in Module Def"""
|
||||
active_domains = frappe.get_active_domains()
|
||||
all_domains = list((frappe.get_hooks('domains') or {}))
|
||||
all_domains = list((frappe.get_hooks("domains") or {}))
|
||||
|
||||
def remove_role(role):
|
||||
frappe.db.delete("Has Role", {"role": role})
|
||||
frappe.set_value('Role', role, 'disabled', 1)
|
||||
frappe.set_value("Role", role, "disabled", 1)
|
||||
|
||||
for domain in all_domains:
|
||||
data = frappe.get_domain_data(domain)
|
||||
if not frappe.db.get_value('Domain', domain):
|
||||
frappe.get_doc(dict(doctype='Domain', domain=domain)).insert()
|
||||
if 'modules' in data:
|
||||
for module in data.get('modules'):
|
||||
frappe.db.set_value('Module Def', module, 'restrict_to_domain', domain)
|
||||
if 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 "restricted_roles" in data:
|
||||
for role in data["restricted_roles"]:
|
||||
if not frappe.db.get_value("Role", role):
|
||||
frappe.get_doc(dict(doctype="Role", role_name=role)).insert()
|
||||
frappe.db.set_value("Role", role, "restrict_to_domain", domain)
|
||||
|
||||
if domain not in active_domains:
|
||||
remove_role(role)
|
||||
|
||||
if 'custom_fields' in data:
|
||||
if "custom_fields" in data:
|
||||
if domain not in active_domains:
|
||||
inactive_domain = frappe.get_doc("Domain", domain)
|
||||
inactive_domain.setup_data()
|
||||
inactive_domain.remove_custom_field()
|
||||
|
||||
|
||||
def get_active_domains():
|
||||
""" get the domains set in the Domain Settings as active domain """
|
||||
"""get the domains set in the Domain Settings as active domain"""
|
||||
|
||||
def _get_active_domains():
|
||||
domains = frappe.get_all("Has Domain", filters={ "parent": "Domain Settings" },
|
||||
fields=["domain"], distinct=True)
|
||||
domains = frappe.get_all(
|
||||
"Has Domain", filters={"parent": "Domain Settings"}, fields=["domain"], distinct=True
|
||||
)
|
||||
|
||||
active_domains = [row.get("domain") for row in domains]
|
||||
active_domains.append("")
|
||||
|
|
@ -72,14 +76,16 @@ def get_active_domains():
|
|||
|
||||
return frappe.cache().get_value("active_domains", _get_active_domains)
|
||||
|
||||
|
||||
def get_active_modules():
|
||||
""" get the active modules from Module Def"""
|
||||
"""get the active modules from Module Def"""
|
||||
|
||||
def _get_active_modules():
|
||||
active_modules = []
|
||||
active_domains = get_active_domains()
|
||||
for m in frappe.get_all("Module Def", fields=['name', 'restrict_to_domain']):
|
||||
for m in frappe.get_all("Module Def", fields=["name", "restrict_to_domain"]):
|
||||
if (not m.restrict_to_domain) or (m.restrict_to_domain in active_domains):
|
||||
active_modules.append(m.name)
|
||||
return active_modules
|
||||
|
||||
return frappe.cache().get_value('active_modules', _get_active_modules)
|
||||
return frappe.cache().get_value("active_modules", _get_active_modules)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue