Merge branch 'develop' into s3-backup-update

This commit is contained in:
Suraj Shetty 2020-05-03 12:54:38 +05:30 committed by GitHub
commit 97f6d8f209
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
264 changed files with 6597 additions and 1895 deletions

View file

@ -78,6 +78,7 @@
"has_common": true,
"has_words": true,
"validate_email": true,
"validate_name": true,
"validate_phone": true,
"get_number_format": true,
"format_number": true,

28
.github/frappe_linter/translation.py vendored Normal file
View file

@ -0,0 +1,28 @@
import re
import sys
errors_encounter = 0
pattern = re.compile(r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)")
start_pattern = re.compile(r"_{1,2}\([\"']{1,3}")
# skip first argument
files = sys.argv[1:]
for _file in files:
if not _file.endswith(('.py', '.js')):
continue
with open(_file, 'r') as f:
print(f'Checking: {_file}')
for num, line in enumerate(f, 1):
all_matches = start_pattern.finditer(line)
if all_matches:
for match in all_matches:
verify = pattern.search(line)
if not verify:
errors_encounter += 1
print(f'A syntax error has been discovered at line number: {num}')
print(f'Syntax error occurred with: {line}')
if errors_encounter > 0:
print('You can visit "https://frappe.io/docs/user/en/translations" to resolve this error.')
assert 1+1 == 3
else:
print('Good To Go!')

View file

@ -0,0 +1,22 @@
name: Frappe Linter
on:
pull_request:
branches:
- develop
- version-12-hotfix
- version-11-hotfix
jobs:
check_translation:
name: Translation Syntax Check
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- name: Setup python3
uses: actions/setup-python@v1
with:
python-version: 3.6
- name: Validating Translation Syntax
run: |
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
python $GITHUB_WORKSPACE/.github/frappe_linter/translation.py $files

View file

@ -7,14 +7,19 @@ addons:
- test_site_producer
mariadb: 10.3
postgresql: 9.5
chrome: stable
git:
depth: 1
cache:
- pip
- npm
- yarn
pip: true
npm: true
yarn: true
directories:
# we also need to cache folder with Cypress binary
# https://docs.cypress.io/guides/guides/continuous-integration.html#Caching
- ~/.cache
matrix:
include:
@ -54,10 +59,9 @@ before_install:
install:
- cd ~
- source ./.nvm/nvm.sh
- nvm install v8.10.0
- nvm install 12
- git clone https://github.com/frappe/bench --depth 1
- pip install -e ./bench
- pip install frappe-bench
- bench init frappe-bench --skip-assets --python $(which python) --frappe-path $TRAVIS_BUILD_DIR
@ -99,8 +103,7 @@ install:
- if [ $TYPE == "server" ]; then sed -i 's/socketio:/# socketio:/g' Procfile; fi
- if [ $TYPE == "server" ]; then sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile; fi
- if [ $TYPE == "ui" ]; then bench setup requirements --node; fi
- bench setup requirements --node
- bench start &
- bench --site test_site reinstall --yes
- bench --site test_site_producer reinstall --yes

View file

@ -1,5 +1,7 @@
{
"baseUrl": "http://test_site_ui:8000",
"projectId": "92odwv",
"adminPassword": "admin"
"adminPassword": "admin",
"defaultCommandTimeout": 10000,
"pageLoadTimeout": 15000
}

View file

@ -1,8 +1,4 @@
context('Depends On', () => {
beforeEach(() => {
cy.login();
return cy.new_form('Test Depends On');
});
before(() => {
cy.login();
cy.visit('/desk#workspace/Website');

View file

@ -50,7 +50,7 @@ context('FileUploader', () => {
open_upload_dialog();
cy.get_open_dialog().find('a:contains("web link")').click();
cy.get_open_dialog().find('.file-web-link input').type('https://github.com');
cy.get_open_dialog().find('.file-web-link input').type('https://github.com', { delay: 100, force: true });
cy.server();
cy.route('POST', '/api/method/upload_file').as('upload_file');
cy.get_open_dialog().find('.btn-primary').click();

View file

@ -6,14 +6,17 @@ context('Form', () => {
return frappe.call("frappe.tests.ui_test_helpers.create_contact_records");
});
});
beforeEach(() => {
cy.visit('/desk#workspace/Website');
});
it('create a new form', () => {
cy.visit('/desk#Form/ToDo/New ToDo 1');
cy.fill_field('description', 'this is a test todo', 'Text Editor').blur();
cy.get('.page-title').should('contain', 'Not Saved');
cy.server();
cy.route({
method: 'POST',
url: 'api/method/frappe.desk.form.save.savedocs'
}).as('form_save');
cy.get('.primary-action').click();
cy.wait('@form_save').its('status').should('eq', 200);
cy.visit('/desk#List/ToDo');
cy.location('hash').should('eq', '#List/ToDo/List');
cy.get('h1').should('be.visible').and('contain', 'To Do');
@ -41,4 +44,21 @@ context('Form', () => {
list_view.filter_area.filter_list.clear_filters();
});
});
it('validates behaviour of Data options validations in child table', () => {
// test email validations for set_invalid controller
let website_input = 'website.in';
let expectBackgroundColor = 'rgb(255, 220, 220)';
cy.visit('/desk#Form/Contact/New Contact 1');
cy.get('.frappe-control[data-fieldname="email_ids"]').as('table');
cy.get('@table').find('button.grid-add-row').click();
cy.get('.grid-body .rows [data-fieldname="email_id"]').click();
cy.get('@table').find('input.input-with-feedback.form-control').as('email_input');
cy.get('@email_input').type(website_input, { waitForAnimations: false });
cy.fill_field('company_name', 'Test Company');
cy.get('@email_input').should($div => {
const style = window.getComputedStyle($div[0]);
expect(style.backgroundColor).to.equal(expectBackgroundColor);
});
});
});

View file

@ -186,7 +186,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
if (fieldtype === 'Select') {
cy.get('@input').select(value);
} else {
cy.get('@input').type(value, { waitForAnimations: false });
cy.get('@input').type(value, { waitForAnimations: false, force: true });
}
return cy.get('@input');
});

View file

@ -219,7 +219,10 @@ class LoginManager:
user = frappe.db.get_value("User", filters={"username": user}, fieldname="name") or user
self.check_if_enabled(user)
self.user = self.check_password(user, pwd)
if not frappe.form_dict.get('tmp_id'):
self.user = self.check_password(user, pwd)
else:
self.user = user
def force_user_to_reset_password(self):
if not self.user:

View file

@ -3,7 +3,7 @@
{
"hidden": 0,
"label": "Tools",
"links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]"
"links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Video\",\n \"name\": \"Video\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]"
},
{
"hidden": 0,
@ -32,7 +32,7 @@
"idx": 0,
"is_standard": 1,
"label": "Tools",
"modified": "2020-04-01 11:24:40.804346",
"modified": "2020-04-20 18:21:14.152537",
"modified_by": "Administrator",
"module": "Automation",
"name": "Tools",

View file

@ -17,6 +17,7 @@ from frappe.utils.change_log import get_versions
from frappe.translate import get_lang_dict
from frappe.email.inbox import get_email_accounts
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
from frappe.social.doctype.post.post import frequently_visited_links
@ -79,6 +80,7 @@ def get_bootinfo():
bootinfo.success_action = get_success_action()
bootinfo.update(get_email_accounts(user=frappe.session.user))
bootinfo.energy_points_enabled = is_energy_point_enabled()
bootinfo.website_tracking_enabled = is_tracking_enabled()
bootinfo.points = get_energy_points(frappe.session.user)
bootinfo.frequently_visited_links = frequently_visited_links()
bootinfo.link_preview_doctypes = get_link_preview_doctypes()
@ -268,4 +270,18 @@ def get_success_action():
return frappe.get_all("Success Action", fields=["*"])
def get_link_preview_doctypes():
return [d.name for d in frappe.db.get_all('DocType', {'show_preview_popup': 1})]
from frappe.utils import cint
link_preview_doctypes = [d.name for d in frappe.db.get_all('DocType', {'show_preview_popup': 1})]
customizations = frappe.get_all("Property Setter",
fields=['doc_type', 'value'],
filters={'property': 'show_preview_popup'}
)
for custom in customizations:
if not cint(custom.value) and custom.doc_type in link_preview_doctypes:
link_preview_doctypes.remove(custom.doc_type)
else:
link_preview_doctypes.append(custom.doc_type)
return link_preview_doctypes

View file

@ -16,7 +16,7 @@ global_cache_keys = ("app_hooks", "installed_apps",
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version',
'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts',
'sitemap_routes')
'sitemap_routes', 'db_tables')
user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang",
"defaults", "user_permissions", "home_page", "linked_with",

View file

@ -1,13 +1,23 @@
from __future__ import unicode_literals, absolute_import, print_function
# imports - standard imports
import atexit
import compileall
import hashlib
import os
import re
import shutil
import sys
# imports - third party imports
import click
import hashlib, os, sys, compileall, re
# imports - module imports
import frappe
from frappe import _
from frappe.commands import pass_context, get_site
from frappe.commands import get_site, pass_context
from frappe.commands.scheduler import _is_scheduler_enabled
from frappe.installer import update_site_config
from frappe.utils import touch_file, get_site_path
from six import text_type
from frappe.utils import get_site_path, touch_file
@click.command('new-site')
@click.argument('site')
@ -68,32 +78,33 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
make_site_dirs()
installing = None
try:
installing = touch_file(get_site_path('locks', 'installing.lock'))
installing = touch_file(get_site_path('locks', 'installing.lock'))
atexit.register(_new_site_cleanup, site, mariadb_root_username, mariadb_root_password)
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password,
db_name=db_name, admin_password=admin_password, verbose=verbose,
source_sql=source_sql, force=force, reinstall=reinstall, db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port, no_mariadb_socket=no_mariadb_socket)
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, db_name=db_name,
admin_password=admin_password, verbose=verbose, source_sql=source_sql, force=force, reinstall=reinstall,
db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port, no_mariadb_socket=no_mariadb_socket)
apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
for app in apps_to_install:
_install_app(app, verbose=verbose, set_as_patched=not source_sql)
apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
for app in apps_to_install:
_install_app(app, verbose=verbose, set_as_patched=not source_sql)
os.remove(installing)
frappe.utils.scheduler.toggle_scheduler(enable_scheduler)
frappe.db.commit()
frappe.utils.scheduler.toggle_scheduler(enable_scheduler)
frappe.db.commit()
scheduler_status = "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled"
print("*** Scheduler is", scheduler_status, "***")
scheduler_status = "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled"
print("*** Scheduler is", scheduler_status, "***")
except frappe.exceptions.ImproperDBConfigurationError:
_drop_site(site, mariadb_root_username, mariadb_root_password, force=True)
def _new_site_cleanup(site, mariadb_root_username, mariadb_root_password):
installing = get_site_path('locks', 'installing.lock')
finally:
if installing and os.path.exists(installing):
os.remove(installing)
if installing and os.path.exists(installing):
if mariadb_root_password:
_drop_site(site, mariadb_root_username, mariadb_root_password, force=True)
shutil.rmtree(site)
frappe.destroy()
frappe.destroy()
@click.command('restore')
@click.argument('sql-file-path')
@ -317,10 +328,18 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non
"Backup"
from frappe.utils.backups import scheduled_backup
verbose = context.verbose
exit_code = 0
for site in context.sites:
frappe.init(site=site)
frappe.connect()
odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True)
try:
frappe.init(site=site)
frappe.connect()
odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True)
except Exception as e:
if verbose:
print("Backup failed for {0}. Database or site_config.json may be corrupted".format(site))
exit_code = 1
continue
if verbose:
from frappe.utils import now
print("database backup taken -", odb.backup_path_db, "- on", now())
@ -329,6 +348,7 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non
print("private files backup taken -", odb.backup_path_private_files, "- on", now())
frappe.destroy()
sys.exit(exit_code)
@click.command('remove-from-installed-apps')
@click.argument('app')

View file

@ -522,7 +522,7 @@ def run_ui_tests(context, app, headless=False):
password_env = 'CYPRESS_adminPassword={}'.format(admin_password) if admin_password else ''
# run for headless mode
run_or_open = 'run --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open'
run_or_open = 'run --browser chrome --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open'
command = '{site_env} {password_env} yarn run cypress {run_or_open}'
formatted_command = command.format(site_env=site_env, password_env=password_env, run_or_open=run_or_open)
frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True)

View file

@ -27,7 +27,7 @@
"idx": 0,
"is_standard": 1,
"label": "Users",
"modified": "2020-04-01 11:24:40.767676",
"modified": "2020-04-26 22:36:14.311554",
"modified_by": "Administrator",
"module": "Core",
"name": "Users",
@ -46,12 +46,12 @@
"type": "DocType"
},
{
"label": "permission-manager",
"label": "Permission Manager",
"link_to": "permission-manager",
"type": "Page"
},
{
"label": "user-profile",
"label": "User Profile",
"link_to": "user-profile",
"type": "Page"
}

View file

@ -71,7 +71,11 @@ class Exporter:
return parent_fields
def get_exportable_children_fields(self):
children = [df.options for df in self.meta.fields if df.fieldtype in table_fields]
child_table_fields = [df for df in self.meta.fields if df.fieldtype in table_fields]
if self.export_fields == "Mandatory":
child_table_fields = [df for df in child_table_fields if df.reqd]
children = [df.options for df in child_table_fields]
children_fields = []
for child in children:
children_fields += self.get_exportable_fields(child)

View file

@ -351,9 +351,9 @@ class Importer:
value = cstr(value)
# convert boolean values to 0 or 1
if df.fieldtype == "Check" and value.lower().strip() in ["t", "f", "true", "false"]:
if df.fieldtype == "Check" and value.lower().strip() in ["t", "f", "true", "false", "yes", "no", "y", "n"]:
value = value.lower().strip()
value = 1 if value in ["t", "true"] else 0
value = 1 if value in ["t", "true", "y", "yes"] else 0
if df.fieldtype in ["Int", "Check"]:
value = cint(value)
@ -610,7 +610,7 @@ class Importer:
"message": msg,
}
)
return False
return
elif df.fieldtype == "Link":
d = self.get_missing_link_field_values(df.options)
@ -643,8 +643,10 @@ class Importer:
if value in INVALID_VALUES:
value = None
value = validate_value(value, df)
if value:
if value is not None:
value = validate_value(value, df)
if value is not None:
doc[df.fieldname] = self.parse_value(value, df)
is_table = frappe.get_meta(doctype).istable

View file

@ -20,7 +20,7 @@ class TestExporter(unittest.TestCase):
e = Exporter('Web Page', export_fields='All')
csv_array = e.get_csv_array()
header = csv_array[0]
self.assertEqual(len(header), 24)
self.assertEqual(len(header), 36)
def test_exports_selected_fields(self):

View file

@ -337,7 +337,12 @@ frappe.ui.form.on('Data Import Beta', {
let message = warnings_by_row[row_number]
.map(w => {
if (w.field) {
return `<li>${w.field.label}: ${w.message}</li>`;
let label =
w.field.label +
(w.field.parent !== frm.doc.reference_doctype
? ` (${w.field.parent})`
: '');
return `<li>${label}: ${w.message}</li>`;
}
return `<li>${w.message}</li>`;
})

View file

@ -11,9 +11,9 @@
"label",
"fieldtype",
"fieldname",
"reqd",
"precision",
"length",
"reqd",
"search_index",
"in_list_view",
"in_standard_filter",
@ -453,7 +453,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2020-04-15 02:26:03.310781",
"modified": "2020-04-19 21:54:13.783908",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",

View file

@ -477,7 +477,8 @@ class DocType(Document):
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict['fields']))
if field_dict:
new_field_dicts.append(field_dict[0])
remaining_field_names.remove(fieldname)
if fieldname in remaining_field_names:
remaining_field_names.remove(fieldname)
for fieldname in remaining_field_names:
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict['fields']))
@ -498,7 +499,8 @@ class DocType(Document):
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict.get('fields', [])))
if field_dict:
new_field_dicts.append(field_dict[0])
remaining_field_names.remove(fieldname)
if fieldname in remaining_field_names:
remaining_field_names.remove(fieldname)
for fieldname in remaining_field_names:
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict.get('fields', [])))
@ -710,9 +712,10 @@ def validate_fields(meta):
if d.fieldtype == "Currency" and cint(d.width) < 100:
frappe.throw(_("Max width for type Currency is 100px in row {0}").format(d.idx))
def check_in_list_view(d):
def check_in_list_view(is_table, d):
if d.in_list_view and (d.fieldtype in not_allowed_in_list_view):
frappe.throw(_("'In List View' not allowed for type {0} in row {1}").format(d.fieldtype, d.idx))
property_label = 'In Grid View' if is_table else 'In List View'
frappe.throw(_("'{0}' not allowed for type {1} in row {2}").format(property_label, d.fieldtype, d.idx))
def check_in_global_search(d):
if d.in_global_search and d.fieldtype in no_value_fields:
@ -731,8 +734,11 @@ def validate_fields(meta):
d.default = '0'
if d.fieldtype == "Check" and d.default not in ('0', '1'):
frappe.throw(_("Default for 'Check' type of field must be either '0' or '1'"))
if d.fieldtype == "Select" and d.default and (d.default not in d.options.split("\n")):
frappe.throw(_("Default for {0} must be an option").format(d.fieldname))
if d.fieldtype == "Select" and d.default:
if not d.options:
frappe.throw(_("Options for {0} must be set before setting the default value.").format(frappe.bold(d.fieldname)))
elif d.default not in d.options.split("\n"):
frappe.throw(_("Default value for {0} must be in the list of options.").format(frappe.bold(d.fieldname)))
def check_precision(d):
if d.fieldtype in ("Currency", "Float", "Percent") and d.precision is not None and not (1 <= cint(d.precision) <= 6):
@ -893,7 +899,7 @@ def validate_fields(meta):
field.fetch_from = field.fetch_from.strip('\n').strip()
def validate_data_field_type(docfield):
if docfield.fieldtype == "Data":
if docfield.fieldtype == "Data" and not (docfield.oldfieldtype and docfield.oldfieldtype != "Data"):
if docfield.options and (docfield.options not in data_field_options):
df_str = frappe.bold(_(docfield.label))
text_str = _("{0} is an invalid Data field.").format(df_str) + "<br>" * 2 + _("Only Options allowed for Data field are:") + "<br>"
@ -901,6 +907,16 @@ def validate_fields(meta):
frappe.msgprint(text_str + df_options_str, title="Invalid Data Field", raise_exception=True)
def check_child_table_option(docfield):
if docfield.fieldtype not in ['Table MultiSelect', 'Table']: return
doctype = docfield.options
meta = frappe.get_meta(doctype)
if not meta.istable:
frappe.throw(_('Option {0} for field {1} is not a child table')
.format(frappe.bold(doctype), frappe.bold(docfield.fieldname)), title=_("Invalid Option"))
fields = meta.get("fields")
fieldname_list = [d.fieldname for d in fields]
@ -924,11 +940,12 @@ def validate_fields(meta):
check_link_table_options(meta.get("name"), d)
check_dynamic_link_options(d)
check_hidden_and_mandatory(meta.get("name"), d)
check_in_list_view(d)
check_in_list_view(meta.get('istable'), d)
check_in_global_search(d)
check_illegal_default(d)
check_unique_and_text(meta.get("name"), d)
check_illegal_depends_on_conditions(d)
check_child_table_option(d)
check_table_multiselect_option(d)
scrub_options_in_select(d)
scrub_fetch_from(d)

View file

@ -1,46 +1,45 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
from frappe import _
"""
record of files
naming for same name files: file.gif, file-1.gif, file-2.gif etc
"""
import frappe
import json
import os
from __future__ import unicode_literals
import base64
import re
import hashlib
import mimetypes
import imghdr
import io
import json
import mimetypes
import os
import re
import shutil
import zipfile
import requests
import requests.exceptions
import imghdr
from PIL import Image, ImageFile, ImageOps
from six import PY2, StringIO, string_types, text_type
from six.moves.urllib.parse import quote, unquote
from frappe.utils import get_hook_method, get_files_path, random_string, encode, cstr, call_hook_method, cint
from frappe import _
from frappe import conf
from frappe.utils.nestedset import NestedSet
import frappe
from frappe import _, conf
from frappe.model.document import Document
from frappe.utils import strip
from PIL import Image, ImageOps
from six import StringIO, string_types
from six.moves.urllib.parse import unquote, quote
from six import text_type, PY2
import zipfile
from frappe.utils import call_hook_method, cint, cstr, encode, get_files_path, get_hook_method, random_string, strip
class MaxFileSizeReachedError(frappe.ValidationError):
pass
class FolderNotEmpty(frappe.ValidationError): pass
class FolderNotEmpty(frappe.ValidationError):
pass
exclude_from_linked_with = True
ImageFile.LOAD_TRUNCATED_IMAGES = True
class File(Document):
@ -608,8 +607,7 @@ def get_local_image(file_url):
try:
image = Image.open(file_path)
except IOError:
frappe.msgprint(_("Unable to read file format for {0}").format(file_url))
raise
frappe.msgprint(_("Unable to read file format for {0}").format(file_url), raise_exception=True)
content = None
@ -698,7 +696,7 @@ def remove_file(fid=None, attached_to_doctype=None, attached_to_name=None, from_
def get_max_file_size():
return conf.get('max_file_size') or 10485760
return cint(conf.get('max_file_size')) or 10485760
def remove_all(dt, dn, from_delete=False):
@ -715,7 +713,10 @@ def has_permission(doc, ptype=None, user=None):
has_access = False
user = user or frappe.session.user
if not doc.is_private or doc.owner == user or user == 'Administrator':
if ptype == 'create':
has_access = frappe.has_permission('File', 'create', user=user)
if not doc.is_private or doc.owner in [user, 'Guest'] or user == 'Administrator':
has_access = True
if doc.attached_to_doctype and doc.attached_to_name:

View file

@ -1,4 +1,5 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:language_code",
"creation": "2014-08-22 16:12:17.249590",
@ -41,7 +42,9 @@
}
],
"icon": "fa fa-globe",
"modified": "2019-07-19 16:32:12.652550",
"in_create": 1,
"links": [],
"modified": "2020-04-16 22:11:33.066852",
"modified_by": "Administrator",
"module": "Core",
"name": "Language",

