Merge branch 'develop' of https://github.com/frappe/frappe into select-field-placholder

This commit is contained in:
prssanna 2021-02-25 14:04:38 +05:30
commit 24e2b3d2ad
49 changed files with 587 additions and 301 deletions

View file

@ -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');

View file

@ -17,7 +17,6 @@ from werkzeug.local import Local, release_local
import os, sys, importlib, inspect, json
from past.builtins import cmp
import click
from faker import Faker
# public
from .exceptions import *
@ -196,17 +195,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
@ -1747,6 +1749,8 @@ def parse_json(val):
return parse_json(val)
def mock(type, size=1, locale='en'):
from faker import Faker
results = []
faker = Faker(locale)
if not type in dir(faker):

View file

@ -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]

View file

@ -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

View file

@ -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');
},

View file

@ -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)
frappe.cache_manager.clear_doctype_map('Assignment Rule', 'due_date_rules_for_' + rule.document_type)

View file

@ -15,7 +15,6 @@ import frappe
from frappe.utils.minify import JavascriptMinify
import click
from requests import get
from six import iteritems, text_type
from six.moves.urllib.parse import urlparse
@ -26,6 +25,8 @@ sites_path = os.path.abspath(os.getcwd())
def download_file(url, prefix):
from requests import get
filename = urlparse(url).path.split("/")[-1]
local_filename = os.path.join(prefix, filename)
with get(url, stream=True, allow_redirects=True) as r:

View file

@ -9,7 +9,6 @@ import click
import frappe
from frappe.commands import get_site, pass_context
from frappe.exceptions import SiteNotSpecifiedError
from frappe.installer import _new_site
@click.command('new-site')
@ -31,6 +30,8 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin
verbose=False, install_apps=None, source_sql=None, force=None, no_mariadb_socket=False,
install_app=None, db_name=None, db_password=None, db_type=None, db_host=None, db_port=None):
"Create a new site"
from frappe.installer import _new_site
frappe.init(site=site, new_site=True)
_new_site(db_name, site, mariadb_root_username=mariadb_root_username,
@ -57,6 +58,7 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin
def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_password=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, with_private_files=None):
"Restore site database from an sql file"
from frappe.installer import (
_new_site,
extract_sql_from_archive,
extract_files,
is_downgrade,
@ -145,6 +147,8 @@ def reinstall(context, admin_password=None, mariadb_root_username=None, mariadb_
_reinstall(site, admin_password, mariadb_root_username, mariadb_root_password, yes, verbose=context.verbose)
def _reinstall(site, admin_password=None, mariadb_root_username=None, mariadb_root_password=None, yes=False, verbose=False):
from frappe.installer import _new_site
if not yes:
click.confirm('This will wipe your database. Are you sure you want to reinstall?', abort=True)
try:

View file

@ -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)

View file

@ -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

View file

@ -16,7 +16,6 @@ import frappe.model.meta
from frappe import _
from time import time
from frappe.utils import now, getdate, cast_fieldtype, get_datetime
from frappe.utils.background_jobs import execute_job, get_queue
from frappe.model.utils.link_count import flush_local_link_count
from frappe.utils import cint
@ -1032,6 +1031,8 @@ class Database(object):
insert_list = []
def enqueue_jobs_after_commit():
from frappe.utils.background_jobs import execute_job, get_queue
if frappe.flags.enqueue_after_commit and len(frappe.flags.enqueue_after_commit) > 0:
for job in frappe.flags.enqueue_after_commit:
q = get_queue(job.get("queue"), is_async=job.get("is_async"))

View file

@ -8,8 +8,7 @@ from pymysql.times import TimeDelta
from pymysql.constants import ER, FIELD_TYPE
from pymysql.converters import conversions
from frappe.utils import get_datetime, cstr
from markdown2 import UnicodeWithAttrs
from frappe.utils import get_datetime, cstr, UnicodeWithAttrs
from frappe.database.database import Database
from six import PY2, binary_type, text_type, string_types
from frappe.database.mariadb.schema import MariaDBTable

View file

@ -147,6 +147,8 @@ def get_version(doctype, doc_name, frequency, user):
return timeline
def get_comments(doctype, doc_name, frequency, user):
from html2text import html2text
timeline = []
filters = get_filters("reference_name", doc_name, frequency, user)
comments = frappe.get_all("Comment",
@ -166,7 +168,7 @@ def get_comments(doctype, doc_name, frequency, user):
"time": comment.modified,
"data": {
"time": time,
"comment": frappe.utils.html2text(str(comment.content)),
"comment": html2text(str(comment.content)),
"by": by
},
"doctype": doctype,
@ -197,6 +199,8 @@ def get_follow_users(doctype, doc_name):
)
def get_row_changed(row_changed, time, doctype, doc_name, v):
from html2text import html2text
items = []
for d in row_changed:
d[2] = d[2] if d[2] else ' '
@ -209,8 +213,8 @@ def get_row_changed(row_changed, time, doctype, doc_name, v):
"table_field": d[0],
"row": str(d[1]),
"field": d[3][0][0],
"from": frappe.utils.html2text(str(d[3][0][1])),
"to": frappe.utils.html2text(str(d[3][0][2]))
"from": html2text(str(d[3][0][1])),
"to": html2text(str(d[3][0][2]))
},
"doctype": doctype,
"doc_name": doc_name,
@ -236,6 +240,8 @@ def get_added_row(added, time, doctype, doc_name, v):
return items
def get_field_changed(changed, time, doctype, doc_name, v):
from html2text import html2text
items = []
for d in changed:
d[1] = d[1] if d[1] else ' '
@ -246,8 +252,8 @@ def get_field_changed(changed, time, doctype, doc_name, v):
"data": {
"time": time,
"field": d[0],
"from": frappe.utils.html2text(str(d[1])),
"to": frappe.utils.html2text(str(d[2]))
"from": html2text(str(d[1])),
"to": html2text(str(d[2]))
},
"doctype": doctype,
"doc_name": doc_name,

