Merge branch 'develop' of https://github.com/frappe/frappe into select-field-placholder
This commit is contained in:
commit
24e2b3d2ad
49 changed files with 587 additions and 301 deletions
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
175
frappe/auth.py
175
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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
""")
|
||||
|
|
@ -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 |
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>`;
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
47
frappe/tests/test_auth.py
Normal 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())
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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])))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 = ""
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
13
yarn.lock
13
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue