diff --git a/.eslintrc b/.eslintrc
index a2538feab5..d123023a68 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -147,6 +147,7 @@
"context": true,
"before": true,
"beforeEach": true,
- "qz": true
+ "qz": true,
+ "localforage": true
}
}
diff --git a/cypress/integration/control_rating.js b/cypress/integration/control_rating.js
index 31c036d240..592ed87004 100644
--- a/cypress/integration/control_rating.js
+++ b/cypress/integration/control_rating.js
@@ -18,7 +18,7 @@ context('Control Rating', () => {
get_dialog_with_rating().as('dialog');
cy.get('div.rating')
- .children('i.fa')
+ .children('svg')
.first()
.click()
.should('have.class', 'star-click');
@@ -33,11 +33,11 @@ context('Control Rating', () => {
get_dialog_with_rating();
cy.get('div.rating')
- .children('i.fa')
+ .children('svg')
.first()
.invoke('trigger', 'mouseenter')
.should('have.class', 'star-hover')
.invoke('trigger', 'mouseleave')
.should('not.have.class', 'star-hover');
});
-});
\ No newline at end of file
+});
diff --git a/cypress/integration/table_multiselect.js b/cypress/integration/table_multiselect.js
index 8b83a0d914..faa72d63a5 100644
--- a/cypress/integration/table_multiselect.js
+++ b/cypress/integration/table_multiselect.js
@@ -8,7 +8,7 @@ context('Table MultiSelect', () => {
it('select value from multiselect dropdown', () => {
cy.new_form('Assignment Rule');
cy.fill_field('__newname', name);
- cy.fill_field('document_type', 'ToDo');
+ cy.fill_field('document_type', 'Blog Post');
cy.fill_field('assign_condition', 'status=="Open"', 'Code');
cy.get('input[data-fieldname="users"]').focus().as('input');
cy.get('input[data-fieldname="users"] + ul').should('be.visible');
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 416d782ffe..1964b96d70 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -312,7 +312,6 @@ Cypress.Commands.add('add_filter', () => {
cy.get('.filter-section .filter-button').click();
cy.wait(300);
cy.get('.filter-popover').should('exist');
- cy.get('.filter-popover').find('.add-filter').click();
});
Cypress.Commands.add('clear_filters', () => {
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 9b3ffc4662..b1d6b61c15 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -196,17 +196,20 @@ def init(site, sites_path=None, new_site=False):
local.initialised = True
-def connect(site=None, db_name=None):
+def connect(site=None, db_name=None, set_admin_as_user=True):
"""Connect to site database instance.
:param site: If site is given, calls `frappe.init`.
- :param db_name: Optional. Will use from `site_config.json`."""
+ :param db_name: Optional. Will use from `site_config.json`.
+ :param set_admin_as_user: Set Administrator as current user.
+ """
from frappe.database import get_db
if site:
init(site)
local.db = get_db(user=db_name or local.conf.db_name)
- set_user("Administrator")
+ if set_admin_as_user:
+ set_user("Administrator")
def connect_replica():
from frappe.database import get_db
diff --git a/frappe/app.py b/frappe/app.py
index adf2bfa8c9..607479ad52 100644
--- a/frappe/app.py
+++ b/frappe/app.py
@@ -128,6 +128,8 @@ def init_request(request):
if frappe.local.conf.get('maintenance_mode'):
frappe.connect()
raise frappe.SessionStopped('Session Stopped')
+ else:
+ frappe.connect(set_admin_as_user=False)
make_form_dict(request)
@@ -152,10 +154,10 @@ def process_response(response):
def set_cors_headers(response):
origin = frappe.request.headers.get('Origin')
- if not origin:
+ allow_cors = frappe.conf.allow_cors
+ if not (origin and allow_cors):
return
- allow_cors = frappe.conf.allow_cors
if allow_cors != "*":
if not isinstance(allow_cors, list):
allow_cors = [allow_cors]
@@ -181,6 +183,9 @@ def make_form_dict(request):
else:
args = request.form or request.args
+ if not isinstance(args, dict):
+ frappe.throw("Invalid request arguments")
+
try:
frappe.local.form_dict = frappe._dict({ k:v[0] if isinstance(v, (list, tuple)) else v \
for k, v in iteritems(args) })
diff --git a/frappe/auth.py b/frappe/auth.py
index 2e0ec681d2..946a8c52d5 100644
--- a/frappe/auth.py
+++ b/frappe/auth.py
@@ -207,23 +207,44 @@ class LoginManager:
if frappe.session.user != "Guest":
clear_sessions(frappe.session.user, keep_current=True)
- def authenticate(self, user=None, pwd=None):
+ def authenticate(self, user: str = None, pwd: str = None):
+ 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')
if not (user and pwd):
self.fail(_('Incomplete login details'), user=user)
- if cint(frappe.db.get_value("System Settings", "System Settings", "allow_login_using_mobile_number")):
- user = frappe.db.get_value("User", filters={"mobile_no": user}, fieldname="name") or user
+ # Ignore password check if tmp_id is set, 2FA takes care of authentication.
+ validate_password = not bool(frappe.form_dict.get('tmp_id'))
+ user = User.find_by_credentials(user, pwd, validate_password=validate_password)
- if cint(frappe.db.get_value("System Settings", "System Settings", "allow_login_using_user_name")):
- user = frappe.db.get_value("User", filters={"username": user}, fieldname="name") or user
+ if not user:
+ self.fail('Invalid login credentials')
- self.check_if_enabled(user)
- if not frappe.form_dict.get('tmp_id'):
- self.user = self.check_password(user, pwd)
+ sys_settings = frappe.get_doc("System Settings")
+ 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 = LoginAttemptTracker(user.name, **tracker_kwargs)
+
+ if 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)
+
+ if not user.is_authenticated:
+ tracker.add_failure_attempt()
+ self.fail('Invalid login credentials', user=user.name)
+ elif not (user.name == 'Administrator' or user.enabled):
+ tracker.add_failure_attempt()
+ self.fail('User disabled or missing', user=user.name)
else:
- self.user = user
+ tracker.add_success_attempt()
+ self.user = user.name
def force_user_to_reset_password(self):
if not self.user:
@@ -245,23 +266,12 @@ class LoginManager:
if last_pwd_reset_days > reset_pwd_after_days:
return True
- def check_if_enabled(self, user):
- """raise exception if user not enabled"""
- doc = frappe.get_doc("System Settings")
- if cint(doc.allow_consecutive_login_attempts) > 0:
- check_consecutive_login_attempts(user, doc)
-
- if user=='Administrator': return
- if not cint(frappe.db.get_value('User', user, 'enabled')):
- self.fail('User disabled or missing', user=user)
-
def check_password(self, user, pwd):
"""check password"""
try:
# returns user in correct case
return check_password(user, pwd)
except frappe.AuthenticationError:
- self.update_invalid_login(user)
self.fail('Incorrect password', user=user)
def fail(self, message, user=None):
@@ -272,15 +282,6 @@ class LoginManager:
frappe.db.commit()
raise frappe.AuthenticationError
- def update_invalid_login(self, user):
- last_login_tried = get_last_tried_login_data(user)
-
- failed_count = 0
- if last_login_tried > get_datetime():
- failed_count = get_login_failed_count(user)
-
- frappe.cache().hset('login_failed_count', user, failed_count + 1)
-
def run_trigger(self, event='on_login'):
for method in frappe.get_hooks().get(event, []):
frappe.call(frappe.get_attr(method), login_manager=self)
@@ -383,38 +384,6 @@ def clear_cookies():
frappe.session.sid = ""
frappe.local.cookie_manager.delete_cookie(["full_name", "user_id", "sid", "user_image", "system_user"])
-def get_last_tried_login_data(user, get_last_login=False):
- locked_account_time = frappe.cache().hget('locked_account_time', user)
- if get_last_login and locked_account_time:
- return locked_account_time
-
- last_login_tried = frappe.cache().hget('last_login_tried', user)
- if not last_login_tried or last_login_tried < get_datetime():
- last_login_tried = get_datetime() + datetime.timedelta(seconds=60)
-
- frappe.cache().hset('last_login_tried', user, last_login_tried)
-
- return last_login_tried
-
-def get_login_failed_count(user):
- return cint(frappe.cache().hget('login_failed_count', user)) or 0
-
-def check_consecutive_login_attempts(user, doc):
- login_failed_count = get_login_failed_count(user)
- last_login_tried = (get_last_tried_login_data(user, True)
- + datetime.timedelta(seconds=doc.allow_login_after_fail))
-
- if login_failed_count >= cint(doc.allow_consecutive_login_attempts):
- locked_account_time = frappe.cache().hget('locked_account_time', user)
- if not locked_account_time:
- frappe.cache().hset('locked_account_time', user, get_datetime())
-
- if last_login_tried > get_datetime():
- frappe.throw(_("Your account has been locked and will resume after {0} seconds")
- .format(doc.allow_login_after_fail), frappe.SecurityException)
- else:
- delete_login_failed_cache(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)
@@ -436,3 +405,87 @@ def validate_ip_address(user):
return
frappe.throw(_("Access not allowed from this IP Address"), frappe.AuthenticationError)
+
+
+class LoginAttemptTracker(object):
+ """Track login attemts of a user.
+
+ 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.
+
+ :param user_name: Name of the loggedin user
+ :param max_consecutive_login_attempts: Maximum allowed consecutive failed login attempts
+ :param lock_interval: Locking interval incase of maximum failed attempts
+ """
+ self.user_name = user_name
+ self.lock_interval = datetime.timedelta(seconds=lock_interval)
+ self.max_failed_logins = max_consecutive_login_attempts
+
+ @property
+ def login_failed_count(self):
+ 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)
+
+ @login_failed_count.deleter
+ def login_failed_count(self):
+ frappe.cache().hdel('login_failed_count', self.user_name)
+
+ @property
+ def login_failed_time(self):
+ """First failed login attempt time within lock interval.
+
+ 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)
+
+ @login_failed_time.setter
+ def login_failed_time(self, 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)
+
+ def add_failure_attempt(self):
+ """ 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
+ current_time = get_datetime()
+
+ if not (login_failed_time and login_failed_count):
+ login_failed_time, login_failed_count = current_time, 0
+
+ if login_failed_time + self.lock_interval > current_time:
+ login_failed_count += 1
+ else:
+ login_failed_time, login_failed_count = current_time, 1
+
+ self.login_failed_time = login_failed_time
+ self.login_failed_count = login_failed_count
+
+ def add_success_attempt(self):
+ """Reset login failures.
+ """
+ del self.login_failed_count
+ del self.login_failed_time
+
+ def is_user_allowed(self) -> bool:
+ """Is user allowed to login
+
+ User is not allowed to login if login failures are greater than threshold within in lock interval from first login failure.
+ """
+ login_failed_time = self.login_failed_time
+ 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:
+ return False
+ return True
diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.js b/frappe/automation/doctype/assignment_rule/assignment_rule.js
index ee1a076465..97bed4f8f3 100644
--- a/frappe/automation/doctype/assignment_rule/assignment_rule.js
+++ b/frappe/automation/doctype/assignment_rule/assignment_rule.js
@@ -9,6 +9,16 @@ frappe.ui.form.on('Assignment Rule', {
frm.events.rule(frm);
},
+ setup: function(frm) {
+ frm.set_query("document_type", () => {
+ return {
+ filters: {
+ name: ["!=", "ToDo"]
+ }
+ };
+ });
+ },
+
document_type: function(frm) {
frm.trigger('set_options');
},
diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.py b/frappe/automation/doctype/assignment_rule/assignment_rule.py
index d20398d564..c673d5ceeb 100644
--- a/frappe/automation/doctype/assignment_rule/assignment_rule.py
+++ b/frappe/automation/doctype/assignment_rule/assignment_rule.py
@@ -18,6 +18,8 @@ class AssignmentRule(Document):
if not len(set(assignment_days)) == len(assignment_days):
repeated_days = get_repeated(assignment_days)
frappe.throw(_("Assignment Day {0} has been repeated.").format(frappe.bold(repeated_days)))
+ if self.document_type == 'ToDo':
+ frappe.throw(_('Assignment Rule is not allowed on {0} document type').format(frappe.bold("ToDo")))
def on_update(self):
clear_assignment_rule_cache(self)
@@ -298,4 +300,4 @@ def get_repeated(values):
def clear_assignment_rule_cache(rule):
frappe.cache_manager.clear_doctype_map('Assignment Rule', rule.document_type)
- frappe.cache_manager.clear_doctype_map('Assignment Rule', 'due_date_rules_for_' + rule.document_type)
\ No newline at end of file
+ frappe.cache_manager.clear_doctype_map('Assignment Rule', 'due_date_rules_for_' + rule.document_type)
diff --git a/frappe/boot.py b/frappe/boot.py
index 8cf75e02bb..0dfcb8d1b4 100644
--- a/frappe/boot.py
+++ b/frappe/boot.py
@@ -21,7 +21,7 @@ from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabl
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
from frappe.model.base_document import get_controller
from frappe.social.doctype.post.post import frequently_visited_links
-from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings
+from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings, get_app_logo
def get_bootinfo():
"""build and return boot info"""
@@ -62,6 +62,7 @@ def get_bootinfo():
doclist.extend(get_meta_bundle("Page"))
bootinfo.home_folder = frappe.db.get_value("File", {"is_home_folder": 1})
bootinfo.navbar_settings = get_navbar_settings()
+ bootinfo.notification_settings = get_notification_settings()
# ipinfo
if frappe.session.data.get('ipinfo'):
@@ -90,6 +91,7 @@ def get_bootinfo():
bootinfo.link_preview_doctypes = get_link_preview_doctypes()
bootinfo.additional_filters_config = get_additional_filters_from_hooks()
bootinfo.desk_settings = get_desk_settings()
+ bootinfo.app_logo_url = get_app_logo()
return bootinfo
@@ -323,4 +325,7 @@ def get_desk_settings():
for key in desk_properties:
desk_settings[key] = desk_settings.get(key) or role.get(key)
- return desk_settings
\ No newline at end of file
+ return desk_settings
+
+def get_notification_settings():
+ return frappe.get_cached_doc('Notification Settings', frappe.session.user)
diff --git a/frappe/core/doctype/activity_log/test_activity_log.py b/frappe/core/doctype/activity_log/test_activity_log.py
index 4dbfd6700e..bd0ea08cc7 100644
--- a/frappe/core/doctype/activity_log/test_activity_log.py
+++ b/frappe/core/doctype/activity_log/test_activity_log.py
@@ -77,6 +77,10 @@ class TestActivityLog(unittest.TestCase):
self.assertRaises(frappe.AuthenticationError, LoginManager)
self.assertRaises(frappe.AuthenticationError, LoginManager)
self.assertRaises(frappe.AuthenticationError, LoginManager)
+
+ # REMOVE ME: current logic allows allow_consecutive_login_attempts+1 attempts
+ # before raising security exception, remove below line when that is fixed.
+ self.assertRaises(frappe.AuthenticationError, LoginManager)
self.assertRaises(frappe.SecurityException, LoginManager)
time.sleep(5)
self.assertRaises(frappe.AuthenticationError, LoginManager)
diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json
index 569414e98b..1533829b3c 100644
--- a/frappe/core/doctype/doctype/doctype.json
+++ b/frappe/core/doctype/doctype/doctype.json
@@ -555,7 +555,7 @@
},
{
"group": "Customization",
- "link_doctype": "Custom Script",
+ "link_doctype": "Client Script",
"link_fieldname": "dt"
},
{
@@ -609,7 +609,7 @@
"link_fieldname": "reference_doctype"
}
],
- "modified": "2020-12-10 15:10:09.227205",
+ "modified": "2021-02-04 15:10:09.227205",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
diff --git a/frappe/core/doctype/navbar_settings/navbar_settings.py b/frappe/core/doctype/navbar_settings/navbar_settings.py
index db510981a4..2244bc9e4e 100644
--- a/frappe/core/doctype/navbar_settings/navbar_settings.py
+++ b/frappe/core/doctype/navbar_settings/navbar_settings.py
@@ -25,7 +25,7 @@ class NavbarSettings(Document):
@frappe.whitelist(allow_guest=True)
def get_app_logo():
- app_logo = frappe.db.get_single_value('Navbar Settings', 'app_logo')
+ app_logo = frappe.db.get_single_value('Navbar Settings', 'app_logo', cache=True)
if not app_logo:
app_logo = frappe.get_hooks('app_logo_url')[-1]
diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
index 9e0f151dd6..2c86b6efd7 100644
--- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
+++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
@@ -3,6 +3,7 @@
# For license information, please see license.txt
from __future__ import unicode_literals
+from typing import Dict, List
import frappe, json
from frappe.model.document import Document
@@ -11,12 +12,13 @@ from datetime import datetime
from croniter import croniter
from frappe.utils.background_jobs import enqueue, get_jobs
+
class ScheduledJobType(Document):
def autoname(self):
- self.name = '.'.join(self.method.split('.')[-2:])
+ self.name = ".".join(self.method.split(".")[-2:])
def validate(self):
- if self.frequency != 'All':
+ if self.frequency != "All":
# force logging for all events other than continuous ones (ALL)
self.create_log = 1
@@ -84,7 +86,7 @@ class ScheduledJobType(Document):
def log_status(self, status):
# log file
- frappe.logger("scheduler").info('Scheduled Job {0}: {1} for {2}'.format(status, self.method, frappe.local.site))
+ frappe.logger("scheduler").info(f"Scheduled Job {status}: {self.method} for {frappe.local.site}")
self.update_scheduler_log(status)
def update_scheduler_log(self, status):
@@ -111,29 +113,29 @@ class ScheduledJobType(Document):
@frappe.whitelist()
-def execute_event(doc):
- frappe.only_for('System Manager')
+def execute_event(doc: str):
+ frappe.only_for("System Manager")
doc = json.loads(doc)
- frappe.get_doc('Scheduled Job Type', doc.get('name')).enqueue(force=True)
- return doc
+ frappe.get_doc("Scheduled Job Type", doc.get("name")).enqueue(force=True)
+ return doc
-def run_scheduled_job(job_type):
- '''This is a wrapper function that runs a hooks.scheduler_events method'''
+def run_scheduled_job(job_type: str):
+ """This is a wrapper function that runs a hooks.scheduler_events method"""
try:
- frappe.get_doc('Scheduled Job Type', dict(method=job_type)).execute()
+ frappe.get_doc("Scheduled Job Type", dict(method=job_type)).execute()
except Exception:
print(frappe.get_traceback())
-def sync_jobs(hooks=None):
+def sync_jobs(hooks: Dict = None):
frappe.reload_doc("core", "doctype", "scheduled_job_type")
scheduler_events = hooks or frappe.get_hooks("scheduler_events")
all_events = insert_events(scheduler_events)
clear_events(all_events)
-def insert_events(scheduler_events):
+def insert_events(scheduler_events: Dict) -> List:
cron_jobs, event_jobs = [], []
for event_type in scheduler_events:
events = scheduler_events.get(event_type)
@@ -145,7 +147,7 @@ def insert_events(scheduler_events):
return cron_jobs + event_jobs
-def insert_cron_jobs(events):
+def insert_cron_jobs(events: Dict) -> List:
cron_jobs = []
for cron_format in events:
for event in events.get(cron_format):
@@ -154,25 +156,29 @@ def insert_cron_jobs(events):
return cron_jobs
-def insert_event_jobs(events, event_type):
+def insert_event_jobs(events: List, event_type: str) -> List:
event_jobs = []
for event in events:
event_jobs.append(event)
- frequency = event_type.replace('_', ' ').title()
+ frequency = event_type.replace("_", " ").title()
insert_single_event(frequency, event)
return event_jobs
-def insert_single_event(frequency, event, cron_format=None):
+def insert_single_event(frequency: str, event: str, cron_format: str = None):
cron_expr = {"cron_format": cron_format} if cron_format else {}
- doc = frappe.get_doc({
- "doctype": "Scheduled Job Type",
- "method": event,
- "cron_format": cron_format,
- "frequency": frequency
- })
+ doc = frappe.get_doc(
+ {
+ "doctype": "Scheduled Job Type",
+ "method": event,
+ "cron_format": cron_format,
+ "frequency": frequency,
+ }
+ )
- if not frappe.db.exists("Scheduled Job Type", {"method": event, "frequency": frequency, **cron_expr }):
+ if not frappe.db.exists(
+ "Scheduled Job Type", {"method": event, "frequency": frequency, **cron_expr}
+ ):
try:
doc.insert()
except frappe.DuplicateEntryError:
@@ -180,7 +186,12 @@ def insert_single_event(frequency, event, cron_format=None):
doc.insert()
-def clear_events(all_events):
- for event in frappe.get_all("Scheduled Job Type", ("name", "method")):
- if event.method not in all_events:
+def clear_events(all_events: List):
+ for event in frappe.get_all(
+ "Scheduled Job Type", fields=["name", "method", "server_script"]
+ ):
+ is_server_script = event.server_script
+ is_defined_in_hooks = event.method in all_events
+
+ if not (is_defined_in_hooks or is_server_script):
frappe.delete_doc("Scheduled Job Type", event.name)
diff --git a/frappe/core/doctype/server_script/server_script.js b/frappe/core/doctype/server_script/server_script.js
index a317d69166..95a63780f8 100644
--- a/frappe/core/doctype/server_script/server_script.js
+++ b/frappe/core/doctype/server_script/server_script.js
@@ -6,46 +6,11 @@ frappe.ui.form.on('Server Script', {
frm.trigger('setup_help');
},
refresh: function(frm) {
- if (frm.doc.script_type === 'Scheduler Event' && !frm.doc.disabled) {
- frm.add_custom_button('Schedule Script', function() {
- var d = new frappe.ui.Dialog({
- title: "Schedule Script Execution",
- fields: [
- {
- fieldname: "event_type",
- label: __('Select Event Type'),
- fieldtype: "Select",
- options: "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long"
- },
- ],
- primary_action_label: __('Schedule Script'),
- primary_action: () => {
- d.get_primary_btn().attr('disabled', true);
- var data = d.get_values();
- d.hide();
- if(data) {
- frm.events.schedule_script(frm, data);
- }
-
- }
- });
-
- d.show();
-
- });
+ if (frm.doc.script_type != 'Scheduler Event') {
+ frm.dashboard.hide();
}
},
- schedule_script(frm, data) {
- frm.call({
- method: "frappe.core.doctype.server_script.server_script.setup_scheduler_events",
- args: {
- 'script_name': frm.doc.name,
- 'frequency': data.event_type
- }
- });
- },
-
setup_help(frm) {
frm.get_field('help_html').html(`
DocType Event
diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json
index 9aa7b5afe5..b7e49673f8 100644
--- a/frappe/core/doctype/server_script/server_script.json
+++ b/frappe/core/doctype/server_script/server_script.json
@@ -8,6 +8,7 @@
"field_order": [
"script_type",
"reference_doctype",
+ "event_frequency",
"doctype_event",
"api_method",
"allow_guest",
@@ -84,11 +85,24 @@
{
"fieldname": "help_html",
"fieldtype": "HTML"
+ },
+ {
+ "depends_on": "eval:doc.script_type == \"Scheduler Event\"",
+ "fieldname": "event_frequency",
+ "fieldtype": "Select",
+ "label": "Event Frequency",
+ "mandatory_depends_on": "eval:doc.script_type == \"Scheduler Event\"",
+ "options": "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long"
}
],
"index_web_pages_for_search": 1,
- "links": [],
- "modified": "2021-01-03 18:50:14.767595",
+ "links": [
+ {
+ "link_doctype": "Scheduled Job Type",
+ "link_fieldname": "server_script"
+ }
+ ],
+ "modified": "2021-02-18 12:36:19.803425",
"modified_by": "Administrator",
"module": "Core",
"name": "Server Script",
diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py
index 88d68dba14..8838d9e954 100644
--- a/frappe/core/doctype/server_script/server_script.py
+++ b/frappe/core/doctype/server_script/server_script.py
@@ -5,6 +5,7 @@
from __future__ import unicode_literals
import ast
+from typing import Dict, List
import frappe
from frappe.model.document import Document
@@ -14,67 +15,146 @@ from frappe import _
class ServerScript(Document):
def validate(self):
- frappe.only_for('Script Manager', True)
+ frappe.only_for("Script Manager", True)
+ self.validate_script()
+ self.sync_scheduled_jobs()
+ self.clear_scheduled_events()
+
+ def on_update(self):
+ frappe.cache().delete_value("server_script_map")
+ self.sync_scheduler_events()
+
+ def on_trash(self):
+ if self.script_type == "Scheduler Event":
+ for job in self.scheduled_jobs:
+ frappe.delete_doc("Scheduled Job Type", job.name)
+
+ @property
+ def scheduled_jobs(self) -> List[Dict[str, str]]:
+ return frappe.get_all(
+ "Scheduled Job Type",
+ filters={"server_script": self.name},
+ fields=["name", "stopped"],
+ )
+
+ def validate_script(self):
+ """Utilizes the ast module to check for syntax errors
+ """
ast.parse(self.script)
- @staticmethod
- def on_update():
- frappe.cache().delete_value('server_script_map')
+ def sync_scheduled_jobs(self):
+ """Sync Scheduled Job Type statuses if Server Script's disabled status is changed
+ """
+ if self.script_type != "Scheduler Event" or not self.has_value_changed("disabled"):
+ return
- def execute_method(self):
- if self.script_type == 'API':
- # validate if guest is allowed
- if frappe.session.user == 'Guest' and not self.allow_guest:
- raise frappe.PermissionError
- _globals, _locals = safe_exec(self.script)
- return _globals.frappe.flags # output can be stored in flags
- else:
- # wrong report type!
+ for scheduled_job in self.scheduled_jobs:
+ if bool(scheduled_job.stopped) != bool(self.disabled):
+ job = frappe.get_doc("Scheduled Job Type", scheduled_job.name)
+ job.stopped = self.disabled
+ job.save()
+
+ def sync_scheduler_events(self):
+ """Create or update Scheduled Job Type documents for Scheduler Event Server Scripts
+ """
+ if not self.disabled and self.event_frequency and self.script_type == "Scheduler Event":
+ setup_scheduler_events(script_name=self.name, frequency=self.event_frequency)
+
+ def clear_scheduled_events(self):
+ """Deletes existing scheduled jobs by Server Script if self.event_frequency has changed
+ """
+ if self.script_type == "Scheduler Event" and self.has_value_changed("event_frequency"):
+ for scheduled_job in self.scheduled_jobs:
+ frappe.delete_doc("Scheduled Job Type", scheduled_job.name)
+
+ def execute_method(self) -> Dict:
+ """Specific to API endpoint Server Scripts
+
+ Raises:
+ frappe.DoesNotExistError: If self.script_type is not API
+ frappe.PermissionError: If self.allow_guest is unset for API accessed by Guest user
+
+ Returns:
+ dict: Evaluates self.script with frappe.utils.safe_exec.safe_exec and returns the flags set in it's safe globals
+ """
+ # wrong report type!
+ if self.script_type != "API":
raise frappe.DoesNotExistError
- def execute_doc(self, doc):
- # execute event
- safe_exec(self.script, None, dict(doc = doc))
+ # validate if guest is allowed
+ if frappe.session.user == "Guest" and not self.allow_guest:
+ raise frappe.PermissionError
+
+ # output can be stored in flags
+ _globals, _locals = safe_exec(self.script)
+ return _globals.frappe.flags
+
+ def execute_doc(self, doc: Document):
+ """Specific to Document Event triggered Server Scripts
+
+ Args:
+ doc (Document): Executes script with for a certain document's events
+ """
+ safe_exec(self.script, _locals={"doc": doc})
def execute_scheduled_method(self):
- if self.script_type == 'Scheduler Event':
- safe_exec(self.script)
- else:
- # wrong report type!
+ """Specific to Scheduled Jobs via Server Scripts
+
+ Raises:
+ frappe.DoesNotExistError: If script type is not a scheduler event
+ """
+ if self.script_type != "Scheduler Event":
raise frappe.DoesNotExistError
- def get_permission_query_conditions(self, user):
+ safe_exec(self.script)
+
+ def get_permission_query_conditions(self, user: str) -> List[str]:
+ """Specific to Permission Query Server Scripts
+
+ Args:
+ user (str): Takes user email to execute script and return list of conditions
+
+ Returns:
+ list: Returns list of conditions defined by rules in self.script
+ """
locals = {"user": user, "conditions": ""}
safe_exec(self.script, None, locals)
if locals["conditions"]:
return locals["conditions"]
+
@frappe.whitelist()
def setup_scheduler_events(script_name, frequency):
- method = frappe.scrub('{0}-{1}'.format(script_name, frequency))
- scheduled_script = frappe.db.get_value('Scheduled Job Type',
- dict(method=method))
+ """Creates or Updates Scheduled Job Type documents based on the specified script name and frequency
+
+ Args:
+ script_name (str): Name of the Server Script document
+ frequency (str): Event label compatible with the Frappe scheduler
+ """
+ method = frappe.scrub(f"{script_name}-{frequency}")
+ scheduled_script = frappe.db.get_value("Scheduled Job Type", {"method": method})
if not scheduled_script:
- doc = frappe.get_doc(dict(
- doctype = 'Scheduled Job Type',
- method = method,
- frequency = frequency,
- server_script = script_name
- ))
+ frappe.get_doc(
+ {
+ "doctype": "Scheduled Job Type",
+ "method": method,
+ "frequency": frequency,
+ "server_script": script_name,
+ }
+ ).insert()
- doc.insert()
-
- frappe.msgprint(_('Enabled scheduled execution for script {0}').format(script_name))
+ frappe.msgprint(_("Enabled scheduled execution for script {0}").format(script_name))
else:
- doc = frappe.get_doc('Scheduled Job Type', scheduled_script)
- doc.update(dict(
- doctype = 'Scheduled Job Type',
- method = method,
- frequency = frequency,
- server_script = script_name
- ))
+ doc = frappe.get_doc("Scheduled Job Type", scheduled_script)
+
+ if doc.frequency == frequency:
+ return
+
+ doc.frequency = frequency
doc.save()
- frappe.msgprint(_('Scheduled execution for script {0} has updated').format(script_name))
+ frappe.msgprint(
+ _("Scheduled execution for script {0} has updated").format(script_name)
+ )
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 142cc1ee26..3f19a6ef7b 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -6,7 +6,7 @@ import frappe
from frappe.model.document import Document
from frappe.utils import cint, flt, has_gravatar, escape_html, format_datetime, now_datetime, get_formatted_email, today
from frappe import throw, msgprint, _
-from frappe.utils.password import update_password as _update_password
+from frappe.utils.password import update_password as _update_password, check_password
from frappe.desk.notifications import clear_notifications
from frappe.desk.doctype.notification_settings.notification_settings import create_notification_settings
from frappe.utils.user import get_system_managers
@@ -527,6 +527,27 @@ class User(Document):
return [i.strip() for i in self.restrict_ip.split(",")]
+ @classmethod
+ def find_by_credentials(cls, user_name: str, password: str, validate_password: bool = True):
+ """Find the user by credentials.
+ """
+ login_with_mobile = cint(frappe.db.get_value("System Settings", "System Settings", "allow_login_using_mobile_number"))
+ filter = {"mobile_no": user_name} if login_with_mobile else {"name": user_name}
+
+ user = frappe.db.get_value("User", filters=filter, fieldname=['name', 'enabled'], as_dict=True) or {}
+ if not user:
+ return
+
+ user['is_authenticated'] = True
+ if validate_password:
+ try:
+ check_password(user_name, password)
+ except frappe.AuthenticationError:
+ user['is_authenticated'] = False
+
+ return user
+
+
@frappe.whitelist()
def get_timezones():
import pytz
diff --git a/frappe/core/workspace/build/build.json b/frappe/core/workspace/build/build.json
index c4bde55d7f..aefda698b1 100644
--- a/frappe/core/workspace/build/build.json
+++ b/frappe/core/workspace/build/build.json
@@ -11,6 +11,7 @@
"hide_custom": 0,
"icon": "tool",
"idx": 0,
+ "is_default": 0,
"is_standard": 1,
"label": "Build",
"links": [
@@ -163,8 +164,8 @@
{
"hidden": 0,
"is_query_report": 0,
- "label": "Custom Script",
- "link_to": "Custom Script",
+ "label": "Client Script",
+ "link_to": "Client Script",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
@@ -181,7 +182,7 @@
"type": "Link"
}
],
- "modified": "2021-01-02 14:03:15.029699",
+ "modified": "2021-02-04 13:48:48.493146",
"modified_by": "Administrator",
"module": "Core",
"name": "Build",
diff --git a/frappe/custom/doctype/custom_script/README.md b/frappe/custom/doctype/client_script/README.md
similarity index 100%
rename from frappe/custom/doctype/custom_script/README.md
rename to frappe/custom/doctype/client_script/README.md
diff --git a/frappe/custom/doctype/custom_script/__init__.py b/frappe/custom/doctype/client_script/__init__.py
similarity index 100%
rename from frappe/custom/doctype/custom_script/__init__.py
rename to frappe/custom/doctype/client_script/__init__.py
diff --git a/frappe/custom/doctype/custom_script/custom_script.js b/frappe/custom/doctype/client_script/client_script.js
similarity index 97%
rename from frappe/custom/doctype/custom_script/custom_script.js
rename to frappe/custom/doctype/client_script/client_script.js
index 711e7d1796..21e7334b82 100644
--- a/frappe/custom/doctype/custom_script/custom_script.js
+++ b/frappe/custom/doctype/client_script/client_script.js
@@ -1,7 +1,7 @@
// Copyright (c) 2016, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Custom Script', {
+frappe.ui.form.on('Client Script', {
refresh(frm) {
if (frm.doc.dt && frm.doc.script) {
frm.add_custom_button(__('Go to {0}', [frm.doc.dt]),
diff --git a/frappe/custom/doctype/custom_script/custom_script.json b/frappe/custom/doctype/client_script/client_script.json
similarity index 86%
rename from frappe/custom/doctype/custom_script/custom_script.json
rename to frappe/custom/doctype/client_script/client_script.json
index 328b247c49..57e6c68094 100644
--- a/frappe/custom/doctype/custom_script/custom_script.json
+++ b/frappe/custom/doctype/client_script/client_script.json
@@ -2,7 +2,7 @@
"actions": [],
"allow_import": 1,
"creation": "2013-01-10 16:34:01",
- "description": "Adds a client custom script to a DocType",
+ "description": "Adds a custom client script to a DocType",
"doctype": "DocType",
"document_type": "Document",
"engine": "InnoDB",
@@ -22,9 +22,7 @@
"oldfieldname": "dt",
"oldfieldtype": "Link",
"options": "DocType",
- "reqd": 1,
- "show_days": 1,
- "show_seconds": 1
+ "reqd": 1
},
{
"fieldname": "script",
@@ -32,35 +30,29 @@
"label": "Script",
"oldfieldname": "script",
"oldfieldtype": "Code",
- "options": "JS",
- "show_days": 1,
- "show_seconds": 1
+ "options": "JS"
},
{
"fieldname": "sample",
"fieldtype": "HTML",
"label": "Sample",
- "options": "
Custom Script Help
\n
Custom Scripts are executed only on the client-side (i.e. in Forms). Here are some examples to get you started
\n
\n\n// fetch local_tax_no on selection of customer \n// cur_frm.add_fetch(link_field, source_fieldname, target_fieldname); \ncur_frm.add_fetch('customer', 'local_tax_no', 'local_tax_no');\n\n// additional validation on dates \nfrappe.ui.form.on('Task', 'validate', function(frm) {\n if (frm.doc.from_date < get_today()) {\n msgprint('You can not select past date in From Date');\n validated = false;\n } \n});\n\n// make a field read-only after saving \nfrappe.ui.form.on('Task', {\n refresh: function(frm) {\n // use the __islocal value of doc, to check if the doc is saved or not\n frm.set_df_property('myfield', 'read_only', frm.doc.__islocal ? 0 : 1);\n } \n});\n\n// additional permission check\nfrappe.ui.form.on('Task', {\n validate: function(frm) {\n if(user=='user1@example.com' && frm.doc.purpose!='Material Receipt') {\n msgprint('You are only allowed Material Receipt');\n validated = false;\n }\n } \n});\n\n// calculate sales incentive\nfrappe.ui.form.on('Sales Invoice', {\n validate: function(frm) {\n // calculate incentives for each person on the deal\n total_incentive = 0\n $.each(frm.doc.sales_team, function(i, d) {\n // calculate incentive\n var incentive_percent = 2;\n if(frm.doc.base_grand_total > 400) incentive_percent = 4;\n // actual incentive\n d.incentives = flt(frm.doc.base_grand_total) * incentive_percent / 100;\n total_incentive += flt(d.incentives)\n });\n frm.doc.total_incentive = total_incentive;\n } \n})\n\n
Client Scripts are executed only on the client-side (i.e. in Forms). Here are some examples to get you started
\n
\n\n// fetch local_tax_no on selection of customer \n// cur_frm.add_fetch(link_field, source_fieldname, target_fieldname); \ncur_frm.add_fetch('customer', 'local_tax_no', 'local_tax_no');\n\n// additional validation on dates \nfrappe.ui.form.on('Task', 'validate', function(frm) {\n if (frm.doc.from_date < get_today()) {\n msgprint('You can not select past date in From Date');\n validated = false;\n } \n});\n\n// make a field read-only after saving \nfrappe.ui.form.on('Task', {\n refresh: function(frm) {\n // use the __islocal value of doc, to check if the doc is saved or not\n frm.set_df_property('myfield', 'read_only', frm.doc.__islocal ? 0 : 1);\n } \n});\n\n// additional permission check\nfrappe.ui.form.on('Task', {\n validate: function(frm) {\n if(user=='user1@example.com' && frm.doc.purpose!='Material Receipt') {\n msgprint('You are only allowed Material Receipt');\n validated = false;\n }\n } \n});\n\n// calculate sales incentive\nfrappe.ui.form.on('Sales Invoice', {\n validate: function(frm) {\n // calculate incentives for each person on the deal\n total_incentive = 0\n $.each(frm.doc.sales_team, function(i, d) {\n // calculate incentive\n var incentive_percent = 2;\n if(frm.doc.base_grand_total > 400) incentive_percent = 4;\n // actual incentive\n d.incentives = flt(frm.doc.base_grand_total) * incentive_percent / 100;\n total_incentive += flt(d.incentives)\n });\n frm.doc.total_incentive = total_incentive;\n } \n})\n\n
"
},
{
"default": "0",
"fieldname": "enabled",
"fieldtype": "Check",
- "label": "Enabled",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Enabled"
}
],
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-08-24 21:56:07.719579",
+ "modified": "2021-02-04 13:57:56.509437",
"modified_by": "Administrator",
"module": "Custom",
- "name": "Custom Script",
+ "name": "Client Script",
"owner": "Administrator",
"permissions": [
{
@@ -86,6 +78,7 @@
"write": 1
}
],
+ "sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/custom/doctype/custom_script/custom_script.py b/frappe/custom/doctype/client_script/client_script.py
similarity index 84%
rename from frappe/custom/doctype/custom_script/custom_script.py
rename to frappe/custom/doctype/client_script/client_script.py
index e15819de65..e252e2a750 100644
--- a/frappe/custom/doctype/custom_script/custom_script.py
+++ b/frappe/custom/doctype/client_script/client_script.py
@@ -5,9 +5,9 @@ import frappe
from frappe.model.document import Document
-class CustomScript(Document):
+class ClientScript(Document):
def autoname(self):
- self.name = self.dt + "-Client"
+ self.name = self.dt
def on_update(self):
frappe.clear_cache(doctype=self.dt)
diff --git a/frappe/custom/doctype/custom_script/test_custom_script.py b/frappe/custom/doctype/client_script/test_client_script.py
similarity index 65%
rename from frappe/custom/doctype/custom_script/test_custom_script.py
rename to frappe/custom/doctype/client_script/test_client_script.py
index 6947e6060d..de113c1ce7 100644
--- a/frappe/custom/doctype/custom_script/test_custom_script.py
+++ b/frappe/custom/doctype/client_script/test_client_script.py
@@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe
import unittest
-# test_records = frappe.get_test_records('Custom Script')
+# test_records = frappe.get_test_records('Client Script')
-class TestCustomScript(unittest.TestCase):
+class TestClientScript(unittest.TestCase):
pass
diff --git a/frappe/custom/workspace/customization/customization.json b/frappe/custom/workspace/customization/customization.json
index 3631914249..cdc3b73366 100644
--- a/frappe/custom/workspace/customization/customization.json
+++ b/frappe/custom/workspace/customization/customization.json
@@ -10,6 +10,7 @@
"hide_custom": 0,
"icon": "customization",
"idx": 0,
+ "is_default": 0,
"is_standard": 1,
"label": "Customization",
"links": [
@@ -81,8 +82,8 @@
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
- "label": "Custom Script",
- "link_to": "Custom Script",
+ "label": "Client Script",
+ "link_to": "Client Script",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
@@ -115,7 +116,7 @@
"type": "Link"
}
],
- "modified": "2020-12-01 13:38:39.843773",
+ "modified": "2021-02-04 13:50:35.750463",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customization",
@@ -134,8 +135,14 @@
"type": "DocType"
},
{
- "label": "Custom Script",
- "link_to": "Custom Script",
+ "label": "Client Script",
+ "link_to": "Client Script",
+ "type": "DocType"
+ },
+ {
+ "doc_view": "",
+ "label": "Server Script",
+ "link_to": "Server Script",
"type": "DocType"
}
]
diff --git a/frappe/desk/doctype/kanban_board/kanban_board.py b/frappe/desk/doctype/kanban_board/kanban_board.py
index f1ad41db6c..a655e9e1da 100644
--- a/frappe/desk/doctype/kanban_board/kanban_board.py
+++ b/frappe/desk/doctype/kanban_board/kanban_board.py
@@ -17,6 +17,10 @@ class KanbanBoard(Document):
def on_update(self):
frappe.clear_cache(doctype=self.reference_doctype)
+ def before_insert(self):
+ for column in self.columns:
+ column.order = get_order_for_column(self, column.column_name)
+
def validate_column_name(self):
for column in self.columns:
if not column.column_name:
@@ -125,6 +129,53 @@ def update_order(board_name, order):
board.save()
return board, updated_cards
+@frappe.whitelist()
+def update_order_for_single_card(board_name, docname, from_colname, to_colname, old_index, new_index):
+ '''Save the order of cards in columns'''
+ board = frappe.get_doc('Kanban Board', board_name)
+ doctype = board.reference_doctype
+ fieldname = board.field_name
+ old_index = frappe.parse_json(old_index)
+ new_index = frappe.parse_json(new_index)
+
+ # save current order and index of columns to be updated
+ from_col_order, from_col_idx = get_kanban_column_order_and_index(board, from_colname)
+ to_col_order, to_col_idx = get_kanban_column_order_and_index(board, to_colname)
+
+ if from_colname == to_colname:
+ from_col_order = to_col_order
+
+ to_col_order.insert(new_index, from_col_order.pop((old_index)))
+
+ # save updated order
+ board.columns[from_col_idx].order = frappe.as_json(from_col_order)
+ board.columns[to_col_idx].order = frappe.as_json(to_col_order)
+ board.save()
+
+ # update changed value in doc
+ frappe.set_value(doctype, docname, fieldname, to_colname)
+
+ return board
+
+def get_kanban_column_order_and_index(board, colname):
+ for i, col in enumerate(board.columns):
+ if col.column_name == colname:
+ col_order = frappe.parse_json(col.order)
+ col_idx = i
+
+ return col_order, col_idx
+
+@frappe.whitelist()
+def add_card(board_name, docname, colname):
+ board = frappe.get_doc('Kanban Board', board_name)
+
+ col_order, col_idx = get_kanban_column_order_and_index(board, colname)
+ col_order.insert(0, docname)
+
+ board.columns[col_idx].order = frappe.as_json(col_order)
+
+ board.save()
+ return board
@frappe.whitelist()
def quick_kanban_board(doctype, board_name, field_name, project=None):
@@ -133,6 +184,13 @@ def quick_kanban_board(doctype, board_name, field_name, project=None):
doc = frappe.new_doc('Kanban Board')
meta = frappe.get_meta(doctype)
+ doc.kanban_board_name = board_name
+ doc.reference_doctype = doctype
+ doc.field_name = field_name
+
+ if project:
+ doc.filters = '[["Task","project","=","{0}"]]'.format(project)
+
options = ''
for field in meta.fields:
if field.fieldname == field_name:
@@ -149,12 +207,6 @@ def quick_kanban_board(doctype, board_name, field_name, project=None):
column_name=column
))
- doc.kanban_board_name = board_name
- doc.reference_doctype = doctype
- doc.field_name = field_name
-
- if project:
- doc.filters = '[["Task","project","=","{0}"]]'.format(project)
if doctype in ['Note', 'ToDo']:
doc.private = 1
@@ -162,6 +214,12 @@ def quick_kanban_board(doctype, board_name, field_name, project=None):
doc.save()
return doc
+def get_order_for_column(board, colname):
+ filters = [[board.reference_doctype, board.field_name, '=', colname]]
+ if board.filters:
+ filters.append(frappe.parse_json(board.filters)[0])
+
+ return frappe.as_json(frappe.get_list(board.reference_doctype, filters=filters, pluck='name'))
@frappe.whitelist()
def update_column_order(board_name, order):
diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py
index d5428b1da2..c63da93a33 100644
--- a/frappe/desk/form/meta.py
+++ b/frappe/desk/form/meta.py
@@ -130,7 +130,7 @@ class FormMeta(Meta):
def add_custom_script(self):
"""embed all require files"""
# custom script
- custom = frappe.db.get_value("Custom Script", {"dt": self.name, "enabled": 1}, "script") or ""
+ custom = frappe.db.get_value("Client Script", {"dt": self.name, "enabled": 1}, "script") or ""
self.set("__custom_js", custom)
diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py
index 4c3bab2e23..395d2b9571 100644
--- a/frappe/desk/form/utils.py
+++ b/frappe/desk/form/utils.py
@@ -47,7 +47,7 @@ def validate_link():
except Exception as e:
error_message = str(e).split("Unknown column '")
fieldname = None if len(error_message)<=1 else error_message[1].split("'")[0]
- frappe.msgprint(_("Wrong fieldname {0} in add_fetch configuration of custom script").format(fieldname))
+ frappe.msgprint(_("Wrong fieldname {0} in add_fetch configuration of custom client script").format(fieldname))
frappe.errprint(frappe.get_traceback())
if fetch_value:
diff --git a/frappe/desk/page/leaderboard/leaderboard.js b/frappe/desk/page/leaderboard/leaderboard.js
index 825e1d959b..b3fccf84f9 100644
--- a/frappe/desk/page/leaderboard/leaderboard.js
+++ b/frappe/desk/page/leaderboard/leaderboard.js
@@ -141,7 +141,7 @@ class Leaderboard {
}
create_date_range_field() {
- let timespan_field = $(this.parent).find(`.frappe-control[data-original-title='Timespan']`);
+ let timespan_field = $(this.parent).find(`.frappe-control[data-original-title=${__('Timespan')}]`);
this.date_range_field = $(``).insertAfter(timespan_field).hide();
let date_field = frappe.ui.form.make_control({
diff --git a/frappe/desk/search.py b/frappe/desk/search.py
index f4e6543844..6faa827dde 100644
--- a/frappe/desk/search.py
+++ b/frappe/desk/search.py
@@ -80,13 +80,15 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
is_whitelisted(frappe.get_attr(query))
frappe.response["values"] = frappe.call(query, doctype, txt,
searchfield, start, page_length, filters, as_dict=as_dict)
- except Exception as e:
+ except frappe.exceptions.PermissionError as e:
if frappe.local.conf.developer_mode:
raise e
else:
frappe.respond_as_web_page(title='Invalid Method', html='Method not found',
indicator_color='red', http_status_code=404)
return
+ except Exception as e:
+ raise e
elif not query and doctype in standard_queries:
# from standard queries
search_widget(doctype, txt, standard_queries[doctype][0],
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index ca4dbb83e2..4869c5a9bf 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -90,6 +90,29 @@ class EmailAccount(Document):
if self.append_to not in valid_doctypes:
frappe.throw(_("Append To can be one of {0}").format(comma_or(valid_doctypes)))
+ def before_save(self):
+ messages = []
+ as_list = 1
+ if not self.enable_incoming and self.default_incoming:
+ self.default_incoming = False
+ messages.append(_("{} has been disabled. It can only be enabled if {} is checked.")
+ .format(
+ frappe.bold(_('Default Incoming')),
+ frappe.bold(_('Enable Incoming'))
+ )
+ )
+ if not self.enable_outgoing and self.default_outgoing:
+ self.default_outgoing = False
+ messages.append(_("{} has been disabled. It can only be enabled if {} is checked.")
+ .format(
+ frappe.bold(_('Default Outgoing')),
+ frappe.bold(_('Enable Outgoing'))
+ )
+ )
+ if messages:
+ if len(messages) == 1: (as_list, messages) = (0, messages[0])
+ frappe.msgprint(messages, as_list= as_list, indicator='orange', title=_("Defaults Updated"))
+
def on_update(self):
"""Check there is only one default of each type."""
from frappe.core.doctype.user.user import setup_user_email_inbox
diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py
index 9b0b5e41d7..3fcabb9495 100644
--- a/frappe/email/test_email_body.py
+++ b/frappe/email/test_email_body.py
@@ -17,7 +17,7 @@ class TestEmailBody(unittest.TestCase):
Hey John Doe!
This is embedded image you asked for
-
+
'''
email_text = '''
@@ -25,7 +25,7 @@ Hey John Doe!
This is the text version of this email
'''
- img_path = os.path.abspath('assets/frappe/images/favicon.png')
+ img_path = os.path.abspath('assets/frappe/images/frappe-favicon.svg')
with open(img_path, 'rb') as f:
img_content = f.read()
img_base64 = base64.b64encode(img_content).decode()
@@ -77,12 +77,11 @@ This is the text version of this email
def test_image(self):
img_signature = '''
-Content-Type: image/png
+Content-Type: image/svg+xml
MIME-Version: 1.0
Content-Transfer-Encoding: base64
-Content-Disposition: inline; filename="favicon.png"
+Content-Disposition: inline; filename="frappe-favicon.svg"
'''
-
self.assertTrue(img_signature in self.email_string)
self.assertTrue(self.img_base64 in self.email_string)
@@ -117,7 +116,7 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
def test_replace_filename_with_cid(self):
original_message = '''