View file

@ -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

View file

@ -108,13 +108,8 @@ class TestConnectedApp(unittest.TestCase):
session = requests.Session()
# first login of a new user on a new site fails with "401 UNAUTHORIZED"
# when anybody fixes that, the two lines below can be removed
first_login = login()
self.assertEqual(first_login.status_code, 401)
second_login = login()
self.assertEqual(second_login.status_code, 200)
self.assertEqual(first_login.status_code, 200)
authorization_url = self.connected_app.initiate_web_application_flow(user=self.user_name)

View file

@ -163,10 +163,13 @@ def openid_profile(*args, **kwargs):
first_name, last_name, avatar, name = frappe.db.get_value("User", frappe.session.user, ["first_name", "last_name", "user_image", "name"])
frappe_userid = frappe.db.get_value("User Social Login", {"parent":frappe.session.user, "provider": "frappe"}, "userid")
request_url = urlparse(frappe.request.url)
base_url = frappe.db.get_value("Social Login Key", "frappe", "base_url") or None
if avatar:
if validate_url(avatar):
picture = avatar
elif base_url:
picture = base_url + '/' + avatar
else:
picture = request_url.scheme + "://" + request_url.netloc + avatar

View file

@ -12,11 +12,9 @@ from frappe.model.naming import set_new_name
from frappe.model.utils.link_count import notify_link_count
from frappe.modules import load_doctype_module
from frappe.model import display_fieldtypes
from frappe.utils.password import get_decrypted_password, set_encrypted_password
from frappe.utils import (cint, flt, now, cstr, strip_html,
sanitize_html, sanitize_email, cast_fieldtype)
from frappe.utils.html_utils import unescape_html
from bs4 import BeautifulSoup
max_positive_value = {
'smallint': 2 ** 15,
@ -419,25 +417,60 @@ class BaseDocument(object):
doc.db_update()
def show_unique_validation_message(self, e):
# TODO: Find a better way to extract fieldname
if frappe.db.db_type != 'postgres':
fieldname = str(e).split("'")[-2]
label = None
# unique_first_fieldname_second_fieldname is the constraint name
# created using frappe.db.add_unique
if "unique_" in fieldname:
fieldname = fieldname.split("_", 1)[1]
# MariaDB gives key_name in error. Extracting fieldname from key name
try:
fieldname = self.get_field_name_by_key_name(fieldname)
except IndexError:
pass
df = self.meta.get_field(fieldname)
if df:
label = df.label
label = self.get_label_from_fieldname(fieldname)
frappe.msgprint(_("{0} must be unique").format(label or fieldname))
# this is used to preserve traceback
raise frappe.UniqueValidationError(self.doctype, self.name, e)
def get_field_name_by_key_name(self, key_name):
"""MariaDB stores a mapping between `key_name` and `column_name`.
This function returns the `column_name` associated with the `key_name` passed
Args:
key_name (str): The name of the database index.
Raises:
IndexError: If the key is not found in the table.
Returns:
str: The column name associated with the key.
"""
return frappe.db.sql(f"""
SHOW
INDEX
FROM
`tab{self.doctype}`
WHERE
key_name=%s
AND
Non_unique=0
""", key_name, as_dict=True)[0].get("Column_name")
def get_label_from_fieldname(self, fieldname):
"""Returns the associated label for fieldname
Args:
fieldname (str): The fieldname in the DocType to use to pull the label.
Returns:
str: The label associated with the fieldname, if found, otherwise `None`.
"""
df = self.meta.get_field(fieldname)
if df:
return df.label
def update_modified(self):
"""Update modified timestamp"""
self.set("modified", now())
@ -721,6 +754,8 @@ class BaseDocument(object):
- Ignore if 'Ignore XSS Filter' is checked or fieldtype is 'Code'
"""
from bs4 import BeautifulSoup
if frappe.flags.in_install:
return
@ -757,6 +792,8 @@ class BaseDocument(object):
def _save_passwords(self):
"""Save password field values in __Auth table"""
from frappe.utils.password import set_encrypted_password
if self.flags.ignore_save_passwords is True:
return
@ -771,6 +808,8 @@ class BaseDocument(object):
self.set(df.fieldname, '*'*len(new_password))
def get_password(self, fieldname='password', raise_exception=True):
from frappe.utils.password import get_decrypted_password
if self.get(fieldname) and not self.is_dummy_password(self.get(fieldname)):
return self.get(fieldname)

View file

@ -6,7 +6,6 @@ import frappe
import time
from frappe import _, msgprint
from frappe.utils import flt, cstr, now, get_datetime_str, file_lock, date_diff
from frappe.utils.background_jobs import enqueue
from frappe.model.base_document import BaseDocument, get_controller
from frappe.model.naming import set_new_name
from six import iteritems, string_types
@ -1269,6 +1268,8 @@ class Document(BaseDocument):
# call _submit instead of submit, so you can override submit to call
# run_delayed based on some action
# See: Stock Reconciliation
from frappe.utils.background_jobs import enqueue
if hasattr(self, '_' + action):
action = '_' + action

View file

@ -35,6 +35,7 @@ frappe.patches.v11_0.change_email_signature_fieldtype
execute:frappe.reload_doc('core', 'doctype', 'activity_log')
execute:frappe.reload_doc('core', 'doctype', 'deleted_document')
execute:frappe.reload_doc('core', 'doctype', 'domain_settings')
frappe.patches.v13_0.rename_custom_client_script
frappe.patches.v8_0.rename_page_role_to_has_role #2017-03-16
frappe.patches.v7_2.setup_custom_perms #2017-01-19
frappe.patches.v8_0.set_user_permission_for_page_and_report #2017-03-20
@ -330,4 +331,3 @@ execute:frappe.get_doc('Role', 'Guest').save() # remove desk access
frappe.patches.v13_0.rename_desk_page_to_workspace # 02.02.2021
frappe.patches.v13_0.delete_package_publish_tool
frappe.patches.v13_0.rename_list_view_setting_to_list_view_settings
frappe.patches.v13_0.rename_custom_client_script

View file

@ -5,9 +5,8 @@ from __future__ import unicode_literals
import frappe
def execute():
"""Enable all the existing custom script"""
frappe.reload_doc("Custom", "doctype", "Custom Script")
"""Enable all the existing Client script"""
frappe.db.sql("""
UPDATE `tabCustom Script` SET enabled=1
UPDATE `tabClient Script` SET enabled=1
""")

View file

@ -268,7 +268,7 @@
<path d="M16.127 13.077l3.194 3.194a2.588 2.588 0 0 1 0 3.66 2.589 2.589 0 0 1-3.66 0l-2.902-2.902" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M11.315 10.095l-4.96-4.96a1.294 1.294 0 1 0-1.83 1.83l4.877 4.877" stroke="var(--icon-stroke)" stroke-miterlimit="10"></path>
<path d="M18.56 11.949l-.353.353a.5.5 0 0 0 .707 0l-.354-.353zM21 9.509l.354.353a.5.5 0 0 0 0-.705L21 9.509zm-5.47-5.51l.354-.352-.004-.004-.35.357zm-4.9.02l-.353-.353a.5.5 0 0 0 0 .707l.353-.354zm8.284 8.283l2.44-2.44-.707-.707-2.44 2.44.707.707zm2.44-3.145l-5.47-5.51-.71.705 5.471 5.51.71-.705zM15.88 3.643A3.977 3.977 0 0 0 13.074 2.5l.004 1a2.977 2.977 0 0 1 2.1.856l.702-.713zM13.074 2.5a3.977 3.977 0 0 0-2.797 1.166l.707.706a2.977 2.977 0 0 1 2.094-.872l-.004-1zm-2.797 1.873l7.93 7.93.707-.708-7.93-7.93-.707.708z"
fill="#4C5A67" stroke="none"></path>
fill="var(--icon-stroke)" stroke="none"></path>
<path d="M14.133 7.522L3.398 17.325a1.219 1.219 0 0 0-.04 1.764L4.6 20.331a1.22 1.22 0 0 0 1.764-.04l9.789-10.75" stroke="var(--icon-stroke)" stroke-miterlimit="10"></path>
</symbol>
<symbol viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" id="icon-support">

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

View file

@ -128,7 +128,7 @@ frappe.Application = Class.extend({
}
// REDESIGN-TODO: Fix preview popovers
//this.link_preview = new frappe.ui.LinkPreview();
this.link_preview = new frappe.ui.LinkPreview();
if (!frappe.boot.developer_mode) {
setInterval(function() {

View file

@ -138,9 +138,9 @@ frappe.ui.form.ControlSelect = frappe.ui.form.ControlData.extend({
this.prop('disabled', false);
};
let originalVal = $.fn.val;
$.fn.val = function () {
let result = originalVal.apply(this, arguments);
let original_val = $.fn.val;
$.fn.val = function() {
let result = original_val.apply(this, arguments);
if (arguments.length > 0) $(this).trigger('select-change');
return result;
};

View file

@ -358,10 +358,11 @@ class FormTimeline extends BaseTimeline {
const args = {
doc: this.frm.doc,
frm: this.frm,
recipients: communication_doc ? communication_doc.sender : this.get_recipient(),
recipients: communication_doc && communication_doc.sender != frappe.session.user_email ? communication_doc.sender : this.get_recipient(),
is_a_reply: Boolean(communication_doc),
title: communication_doc ? __('Reply') : null,
last_email: communication_doc
last_email: communication_doc,
subject: communication_doc && communication_doc.subject
};
if (communication_doc && reply_all) {

View file

@ -22,7 +22,6 @@ frappe.ui.LinkPreview = class {
}
});
this.handle_popover_hide();
}
identify_doc() {
@ -122,7 +121,7 @@ frappe.ui.LinkPreview = class {
}
});
$(window).on('hashchange', () => {
frappe.router.on('change', () => {
this.clear_all_popovers();
});
}
@ -142,18 +141,22 @@ frappe.ui.LinkPreview = class {
let popover_content = this.get_popover_html(preview_data);
this.element.popover({
container: 'body',
template: `
<div class="link-preview-popover popover">
<div class="arrow"></div>
<div class="popover-body popover-content">
</div>
</div>
`,
html: true,
sanitizeFn: (content) => content,
content: popover_content,
trigger: 'manual',
placement: 'top auto',
animation: false,
placement: 'top',
});
const $popover = this.element.data('bs.popover').tip();
$popover.addClass('link-preview-popover');
const $popover = $(this.element.data('bs.popover').tip);
$popover.toggleClass('control-field-popover', this.is_link);
this.popovers_list.push(this.element.data('bs.popover'));
}
@ -167,53 +170,63 @@ frappe.ui.LinkPreview = class {
this.href = this.href.replace(new RegExp(' ', 'g'), '%20');
}
let image_html = '';
let id_html = '';
let content_html = '';
if (preview_data.preview_image) {
let image_url = encodeURI(preview_data.preview_image);
image_html = `
let popover_content =`
<div class="preview-popover-header">
<div class="preview-header">
<img src="${image_url}" onerror="this.src='/assets/frappe/images/fallback-thumbnail.jpg'" class="preview-image"></img>
${this.get_image_html(preview_data)}
<div class="preview-name">
<a href=${this.href}>${__(preview_data.preview_title)}</a>
</div>
<div class="text-muted preview-title">${this.get_id_html(preview_data)}</div>
</div>
`;
}
</div>
<hr>
<div class="popover-body">
${this.get_content_html(preview_data)}
</div>
`;
if (preview_data.preview_title != preview_data.name) {
return popover_content;
}
get_id_html(preview_data) {
let id_html = '';
if (preview_data.preview_title !== preview_data.name) {
id_html = `<a class="text-muted" href=${this.href}>${preview_data.name}</a>`;
}
return id_html;
}
get_image_html(preview_data) {
let avatar_html = frappe.get_avatar(
"avatar-medium",
preview_data.preview_title,
preview_data.preview_image
);
return `<div class="preview-image">
${avatar_html}
</div>`;
}
get_content_html(preview_data) {
let content_html = '';
Object.keys(preview_data).forEach(key => {
if (!['preview_image', 'preview_title', 'name'].includes(key)) {
let value = frappe.ellipsis(preview_data[key], 280);
let label = key;
content_html += `
<div class="preview-field">
<div class='small preview-label text-muted bold'>${__(label)}</div>
<div class="small preview-value">${value}</div>
<div class="preview-label text-muted">${__(label)}</div>
<div class="preview-value">${value}</div>
</div>
`;
}
});
content_html = `<div class="preview-table">${content_html}</div>`;
let popover_content =`
<div class="preview-popover-header">${image_html}
<div class="preview-header">
<div class="preview-main">
<a class="preview-name bold" href=${this.href}>${__(preview_data.preview_title)}</a>
<span class="text-muted small">${__(this.doctype)} ${id_html}</span>
</div>
</div>
</div>
<hr>
<div class="popover-body">
${content_html}
</div>
`;
return popover_content;
return `<div class="preview-table">${content_html}</div>`;
}
};

View file

@ -20,27 +20,25 @@ frappe.avatar = function (user, css_class, title, image_url=null, remove_color=f
title = user_info.fullname;
}
return frappe.get_avatar(
user, css_class, title, image_url || user_info.image, remove_color, filterable
);
};
frappe.get_avatar = function(user, css_class, title, image_url=null, remove_color, filterable) {
let data_attr = '';
if (!css_class) {
css_class = "avatar-small";
}
if (filterable) {
css_class += " filterable";
data_attr = `data-filter="_assign,like,%${user}%"`;
}
return frappe.get_avatar(
css_class, title, image_url || user_info.image, remove_color, data_attr
);
};
frappe.get_avatar = function(css_class, title, image_url=null, remove_color, data_attributes) {
if (!css_class) {
css_class = "avatar-small";
}
if (image_url) {
const image = (window.cordova && image_url.indexOf('http') === -1) ? frappe.base_url + image_url : image_url;
return `<span class="avatar ${css_class}" title="${title}" ${data_attr}>
return `<span class="avatar ${css_class}" title="${title}" ${data_attributes}>
<span class="avatar-frame" style='background-image: url("${image}")'
title="${title}"></span>
</span>`;
@ -55,7 +53,8 @@ frappe.get_avatar = function(user, css_class, title, image_url=null, remove_colo
if (css_class === 'avatar-small' || css_class == 'avatar-xs') {
abbr = abbr.substr(0, 1);
}
return `<span class="avatar ${css_class}" title="${title}" ${data_attr}>
return `<span class="avatar ${css_class}" title="${title}" ${data_attributes}>
<div class="avatar-frame standard-image"
style="${style}">
${abbr}

View file

@ -386,21 +386,18 @@ frappe.views.CommunicationComposer = Class.extend({
},
setup_print_language: function() {
var me = this;
var doc = this.doc || cur_frm.doc;
var fields = this.dialog.fields_dict;
//Load default print language from doctype
this.lang_code = doc.language
if (this.get_print_format().default_print_language) {
var default_print_language_code = this.get_print_format().default_print_language;
me.lang_code = default_print_language_code;
} else {
var default_print_language_code = null;
if (!this.lang_code && this.get_print_format().default_print_language) {
this.lang_code = this.get_print_format().default_print_language;
}
//On selection of language retrieve language code
var me = this;
$(fields.language_sel.input).change(function(){
me.lang_code = this.value
})
@ -410,10 +407,8 @@ frappe.views.CommunicationComposer = Class.extend({
.empty()
.add_options(frappe.get_languages());
if (default_print_language_code) {
$(fields.language_sel.input).val(default_print_language_code);
} else {
$(fields.language_sel.input).val(doc.language);
if (this.lang_code) {
$(fields.language_sel.input).val(this.lang_code);
}
},
@ -440,6 +435,7 @@ frappe.views.CommunicationComposer = Class.extend({
}
},
setup_attach: function() {
var fields = this.dialog.fields_dict;
var attach = $(fields.select_attachments.wrapper);

View file

@ -172,6 +172,23 @@ frappe.views.TreeView = Class.extend({
this.post_render();
},
rebuild_tree: function() {
let me = this;
frappe.call({
"method": "frappe.utils.nestedset.rebuild_tree",
"args": {
'doctype': me.doctype,
'parent_field': "parent_"+me.doctype.toLowerCase().replace(/ /g, '_'),
},
"callback": function(r) {
if (!r.exc) {
me.make_tree();
}
}
});
},
post_render: function() {
var me = this;
me.opts.post_render && me.opts.post_render(me);
@ -368,7 +385,7 @@ frappe.views.TreeView = Class.extend({
}, "add");
}
},
set_menu_item: function(){
set_menu_item: function() {
var me = this;
this.menu_items = [
@ -393,6 +410,17 @@ frappe.views.TreeView = Class.extend({
},
];
if (frappe.user.has_role('System Manager')) {
this.menu_items.push(
{
label: __('Rebuild Tree'),
action: function() {
me.rebuild_tree();
}
}
);
}
if (me.opts.menu_items) {
me.menu_items.push.apply(me.menu_items, me.opts.menu_items)
}

View file

@ -162,3 +162,34 @@ html.firefox, html.safari {
-webkit-transform: translate(-50%, -50%);
}
.hide {
display: none !important;
}
.btn-link {
box-shadow: none !important;
outline: none;
.icon, &:hover {
text-decoration: none !important;
}
}
.hidden {
@extend .d-none;
}
.margin {
margin: var(--margin-sm);
}
.margin-top {
margin-top: var(--margin-sm);
}
.margin-bottom {
margin-bottom: var(--margin-sm);
}
.margin-left {
margin-left: var(--margin-sm);
}
.margin-right {
margin-right: var(--margin-sm);
}

View file

@ -105,10 +105,6 @@ pre {
color: var(--text-light)
}
.hide {
display: none !important;
}
.col-xs-1 { @extend .col-1; }
.col-xs-2 { @extend .col-2; }
.col-xs-3 { @extend .col-3; }
@ -150,23 +146,6 @@ footer {
float: right;
}
// .border-${position} {
// .border-{$position} {
// border-{$position}: 1px solid var(--border-color);
// }
// }
// .border-#{$position} {
// .border-#{$position} {
// border-#{$position}: 1px solid var(--border-color);
// }
// }
// @include border-(top);
// @include border-(bottom);
// @include border-(left);
// @include border-(right);
img {
max-width: 100%;
height: auto;
@ -191,9 +170,6 @@ img {
}
}
.hidden {
@extend .d-none;
}
.hide-control {
@extend .d-none;
@ -224,10 +200,6 @@ p {
font-size: var(--text-sm);
}
.fill-width {
flex: 1
}
h1 {
font-size: $font-size-3xl;
font-weight: 800;
@ -280,6 +252,7 @@ select.input-xs {
/* popover */
.popover {
background-color: var(--popover-bg);
border: 0;
}
.bold {
@ -386,31 +359,6 @@ kbd {
cursor: default;
}
.btn-link {
box-shadow: none !important;
outline: none;
.icon, &:hover {
text-decoration: none !important;
}
}
.margin {
margin: var(--margin-sm);
}
.margin-top {
margin-top: var(--margin-sm);
}
.margin-bottom {
margin-bottom: var(--margin-sm);
}
.margin-left {
margin-left: var(--margin-sm);
}
.margin-right {
margin-right: var(--margin-sm);
}
.standard-sidebar {
font-size: var(--text-base);

View file

@ -1,48 +1,61 @@
.link-preview-popover {
border-radius: 0;
max-width: 100%;
.popover-content {
padding: 0;
.preview-popover-header {
padding: var(--padding-md);
padding: var(--padding-md) 25px;
.preview-header {
display: inline-block;
vertical-align: top;
}
.preview-header {
@include flex(flex, null, center, column);
}
.preview-image {
width: 70px;
height: 70px;
object-fit: cover;
margin-right: var(--margin-sm);
border-radius: var(--border-radius);
}
.preview-image {
margin-bottom: var(--margin-sm);
.preview-name {
display: block;
}
.avatar {
width: 52px;
height: 52px;
.preview-title {
display: block;
.standard-image {
font-size: var(--text-lg);
}
}
}
hr {
margin: 0;
.preview-name {
font-size: var(--text-base);
font-weight: 500;
}
.preview-table {
padding: var(--padding-md);
padding-bottom: var(--padding-xs);
max-width: 330px;
min-width: 200px;
overflow-wrap: break-word;
.preview-title:not(:empty) {
margin-top: var(--margin-xs);
font-size: var(--text-md);
}
.preview-field {
padding-bottom: var(--padding-sm);
.preview-label {
padding-bottom: 4px;
.popover-body {
padding: 0;
.preview-table {
padding-bottom: var(--padding-xs);
max-width: 330px;
min-width: 200px;
overflow-wrap: break-word;
.preview-field {
.preview-label {
padding-bottom: 4px;
}
.preview-value {
font-weight: 500;
}
.ql-snow .ql-editor {
min-height: 0;
}
&:not(:last-child) {
margin-bottom: var(--margin-md);
}
}
}
}

View file

@ -98,7 +98,7 @@ $mark-padding: 0;
$enable-shadows: true;
$popover-border-radius: var(--border-radius);
$popover-bg: var(--popover-bg);
$popover-box-shadow: var(--shadow-md);
$popover-box-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1);
$popover-body-padding-x: var(--padding-md);
$popover-body-padding-y: var(--padding-md);
$popover-border-color: var(--dark-border-color);

View file

@ -2,14 +2,14 @@
@import 'variables';
@import 'css_variables';
@import '~bootstrap/scss/bootstrap';
@import "../common/mixins.scss";
@import "../common/global.scss";
@import "../common/icons.scss";
@import "../common/mixins";
@import "../common/global";
@import "../common/icons";
@import 'base';
@import "../common/flex";
@import "../common/buttons";
@import "../common/modal";
@import "../common/indicator.scss";
@import "../common/indicator";
@import "../common/controls";
@import 'multilevel_dropdown';
@import 'website_image';
@ -241,4 +241,4 @@ h5.modal-title {
}
.about-footer {
padding-top: 1rem;
}
}

View file

@ -47,6 +47,10 @@ $font-sizes: (
}
}
$border-radius: var(--border-radius);
$border-radius-sm: var(--border-radius-sm);
$border-radius-lg: var(--border-radius-lg);
$font-size-xs: 0.75rem !default;
$font-size-sm: 0.875rem !default;
$font-size-base: 1rem !default;

View file

@ -15,7 +15,6 @@ from frappe.utils import cint, cstr
import frappe.model.meta
import frappe.defaults
import frappe.translate
from frappe.utils.change_log import get_change_log
import redis
from six.moves.urllib.parse import unquote
from six import text_type
@ -117,6 +116,7 @@ def clear_expired_sessions():
def get():
"""get session boot info"""
from frappe.boot import get_bootinfo, get_unseen_notes
from frappe.utils.change_log import get_change_log
bootinfo = None
if not getattr(frappe.conf,'disable_session_cache', None):

View file

@ -140,7 +140,7 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}"
style="width: 12px; height: 12px; margin-top: 5px;">
<path d="M2 9.66667L5.33333 13L14 3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
{% elif df.fieldtype=="Image" %}
{% elif df.fieldtype in ("Image", "Attach Image") and frappe.utils.is_image(doc[doc.meta.get_field(df.fieldname).options]) %}
<img src="{{ doc[doc.meta.get_field(df.fieldname).options] }}"
class="img-responsive"
{%- if df.print_width %} style="width: {{ get_width(df) }};"{% endif %}>
@ -151,7 +151,7 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}"
{% elif df.fieldtype=="Signature" %}
<img src="{{ doc[df.fieldname] }}" class="signature-img img-responsive"
{%- if df.print_width %} style="width: {{ get_width(df) }};"{% endif %}>
{% elif df.fieldtype == "Attach" and doc[df.fieldname] and frappe.utils.is_image(doc[df.fieldname]) %}
{% elif df.fieldtype in ("Attach", "Attach Image") and frappe.utils.is_image(doc[df.fieldname]) %}
<img src="{{ doc[df.fieldname] }}" class="img-responsive"
{%- if df.print_width %} style="width: {{ get_width(df) }};"{% endif %}>
{% elif df.fieldtype=="HTML" %}

47
frappe/tests/test_auth.py Normal file
View file

@ -0,0 +1,47 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import time
import unittest
from frappe.auth import LoginAttemptTracker
class TestLoginAttemptTracker(unittest.TestCase):
def test_account_lock(self):
"""Make sure that account locks after `n consecutive failures
"""
tracker = LoginAttemptTracker(user_name='tester', max_consecutive_login_attempts=3, lock_interval=60)
# Clear the cache by setting attempt as success
tracker.add_success_attempt()
tracker.add_failure_attempt()
self.assertTrue(tracker.is_user_allowed())
tracker.add_failure_attempt()
self.assertTrue(tracker.is_user_allowed())
tracker.add_failure_attempt()
self.assertTrue(tracker.is_user_allowed())
tracker.add_failure_attempt()
self.assertFalse(tracker.is_user_allowed())
def test_account_unlock(self):
"""Make sure that locked account gets unlocked after lock_interval of time.
"""
lock_interval = 10 # In sec
tracker = LoginAttemptTracker(user_name='tester', max_consecutive_login_attempts=1, lock_interval=lock_interval)
# Clear the cache by setting attempt as success
tracker.add_success_attempt()
tracker.add_failure_attempt()
self.assertTrue(tracker.is_user_allowed())
tracker.add_failure_attempt()
self.assertFalse(tracker.is_user_allowed())
# Sleep for lock_interval of time, so that next request con unlock the user access.
time.sleep(lock_interval)
tracker.add_failure_attempt()
self.assertTrue(tracker.is_user_allowed())

View file

@ -17,7 +17,6 @@ from frappe.utils import cstr
import frappe, os, re, io, codecs, json
from frappe.model.utils import render_include, InvalidIncludePath
from frappe.utils import strip, strip_html_tags, is_html
from jinja2 import TemplateError
import itertools, operator
def guess_language(lang_list=None):
@ -526,6 +525,8 @@ def extract_messages_from_code(code):
:param code: code from which translatable files are to be extracted
:param is_py: include messages in triple quotes e.g. `_('''message''')`
"""
from jinja2 import TemplateError
try:
code = frappe.as_unicode(render_include(code))
except (TemplateError, ImportError, InvalidIncludePath, IOError):

View file

@ -11,12 +11,12 @@ import os
import re
import sys
import traceback
from email.header import decode_header, make_header
from email.utils import formataddr, parseaddr
from gzip import GzipFile
from typing import Generator, Iterable
import requests
from six import string_types, text_type
from six.moves.urllib.parse import quote
from werkzeug.test import Client
@ -24,7 +24,6 @@ from werkzeug.test import Client
import frappe
# utility functions like cint, int, flt, etc.
from frappe.utils.data import *
from frappe.utils.identicon import Identicon
from frappe.utils.html_utils import sanitize_html
@ -170,6 +169,8 @@ def random_string(length):
def has_gravatar(email):
'''Returns gravatar url if user has set an avatar at gravatar.com'''
import requests
if (frappe.flags.in_import
or frappe.flags.in_install
or frappe.flags.in_test):
@ -193,6 +194,8 @@ def get_gravatar_url(email):
return "https://secure.gravatar.com/avatar/{hash}?d=mm&s=200".format(hash=hashlib.md5(email.encode('utf-8')).hexdigest())
def get_gravatar(email):
from frappe.utils.identicon import Identicon
gravatar_url = has_gravatar(email)
if not gravatar_url:
@ -457,6 +460,7 @@ def get_sites(sites_path=None):
return sorted(sites)
def get_request_session(max_retries=3):
import requests
from urllib3.util import Retry
session = requests.Session()
session.mount("http://", requests.adapters.HTTPAdapter(max_retries=Retry(total=5, status_forcelist=[500])))

View file

@ -4,18 +4,10 @@
from __future__ import unicode_literals
import frappe
from dateutil.parser._parser import ParserError
import operator
import json
import re, datetime, math, time
import babel.dates
from babel.core import UnknownLocaleError
from dateutil import parser
from num2words import num2words
from six.moves import html_parser as HTMLParser
from six.moves.urllib.parse import quote, urljoin
from html2text import html2text
from markdown2 import markdown as _markdown, MarkdownError
from six import iteritems, text_type, string_types, integer_types
from frappe.desk.utils import slug
@ -34,6 +26,8 @@ def getdate(string_date=None):
Converts string date (yyyy-mm-dd) to datetime.date object.
If no input is provided, current date is returned.
"""
from dateutil import parser
from dateutil.parser._parser import ParserError
if not string_date:
return get_datetime().date()
@ -53,6 +47,8 @@ def getdate(string_date=None):
), title=frappe._('Invalid Date'))
def get_datetime(datetime_str=None):
from dateutil import parser
if datetime_str is None:
return now_datetime()
@ -74,6 +70,8 @@ def get_datetime(datetime_str=None):
return parser.parse(datetime_str)
def to_timedelta(time_str):
from dateutil import parser
if isinstance(time_str, string_types):
t = parser.parse(time_str)
return datetime.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second, microseconds=t.microsecond)
@ -83,6 +81,8 @@ def to_timedelta(time_str):
def add_to_date(date, years=0, months=0, weeks=0, days=0, hours=0, minutes=0, seconds=0, as_string=False, as_datetime=False):
"""Adds `days` to the given date"""
from dateutil import parser
from dateutil.parser._parser import ParserError
from dateutil.relativedelta import relativedelta
if date==None:
@ -262,6 +262,8 @@ def get_year_ending(date):
return add_to_date(date, days=-1)
def get_time(time_str):
from dateutil import parser
if isinstance(time_str, datetime.datetime):
return time_str.time()
elif isinstance(time_str, datetime.time):
@ -315,6 +317,8 @@ def format_date(string_date=None, format_string=None):
* mm-dd-yyyy
* dd/mm/yyyy
"""
import babel.dates
from babel.core import UnknownLocaleError
if not string_date:
return ''
@ -343,6 +347,8 @@ def format_time(time_string=None, format_string=None):
* HH:mm:ss
* HH:mm
"""
import babel.dates
from babel.core import UnknownLocaleError
if not time_string:
return ''
@ -367,6 +373,9 @@ def format_datetime(datetime_string, format_string=None):
* dd-mm-yyyy HH:mm:ss
* mm-dd-yyyy HH:mm
"""
import babel.dates
from babel.core import UnknownLocaleError
if not datetime_string:
return
@ -488,6 +497,8 @@ def get_timespan_date_range(timespan):
def global_date_format(date, format="long"):
"""returns localized date in the form of January 1, 2012"""
import babel.dates
date = getdate(date)
formatted_date = babel.dates.format_date(date, locale=(frappe.local.lang or "en").replace("-", "_"), format=format)
return formatted_date
@ -550,13 +561,13 @@ def flt(s, precision=None):
return num
def cint(s):
def cint(s, default=0):
"""Convert to integer
:param s: Number in string or other numeric format.
:returns: Converted number in python integer type.
Returns 0 if input can not be converted to integer.
Returns default if input can not be converted to integer.
Examples:
>>> cint("100")
@ -565,9 +576,10 @@ def cint(s):
0
"""
try: num = int(float(s))
except: num = 0
return num
try:
return int(float(s))
except Exception:
return default
def floor(s):
"""
@ -846,6 +858,8 @@ def in_words(integer, in_million=True):
"""
Returns string in words for the given integer.
"""
from num2words import num2words
locale = 'en_IN' if not in_million else frappe.local.lang
integer = int(integer)
try:
@ -865,7 +879,7 @@ def is_image(filepath):
from mimetypes import guess_type
# filepath can be https://example.com/bed.jpg?v=129
filepath = filepath.split('?')[0]
filepath = (filepath or "").split('?')[0]
return (guess_type(filepath)[0] or "").startswith("image/")
def get_thumbnail_base64_for_image(src):
@ -1338,6 +1352,9 @@ def strip(val, chars=None):
return (val or "").replace("\ufeff", "").replace("\u200b", "").strip(chars)
def to_markdown(html):
from html2text import html2text
from six.moves import html_parser as HTMLParser
text = None
try:
text = html2text(html or '')
@ -1347,6 +1364,8 @@ def to_markdown(html):
return text
def md_to_html(markdown_text):
from markdown2 import markdown as _markdown, MarkdownError
extras = {
'fenced-code-blocks': None,
'tables': None,
@ -1361,7 +1380,7 @@ def md_to_html(markdown_text):
html = None
try:
html = _markdown(markdown_text or '', extras=extras)
html = UnicodeWithAttrs(_markdown(markdown_text or '', extras=extras))
except MarkdownError:
pass
@ -1471,3 +1490,9 @@ def get_user_info_for_avatar(user_id):
except Exception:
frappe.local.message_log = []
return user_info
class UnicodeWithAttrs(text_type):
def __init__(self, text):
self.toc_html = text.toc_html
self.metadata = text.metadata

View file

@ -8,7 +8,6 @@ import re
import redis
import json
import os
from bs4 import BeautifulSoup
from frappe.utils import cint, strip_html_tags
from frappe.utils.html_utils import unescape_html
from frappe.model.base_document import get_controller
@ -310,6 +309,7 @@ def get_routes_to_index():
def add_route_to_global_search(route):
from bs4 import BeautifulSoup
from frappe.website.render import render_page
from frappe.utils import set_request
frappe.set_user('Guest')

View file

@ -2,12 +2,12 @@ from __future__ import unicode_literals
import frappe
import json
import re
import bleach
import bleach_whitelist.bleach_whitelist as bleach_whitelist
from six import string_types
from bs4 import BeautifulSoup
def clean_html(html):
import bleach
if not isinstance(html, string_types):
return html
@ -19,6 +19,8 @@ def clean_html(html):
strip=True, strip_comments=True)
def clean_email_html(html):
import bleach
if not isinstance(html, string_types):
return html
@ -41,6 +43,8 @@ def clean_email_html(html):
def clean_script_and_style(html):
# remove script and style
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'html5lib')
for s in soup(['script', 'style']):
s.decompose()
@ -53,6 +57,9 @@ def sanitize_html(html, linkify=False):
Does not sanitize JSON, as it could lead to future problems
"""
import bleach
from bs4 import BeautifulSoup
if not isinstance(html, string_types):
return html

View file

@ -137,10 +137,16 @@ def update_move_node(doc, parent_field):
frappe.db.sql("""update `tab{0}` set lft = -lft + %s, rgt = -rgt + %s, modified=%s
where lft < 0""".format(doc.doctype), (new_diff, new_diff, n))
@frappe.whitelist()
def rebuild_tree(doctype, parent_field):
"""
call rebuild_node for all root nodes
"""
# Check for perm if called from client-side
if frappe.request and frappe.local.form_dict.cmd == 'rebuild_tree':
frappe.only_for('System Manager')
# get all roots
frappe.db.auto_commit_on_many_writes = 1

View file

@ -17,7 +17,6 @@ from werkzeug.local import LocalProxy
from werkzeug.wsgi import wrap_file
from werkzeug.wrappers import Response
from werkzeug.exceptions import NotFound, Forbidden
from frappe.website.render import render
from frappe.utils import cint
from six import text_type
from six.moves.urllib.parse import quote
@ -150,6 +149,7 @@ def json_handler(obj):
def as_page():
"""print web page"""
from frappe.website.render import render
return render(frappe.response['route'], http_status_code=frappe.response.get("http_status_code"))
def redirect():

View file

@ -2,7 +2,6 @@
# MIT License. See license.txt
from __future__ import unicode_literals
import requests
import frappe
from frappe import _
from frappe.utils import get_request_site_address, encode
@ -77,6 +76,8 @@ class WebsiteSettings(Document):
frappe.clear_cache()
def get_access_token(self):
import requests
google_settings = frappe.get_doc("Google Settings")
if not google_settings.enable:

View file

@ -10,7 +10,6 @@ import os, mimetypes, json
import re
import six
from bs4 import BeautifulSoup
from six import iteritems
from werkzeug.wrappers import Response
from werkzeug.routing import Rule
@ -139,6 +138,8 @@ def build_response(path, data, http_status_code, headers=None):
def add_preload_headers(response):
from bs4 import BeautifulSoup
try:
preload = []
soup = BeautifulSoup(response.data, "lxml")

View file

@ -7,8 +7,6 @@ import io
import os
import re
import yaml
import frappe
from frappe.model.document import get_controller
from frappe.website.utils import can_cache, delete_page_cache, extract_comment_tag, extract_title
@ -283,6 +281,7 @@ def get_frontmatter(string):
"""
Reference: https://github.com/jonbeebe/frontmatter
"""
import yaml
fmatter = ""
body = ""

View file

@ -3,7 +3,7 @@
"scripts": {
"build": "node rollup/build.js",
"production": "FRAPPE_ENV=production node rollup/build.js",
"watch": "node rollup/watch.js",
"watch": "node --max_old_space_size=1280 rollup/watch.js",
"snyk-protect": "snyk protect",
"prepare": "yarn run snyk-protect"
},

View file

@ -961,15 +961,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000939:
version "1.0.30001116"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001116.tgz"
integrity sha512-f2lcYnmAI5Mst9+g0nkMIznFGsArRmZ0qU+dnq8l91hymdc2J3SFbiPhOJEeDqC1vtE8nc1qNQyklzB8veJefQ==
caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001111:
version "1.0.30001118"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001118.tgz#116a9a670e5264aec895207f5e918129174c6f62"
integrity sha512-RNKPLojZo74a0cP7jFMidQI7nvLER40HgNfgKQEJ2PFm225L0ectUungNQoK3Xk3StQcFbpBPNEvoWD59436Hg==
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000939, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001111:
version "1.0.30001191"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001191.tgz"
integrity sha512-xJJqzyd+7GCJXkcoBiQ1GuxEiOBCLQ0aVW9HMekifZsAVGdj5eJ4mFB9fEhSHipq9IOk/QXFJUiIr9lZT+EsGw==
caseless@~0.12.0:
version "0.12.0"