View file

@ -13,6 +13,7 @@
"field_order": [
"stopped",
"method",
"server_script",
"frequency",
"cron_format",
"last_execution",
@ -63,6 +64,14 @@
"options": "All\nHourly\nHourly Long\nDaily\nDaily Long\nWeekly\nWeekly Long\nMonthly\nMonthly Long\nCron\nYearly\nAnnual",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "server_script",
"fieldtype": "Link",
"label": "Server Script",
"options": "Server Script",
"read_only": 1,
"search_index": 1
}
],
"in_create": 1,
@ -72,7 +81,7 @@
"link_fieldname": "scheduled_job_type"
}
],
"modified": "2019-12-09 11:10:21.259929",
"modified": "2020-04-05 17:27:33.480562",
"modified_by": "Administrator",
"module": "Core",
"name": "Scheduled Job Type",

View file

@ -70,7 +70,12 @@ class ScheduledJobType(Document):
self.scheduler_log = None
try:
self.log_status('Start')
frappe.get_attr(self.method)()
if self.server_script:
script_name = frappe.db.get_value("Server Script", self.server_script)
if script_name:
frappe.get_doc('Server Script', script_name).execute_scheduled_method()
else:
frappe.get_attr(self.method)()
frappe.db.commit()
self.log_status('Complete')
except Exception:

View file

@ -2,7 +2,45 @@
// For license information, please see license.txt
frappe.ui.form.on('Server Script', {
// refresh: function(frm) {
refresh: function(frm) {
if(frm.doc.script_type === 'Scheduler Event' && !frm.doc.disabled){
frm.add_custom_button('Schedule Script', function() {
var d = new frappe.ui.Dialog({
title: "Schedule Script Execution",
fields: [
{
fieldname: "event_type",
label: __('Select Event Type'),
fieldtype: "Select",
options: "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long"
},
],
primary_action_label: __('Schedule Script'),
primary_action: () => {
d.get_primary_btn().attr('disabled', true);
var data = d.get_values();
d.hide();
if(data) {
frm.events.schedule_script(frm, data);
}
}
});
d.show();
});
}
},
schedule_script(frm, data){
frm.call({
method: "frappe.core.doctype.server_script.server_script.setup_scheduler_events",
args: {
'script_name': frm.doc.name,
'frequency': data.event_type
}
})
}
// }
});

View file

@ -22,7 +22,7 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Script Type",
"options": "DocType Event\nAPI",
"options": "DocType Event\nScheduler Event\nAPI",
"reqd": 1
},
{
@ -75,7 +75,7 @@
}
],
"links": [],
"modified": "2019-12-17 12:55:07.389775",
"modified": "2020-04-06 11:24:38.161555",
"modified_by": "Administrator",
"module": "Core",
"name": "Server Script",

View file

@ -7,6 +7,8 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils.safe_exec import safe_exec
from frappe import _
class ServerScript(Document):
@staticmethod
@ -31,3 +33,39 @@ class ServerScript(Document):
# execute event
safe_exec(self.script, None, dict(doc = doc))
def execute_scheduled_method(self):
if self.script_type == 'Scheduler Event':
safe_exec(self.script)
else:
# wrong report type!
raise frappe.DoesNotExistError
@frappe.whitelist()
def setup_scheduler_events(script_name, frequency):
method = frappe.scrub(script_name) + '_' + frequency.lower()
scheduled_script = frappe.db.get_value('Scheduled Job Type',
dict(method=method))
if not scheduled_script:
doc = frappe.get_doc(dict(
doctype = 'Scheduled Job Type',
method = method,
frequency = frequency,
server_script = script_name
))
doc.insert()
frappe.msgprint(_('Enabled scheduled execution for script {0}').format(script_name))
else:
doc = frappe.get_doc('Scheduled Job Type', scheduled_script)
doc.update(dict(
doctype = 'Scheduled Job Type',
method = method,
frequency = frequency,
server_script = script_name
))
doc.save()
frappe.msgprint(_('Scheduled execution for script {0} has updated').format(script_name))

View file

@ -66,6 +66,7 @@ def get_server_script_map():
script_map.setdefault(script.reference_doctype, {}).setdefault(script.doctype_event, []).append(script.name)
else:
script_map.setdefault('_api', {})[script.api_method] = script.name
frappe.cache().set_value('server_script_map', script_map)
return script_map
return script_map

View file

@ -97,47 +97,49 @@ frappe.ui.form.on('User', {
});
}, __("Password"));
frappe.db.get_single_value("LDAP Settings", "enabled").then((value) => {
if (value === 1 && frm.doc.name != "Administrator") {
frm.add_custom_button(__("Reset LDAP Password"), function() {
const d = new frappe.ui.Dialog({
title: __("Reset LDAP Password"),
fields: [
{
label: __("New Password"),
fieldtype: "Password",
fieldname: "new_password",
reqd: 1
},
{
label: __("Confirm New Password"),
fieldtype: "Password",
fieldname: "confirm_password",
reqd: 1
},
{
label: __("Logout All Sessions"),
fieldtype: "Check",
fieldname: "logout_sessions"
if (frappe.user.has_role("System Manager")) {
frappe.db.get_single_value("LDAP Settings", "enabled").then((value) => {
if (value === 1 && frm.doc.name != "Administrator") {
frm.add_custom_button(__("Reset LDAP Password"), function() {
const d = new frappe.ui.Dialog({
title: __("Reset LDAP Password"),
fields: [
{
label: __("New Password"),
fieldtype: "Password",
fieldname: "new_password",
reqd: 1
},
{
label: __("Confirm New Password"),
fieldtype: "Password",
fieldname: "confirm_password",
reqd: 1
},
{
label: __("Logout All Sessions"),
fieldtype: "Check",
fieldname: "logout_sessions"
}
],
primary_action: (values) => {
d.hide();
if (values.new_password !== values.confirm_password) {
frappe.throw(__("Passwords do not match!"));
}
frappe.call(
"frappe.integrations.doctype.ldap_settings.ldap_settings.reset_password", {
user: frm.doc.email,
password: values.new_password,
logout: values.logout_sessions
});
}
],
primary_action: (values) => {
d.hide();
if (values.new_password !== values.confirm_password) {
frappe.throw(__("Passwords do not match!"));
}
frappe.call(
"frappe.integrations.doctype.ldap_settings.ldap_settings.reset_password", {
user: frm.doc.email,
password: values.new_password,
logout: values.logout_sessions
});
}
});
d.show();
}, __("Password"));
}
});
});
d.show();
}, __("Password"));
}
});
}
frm.add_custom_button(__("Reset OTP Secret"), function() {
frappe.call({

View file

@ -551,6 +551,7 @@ def update_password(new_password, logout_all_sessions=0, key=None, old_password=
res = _get_user_for_update_password(key, old_password)
if res.get('message'):
frappe.local.response.http_status_code = 410
return res['message']
else:
user = res['user']
@ -718,7 +719,7 @@ def _get_user_for_update_password(key, old_password):
user = frappe.db.get_value("User", {"reset_password_key": key})
if not user:
return {
'message': _("Cannot Update: Incorrect / Expired Link.")
'message': _("The Link specified has either been used before or Invalid")
}
elif old_password:

View file

@ -9,7 +9,8 @@ import unittest
class TestUserPermission(unittest.TestCase):
def setUp(self):
frappe.db.sql("DELETE FROM `tabUser Permission` WHERE `user`='test_bulk_creation_update@example.com'")
frappe.db.sql("""DELETE FROM `tabUser Permission`
WHERE `user` in ('test_bulk_creation_update@example.com', 'test_user_perm1@example.com')""")
def test_default_user_permission_validation(self):
user = create_user('test_default_permission@example.com')
@ -20,6 +21,26 @@ class TestUserPermission(unittest.TestCase):
param = get_params(user, 'User', perm_user.name, is_default=1)
self.assertRaises(frappe.ValidationError, add_user_permissions, param)
def test_default_user_permission(self):
frappe.set_user('Administrator')
user = create_user('test_user_perm1@example.com', 'Website Manager')
for category in ['general', 'public']:
if not frappe.db.exists('Blog Category', category):
frappe.get_doc({'doctype': 'Blog Category',
'category_name': category, 'title': category}).insert()
param = get_params(user, 'Blog Category', 'general', is_default=1)
add_user_permissions(param)
param = get_params(user, 'Blog Category', 'public')
add_user_permissions(param)
frappe.set_user('test_user_perm1@example.com')
doc = frappe.new_doc("Blog Post")
self.assertEquals(doc.blog_category, 'general')
frappe.set_user('Administrator')
def test_apply_to_all(self):
''' Create User permission for User having access to all applicable Doctypes'''
user = create_user('test_bulk_creation_update@example.com')
@ -88,7 +109,7 @@ class TestUserPermission(unittest.TestCase):
self.assertIsNone(removed_applicable_second)
self.assertEquals(is_created, 1)
def create_user(email):
def create_user(email, role="System Manager"):
''' create user with role system manager '''
if frappe.db.exists('User', email):
return frappe.get_doc('User', email)
@ -96,7 +117,7 @@ def create_user(email):
user = frappe.new_doc('User')
user.email = email
user.first_name = email.split("@")[0]
user.add_roles("System Manager")
user.add_roles(role)
return user
def get_params(user, doctype, docname, is_default=0, applicable=None):

View file

@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
import unittest
class TestCSSClass(unittest.TestCase):
class TestVideo(unittest.TestCase):
pass

View file

@ -1,7 +1,7 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Web View', {
frappe.ui.form.on('Video', {
// refresh: function(frm) {
// }

View file

@ -0,0 +1,106 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:title",
"creation": "2018-10-17 05:47:13.087395",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"provider",
"url",
"column_break_4",
"publish_date",
"duration",
"section_break_7",
"description"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1,
"unique": 1
},
{
"fieldname": "provider",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Provider",
"options": "YouTube\nVimeo",
"reqd": 1
},
{
"fieldname": "url",
"fieldtype": "Data",
"in_list_view": 1,
"label": "URL",
"reqd": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "publish_date",
"fieldtype": "Date",
"label": "Publish Date"
},
{
"fieldname": "duration",
"fieldtype": "Data",
"label": "Duration"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break"
},
{
"fieldname": "description",
"fieldtype": "Text Editor",
"in_list_view": 1,
"label": "Description",
"reqd": 1
}
],
"links": [],
"modified": "2020-04-22 12:09:49.057403",
"modified_by": "Administrator",
"module": "Core",
"name": "Video",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class CSSClass(Document):
class Video(Document):
pass

View file

@ -28,6 +28,7 @@ def get_info(show_failed=False):
if j.kwargs.get('site')==frappe.local.site:
jobs.append({
'job_name': j.kwargs.get('kwargs', {}).get('playbook_method') \
or j.kwargs.get('kwargs', {}).get('job_type') \
or str(j.kwargs.get('job_name')),
'status': j.get_status(), 'queue': name,
'creation': format_datetime(convert_utc_to_user_timezone(j.created_at)),

View file

@ -76,7 +76,16 @@ class Dashboard {
}
refresh() {
this.get_permitted_dashboard_charts().then(charts => {
frappe.run_serially([
() => this.render_cards(),
() => this.render_charts()
]);
}
render_charts() {
return this.get_permitted_items(
'frappe.desk.doctype.dashboard.dashboard.get_permitted_charts'
).then(charts => {
if (!charts.length) {
frappe.msgprint(__('No Permitted Charts on this Dashboard'), __('No Permitted Charts'))
}
@ -92,6 +101,7 @@ class Dashboard {
...chart
}
});
this.chart_group = new frappe.widget.WidgetGroup({
title: null,
container: this.container,
@ -110,14 +120,46 @@ class Dashboard {
});
}
get_permitted_dashboard_charts() {
render_cards() {
return this.get_permitted_items(
'frappe.desk.doctype.dashboard.dashboard.get_permitted_cards'
).then(cards => {
if (!cards.length) {
return;
}
this.number_cards =
cards.map(card => {
return {
name: card.card,
};
});
this.number_card_group = new frappe.widget.WidgetGroup({
container: this.container,
type: "number_card",
columns: 3,
options: {
allow_sorting: false,
allow_create: false,
allow_delete: false,
allow_hiding: false,
allow_edit: false,
},
widgets: this.number_cards,
});
});
}
get_permitted_items(method) {
return frappe.xcall(
'frappe.desk.doctype.dashboard.dashboard.get_permitted_charts',
method,
{
dashboard_name: this.dashboard_name
}).then(charts => {
return charts;
});
}
).then(items => {
return items;
});
}
set_dropdown() {

View file

@ -45,7 +45,7 @@ frappe.PermissionEngine = Class.extend({
setup_page: function() {
var me = this;
this.doctype_select
= this.wrapper.page.add_select(__("Document Types"),
= this.wrapper.page.add_select(__("Document Type"),
[{value: "", label: __("Select Document Type")+"..."}].concat(this.options.doctypes))
.change(function() {
frappe.set_route("permission-manager", $(this).val());

View file

@ -41,6 +41,7 @@
"in_list_view",
"in_standard_filter",
"in_global_search",
"in_preview",
"bold",
"report_hide",
"search_index",
@ -371,12 +372,18 @@
"fieldname": "allow_in_quick_entry",
"fieldtype": "Check",
"label": "Allow in Quick Entry"
},
{
"default": "0",
"fieldname": "in_preview",
"fieldtype": "Check",
"label": "In Preview"
}
],
"icon": "fa fa-glass",
"idx": 1,
"links": [],
"modified": "2020-03-16 14:52:43.954709",
"modified": "2020-04-10 11:57:10.392218",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",

View file

@ -20,6 +20,7 @@
"track_views",
"allow_auto_repeat",
"allow_import",
"show_preview_popup",
"image_view",
"column_break_5",
"title_field",
@ -203,6 +204,12 @@
"depends_on": "doc_type",
"fieldname": "section_break_23",
"fieldtype": "Section Break"
},
{
"default": "0",
"fieldname": "show_preview_popup",
"fieldtype": "Check",
"label": "Show Preview Popup"
}
],
"hide_toolbar": 1,
@ -210,7 +217,7 @@
"idx": 1,
"issingle": 1,
"links": [],
"modified": "2020-03-27 15:06:35.443861",
"modified": "2020-04-10 12:16:01.320411",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",

View file

@ -32,6 +32,7 @@ doctype_properties = {
'track_views': 'Check',
'allow_auto_repeat': 'Check',
'allow_import': 'Check',
'show_preview_popup': 'Check',
'email_append_to': 'Check',
'subject_field': 'Data',
'sender_field': 'Data'
@ -53,6 +54,7 @@ docfield_properties = {
'in_list_view': 'Check',
'in_standard_filter': 'Check',
'in_global_search': 'Check',
'in_preview': 'Check',
'bold': 'Check',
'hidden': 'Check',
'collapsible': 'Check',

View file

@ -16,6 +16,7 @@
"in_list_view",
"in_standard_filter",
"in_global_search",
"in_preview",
"bold",
"allow_in_quick_entry",
"translatable",
@ -381,12 +382,18 @@
"fieldtype": "Code",
"label": "Read Only Depends On",
"options": "JS"
},
{
"default": "0",
"fieldname": "in_preview",
"fieldtype": "Check",
"label": "In Preview"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2020-04-15 02:26:59.673750",
"modified": "2020-04-10 11:58:44.573537",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",

View file

@ -124,6 +124,8 @@ class Database(object):
# in transaction validations
self.check_transaction_status(query)
self.clear_db_table_cache(query)
# autocommit
if auto_commit: self.commit()
@ -277,6 +279,11 @@ class Database(object):
ret.append(frappe._dict(zip(keys, values)))
return ret
@staticmethod
def clear_db_table_cache(query):
if query and query.strip().split()[0].lower() in {'drop', 'create'}:
frappe.cache().delete_key('db_tables')
@staticmethod
def needs_formatting(result, formatted):
"""Returns true if the first row in the result has a Date, Datetime, Long Int."""
@ -769,7 +776,16 @@ class Database(object):
return ("tab" + doctype) in self.get_tables()
def get_tables(self):
return [d[0] for d in self.sql("select table_name from information_schema.tables where table_schema not in ('pg_catalog', 'information_schema')")]
tables = frappe.cache().get_value('db_tables')
if not tables:
table_rows = self.sql("""
SELECT table_name
FROM information_schema.tables
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
""")
tables = {d[0] for d in table_rows}
frappe.cache().set_value('db_tables', tables)
return tables
def a_row_exists(self, doctype):
"""Returns True if atleast one row exists."""

View file

@ -137,16 +137,14 @@ class DBTable:
if frappe.db.is_missing_column(e):
# Unknown column 'column_name' in 'field list'
continue
else:
raise
raise
if max_length and max_length[0][0] and max_length[0][0] > new_length:
if col.fieldname in self.columns:
self.columns[col.fieldname].length = current_length
frappe.msgprint(_("""Reverting length to {0} for '{1}' in '{2}';
Setting the length as {3} will cause truncation of data.""")
.format(current_length, col.fieldname, self.doctype, new_length))
info_message = _("Reverting length to {0} for '{1}' in '{2}'. Setting the length as {3} will cause truncation of data.") \
.format(current_length, col.fieldname, self.doctype, new_length)
frappe.msgprint(info_message)
def is_new(self):
return self.table_name not in frappe.db.get_tables()

View file

@ -4,5 +4,21 @@
frappe.ui.form.on('Dashboard', {
refresh: function(frm) {
frm.add_custom_button(__("Show Dashboard"), () => frappe.set_route('dashboard', frm.doc.name));
frm.set_query("chart", "charts", function() {
return {
filters: {
is_public: 1
}
};
});
frm.set_query("card", "cards", function() {
return {
filters: {
is_public: 1
}
};
});
}
});

View file

@ -8,7 +8,8 @@
"field_order": [
"dashboard_name",
"is_default",
"charts"
"charts",
"cards"
],
"fields": [
{
@ -31,10 +32,16 @@
"label": "Charts",
"options": "Dashboard Chart Link",
"reqd": 1
},
{
"fieldname": "cards",
"fieldtype": "Table",
"label": "Cards",
"options": "Number Card Link"
}
],
"links": [],
"modified": "2020-03-25 21:09:37.080132",
"modified": "2020-04-19 17:44:36.237163",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard",

View file

@ -21,3 +21,12 @@ def get_permitted_charts(dashboard_name):
if frappe.has_permission('Dashboard Chart', doc=chart.chart):
permitted_charts.append(chart)
return permitted_charts
@frappe.whitelist()
def get_permitted_cards(dashboard_name):
permitted_cards = []
dashboard = frappe.get_doc('Dashboard', dashboard_name)
for card in dashboard.cards:
if frappe.has_permission('Number Card', doc=card.card):
permitted_cards.append(card)
return permitted_cards

View file

@ -9,6 +9,7 @@ frappe.ui.form.on('Dashboard Chart', {
frm.add_fetch('source', 'timeseries', 'timeseries');
},
refresh: function(frm) {
frm.chart_filters = null;
frm.add_custom_button('Add Chart to Dashboard', () => {
@ -59,6 +60,10 @@ frappe.ui.form.on('Dashboard Chart', {
if (frm.doc.report_name) {
frm.trigger('set_chart_report_filters');
}
if (!frappe.boot.developer_mode) {
frm.set_df_property("custom_options", "hidden", 1);
}
},
source: function(frm) {

View file

@ -22,6 +22,7 @@
"aggregate_function_based_on",
"number_of_groups",
"column_break_6",
"is_public",
"timespan",
"from_date",
"to_date",
@ -33,6 +34,7 @@
"type",
"column_break_2",
"color",
"custom_options",
"section_break_10",
"last_synced_on"
],
@ -98,7 +100,7 @@
},
{
"default": "0",
"depends_on": "eval:doc.chart_type !== 'Group By'",
"depends_on": "eval: ['Count', 'Sum', 'Average'].includes(doc.chart_type)",
"fieldname": "timeseries",
"fieldtype": "Check",
"label": "Time Series"
@ -124,7 +126,7 @@
"fieldname": "type",
"fieldtype": "Select",
"label": "Type",
"options": "Line\nBar\nPercentage\nPie",
"options": "Line\nBar\nPercentage\nPie\nDonut",
"reqd": 1
},
{
@ -213,10 +215,23 @@
"label": "Y Axis",
"mandatory_depends_on": "eval:doc.report_name && !doc.is_custom",
"options": "Dashboard Chart Field"
},
{
"description": "Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"]",
"fieldname": "custom_options",
"fieldtype": "Code",
"label": "Custom Options"
},
{
"default": "0",
"description": "This chart will be available to all Users if this is set",
"fieldname": "is_public",
"fieldtype": "Check",
"label": "Is Public"
}
],
"links": [],
"modified": "2020-04-08 18:54:36.739183",
"modified": "2020-05-01 15:22:59.119341",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",
@ -247,6 +262,7 @@
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,

View file

@ -76,10 +76,10 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
if to_date and len(to_date):
to_date = get_datetime(to_date)
else:
to_date = chart.to_date
to_date = get_datetime(chart.to_date)
timegrain = time_interval or chart.time_interval
filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json)
filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json) or []
# don't include cancelled documents
filters.append([chart.document_type, 'docstatus', '<', 2, False])
@ -92,22 +92,33 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
return chart_config
@frappe.whitelist()
def create_report_chart(args):
def create_dashboard_chart(args):
args = frappe.parse_json(args)
_doc = frappe.new_doc('Dashboard Chart')
doc = frappe.new_doc('Dashboard Chart')
doc.update(args)
if args.get('custom_options'):
doc.custom_options = json.dumps(args.get('custom_options'))
_doc.update(args)
if frappe.db.exists('Dashboard Chart', args.chart_name):
args.chart_name = append_number_if_name_exists('Dashboard Chart', args.chart_name)
_doc.chart_name = args.chart_name
_doc.insert(ignore_permissions=True)
doc.chart_name = args.chart_name
doc.insert(ignore_permissions=True)
return doc
@frappe.whitelist()
def create_report_chart(args):
create_dashboard_chart(args)
args = frappe.parse_json(args)
if args.dashboard:
add_chart_to_dashboard(json.dumps(args))
@frappe.whitelist()
def add_chart_to_dashboard(args):
args = frappe.parse_json(args)
dashboard = frappe.get_doc('Dashboard', args.dashboard)
dashboard_link = frappe.new_doc('Dashboard Chart Link')
dashboard_link.chart = args.chart_name
@ -351,6 +362,13 @@ def get_year_ending(date):
# last day of this month
return add_to_date(date, days=-1)
def get_charts_for_user(doctype, txt, searchfield, start, page_len, filters):
or_filters = {'owner': frappe.session.user, 'is_public': 1}
return frappe.db.get_list('Dashboard Chart',
fields=['name'],
filters=filters,
or_filters=or_filters,
as_list = 1)
class DashboardChart(Document):
@ -362,6 +380,8 @@ class DashboardChart(Document):
self.check_required_field()
self.check_document_type()
self.validate_custom_options()
def check_required_field(self):
if not self.document_type:
frappe.throw(_("Document type is required to create a dashboard chart"))
@ -378,3 +398,10 @@ class DashboardChart(Document):
def check_document_type(self):
if frappe.get_meta(self.document_type).issingle:
frappe.throw("You cannot create a dashboard chart from single DocTypes")
def validate_custom_options(self):
if self.custom_options:
try:
json.loads(self.custom_options)
except ValueError as error:
frappe.throw("Invalid json added in the custom options: %s" % error)

View file

@ -0,0 +1,119 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Number Card', {
refresh: function(frm) {
frm.set_df_property("filters_section", "hidden", 1);
frm.trigger('set_options');
frm.trigger('render_filters_table');
},
document_type: function(frm) {
frm.set_query('document_type', function() {
return {
filters: {
'issingle': false
}
};
});
frm.set_value('filters_json', '[]');
frm.set_value('aggregate_function_based_on', '');
frm.trigger('set_options');
},
set_options: function(frm) {
let aggregate_based_on_fields = [];
const doctype = frm.doc.document_type;
if (doctype) {
frappe.model.with_doctype(doctype, () => {
frappe.get_meta(doctype).fields.map(df => {
if (frappe.model.numeric_fieldtypes.includes(df.fieldtype)) {
if (df.fieldtype == 'Currency') {
if (!df.options || df.options !== 'Company:company:default_currency') {
return;
}
}
aggregate_based_on_fields.push({label: df.label, value: df.fieldname});
}
});
frm.set_df_property('aggregate_function_based_on', 'options', aggregate_based_on_fields);
});
}
},
render_filters_table: function(frm) {
frm.set_df_property("filters_section", "hidden", 0);
let wrapper = $(frm.get_field('filters_json').wrapper).empty();
frm.filter_table = $(`<table class="table table-bordered" style="cursor:pointer; margin:0px;">
<thead>
<tr>
<th style="width: 33%">${__('Filter')}</th>
<th style="width: 33%">${__('Condition')}</th>
<th>${__('Value')}</th>
</tr>
</thead>
<tbody></tbody>
</table>`).appendTo(wrapper);
frm.filters = JSON.parse(frm.doc.filters_json || '[]');
frm.trigger('set_filters_in_table');
frm.filter_table.on('click', () => {
let dialog = new frappe.ui.Dialog({
title: __('Set Filters'),
fields: [{
fieldtype: 'HTML',
fieldname: 'filter_area',
}],
primary_action: function() {
let values = this.get_values();
if (values) {
this.hide();
frm.filters = frm.filter_group.get_filters();
frm.set_value('filters_json', JSON.stringify(frm.filters));
frm.trigger('set_filters_in_table');
}
},
primary_action_label: "Set"
});
frappe.dashboards.filters_dialog = dialog;
frm.filter_group = new frappe.ui.FilterGroup({
parent: dialog.get_field('filter_area').$wrapper,
doctype: frm.doc.document_type,
on_change: () => {},
});
frm.filter_group.add_filters_to_filter_group(frm.filters);
dialog.show();
dialog.set_values(frm.filters);
});
},
set_filters_in_table: function(frm) {
if (!frm.filters.length) {
const filter_row = $(`<tr><td colspan="3" class="text-muted text-center">
${__("Click to Set Filters")}</td></tr>`);
frm.filter_table.find('tbody').html(filter_row);
} else {
let filter_rows = '';
frm.filters.forEach(filter => {
filter_rows +=
`<tr>
<td>${filter[1]}</td>
<td>${filter[2] || ""}</td>
<td>${filter[3]}</td>
</tr>`;
});
frm.filter_table.find('tbody').html(filter_rows);
}
}
});

View file

@ -0,0 +1,147 @@
{
"actions": [],
"autoname": "CARD.#####",
"creation": "2020-04-15 18:06:39.444683",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"label",
"function",
"aggregate_function_based_on",
"column_break_2",
"document_type",
"is_public",
"stats_section",
"show_percentage_stats",
"stats_time_interval",
"filters_section",
"filters_json",
"color"
],
"fields": [
{
"fieldname": "document_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Document Type",
"options": "DocType",
"reqd": 1
},
{
"depends_on": "eval: doc.document_type",
"fieldname": "function",
"fieldtype": "Select",
"label": "Function",
"options": "Count\nSum\nAverage\nMinimum\nMaximum",
"reqd": 1
},
{
"depends_on": "eval: doc.function !== 'Count'",
"fieldname": "aggregate_function_based_on",
"fieldtype": "Select",
"label": "Aggregate Function Based On",
"mandatory_depends_on": "eval: doc.function !== 'Count'"
},
{
"fieldname": "filters_json",
"fieldtype": "Code",
"label": "Filters JSON",
"options": "JSON"
},
{
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label",
"reqd": 1
},
{
"fieldname": "color",
"fieldtype": "Color",
"label": "Color"
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "filters_section",
"fieldtype": "Section Break",
"label": "Filters Section"
},
{
"default": "0",
"description": "This card will be available to all Users if this is set",
"fieldname": "is_public",
"fieldtype": "Check",
"label": "Is Public"
},
{
"default": "1",
"fieldname": "show_percentage_stats",
"fieldtype": "Check",
"label": "Show Percentage Stats"
},
{
"default": "Daily",
"depends_on": "eval: doc.show_percentage_stats",
"description": "Show percentage difference according to this time interval",
"fieldname": "stats_time_interval",
"fieldtype": "Select",
"label": "Stats Time Interval",
"options": "Daily\nWeekly\nMonthly\nYearly"
},
{
"fieldname": "stats_section",
"fieldtype": "Section Break",
"label": "Stats"
}
],
"links": [],
"modified": "2020-05-01 15:23:29.550243",
"modified_by": "Administrator",
"module": "Desk",
"name": "Number Card",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Dashboard Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1
}
],
"search_fields": "label, document_type",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "label",
"track_changes": 1
}

View file

@ -0,0 +1,144 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils import cint
class NumberCard(Document):
pass
def get_permission_query_conditions(user=None):
if not user:
user = frappe.session.user
if user == 'Administrator':
return
roles = frappe.get_roles(user)
if "System Manager" in roles:
return None
allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read())
return '''
`tabNumber Card`.`document_type` in {allowed_doctypes}
'''.format(
allowed_doctypes=allowed_doctypes,
)
def has_permission(doc, ptype, user):
roles = frappe.get_roles(user)
if "System Manager" in roles:
return True
allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read())
if doc.document_type in allowed_doctypes:
return True
return False
@frappe.whitelist()
def get_result(doc, to_date=None):
doc = frappe.parse_json(doc)
fields = []
sql_function_map = {
'Count': 'count',
'Sum': 'sum',
'Average': 'avg',
'Minimum': 'min',
'Maximum': 'max'
}
function = sql_function_map[doc.function]
if function == 'count':
fields = ['{function}(*) as result'.format(function=function)]
else:
fields = ['{function}({based_on}) as result'.format(function=function, based_on=doc.aggregate_function_based_on)]
filters = frappe.parse_json(doc.filters_json)
if to_date:
filters.append([doc.document_type, 'creation', '<', to_date, False])
res = frappe.db.get_all(doc.document_type, fields=fields, filters=filters)
number = res[0]['result'] if res else 0
return cint(number)
@frappe.whitelist()
def get_percentage_difference(doc, result):
doc = frappe.parse_json(doc)
result = frappe.parse_json(result)
doc = frappe.get_doc('Number Card', doc.name)
if not doc.get('show_percentage_stats'):
return
previous_result = calculate_previous_result(doc)
difference = (result - previous_result)/100.0
return difference
def calculate_previous_result(doc):
from frappe.utils import add_to_date
current_date = frappe.utils.now()
if doc.stats_time_interval == 'Daily':
previous_date = add_to_date(current_date, days=-1)
elif doc.stats_time_interval == 'Weekly':
previous_date = add_to_date(current_date, weeks=-1)
elif doc.stats_time_interval == 'Monthly':
previous_date = add_to_date(current_date, months=-1)
else:
previous_date = add_to_date(current_date, years=-1)
number = get_result(doc, previous_date)
return number
@frappe.whitelist()
def create_number_card(args):
args = frappe.parse_json(args)
doc = frappe.new_doc('Number Card')
doc.update(args)
doc.insert(ignore_permissions=True)
return doc
def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters):
meta = frappe.get_meta(doctype)
searchfields = meta.get_search_fields()
search_conditions = []
if txt:
for field in searchfields:
search_conditions.append('`tab{doctype}`.`{field}` like %(txt)s'.format(field=field, doctype=doctype, txt=txt))
search_conditions = ' or '.join(search_conditions)
search_conditions = 'and (' + search_conditions +')' if search_conditions else ''
conditions, values = frappe.db.build_conditions(filters)
values['txt'] = '%' + txt + '%'
return frappe.db.sql(
'''select
`tabNumber Card`.name, `tabNumber Card`.label, `tabNumber Card`.document_type
from
`tabNumber Card`
where
{conditions} and
(`tabNumber Card`.owner = '{user}' or
`tabNumber Card`.is_public = 1)
{search_conditions}
'''.format(
filters=filters,
user=frappe.session.user,
search_conditions=search_conditions,
conditions=conditions
), values)

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestNumberCard(unittest.TestCase):
pass

View file

@ -0,0 +1,31 @@
{
"actions": [],
"creation": "2020-04-19 17:43:50.858343",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"card"
],
"fields": [
{
"fieldname": "card",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Card",
"options": "Number Card"
}
],
"istable": 1,
"links": [],
"modified": "2020-04-19 17:45:11.878472",
"modified_by": "Administrator",
"module": "Desk",
"name": "Number Card Link",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class NumberCardLink(Document):
pass

View file

@ -196,8 +196,6 @@ class FormMeta(Meta):
self.get("__messages").update(messages, as_value=True)
def load_dashboard(self):
if self.custom:
return
self.set('__dashboard', self.get_dashboard_data())
def load_kanban_meta(self):

View file

@ -268,8 +268,9 @@ def get_open_count(doctype, name, items=[]):
"count": out,
}
module = frappe.get_meta_module(doctype)
if hasattr(module, "get_timeline_data"):
out["timeline_data"] = module.get_timeline_data(doctype, name)
if not meta.custom:
module = frappe.get_meta_module(doctype)
if hasattr(module, "get_timeline_data"):
out["timeline_data"] = module.get_timeline_data(doctype, name)
return out

View file

@ -242,7 +242,7 @@ def get_prepared_report_result(report, filters, dn="", user=None):
columns = json.loads(doc.columns) if doc.columns else data[0]
for column in columns:
if isinstance(column, dict):
if isinstance(column, dict) and column.get("label"):
column["label"] = _(column["label"])
latest_report_data = {
@ -299,6 +299,7 @@ def export_query():
_("You can try changing the filters of your report."))
return
data.columns = [col for col in data.columns if isinstance(col, dict) and not col.get('hidden')]
columns = get_columns_dict(data.columns)
from frappe.utils.xlsxutils import make_xlsx
@ -310,7 +311,7 @@ def export_query():
frappe.response['type'] = 'binary'
def build_xlsx_data(columns, data, visible_idx,include_indentation):
def build_xlsx_data(columns, data, visible_idx, include_indentation):
result = [[]]
# add column headings

View file

@ -10,7 +10,7 @@ import socket
import time
from frappe import _
from frappe.model.document import Document
from frappe.utils import validate_email_address, cint, get_datetime, DATE_FORMAT, strip, comma_or, sanitize_html
from frappe.utils import validate_email_address, cint, get_datetime, DATE_FORMAT, strip, comma_or, sanitize_html, add_days
from frappe.utils.user import is_system_user
from frappe.utils.jinja import render_template
from frappe.email.smtp import SMTPServer
@ -533,28 +533,37 @@ class EmailAccount(Document):
parent = None
in_reply_to = (email.mail.get("In-Reply-To") or "").strip(" <>")
if in_reply_to and "@{0}".format(frappe.local.site) in in_reply_to:
# reply to a communication sent from the system
email_queue = frappe.db.get_value('Email Queue', dict(message_id=in_reply_to), ['communication','reference_doctype', 'reference_name'])
if email_queue:
parent_communication, parent_doctype, parent_name = email_queue
if parent_communication:
communication.in_reply_to = parent_communication
if in_reply_to:
if "@{0}".format(frappe.local.site) in in_reply_to:
# reply to a communication sent from the system
email_queue = frappe.db.get_value('Email Queue', dict(message_id=in_reply_to), ['communication','reference_doctype', 'reference_name'])
if email_queue:
parent_communication, parent_doctype, parent_name = email_queue
if parent_communication:
communication.in_reply_to = parent_communication
else:
reference, domain = in_reply_to.split("@", 1)
parent_doctype, parent_name = 'Communication', reference
if frappe.db.exists(parent_doctype, parent_name):
parent = frappe._dict(doctype=parent_doctype, name=parent_name)
# set in_reply_to of current communication
if parent_doctype=='Communication':
# communication.in_reply_to = email_queue.communication
if parent.reference_name:
# the true parent is the communication parent
parent = frappe.get_doc(parent.reference_doctype,
parent.reference_name)
else:
reference, domain = in_reply_to.split("@", 1)
parent_doctype, parent_name = 'Communication', reference
if frappe.db.exists(parent_doctype, parent_name):
parent = frappe._dict(doctype=parent_doctype, name=parent_name)
# set in_reply_to of current communication
if parent_doctype=='Communication':
# communication.in_reply_to = email_queue.communication
if parent.reference_name:
# the true parent is the communication parent
parent = frappe.get_doc(parent.reference_doctype,
parent.reference_name)
comm = frappe.db.get_value('Communication',
dict(
message_id=in_reply_to,
creation=['>=', add_days(get_datetime(), -30)]),
['reference_doctype', 'reference_name'], as_dict=1)
if comm:
parent = frappe._dict(doctype=comm.reference_doctype, name=comm.reference_name)
return parent

View file

@ -39,7 +39,7 @@ class EmailDomain(Document):
except Exception:
frappe.throw(_("Incoming email account not correct"))
return None
finally:
try:
if self.use_imap:
@ -48,9 +48,10 @@ class EmailDomain(Document):
test.quit()
except Exception:
pass
try:
if self.use_ssl_for_outgoing:
if not self.smtp_port:
if self.get('use_ssl_for_outgoing'):
if not self.get('smtp_port'):
self.smtp_port = 465
sess = smtplib.SMTP_SSL((self.smtp_server or "").encode('utf-8'),
@ -62,28 +63,15 @@ class EmailDomain(Document):
sess.quit()
except Exception:
frappe.throw(_("Outgoing email account not correct"))
return None
return
def on_update(self):
"""update all email accounts using this domain"""
for email_account in frappe.get_all("Email Account",
filters={"domain": self.name}):
for email_account in frappe.get_all("Email Account", filters={"domain": self.name}):
try:
email_account = frappe.get_doc("Email Account",
email_account.name)
email_account.set("email_server",self.email_server)
email_account.set("use_imap",self.use_imap)
email_account.set("use_ssl",self.use_ssl)
email_account.set("use_tls",self.use_tls)
email_account.set("attachment_limit",self.attachment_limit)
email_account.set("smtp_server",self.smtp_server)
email_account.set("smtp_port",self.smtp_port)
email_account.set("use_ssl_for_outgoing", self.use_ssl_for_outgoing)
email_account.set("append_emails_to_sent_folder", self.append_emails_to_sent_folder)
email_account = frappe.get_doc("Email Account", email_account.name)
for attr in ["email_server", "use_imap", "use_ssl", "use_tls", "attachment_limit", "smtp_server", "smtp_port", "use_ssl_for_outgoing", "append_emails_to_sent_folder"]:
email_account.set(attr, self.get(attr, default=0))
email_account.save()
except Exception as e:
frappe.msgprint(email_account.name)
frappe.throw(e)
return None
frappe.msgprint(_("Error has occurred in {0}").format(email_account.name), raise_exception=e.__class__)

View file

@ -6,7 +6,7 @@ import frappe
import sys
from six.moves import html_parser as HTMLParser
import smtplib, quopri, json
from frappe import msgprint, _, safe_decode, safe_encode
from frappe import msgprint, _, safe_decode, safe_encode, enqueue
from frappe.email.smtp import SMTPServer, get_outgoing_email_account
from frappe.email.email_body import get_email, get_formatted_html, add_attachment
from frappe.utils.verified_command import get_signed_params, verify_request
@ -347,8 +347,20 @@ def flush(from_test=False):
if not smtpserver:
smtpserver = SMTPServer()
smtpserver_dict[email.sender] = smtpserver
send_one(email.name, smtpserver, auto_commit, from_test=from_test)
if from_test:
send_one(email.name, smtpserver, auto_commit)
else:
send_one_args = {
'email': email.name,
'smtpserver': smtpserver,
'auto_commit': auto_commit,
}
enqueue(
method = 'frappe.email.queue.send_one',
queue = 'short',
**send_one_args
)
# NOTE: removing commit here because we pass auto_commit
# finally:
@ -366,7 +378,7 @@ def get_queue():
limit 500''', { 'now': now_datetime() }, as_dict=True)
def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=False):
def send_one(email, smtpserver=None, auto_commit=True, now=False):
'''Send Email Queue with given smtpserver'''
email = frappe.db.sql('''select
@ -377,8 +389,13 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals
`tabEmail Queue`
where
name=%s
for update''', email, as_dict=True)[0]
for update''', email, as_dict=True)
if len(email):
email = email[0]
else:
return
recipients_list = frappe.db.sql('''select name, recipient, status from
`tabEmail Queue Recipient` where parent=%s''', email.name, as_dict=1)

View file

@ -78,6 +78,7 @@ class TimestampMismatchError(ValidationError): pass
class EmptyTableError(ValidationError): pass
class LinkExistsError(ValidationError): pass
class InvalidEmailAddressError(ValidationError): pass
class InvalidNameError(ValidationError): pass
class InvalidPhoneNumberError(ValidationError): pass
class TemplateNotFoundError(ValidationError): pass
class UniqueValidationError(ValidationError): pass
@ -95,4 +96,4 @@ class DataTooLongException(ValidationError): pass
# OAuth exceptions
class InvalidAuthorizationHeader(CSRFTokenError): pass
class InvalidAuthorizationPrefix(CSRFTokenError): pass
class InvalidAuthorizationToken(CSRFTokenError): pass
class InvalidAuthorizationToken(CSRFTokenError): pass

View file

@ -14,6 +14,12 @@ from frappe.core.doctype.server_script.server_script_utils import run_server_scr
from werkzeug.wrappers import Response
from six import string_types
ALLOWED_MIMETYPES = ('image/png', 'image/jpeg', 'application/pdf', 'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.spreadsheet')
def handle():
"""handle request"""
validate_auth()
@ -148,12 +154,14 @@ def uploadfile():
@frappe.whitelist(allow_guest=True)
def upload_file():
user = None
if frappe.session.user == 'Guest':
if frappe.get_system_settings('allow_guests_to_upload_files'):
ignore_permissions = True
else:
return
else:
user = frappe.get_doc("User", frappe.session.user)
ignore_permissions = False
files = frappe.request.files
@ -175,11 +183,11 @@ def upload_file():
frappe.local.uploaded_file = content
frappe.local.uploaded_filename = filename
if frappe.session.user == 'Guest':
if frappe.session.user == 'Guest' or (user and not user.has_desk_access()):
import mimetypes
filetype = mimetypes.guess_type(filename)[0]
if filetype not in ['image/png', 'image/jpeg', 'application/pdf']:
frappe.throw("You can only upload JPG, PNG or PDF files.")
if filetype not in ALLOWED_MIMETYPES:
frappe.throw(_("You can only upload JPG, PNG, PDF, or Microsoft documents."))
if method:
method = frappe.get_attr(method)

View file

@ -89,6 +89,7 @@ permission_query_conditions = {
"Dashboard Settings": "frappe.desk.doctype.dashboard_settings.dashboard_settings.get_permission_query_conditions",
"Notification Log": "frappe.desk.doctype.notification_log.notification_log.get_permission_query_conditions",
"Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.get_permission_query_conditions",
"Number Card": "frappe.desk.doctype.number_card.number_card.get_permission_query_conditions",
"Notification Settings": "frappe.desk.doctype.notification_settings.notification_settings.get_permission_query_conditions",
"Note": "frappe.desk.doctype.note.note.get_permission_query_conditions",
"Kanban Board": "frappe.desk.doctype.kanban_board.kanban_board.get_permission_query_conditions",
@ -105,6 +106,7 @@ has_permission = {
"User": "frappe.core.doctype.user.user.has_permission",
"Note": "frappe.desk.doctype.note.note.has_permission",
"Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.has_permission",
"Number Card": "frappe.desk.doctype.number_card.number_card.has_permission",
"Kanban Board": "frappe.desk.doctype.kanban_board.kanban_board.has_permission",
"Contact": "frappe.contacts.address_and_contact.has_permission",
"Address": "frappe.contacts.address_and_contact.has_permission",

View file

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe
from frappe import _, safe_encode
from frappe.model.document import Document
from frappe.twofactor import (should_run_2fa, authenticate_for_2factor,confirm_otp_token)
class LDAPSettings(Document):
def validate(self):
@ -237,6 +237,10 @@ def login():
user = ldap.authenticate(frappe.as_unicode(args.usr), frappe.as_unicode(args.pwd))
frappe.local.login_manager.user = user.name
if should_run_2fa(user.name):
authenticate_for_2factor(user.name)
if not confirm_otp_token(frappe.local.login_manager):
return False
frappe.local.login_manager.post_login()
# because of a GET request!

View file

@ -60,6 +60,7 @@ class Webhook(Document):
if self.request_structure == "Form URL-Encoded":
self.webhook_json = None
elif self.request_structure == "JSON":
validate_json(self.webhook_json)
validate_template(self.webhook_json)
self.webhook_data = []
@ -130,3 +131,10 @@ def get_webhook_data(doc, webhook):
data = json.loads(data)
return data
def validate_json(string):
try:
json.loads(string)
except (TypeError, ValueError):
frappe.throw(_("Request Body consists of an invalid JSON structure"), title=_("Invalid JSON"))

View file

@ -48,7 +48,7 @@ table_fields = ('Table', 'Table MultiSelect')
core_doctypes_list = ('DocType', 'DocField', 'DocPerm', 'DocType Action', 'DocType Link', 'User', 'Role', 'Has Role',
'Page', 'Module Def', 'Print Format', 'Report', 'Customize Form',
'Customize Form Field', 'Property Setter', 'Custom Field', 'Custom Script')
data_field_options = ('Email', 'Phone')
data_field_options = ('Email', 'Name', 'Phone')
def copytables(srctype, src, srcfield, tartype, tar, tarfield, srcfields, tarfields=[]):
if not tarfields:

View file

@ -11,11 +11,12 @@ from frappe.model import default_fields, table_fields
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, data_fieldtypes
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, getdate, get_datetime, to_timedelta,
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,
@ -288,7 +289,7 @@ class BaseDocument(object):
if k in default_fields:
del doc[k]
for key in ("_user_tags", "__islocal", "__onload", "_liked_by", "__run_link_triggers"):
for key in ("_user_tags", "__islocal", "__onload", "_liked_by", "__run_link_triggers", "__unsaved"):
if self.get(key):
doc[key] = self.get(key)
@ -564,13 +565,20 @@ class BaseDocument(object):
for data_field in self.meta.get_data_fields():
data = self.get(data_field.fieldname)
data_field_options = data_field.get("options")
old_fieldtype = data_field.get("oldfieldtype")
if old_fieldtype and old_fieldtype != "Data":
continue
if data_field_options == "Email":
if (self.owner in STANDARD_USERS) and (data in STANDARD_USERS):
return
continue
for email_address in frappe.utils.split_emails(data):
frappe.utils.validate_email_address(email_address, throw=True)
if data_field_options == "Name":
frappe.utils.validate_name(data, throw=True)
if data_field_options == "Phone":
frappe.utils.validate_phone_number(data, throw=True)
@ -678,7 +686,7 @@ class BaseDocument(object):
# doesn't look like html so no need
continue
elif "<!-- markdown -->" in value and not ("<script" in value or "javascript:" in value):
elif "<!-- markdown -->" in value and not bool(BeautifulSoup(value, "html.parser").find()):
# should be handled separately via the markdown converter function
continue

View file

@ -45,6 +45,7 @@ def make_new_doc(doctype):
doc = doc.get_valid_dict(sanitize=False)
doc["doctype"] = doctype
doc["__islocal"] = 1
doc["__unsaved"] = 1
return doc
@ -74,11 +75,9 @@ def set_user_and_static_default_values(doc):
def get_user_default_value(df, defaults, doctype_user_permissions, allowed_records, default_doc):
# don't set defaults for "User" link field using User Permissions!
if df.fieldtype == "Link" and df.options != "User":
# 1 - look in user permissions only for document_type==Setup
# We don't want to include permissions of transactions to be used for defaults.
if (frappe.get_meta(df.options).document_type=="Setup"
and not df.ignore_user_permissions and default_doc):
return default_doc
# If user permission has Is Default enabled or single-user permission has found against respective doctype.
if (not df.ignore_user_permissions and default_doc):
return default_doc
# 2 - Look in user defaults
user_default = defaults.get(df.fieldname)

View file

@ -210,7 +210,7 @@ def check_permission_and_not_submitted(doc):
# check if submitted
if doc.docstatus == 1:
frappe.msgprint(_("{0} {1}: Submitted Record cannot be deleted.").format(_(doc.doctype), doc.name),
frappe.msgprint(_("{0} {1}: Submitted Record cannot be deleted. You must {2} Cancel {3} it first.").format(_(doc.doctype), doc.name, "<a href='https://docs.erpnext.com//docs/user/manual/en/setting-up/articles/delete-submitted-document' target='_blank'>", "</a>"),
raise_exception=True)
def check_if_doc_is_linked(doc, method="Delete"):

View file

@ -268,6 +268,10 @@ class Document(BaseDocument):
if hasattr(self, "__islocal"):
delattr(self, "__islocal")
# clear unsaved flag
if hasattr(self, "__unsaved"):
delattr(self, "__unsaved")
if not (frappe.flags.in_migrate or frappe.local.flags.in_install or frappe.flags.in_setup_wizard):
follow_document(self.doctype, self.name, frappe.session.user)
return self
@ -329,6 +333,10 @@ class Document(BaseDocument):
self.update_children()
self.run_post_save_methods()
# clear unsaved flag
if hasattr(self, "__unsaved"):
delattr(self, "__unsaved")
return self
def copy_attachments_from_amended_from(self):
@ -583,6 +591,9 @@ class Document(BaseDocument):
if high_permlevel_fields:
self.reset_values_if_no_permlevel_access(has_access_to, high_permlevel_fields)
# If new record then don't reset the values for child table
if self.is_new(): return
# check for child tables
for df in self.meta.get_table_fields():
high_permlevel_fields = frappe.get_meta(df.options).get_high_permlevel_fields()
@ -1318,6 +1329,9 @@ def make_event_update_log(doc, update_type):
def check_doctype_has_consumers(doctype):
"""Check if doctype has event consumers for event streaming"""
if not frappe.db.exists("DocType", "Event Consumer"):
return False
event_consumers = frappe.get_all('Event Consumer')
for event_consumer in event_consumers:
consumer = frappe.get_doc('Event Consumer', event_consumer.name)

View file

@ -425,17 +425,19 @@ class Meta(Document):
implemented in other Frappe applications via hooks.
'''
data = frappe._dict()
try:
module = load_doctype_module(self.name, suffix='_dashboard')
if hasattr(module, 'get_data'):
data = frappe._dict(module.get_data())
except ImportError:
pass
if not self.custom:
try:
module = load_doctype_module(self.name, suffix='_dashboard')
if hasattr(module, 'get_data'):
data = frappe._dict(module.get_data())
except ImportError:
pass
self.add_doctype_links(data)
for hook in frappe.get_hooks("override_doctype_dashboards", {}).get(self.name, []):
data = frappe.get_attr(hook)(data=data)
if not self.custom:
for hook in frappe.get_hooks("override_doctype_dashboards", {}).get(self.name, []):
data = frappe.get_attr(hook)(data=data)
return data

View file

@ -38,6 +38,7 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe
("custom", "custom_field"),
("custom", "property_setter"),
("website", "web_form"),
("website", "web_template"),
("website", "web_form_field"),
("website", "portal_menu_item"),
("data_migration", "data_migration_mapping_detail"),
@ -78,7 +79,7 @@ def get_doc_files(files, start_path, force=0, sync_everything = False, verbose=F
# load in sequence - warning for devs
document_types = ['doctype', 'page', 'report', 'dashboard_chart_source', 'print_format',
'website_theme', 'web_form', 'notification', 'print_style',
'website_theme', 'web_form', 'web_template', 'notification', 'print_style',
'data_migration_mapping', 'data_migration_plan', 'onboarding_slide', 'desk_page']
for doctype in document_types:
doctype_path = os.path.join(start_path, doctype)

View file

@ -23,7 +23,7 @@ def start(transaction_type="request", method=None, kwargs=None):
def stop(response=None):
if frappe.conf.monitor and hasattr(frappe.local, "monitor"):
if hasattr(frappe.local, "monitor"):
frappe.local.monitor.dump(response)
@ -79,7 +79,7 @@ class Monitor:
if self.data.transaction_type == "request":
self.data.request.status_code = response.status_code
self.data.request.response_length = int(response.headers["Content-Length"])
self.data.request.response_length = int(response.headers.get("Content-Length", 0))
self.store()
except Exception:

View file

@ -272,3 +272,6 @@ execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Account')
execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Settings')
frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats
execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders()
frappe.patches.v13_0.website_theme_custom_scss
frappe.patches.v13_0.set_existing_dashboard_charts_as_public
frappe.patches.v13_0.set_path_for_homepage_in_web_page_view

View file

@ -1,6 +1,10 @@
import frappe
def execute():
frappe.reload_doc("contacts", "doctype", "contact_email")
frappe.reload_doc("contacts", "doctype", "contact_phone")
frappe.reload_doc("contacts", "doctype", "contact")
contact_details = frappe.db.sql("""
SELECT
`name`, `email_id`, `phone`, `mobile_no`, `modified_by`, `creation`, `modified`
@ -10,10 +14,6 @@ def execute():
and `tabContact Email`.email_id=`tabContact`.email_id)
""", as_dict=True)
frappe.reload_doc("contacts", "doctype", "contact_email")
frappe.reload_doc("contacts", "doctype", "contact_phone")
frappe.reload_doc("contacts", "doctype", "contact")
email_values = []
phone_values = []
for count, contact_detail in enumerate(contact_details):

View file

View file

@ -0,0 +1,6 @@
import frappe
def execute():
frappe.delete_doc_if_exists("DocType", "Web View")
frappe.delete_doc_if_exists("DocType", "Web View Component")
frappe.delete_doc_if_exists("DocType", "CSS Class")

View file

@ -0,0 +1,21 @@
import frappe
def execute():
frappe.reload_doc('desk', 'doctype', 'dashboard_chart')
if not frappe.db.table_exists('Dashboard Chart'):
return
users_with_permission = frappe.get_all(
"Has Role",
fields=["parent"],
filters={"role": ['in', ['System Manager', 'Dashboard Manager']], "parenttype": "User"},
distinct=True,
)
users = [item.parent for item in users_with_permission]
charts = frappe.db.get_all('Dashboard Chart', filters={'owner': ['in', users]})
for chart in charts:
frappe.db.set_value('Dashboard Chart', chart.name, 'is_public', 1)

View file

@ -0,0 +1,5 @@
import frappe
def execute():
frappe.reload_doc('website', 'doctype', 'web_page_view', force=True)
frappe.db.sql("""UPDATE `tabWeb Page View` set path="/" where path=''""")

View file

@ -0,0 +1,10 @@
import frappe
def execute():
frappe.reload_doctype('Website Theme')
for theme in frappe.get_all('Website Theme'):
doc = frappe.get_doc('Website Theme', theme.name)
if not doc.get('custom_scss') and doc.theme_scss:
# move old theme to new theme
doc.custom_scss = doc.theme_scss
doc.save()

View file

@ -441,18 +441,16 @@ frappe.PrintFormatBuilder = Class.extend({
});
},
setup_field_settings: function() {
this.page.main.find(".field-settings").on("click", () => {
var field = $(this).parent();
this.page.main.find(".field-settings").on("click", e => {
const field = $(e.currentTarget).parent();
// new dialog
var d = new frappe.ui.Dialog({
title: "Set Properties",
fields: [
{
label:__("Label"),
fieldname:"label",
fieldtype:"Data"
label: __("Label"),
fieldname: "label",
fieldtype: "Data"
},
{
label: __("Align Value"),
@ -485,7 +483,7 @@ frappe.PrintFormatBuilder = Class.extend({
});
// set current value
if(field.attr('data-align')) {
if (field.attr('data-align')) {
d.set_value('align', field.attr('data-align'));
} else {
d.set_value('align', 'left');

View file

@ -1,4 +1,7 @@
{
"css/tailwind.css": [
"public/tailwind.css"
],
"css/frappe-web-b4.css": [
"public/scss/website.scss",
"public/less/indicator.less"
@ -90,6 +93,7 @@
"public/css/font-awesome.css",
"public/css/octicons/octicons.css",
"public/less/desk.less",
"public/less/module.less",
"public/less/flex.less",
"public/less/indicator.less",
"public/less/avatar.less",
@ -103,6 +107,7 @@
"public/less/form.less",
"public/less/mobile.less",
"public/less/kanban.less",
"public/less/dashboard_view.less",
"public/less/controls.less",
"public/less/chat.less",
"public/less/filters.less",
@ -295,6 +300,7 @@
"public/js/frappe/views/gantt/gantt_view.js",
"public/js/frappe/views/calendar/calendar.js",
"public/js/frappe/views/dashboard/dashboard_view.js",
"public/js/frappe/views/image/image_view.js",
"public/js/frappe/views/kanban/kanban_view.js",
"public/js/frappe/views/inbox/inbox_view.js",

View file

@ -1,64 +1,82 @@
/* csslint ignore:start */
/* palette colors*/
body {
line-height: 1.5;
color: #36414c;
}
p {
margin: 1em 0 !important;
}
hr {
border-top: 1px solid #d1d8dd;
}
.body-table {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
}
.body-table td {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
}
.email-header,
.email-body,
.email-footer {
width: 100% !important;
min-width: 100% !important;
}
.email-body {
font-size: 14px;
}
.email-footer {
border-top: 1px solid #d1d8dd;
font-size: 12px;
}
.email-header {
border: 1px solid #d1d8dd;
border-radius: 4px 4px 0 0;
}
.email-header .brand-image {
width: 24px;
height: 24px;
display: block;
}
.email-header-title {
font-weight: bold;
}
.body-table.has-header .email-body {
border: 1px solid #d1d8dd;
border-radius: 0 0 4px 4px;
border-top: none;
}
.body-table.has-header .email-footer {
border-top: none;
}
.email-footer-container {
margin-top: 30px;
}
.email-footer-container > div:not(:last-child) {
margin-bottom: 5px;
}
.email-unsubscribe a {
color: #8d99a6;
text-decoration: underline;
}
.btn {
text-decoration: none;
padding: 7px 10px;
@ -66,20 +84,24 @@ hr {
border: 1px solid;
border-radius: 3px;
}
.btn.btn-default {
color: #fff;
background-color: #f0f4f7;
border-color: transparent;
}
.btn.btn-primary {
color: #fff;
background-color: #5e64ff;
border-color: #444bff;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table td,
.table th {
padding: 8px;
@ -88,53 +110,68 @@ hr {
border-top: 1px solid #d1d8dd;
text-align: left;
}
.table th {
font-weight: bold;
}
.table > thead > tr > th {
vertical-align: middle;
border-bottom: 2px solid #d1d8dd;
}
.table > thead:first-child > tr:first-child > th {
border-top: none;
}
.table.table-bordered {
border: 1px solid #d1d8dd;
}
.table.table-bordered td,
.table.table-bordered th {
border: 1px solid #d1d8dd;
}
.more-info {
font-size: 80% !important;
color: #8d99a6 !important;
border-top: 1px solid #ebeff2;
padding-top: 10px;
}
.text-right {
text-align: right !important;
}
.text-center {
text-align: center !important;
}
.text-muted {
color: #8d99a6 !important;
}
.text-extra-muted {
color: #d1d8dd !important;
}
.text-regular {
font-size: 14px;
}
.text-medium {
font-size: 12px;
}
.text-small {
font-size: 10px;
}
.text-bold {
font-weight: bold;
}
.indicator {
width: 8px;
height: 8px;
@ -143,33 +180,43 @@ hr {
display: inline-block;
margin-right: 5px;
}
.indicator.indicator-blue {
background-color: #5e64ff;
}
.indicator.indicator-green {
background-color: #98d85b;
}
.indicator.indicator-orange {
background-color: #ffa00a;
}
.indicator.indicator-red {
background-color: #ff5858;
}
.indicator.indicator-yellow {
background-color: #feef72;
}
.screenshot {
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1);
border: 1px solid #d1d8dd;
margin: 8px 0;
max-width: 100%;
}
.list-unstyled {
list-style-type: none;
padding: 0;
}
/* auto email report */
.report-title {
margin-bottom: 20px;
}
/* csslint ignore:end */

View file

@ -2523,7 +2523,7 @@ class extends Component {
h("div",{class:"input-group input-group-lg"},
!frappe._.is_empty(props.actions) ?
h("div",{class:"input-group-btn dropup"},
h(frappe.components.Button,{ class: "dropdown-toggle", "data-toggle": "dropdown"},
h(frappe.components.Button,{ class: (frappe.session.user === "Guest" ? "disabled" : "dropdown-toggle"), "data-toggle": "dropdown"},
h(frappe.components.FontAwesome, { class: "text-muted", type: "paperclip", fixed: true })
),
h("div",{ class:"dropdown-menu dropdown-menu-left", onclick: e => e.stopPropagation() },

View file

@ -86,6 +86,14 @@ frappe.Application = Class.extend({
this.show_update_available();
}
if (!frappe.boot.developer_mode) {
let console_security_message = __("Using this console may allow attackers to impersonate you and steal your information. Do not enter or paste code that you do not understand.");
console.log(
`%c${console_security_message}`,
"font-size: large"
);
}
this.show_notes();
if (frappe.boot.is_first_startup) {

View file

@ -85,6 +85,10 @@ frappe.dom = {
);
},
is_element_in_modal(element) {
return Boolean($(element).parents('.modal').length);
},
set_style: function(txt, id) {
if(!txt) return;

View file

@ -48,6 +48,7 @@ frappe.ui.form.ControlBarcode = frappe.ui.form.ControlData.extend({
const svg = this.barcode_area.find('svg')[0];
JsBarcode(svg, value, this.get_options(value));
$(svg).attr('data-barcode-value', value);
$(svg).attr('width', '100%');
return this.barcode_area.html();
}
},

View file

@ -152,12 +152,14 @@ frappe.ui.form.Control = Class.extend({
() => me.set_model_value(value),
() => {
me.set_mandatory && me.set_mandatory(value);
me.set_invalid && me.set_invalid();
if(me.df.change || me.df.onchange) {
// onchange event specified in df
return (me.df.change || me.df.onchange).apply(me, [e]);
let set = (me.df.change || me.df.onchange).apply(me, [e]);
me.set_invalid && me.set_invalid();
return set;
}
me.set_invalid && me.set_invalid();
}
]);
};

View file

@ -180,7 +180,14 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
this.$wrapper.toggleClass("has-error", (this.df.reqd && is_null(value)) ? true : false);
},
set_invalid: function () {
this.$wrapper.toggleClass("has-error", (this.df.invalid ? true : false));
let invalid = !!this.df.invalid;
if (this.grid) {
this.$wrapper.parents('.grid-static-col').toggleClass('invalid', invalid);
this.$input.toggleClass('invalid', invalid);
this.grid_row.columns[this.df.fieldname].is_invalid = invalid;
} else {
this.$wrapper.toggleClass('has-error', invalid);
}
},
set_bold: function() {
if(this.$input) {

View file

@ -9,6 +9,12 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
this.ace_editor_target = $('<div class="ace-editor-target"></div>')
.appendTo(this.input_area);
this.expanded = false;
this.$expand_button = $(`<button class="btn btn-xs btn-default">${__('Expand')}</button>`).click(() => {
this.expanded = !this.expanded;
this.refresh_height();
this.toggle_label();
}).appendTo(this.$input_wrapper);
// styling
this.ace_editor_target.addClass('border rounded');
this.ace_editor_target.css('height', 300);
@ -26,6 +32,16 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
}, 300));
},
refresh_height() {
this.ace_editor_target.css('height', this.expanded ? 600 : 300);
this.editor.resize();
},
toggle_label() {
const button_label = this.expanded ? __('Collapse') : __('Expand');
this.$expand_button.text(button_label);
},
set_language() {
const language_map = {
'Javascript': 'ace/mode/javascript',
@ -34,7 +50,9 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
'CSS': 'ace/mode/css',
'Markdown': 'ace/mode/markdown',
'SCSS': 'ace/mode/scss',
'JSON': 'ace/mode/json'
'JSON': 'ace/mode/json',
'Golang': 'ace/mode/golang',
'Go': 'ace/mode/golang'
};
const language = this.df.options;

View file

@ -96,6 +96,9 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({
if(this.df.options == 'Phone') {
this.df.invalid = !validate_phone(v);
return v;
} else if (this.df.options == 'Name') {
this.df.invalid = !validate_name(v);
return v;
} else if(this.df.options == 'Email') {
var email_list = frappe.utils.split_emails(v);
if (!email_list) {

View file

@ -18,6 +18,7 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({
this.$list_wrapper = $(template);
this.$input = $('<input>');
this.input = this.$input.get(0);
this.has_input = true;
this.$list_wrapper.prependTo(this.input_area);
this.$filter_input = this.$list_wrapper.find('input');
this.$list_wrapper.on('click', '.dropdown-menu', e => {

View file

@ -147,7 +147,7 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({
[{ 'color': [] }, { 'background': [] }],
['blockquote', 'code-block'],
['link', 'image'],
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
[{ 'list': 'ordered' }, { 'list': 'bullet' }, { 'list': 'check' }],
[{ 'align': [] }],
[{ 'indent': '-1'}, { 'indent': '+1' }],
[{'table': [

Some files were not shown because too many files have changed in this diff Show more