Merge branch 'develop' of https://github.com/frappe/frappe into refactor-website
This commit is contained in:
commit
b39b5effbf
133 changed files with 2161 additions and 1104 deletions
|
|
@ -80,6 +80,7 @@
|
|||
"validate_email": true,
|
||||
"validate_name": true,
|
||||
"validate_phone": true,
|
||||
"validate_url": true,
|
||||
"get_number_format": true,
|
||||
"format_number": true,
|
||||
"format_currency": true,
|
||||
|
|
|
|||
2
.github/helper/semgrep_rules/translate.yml
vendored
2
.github/helper/semgrep_rules/translate.yml
vendored
|
|
@ -42,7 +42,7 @@ rules:
|
|||
|
||||
- id: frappe-translation-python-splitting
|
||||
pattern-either:
|
||||
- pattern: _(...) + ... + _(...)
|
||||
- pattern: _(...) + _(...)
|
||||
- pattern: _("..." + "...")
|
||||
- pattern-regex: '_\([^\)]*\\\s*' # lines broken by `\`
|
||||
- pattern-regex: '_\(\s*\n' # line breaks allowed by python for using ( )
|
||||
|
|
|
|||
65
cypress/fixtures/data_field_validation_doctype.js
Normal file
65
cypress/fixtures/data_field_validation_doctype.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
export default {
|
||||
name: 'Validation Test',
|
||||
custom: 1,
|
||||
actions: [],
|
||||
creation: '2019-03-15 06:29:07.215072',
|
||||
doctype: 'DocType',
|
||||
editable_grid: 1,
|
||||
engine: 'InnoDB',
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'email',
|
||||
fieldtype: 'Data',
|
||||
label: 'Email',
|
||||
options: 'Email'
|
||||
},
|
||||
{
|
||||
fieldname: 'URL',
|
||||
fieldtype: 'Data',
|
||||
label: 'URL',
|
||||
options: 'URL'
|
||||
},
|
||||
{
|
||||
fieldname: 'Phone',
|
||||
fieldtype: 'Data',
|
||||
label: 'Phone',
|
||||
options: 'Phone'
|
||||
},
|
||||
{
|
||||
fieldname: 'person_name',
|
||||
fieldtype: 'Data',
|
||||
label: 'Person Name',
|
||||
options: 'Name'
|
||||
},
|
||||
{
|
||||
fieldname: 'read_only_url',
|
||||
fieldtype: 'Data',
|
||||
label: 'Read Only URL',
|
||||
options: 'URL',
|
||||
read_only: '1',
|
||||
default: 'https://frappe.io'
|
||||
}
|
||||
],
|
||||
issingle: 1,
|
||||
links: [],
|
||||
modified: '2021-04-19 14:40:53.127615',
|
||||
modified_by: 'Administrator',
|
||||
module: 'Custom',
|
||||
owner: 'Administrator',
|
||||
permissions: [
|
||||
{
|
||||
create: 1,
|
||||
delete: 1,
|
||||
email: 1,
|
||||
print: 1,
|
||||
read: 1,
|
||||
role: 'System Manager',
|
||||
share: 1,
|
||||
write: 1
|
||||
}
|
||||
],
|
||||
quick_entry: 1,
|
||||
sort_field: 'modified',
|
||||
sort_order: 'ASC',
|
||||
track_changes: 1
|
||||
};
|
||||
43
cypress/integration/data_field_form_validation.js
Normal file
43
cypress/integration/data_field_form_validation.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import data_field_validation_doctype from '../fixtures/data_field_validation_doctype';
|
||||
const doctype_name = data_field_validation_doctype.name;
|
||||
|
||||
|
||||
context('Data Field Input Validation in New Form', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
return cy.insert_doc('DocType', data_field_validation_doctype, true);
|
||||
});
|
||||
|
||||
function validateField(fieldname, invalid_value, valid_value) {
|
||||
// Invalid, should have has-error class
|
||||
cy.get_field(fieldname).clear().type(invalid_value).blur();
|
||||
cy.get(`.frappe-control[data-fieldname="${fieldname}"]`).should('have.class', 'has-error');
|
||||
// Valid value, should not have has-error class
|
||||
cy.get_field(fieldname).clear().type(valid_value);
|
||||
cy.get(`.frappe-control[data-fieldname="${fieldname}"]`).should('not.have.class', 'has-error');
|
||||
}
|
||||
|
||||
describe('Data Field Options', () => {
|
||||
it('should validate email address', () => {
|
||||
cy.new_form(doctype_name);
|
||||
validateField('email', 'captian', 'hello@test.com');
|
||||
});
|
||||
|
||||
it('should validate URL', () => {
|
||||
validateField('url', 'jkl', 'https://frappe.io');
|
||||
validateField('url', 'abcd.com', 'http://google.com/home');
|
||||
validateField('url', '&&http://google.uae', 'gopher://frappe.io');
|
||||
validateField('url', 'ftt2:://google.in?q=news', 'ftps2://frappe.io/__/#home');
|
||||
validateField('url', 'ftt2://', 'ntps://localhost'); // For intranet URLs
|
||||
});
|
||||
|
||||
it('should validate phone number', () => {
|
||||
validateField('phone', 'america', '89787878');
|
||||
});
|
||||
|
||||
it('should validate name', () => {
|
||||
validateField('person_name', ' 777Hello', 'James Bond');
|
||||
});
|
||||
});
|
||||
});
|
||||
43
cypress/integration/url_data_field.js
Normal file
43
cypress/integration/url_data_field.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import data_field_validation_doctype from '../fixtures/data_field_validation_doctype';
|
||||
|
||||
const doctype_name = data_field_validation_doctype.name;
|
||||
|
||||
context('URL Data Field Input', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
return cy.insert_doc('DocType', data_field_validation_doctype, true);
|
||||
});
|
||||
|
||||
|
||||
describe('URL Data Field Input ', () => {
|
||||
it('should not show URL link button without focus', () => {
|
||||
cy.new_form(doctype_name);
|
||||
cy.get_field('url').clear().type('https://frappe.io');
|
||||
cy.get_field('url').blur().wait(500);
|
||||
cy.get('.link-btn').should('not.be.visible');
|
||||
});
|
||||
|
||||
it('should show URL link button on focus', () => {
|
||||
cy.get_field('url').focus().wait(500);
|
||||
cy.get('.link-btn').should('be.visible');
|
||||
});
|
||||
|
||||
it('should not show URL link button for invalid URL', () => {
|
||||
cy.get_field('url').clear().type('fuzzbuzz');
|
||||
cy.get('.link-btn').should('not.be.visible');
|
||||
});
|
||||
|
||||
it('should have valid URL link with target _blank', () => {
|
||||
cy.get_field('url').clear().type('https://frappe.io');
|
||||
cy.get('.link-btn .btn-open').should('have.attr', 'href', 'https://frappe.io');
|
||||
cy.get('.link-btn .btn-open').should('have.attr', 'target', '_blank');
|
||||
});
|
||||
|
||||
it('should inject anchor tag in read-only URL data field', () => {
|
||||
cy.get('[data-fieldname="read_only_url"]')
|
||||
.find('a')
|
||||
.should('have.attr', 'target', '_blank');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -10,11 +10,9 @@ be used to build database driven apps.
|
|||
|
||||
Read the documentation: https://frappeframework.com/docs
|
||||
"""
|
||||
from __future__ import unicode_literals, print_function
|
||||
|
||||
from six import iteritems, binary_type, text_type, string_types, PY2
|
||||
from werkzeug.local import Local, release_local
|
||||
import os, sys, importlib, inspect, json
|
||||
import os, sys, importlib, inspect, json, warnings
|
||||
import typing
|
||||
from past.builtins import cmp
|
||||
import click
|
||||
|
|
@ -27,19 +25,14 @@ from .utils.lazy_loader import lazy_import
|
|||
# Lazy imports
|
||||
faker = lazy_import('faker')
|
||||
|
||||
|
||||
# Harmless for Python 3
|
||||
# For Python 2 set default encoding to utf-8
|
||||
if PY2:
|
||||
reload(sys)
|
||||
sys.setdefaultencoding("utf-8")
|
||||
|
||||
__version__ = '14.0.0-dev'
|
||||
|
||||
__title__ = "Frappe Framework"
|
||||
|
||||
local = Local()
|
||||
controllers = {}
|
||||
warnings.simplefilter('always', DeprecationWarning)
|
||||
warnings.simplefilter('always', PendingDeprecationWarning)
|
||||
|
||||
class _dict(dict):
|
||||
"""dict like object that exposes keys as attributes"""
|
||||
|
|
@ -97,14 +90,14 @@ def _(msg, lang=None, context=None):
|
|||
|
||||
def as_unicode(text, encoding='utf-8'):
|
||||
'''Convert to unicode if required'''
|
||||
if isinstance(text, text_type):
|
||||
if isinstance(text, str):
|
||||
return text
|
||||
elif text==None:
|
||||
return ''
|
||||
elif isinstance(text, binary_type):
|
||||
return text_type(text, encoding)
|
||||
elif isinstance(text, bytes):
|
||||
return str(text, encoding)
|
||||
else:
|
||||
return text_type(text)
|
||||
return str(text)
|
||||
|
||||
def get_lang_dict(fortype, name=None):
|
||||
"""Returns the translated language dict for the given type and name.
|
||||
|
|
@ -597,7 +590,7 @@ def is_whitelisted(method):
|
|||
# strictly sanitize form_dict
|
||||
# escapes html characters like <> except for predefined tags like a, b, ul etc.
|
||||
for key, value in form_dict.items():
|
||||
if isinstance(value, string_types):
|
||||
if isinstance(value, str):
|
||||
form_dict[key] = sanitize_html(value)
|
||||
|
||||
def read_only():
|
||||
|
|
@ -721,7 +714,7 @@ def has_website_permission(doc=None, ptype='read', user=None, verbose=False, doc
|
|||
user = session.user
|
||||
|
||||
if doc:
|
||||
if isinstance(doc, string_types):
|
||||
if isinstance(doc, str):
|
||||
doc = get_doc(doctype, doc)
|
||||
|
||||
doctype = doc.doctype
|
||||
|
|
@ -790,7 +783,7 @@ def set_value(doctype, docname, fieldname, value=None):
|
|||
return frappe.client.set_value(doctype, docname, fieldname, value)
|
||||
|
||||
def get_cached_doc(*args, **kwargs):
|
||||
if args and len(args) > 1 and isinstance(args[1], text_type):
|
||||
if args and len(args) > 1 and isinstance(args[1], str):
|
||||
key = get_document_cache_key(args[0], args[1])
|
||||
# local cache
|
||||
doc = local.document_cache.get(key)
|
||||
|
|
@ -821,7 +814,7 @@ def clear_document_cache(doctype, name):
|
|||
|
||||
def get_cached_value(doctype, name, fieldname, as_dict=False):
|
||||
doc = get_cached_doc(doctype, name)
|
||||
if isinstance(fieldname, string_types):
|
||||
if isinstance(fieldname, str):
|
||||
if as_dict:
|
||||
throw('Cannot make dict for single fieldname')
|
||||
return doc.get(fieldname)
|
||||
|
|
@ -1027,7 +1020,7 @@ def get_doc_hooks():
|
|||
if not hasattr(local, 'doc_events_hooks'):
|
||||
hooks = get_hooks('doc_events', {})
|
||||
out = {}
|
||||
for key, value in iteritems(hooks):
|
||||
for key, value in hooks.items():
|
||||
if isinstance(key, tuple):
|
||||
for doctype in key:
|
||||
append_hook(out, doctype, value)
|
||||
|
|
@ -1144,7 +1137,7 @@ def get_file_json(path):
|
|||
|
||||
def read_file(path, raise_not_found=False):
|
||||
"""Open a file and return its content as Unicode."""
|
||||
if isinstance(path, text_type):
|
||||
if isinstance(path, str):
|
||||
path = path.encode("utf-8")
|
||||
|
||||
if os.path.exists(path):
|
||||
|
|
@ -1167,7 +1160,7 @@ def get_attr(method_string):
|
|||
|
||||
def call(fn, *args, **kwargs):
|
||||
"""Call a function and match arguments."""
|
||||
if isinstance(fn, string_types):
|
||||
if isinstance(fn, str):
|
||||
fn = get_attr(fn)
|
||||
|
||||
newargs = get_newargs(fn, kwargs)
|
||||
|
|
@ -1178,13 +1171,9 @@ def get_newargs(fn, kwargs):
|
|||
if hasattr(fn, 'fnargs'):
|
||||
fnargs = fn.fnargs
|
||||
else:
|
||||
try:
|
||||
fnargs, varargs, varkw, defaults = inspect.getargspec(fn)
|
||||
except ValueError:
|
||||
fnargs = inspect.getfullargspec(fn).args
|
||||
varargs = inspect.getfullargspec(fn).varargs
|
||||
varkw = inspect.getfullargspec(fn).varkw
|
||||
defaults = inspect.getfullargspec(fn).defaults
|
||||
fnargs = inspect.getfullargspec(fn).args
|
||||
fnargs.extend(inspect.getfullargspec(fn).kwonlyargs)
|
||||
varkw = inspect.getfullargspec(fn).varkw
|
||||
|
||||
newargs = {}
|
||||
for a in kwargs:
|
||||
|
|
@ -1626,6 +1615,12 @@ def enqueue(*args, **kwargs):
|
|||
import frappe.utils.background_jobs
|
||||
return frappe.utils.background_jobs.enqueue(*args, **kwargs)
|
||||
|
||||
def task(**task_kwargs):
|
||||
def decorator_task(f):
|
||||
f.enqueue = lambda **fun_kwargs: enqueue(f, **task_kwargs, **fun_kwargs)
|
||||
return f
|
||||
return decorator_task
|
||||
|
||||
def enqueue_doc(*args, **kwargs):
|
||||
'''
|
||||
Enqueue method to be executed using a background worker
|
||||
|
|
|
|||
|
|
@ -201,12 +201,20 @@ def handle_exception(e):
|
|||
response = None
|
||||
http_status_code = getattr(e, "http_status_code", 500)
|
||||
return_as_message = False
|
||||
accept_header = frappe.get_request_header("Accept") or ""
|
||||
respond_as_json = (
|
||||
frappe.get_request_header('Accept')
|
||||
and (frappe.local.is_ajax or 'application/json' in accept_header)
|
||||
or (
|
||||
frappe.local.request.path.startswith("/api/") and not accept_header.startswith("text")
|
||||
)
|
||||
)
|
||||
|
||||
if frappe.conf.get('developer_mode'):
|
||||
# don't fail silently
|
||||
print(frappe.get_traceback())
|
||||
|
||||
if frappe.get_request_header('Accept') and (frappe.local.is_ajax or 'application/json' in frappe.get_request_header('Accept')):
|
||||
if respond_as_json:
|
||||
# handle ajax responses first
|
||||
# if the request is ajax, send back the trace or error message
|
||||
response = frappe.utils.response.report_error(http_status_code)
|
||||
|
|
@ -285,8 +293,9 @@ def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=No
|
|||
_sites_path = sites_path
|
||||
|
||||
from werkzeug.serving import run_simple
|
||||
patch_werkzeug_reloader()
|
||||
|
||||
if profile:
|
||||
if profile or os.environ.get('USE_PROFILER'):
|
||||
application = ProfilerMiddleware(application, sort_by=('cumtime', 'calls'))
|
||||
|
||||
if not os.environ.get('NO_STATICS'):
|
||||
|
|
@ -315,3 +324,23 @@ def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=No
|
|||
use_debugger=not in_test_env,
|
||||
use_evalex=not in_test_env,
|
||||
threaded=not no_threading)
|
||||
|
||||
def patch_werkzeug_reloader():
|
||||
"""
|
||||
This function monkey patches Werkzeug reloader to ignore reloading files in
|
||||
the __pycache__ directory.
|
||||
|
||||
To be deprecated when upgrading to Werkzeug 2.
|
||||
"""
|
||||
|
||||
from werkzeug._reloader import WatchdogReloaderLoop
|
||||
|
||||
trigger_reload = WatchdogReloaderLoop.trigger_reload
|
||||
|
||||
def custom_trigger_reload(self, filename):
|
||||
if os.path.basename(os.path.dirname(filename)) == "__pycache__":
|
||||
return
|
||||
|
||||
return trigger_reload(self, filename)
|
||||
|
||||
WatchdogReloaderLoop.trigger_reload = custom_trigger_reload
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ frappe.ui.form.on('Auto Repeat', {
|
|||
frappe.auto_repeat.render_schedule = function(frm) {
|
||||
if (!frm.is_dirty() && frm.doc.status !== 'Disabled') {
|
||||
frm.call("get_auto_repeat_schedule").then(r => {
|
||||
frm.dashboard.wrapper.empty();
|
||||
frm.dashboard.reset();
|
||||
frm.dashboard.add_section(
|
||||
frappe.render_template("auto_repeat_schedule", {
|
||||
schedule_details: r.message || []
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ class TestAutoRepeat(unittest.TestCase):
|
|||
fields=['docstatus'],
|
||||
limit=1
|
||||
)
|
||||
self.assertEquals(docnames[0].docstatus, 1)
|
||||
self.assertEqual(docnames[0].docstatus, 1)
|
||||
|
||||
|
||||
def make_auto_repeat(**args):
|
||||
|
|
|
|||
|
|
@ -42,8 +42,6 @@ def get_bootinfo():
|
|||
bootinfo.user_info = get_user_info()
|
||||
bootinfo.sid = frappe.session['sid']
|
||||
|
||||
bootinfo.user_groups = frappe.get_all('User Group', pluck="name")
|
||||
|
||||
bootinfo.modules = {}
|
||||
bootinfo.module_list = []
|
||||
load_desktop_data(bootinfo)
|
||||
|
|
|
|||
|
|
@ -327,7 +327,7 @@ def make_asset_dirs(make_copy=False, restore=False):
|
|||
except OSError:
|
||||
print("Cannot link {} to {}".format(source, target))
|
||||
else:
|
||||
# warnings.warn('Source {source} does not exist.'.format(source = source))
|
||||
warnings.warn('Source {source} does not exist.'.format(source = source))
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
|||
32
frappe/change_log/v13/v13_2_0.md
Normal file
32
frappe/change_log/v13/v13_2_0.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Version 13.2.0 Release Notes
|
||||
|
||||
### Features & Enhancements
|
||||
|
||||
- Add option to mention a group of users ([#12844](https://github.com/frappe/frappe/pull/12844))
|
||||
- Copy DocType / documents across sites ([#12872](https://github.com/frappe/frappe/pull/12872))
|
||||
- Scheduler log in notifications ([#1135](https://github.com/frappe/frappe/pull/1135))
|
||||
- Add Enable/Disable Webhook via Check Field ([#12842](https://github.com/frappe/frappe/pull/12842))
|
||||
- Allow query/custom reports to save custom data in the json field ([#12534](https://github.com/frappe/frappe/pull/12534))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Load server translations in boot (backport #12848) ([#12852](https://github.com/frappe/frappe/pull/12852))
|
||||
- Allow to override dashboard chart properties type/color ([#12846](https://github.com/frappe/frappe/pull/12846))
|
||||
- Multi-column paste in grid ([#12861](https://github.com/frappe/frappe/pull/12861))
|
||||
- Add log_error and FrappeClient to restricted python ([#12857](https://github.com/frappe/frappe/pull/12857))
|
||||
- Redirect Web Form user directly to success URL, if no amount is due ([#12661](https://github.com/frappe/frappe/pull/12661))
|
||||
- Attachment pill lock icon redirects to File ([#12864](https://github.com/frappe/frappe/pull/12864))
|
||||
- Redirect Web Form user directly to success URL, if no amount is due (backport #12661) ([#12856](https://github.com/frappe/frappe/pull/12856))
|
||||
- Remove events to redraw charts ([#12973](https://github.com/frappe/frappe/pull/12973))
|
||||
- Don't allow user to remove/change data source file in data import ([#12827](https://github.com/frappe/frappe/pull/12827))
|
||||
- Load server translations in boot ([#12848](https://github.com/frappe/frappe/pull/12848))
|
||||
- Newly created Workspace not being accessible unless a shortcut u… ([#12866](https://github.com/frappe/frappe/pull/12866))
|
||||
- Currency labels in grids ([#12974](https://github.com/frappe/frappe/pull/12974))
|
||||
- Handle error while session start ([#12933](https://github.com/frappe/frappe/pull/12933))
|
||||
- Add field type check in custom field validation ([#12858](https://github.com/frappe/frappe/pull/12858))
|
||||
- Make language select optional and fix breakpoint issues ([#12860](https://github.com/frappe/frappe/pull/12860))
|
||||
- Form Dashboard reference link ([#12945](https://github.com/frappe/frappe/pull/12945))
|
||||
- Invalid HTML generated by the base template ([#12953](https://github.com/frappe/frappe/pull/12953))
|
||||
- Default values were not triggering change event ([#12975](https://github.com/frappe/frappe/pull/12975))
|
||||
- Make strings translatable ([#12877](https://github.com/frappe/frappe/pull/12877))
|
||||
- Added build-message-files command ([#12950](https://github.com/frappe/frappe/pull/12950))
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
# imports - standard imports
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
# imports - third party imports
|
||||
import click
|
||||
|
|
@ -202,10 +203,13 @@ def install_app(context, apps):
|
|||
|
||||
|
||||
@click.command("list-apps")
|
||||
@click.option("--format", "-f", type=click.Choice(["text", "json"]), default="text")
|
||||
@pass_context
|
||||
def list_apps(context):
|
||||
def list_apps(context, format):
|
||||
"List apps in site"
|
||||
|
||||
summary_dict = {}
|
||||
|
||||
def fix_whitespaces(text):
|
||||
if site == context.sites[-1]:
|
||||
text = text.rstrip()
|
||||
|
|
@ -234,18 +238,23 @@ def list_apps(context):
|
|||
]
|
||||
applications_summary = "\n".join(installed_applications)
|
||||
summary = f"{site_title}\n{applications_summary}\n"
|
||||
summary_dict[site] = [app.app_name for app in apps]
|
||||
|
||||
else:
|
||||
applications_summary = "\n".join(frappe.get_installed_apps())
|
||||
installed_applications = frappe.get_installed_apps()
|
||||
applications_summary = "\n".join(installed_applications)
|
||||
summary = f"{site_title}\n{applications_summary}\n"
|
||||
summary_dict[site] = installed_applications
|
||||
|
||||
summary = fix_whitespaces(summary)
|
||||
|
||||
if applications_summary and summary:
|
||||
if format == "text" and applications_summary and summary:
|
||||
print(summary)
|
||||
|
||||
frappe.destroy()
|
||||
|
||||
if format == "json":
|
||||
click.echo(frappe.as_json(summary_dict))
|
||||
|
||||
@click.command('add-system-manager')
|
||||
@click.argument('email')
|
||||
|
|
@ -547,7 +556,7 @@ def move(dest_dir, site):
|
|||
site_dump_exists = os.path.exists(final_new_path)
|
||||
count = int(count or 0) + 1
|
||||
|
||||
os.rename(old_path, final_new_path)
|
||||
shutil.move(old_path, final_new_path)
|
||||
frappe.destroy()
|
||||
return final_new_path
|
||||
|
||||
|
|
|
|||
|
|
@ -96,22 +96,54 @@ def destroy_all_sessions(context, reason=None):
|
|||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('show-config')
|
||||
@click.option("--format", "-f", type=click.Choice(["text", "json"]), default="text")
|
||||
@pass_context
|
||||
def show_config(context):
|
||||
"print configuration file"
|
||||
print("\t\033[92m{:<50}\033[0m \033[92m{:<15}\033[0m".format('Config','Value'))
|
||||
sites_path = os.path.join(frappe.utils.get_bench_path(), 'sites')
|
||||
site_path = context.sites[0]
|
||||
configuration = frappe.get_site_config(sites_path=sites_path, site_path=site_path)
|
||||
print_config(configuration)
|
||||
def show_config(context, format):
|
||||
"Print configuration file to STDOUT in speified format"
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
def print_config(config):
|
||||
for conf, value in config.items():
|
||||
if isinstance(value, dict):
|
||||
print_config(value)
|
||||
else:
|
||||
print("\t{:<50} {:<15}".format(conf, value))
|
||||
sites_config = {}
|
||||
sites_path = os.getcwd()
|
||||
|
||||
from frappe.utils.commands import render_table
|
||||
|
||||
def transform_config(config, prefix=None):
|
||||
prefix = f"{prefix}." if prefix else ""
|
||||
site_config = []
|
||||
|
||||
for conf, value in config.items():
|
||||
if isinstance(value, dict):
|
||||
site_config += transform_config(value, prefix=f"{prefix}{conf}")
|
||||
else:
|
||||
log_value = json.dumps(value) if isinstance(value, list) else value
|
||||
site_config += [[f"{prefix}{conf}", log_value]]
|
||||
|
||||
return site_config
|
||||
|
||||
for site in context.sites:
|
||||
frappe.init(site)
|
||||
|
||||
if len(context.sites) != 1 and format == "text":
|
||||
if context.sites.index(site) != 0:
|
||||
click.echo()
|
||||
click.secho(f"Site {site}", fg="yellow")
|
||||
|
||||
configuration = frappe.get_site_config(sites_path=sites_path, site_path=site)
|
||||
|
||||
if format == "text":
|
||||
data = transform_config(configuration)
|
||||
data.insert(0, ['Config','Value'])
|
||||
render_table(data)
|
||||
|
||||
if format == "json":
|
||||
sites_config[site] = configuration
|
||||
|
||||
frappe.destroy()
|
||||
|
||||
if format == "json":
|
||||
click.echo(frappe.as_json(sites_config))
|
||||
|
||||
|
||||
@click.command('reset-perms')
|
||||
|
|
@ -470,6 +502,7 @@ def console(context):
|
|||
locals()[app] = __import__(app)
|
||||
except ModuleNotFoundError:
|
||||
failed_to_import.append(app)
|
||||
all_apps.remove(app)
|
||||
|
||||
print("Apps in this namespace:\n{}".format(", ".join(all_apps)))
|
||||
if failed_to_import:
|
||||
|
|
@ -520,7 +553,7 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), profile=Fal
|
|||
|
||||
# Generate coverage report only for app that is being tested
|
||||
source_path = os.path.join(get_bench_path(), 'apps', app or 'frappe')
|
||||
cov = Coverage(source=[source_path], omit=[
|
||||
omit=[
|
||||
'*.html',
|
||||
'*.js',
|
||||
'*.xml',
|
||||
|
|
@ -530,7 +563,12 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), profile=Fal
|
|||
'*.vue',
|
||||
'*/doctype/*/*_dashboard.py',
|
||||
'*/patches/*'
|
||||
])
|
||||
]
|
||||
|
||||
if not app or app == 'frappe':
|
||||
omit.append('*/commands/*')
|
||||
|
||||
cov = Coverage(source=[source_path], omit=omit)
|
||||
cov.start()
|
||||
|
||||
ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests,
|
||||
|
|
@ -652,20 +690,27 @@ def make_app(destination, app_name):
|
|||
@click.command('set-config')
|
||||
@click.argument('key')
|
||||
@click.argument('value')
|
||||
@click.option('-g', '--global', 'global_', is_flag = True, default = False, help = 'Set Global Site Config')
|
||||
@click.option('--as-dict', is_flag=True, default=False)
|
||||
@click.option('-g', '--global', 'global_', is_flag=True, default=False, help='Set value in bench config')
|
||||
@click.option('-p', '--parse', is_flag=True, default=False, help='Evaluate as Python Object')
|
||||
@click.option('--as-dict', is_flag=True, default=False, help='Legacy: Evaluate as Python Object')
|
||||
@pass_context
|
||||
def set_config(context, key, value, global_ = False, as_dict=False):
|
||||
def set_config(context, key, value, global_=False, parse=False, as_dict=False):
|
||||
"Insert/Update a value in site_config.json"
|
||||
from frappe.installer import update_site_config
|
||||
import ast
|
||||
|
||||
if as_dict:
|
||||
from frappe.utils.commands import warn
|
||||
warn("--as-dict will be deprecated in v14. Use --parse instead", category=PendingDeprecationWarning)
|
||||
parse = as_dict
|
||||
|
||||
if parse:
|
||||
import ast
|
||||
value = ast.literal_eval(value)
|
||||
|
||||
if global_:
|
||||
sites_path = os.getcwd() # big assumption.
|
||||
sites_path = os.getcwd()
|
||||
common_site_config_path = os.path.join(sites_path, 'common_site_config.json')
|
||||
update_site_config(key, value, validate = False, site_config_path = common_site_config_path)
|
||||
update_site_config(key, value, validate=False, site_config_path=common_site_config_path)
|
||||
else:
|
||||
for site in context.sites:
|
||||
frappe.init(site=site)
|
||||
|
|
@ -722,50 +767,6 @@ def rebuild_global_search(context, static_pages=False):
|
|||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('auto-deploy')
|
||||
@click.argument('app')
|
||||
@click.option('--migrate', is_flag=True, default=False, help='Migrate after pulling')
|
||||
@click.option('--restart', is_flag=True, default=False, help='Restart after migration')
|
||||
@click.option('--remote', default='upstream', help='Remote, default is "upstream"')
|
||||
@pass_context
|
||||
def auto_deploy(context, app, migrate=False, restart=False, remote='upstream'):
|
||||
'''Pull and migrate sites that have new version'''
|
||||
from frappe.utils.gitutils import get_app_branch
|
||||
from frappe.utils import get_sites
|
||||
|
||||
branch = get_app_branch(app)
|
||||
app_path = frappe.get_app_path(app)
|
||||
|
||||
# fetch
|
||||
subprocess.check_output(['git', 'fetch', remote, branch], cwd = app_path)
|
||||
|
||||
# get diff
|
||||
if subprocess.check_output(['git', 'diff', '{0}..{1}/{0}'.format(branch, remote)], cwd = app_path):
|
||||
print('Updates found for {0}'.format(app))
|
||||
if app=='frappe':
|
||||
# run bench update
|
||||
import shlex
|
||||
subprocess.check_output(shlex.split('bench update --no-backup'), cwd = '..')
|
||||
else:
|
||||
updated = False
|
||||
subprocess.check_output(['git', 'pull', '--rebase', remote, branch],
|
||||
cwd = app_path)
|
||||
# find all sites with that app
|
||||
for site in get_sites():
|
||||
frappe.init(site)
|
||||
if app in frappe.get_installed_apps():
|
||||
print('Updating {0}'.format(site))
|
||||
updated = True
|
||||
subprocess.check_output(['bench', '--site', site, 'clear-cache'], cwd = '..')
|
||||
if migrate:
|
||||
subprocess.check_output(['bench', '--site', site, 'migrate'], cwd = '..')
|
||||
frappe.destroy()
|
||||
|
||||
if updated or restart:
|
||||
subprocess.check_output(['bench', 'restart'], cwd = '..')
|
||||
else:
|
||||
print('No Updates')
|
||||
|
||||
|
||||
commands = [
|
||||
build,
|
||||
|
|
|
|||
|
|
@ -65,12 +65,12 @@ class TestActivityLog(unittest.TestCase):
|
|||
frappe.local.login_manager = LoginManager()
|
||||
|
||||
auth_log = self.get_auth_log()
|
||||
self.assertEquals(auth_log.status, 'Success')
|
||||
self.assertEqual(auth_log.status, 'Success')
|
||||
|
||||
# test user logout log
|
||||
frappe.local.login_manager.logout()
|
||||
auth_log = self.get_auth_log(operation='Logout')
|
||||
self.assertEquals(auth_log.status, 'Success')
|
||||
self.assertEqual(auth_log.status, 'Success')
|
||||
|
||||
# test invalid login
|
||||
frappe.form_dict.update({ 'pwd': 'password' })
|
||||
|
|
|
|||
|
|
@ -272,22 +272,13 @@ def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None)
|
|||
doc.attachments.append(a)
|
||||
|
||||
def set_incoming_outgoing_accounts(doc):
|
||||
doc.incoming_email_account = doc.outgoing_email_account = None
|
||||
from frappe.email.doctype.email_account.email_account import EmailAccount
|
||||
incoming_email_account = EmailAccount.find_incoming(
|
||||
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype)
|
||||
doc.incoming_email_account = incoming_email_account.email_id if incoming_email_account else None
|
||||
|
||||
if not doc.incoming_email_account and doc.sender:
|
||||
doc.incoming_email_account = frappe.db.get_value("Email Account",
|
||||
{"email_id": doc.sender, "enable_incoming": 1}, "email_id")
|
||||
|
||||
if not doc.incoming_email_account and doc.reference_doctype:
|
||||
doc.incoming_email_account = frappe.db.get_value("Email Account",
|
||||
{"append_to": doc.reference_doctype, }, "email_id")
|
||||
|
||||
if not doc.incoming_email_account:
|
||||
doc.incoming_email_account = frappe.db.get_value("Email Account",
|
||||
{"default_incoming": 1, "enable_incoming": 1}, "email_id")
|
||||
|
||||
doc.outgoing_email_account = frappe.email.smtp.get_outgoing_email_account(raise_exception_not_set=False,
|
||||
append_to=doc.doctype, sender=doc.sender)
|
||||
doc.outgoing_email_account = EmailAccount.find_outgoing(
|
||||
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype)
|
||||
|
||||
if doc.sent_or_received == "Sent":
|
||||
doc.db_set("email_account", doc.outgoing_email_account.name)
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ class Importer:
|
|||
return updated_doc
|
||||
else:
|
||||
# throw if no changes
|
||||
frappe.throw("No changes to update")
|
||||
frappe.throw(_("No changes to update"))
|
||||
|
||||
def get_eta(self, current, total, processing_time):
|
||||
self.last_eta = getattr(self, "last_eta", 0)
|
||||
|
|
@ -319,7 +319,7 @@ class ImportFile:
|
|||
self.warnings = []
|
||||
|
||||
self.file_doc = self.file_path = self.google_sheets_url = None
|
||||
if isinstance(file, frappe.string_types):
|
||||
if isinstance(file, str):
|
||||
if frappe.db.exists("File", {"file_url": file}):
|
||||
self.file_doc = frappe.get_doc("File", {"file_url": file})
|
||||
elif "docs.google.com/spreadsheets" in file:
|
||||
|
|
@ -626,7 +626,7 @@ class Row:
|
|||
return
|
||||
elif df.fieldtype in ["Date", "Datetime"]:
|
||||
value = self.get_date(value, col)
|
||||
if isinstance(value, frappe.string_types):
|
||||
if isinstance(value, str):
|
||||
# value was not parsed as datetime object
|
||||
self.warnings.append(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) {year}, {app_publisher} and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
{base_class_import}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) {year}, {app_publisher} and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
|
|
|||
|
|
@ -662,4 +662,4 @@
|
|||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,12 +83,61 @@ class DocType(Document):
|
|||
if not self.is_new():
|
||||
self.before_update = frappe.get_doc('DocType', self.name)
|
||||
self.setup_fields_to_fetch()
|
||||
self.validate_field_name_conflicts()
|
||||
|
||||
check_email_append_to(self)
|
||||
|
||||
if self.default_print_format and not self.custom:
|
||||
frappe.throw(_('Standard DocType cannot have default print format, use Customize Form'))
|
||||
|
||||
if frappe.conf.get('developer_mode'):
|
||||
self.owner = 'Administrator'
|
||||
self.modified_by = 'Administrator'
|
||||
|
||||
def validate_field_name_conflicts(self):
|
||||
"""Check if field names dont conflict with controller properties and methods"""
|
||||
core_doctypes = [
|
||||
"Custom DocPerm",
|
||||
"DocPerm",
|
||||
"Custom Field",
|
||||
"Customize Form Field",
|
||||
"DocField",
|
||||
]
|
||||
|
||||
if self.name in core_doctypes:
|
||||
return
|
||||
|
||||
from frappe.model.base_document import get_controller
|
||||
|
||||
try:
|
||||
controller = get_controller(self.name)
|
||||
except ImportError:
|
||||
controller = Document
|
||||
|
||||
available_objects = {x for x in dir(controller) if isinstance(x, str)}
|
||||
property_set = {
|
||||
x for x in available_objects if isinstance(getattr(controller, x, None), property)
|
||||
}
|
||||
method_set = {
|
||||
x for x in available_objects if x not in property_set and callable(getattr(controller, x, None))
|
||||
}
|
||||
|
||||
for docfield in self.get("fields") or []:
|
||||
conflict_type = None
|
||||
field = docfield.fieldname
|
||||
field_label = docfield.label or docfield.fieldname
|
||||
|
||||
if docfield.fieldname in method_set:
|
||||
conflict_type = "controller method"
|
||||
if docfield.fieldname in property_set:
|
||||
conflict_type = "class property"
|
||||
|
||||
if conflict_type:
|
||||
frappe.throw(
|
||||
_("Fieldname '{0}' conflicting with a {1} of the name {2} in {3}")
|
||||
.format(field_label, conflict_type, field, self.name)
|
||||
)
|
||||
|
||||
def after_insert(self):
|
||||
# clear user cache so that on the next reload this doctype is included in boot
|
||||
clear_user_cache(frappe.session.user)
|
||||
|
|
@ -1174,11 +1223,19 @@ def make_module_and_roles(doc, perm_fieldname="permissions"):
|
|||
else:
|
||||
raise
|
||||
|
||||
def check_if_fieldname_conflicts_with_methods(doctype, fieldname):
|
||||
doc = frappe.get_doc({"doctype": doctype})
|
||||
method_list = [method for method in dir(doc) if isinstance(method, str) and callable(getattr(doc, method))]
|
||||
def check_fieldname_conflicts(doctype, fieldname):
|
||||
"""Checks if fieldname conflicts with methods or properties"""
|
||||
|
||||
if fieldname in method_list:
|
||||
doc = frappe.get_doc({"doctype": doctype})
|
||||
available_objects = [x for x in dir(doc) if isinstance(x, str)]
|
||||
property_list = [
|
||||
x for x in available_objects if isinstance(getattr(type(doc), x, None), property)
|
||||
]
|
||||
method_list = [
|
||||
x for x in available_objects if x not in property_list and callable(getattr(doc, x))
|
||||
]
|
||||
|
||||
if fieldname in method_list + property_list:
|
||||
frappe.throw(_("Fieldname {0} conflicting with meta object").format(fieldname))
|
||||
|
||||
def clear_linked_doctype_cache():
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
# Copyright (c) 2013, {app_publisher} and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
|
||||
def execute(filters=None):
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ class TestReport(unittest.TestCase):
|
|||
else:
|
||||
report = frappe.get_doc('Report', 'Test Report')
|
||||
|
||||
self.assertNotEquals(report.is_permitted(), True)
|
||||
self.assertNotEqual(report.is_permitted(), True)
|
||||
frappe.set_user('Administrator')
|
||||
|
||||
# test for the `_format` method if report data doesn't have sort_by parameter
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ class User(Document):
|
|||
|
||||
def after_insert(self):
|
||||
create_notification_settings(self.name)
|
||||
frappe.cache().delete_key('users_for_mentions')
|
||||
|
||||
def validate(self):
|
||||
self.check_demo()
|
||||
|
|
@ -129,6 +130,9 @@ class User(Document):
|
|||
if self.time_zone:
|
||||
frappe.defaults.set_default("time_zone", self.time_zone, self.name)
|
||||
|
||||
if self.has_value_changed('allow_in_mentions') or self.has_value_changed('user_type'):
|
||||
frappe.cache().delete_key('users_for_mentions')
|
||||
|
||||
def has_website_permission(self, ptype, user, verbose=False):
|
||||
"""Returns true if current user is the session user"""
|
||||
return self.name == frappe.session.user
|
||||
|
|
@ -389,6 +393,9 @@ class User(Document):
|
|||
# delete notification settings
|
||||
frappe.delete_doc("Notification Settings", self.name, ignore_permissions=True)
|
||||
|
||||
if self.get('allow_in_mentions'):
|
||||
frappe.cache().delete_key('users_for_mentions')
|
||||
|
||||
|
||||
def before_rename(self, old_name, new_name, merge=False):
|
||||
self.check_demo()
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import frappe
|
|||
|
||||
class UserGroup(Document):
|
||||
def after_insert(self):
|
||||
frappe.publish_realtime('user_group_added', self.name)
|
||||
frappe.cache().delete_key('user_groups')
|
||||
|
||||
def on_trash(self):
|
||||
frappe.publish_realtime('user_group_deleted', self.name)
|
||||
frappe.cache().delete_key('user_groups')
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class TestUserPermission(unittest.TestCase):
|
|||
frappe.set_user('test_user_perm1@example.com')
|
||||
doc = frappe.new_doc("Blog Post")
|
||||
|
||||
self.assertEquals(doc.blog_category, 'general')
|
||||
self.assertEqual(doc.blog_category, 'general')
|
||||
frappe.set_user('Administrator')
|
||||
|
||||
def test_apply_to_all(self):
|
||||
|
|
@ -54,7 +54,7 @@ class TestUserPermission(unittest.TestCase):
|
|||
user = create_user('test_bulk_creation_update@example.com')
|
||||
param = get_params(user, 'User', user.name)
|
||||
is_created = add_user_permissions(param)
|
||||
self.assertEquals(is_created, 1)
|
||||
self.assertEqual(is_created, 1)
|
||||
|
||||
def test_for_apply_to_all_on_update_from_apply_all(self):
|
||||
user = create_user('test_bulk_creation_update@example.com')
|
||||
|
|
@ -63,11 +63,11 @@ class TestUserPermission(unittest.TestCase):
|
|||
# Initially create User Permission document with apply_to_all checked
|
||||
is_created = add_user_permissions(param)
|
||||
|
||||
self.assertEquals(is_created, 1)
|
||||
self.assertEqual(is_created, 1)
|
||||
is_created = add_user_permissions(param)
|
||||
|
||||
# User Permission should not be changed
|
||||
self.assertEquals(is_created, 0)
|
||||
self.assertEqual(is_created, 0)
|
||||
|
||||
def test_for_applicable_on_update_from_apply_to_all(self):
|
||||
''' Update User Permission from all to some applicable Doctypes'''
|
||||
|
|
@ -77,7 +77,7 @@ class TestUserPermission(unittest.TestCase):
|
|||
# Initially create User Permission document with apply_to_all checked
|
||||
is_created = add_user_permissions(get_params(user, 'User', user.name))
|
||||
|
||||
self.assertEquals(is_created, 1)
|
||||
self.assertEqual(is_created, 1)
|
||||
|
||||
is_created = add_user_permissions(param)
|
||||
frappe.db.commit()
|
||||
|
|
@ -92,7 +92,7 @@ class TestUserPermission(unittest.TestCase):
|
|||
# Check that User Permissions for applicable is created
|
||||
self.assertIsNotNone(is_created_applicable_first)
|
||||
self.assertIsNotNone(is_created_applicable_second)
|
||||
self.assertEquals(is_created, 1)
|
||||
self.assertEqual(is_created, 1)
|
||||
|
||||
def test_for_apply_to_all_on_update_from_applicable(self):
|
||||
''' Update User Permission from some to all applicable Doctypes'''
|
||||
|
|
@ -102,7 +102,7 @@ class TestUserPermission(unittest.TestCase):
|
|||
# create User permissions that with applicable
|
||||
is_created = add_user_permissions(get_params(user, 'User', user.name, applicable = ["Chat Room", "Chat Message"]))
|
||||
|
||||
self.assertEquals(is_created, 1)
|
||||
self.assertEqual(is_created, 1)
|
||||
|
||||
is_created = add_user_permissions(param)
|
||||
is_created_apply_to_all = frappe.db.exists("User Permission", get_exists_param(user))
|
||||
|
|
@ -115,7 +115,7 @@ class TestUserPermission(unittest.TestCase):
|
|||
# Check that all User Permission with applicable is removed
|
||||
self.assertIsNone(removed_applicable_first)
|
||||
self.assertIsNone(removed_applicable_second)
|
||||
self.assertEquals(is_created, 1)
|
||||
self.assertEqual(is_created, 1)
|
||||
|
||||
def test_user_perm_for_nested_doctype(self):
|
||||
"""Test if descendants' visibility is controlled for a nested DocType."""
|
||||
|
|
@ -183,7 +183,7 @@ class TestUserPermission(unittest.TestCase):
|
|||
|
||||
# User perm is created on ToDo but for doctype Assignment Rule only
|
||||
# it should not have impact on Doc A
|
||||
self.assertEquals(new_doc.doc, "ToDo")
|
||||
self.assertEqual(new_doc.doc, "ToDo")
|
||||
|
||||
frappe.set_user('Administrator')
|
||||
remove_applicable(["Assignment Rule"], "new_doc_test@example.com", "DocType", "ToDo")
|
||||
|
|
@ -228,7 +228,7 @@ class TestUserPermission(unittest.TestCase):
|
|||
|
||||
# User perm is created on ToDo but for doctype Assignment Rule only
|
||||
# it should not have impact on Doc A
|
||||
self.assertEquals(new_doc.doc, "ToDo")
|
||||
self.assertEqual(new_doc.doc, "ToDo")
|
||||
|
||||
frappe.set_user('Administrator')
|
||||
clear_session_defaults()
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ def clear_user_permissions(user, for_doctype):
|
|||
def add_user_permissions(data):
|
||||
''' Add and update the user permissions '''
|
||||
frappe.only_for('System Manager')
|
||||
if isinstance(data, frappe.string_types):
|
||||
if isinstance(data, str):
|
||||
data = json.loads(data)
|
||||
data = frappe._dict(data)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
frappe.pages['recorder'].on_page_load = function(wrapper) {
|
||||
frappe.ui.make_app_page({
|
||||
parent: wrapper,
|
||||
title: 'Recorder',
|
||||
title: __('Recorder'),
|
||||
single_column: true,
|
||||
card_layout: true
|
||||
});
|
||||
|
|
|
|||
|
|
@ -64,18 +64,19 @@ class CustomField(Document):
|
|||
self.translatable = 0
|
||||
|
||||
if not self.flags.ignore_validate:
|
||||
from frappe.core.doctype.doctype.doctype import check_if_fieldname_conflicts_with_methods
|
||||
check_if_fieldname_conflicts_with_methods(self.dt, self.fieldname)
|
||||
from frappe.core.doctype.doctype.doctype import check_fieldname_conflicts
|
||||
check_fieldname_conflicts(self.dt, self.fieldname)
|
||||
|
||||
def on_update(self):
|
||||
frappe.clear_cache(doctype=self.dt)
|
||||
if not frappe.flags.in_setup_wizard:
|
||||
frappe.clear_cache(doctype=self.dt)
|
||||
if not self.flags.ignore_validate:
|
||||
# validate field
|
||||
from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype
|
||||
validate_fields_for_doctype(self.dt)
|
||||
|
||||
# update the schema
|
||||
if not frappe.db.get_value('DocType', self.dt, 'issingle'):
|
||||
if not frappe.db.get_value('DocType', self.dt, 'issingle') and not frappe.flags.in_setup_wizard:
|
||||
frappe.db.updatedb(self.dt)
|
||||
|
||||
def on_trash(self):
|
||||
|
|
@ -144,6 +145,10 @@ def create_custom_fields(custom_fields, ignore_validate = False, update=True):
|
|||
'''Add / update multiple custom fields
|
||||
|
||||
:param custom_fields: example `{'Sales Invoice': [dict(fieldname='test')]}`'''
|
||||
|
||||
if not ignore_validate and frappe.flags.in_setup_wizard:
|
||||
ignore_validate = True
|
||||
|
||||
for doctype, fields in custom_fields.items():
|
||||
if isinstance(fields, dict):
|
||||
# only one field
|
||||
|
|
@ -163,6 +168,10 @@ def create_custom_fields(custom_fields, ignore_validate = False, update=True):
|
|||
custom_field.update(df)
|
||||
custom_field.save()
|
||||
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
frappe.db.updatedb(doctype)
|
||||
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_custom_field(doctype, df):
|
||||
|
|
|
|||
|
|
@ -278,6 +278,7 @@
|
|||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"depends_on": "doc_type",
|
||||
"fieldname": "naming_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Naming"
|
||||
|
|
@ -287,6 +288,16 @@
|
|||
"fieldname": "autoname",
|
||||
"fieldtype": "Data",
|
||||
"label": "Auto Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "default_email_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Email Template",
|
||||
"options": "Email Template"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_26",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
|
|
@ -295,7 +306,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-22 12:27:15.462727",
|
||||
"modified": "2021-04-29 21:21:06.476372",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form",
|
||||
|
|
@ -316,4 +327,4 @@
|
|||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -47,64 +47,64 @@ class TestCustomizeForm(unittest.TestCase):
|
|||
self.assertEqual(len(d.get("fields")), 0)
|
||||
|
||||
d = self.get_customize_form("Event")
|
||||
self.assertEquals(d.doc_type, "Event")
|
||||
self.assertEquals(len(d.get("fields")), 36)
|
||||
self.assertEqual(d.doc_type, "Event")
|
||||
self.assertEqual(len(d.get("fields")), 36)
|
||||
|
||||
d = self.get_customize_form("Event")
|
||||
self.assertEquals(d.doc_type, "Event")
|
||||
self.assertEqual(d.doc_type, "Event")
|
||||
|
||||
self.assertEqual(len(d.get("fields")),
|
||||
len(frappe.get_doc("DocType", d.doc_type).fields) + 1)
|
||||
self.assertEquals(d.get("fields")[-1].fieldname, "test_custom_field")
|
||||
self.assertEquals(d.get("fields", {"fieldname": "event_type"})[0].in_list_view, 1)
|
||||
self.assertEqual(d.get("fields")[-1].fieldname, "test_custom_field")
|
||||
self.assertEqual(d.get("fields", {"fieldname": "event_type"})[0].in_list_view, 1)
|
||||
|
||||
return d
|
||||
|
||||
def test_save_customization_property(self):
|
||||
d = self.get_customize_form("Event")
|
||||
self.assertEquals(frappe.db.get_value("Property Setter",
|
||||
self.assertEqual(frappe.db.get_value("Property Setter",
|
||||
{"doc_type": "Event", "property": "allow_copy"}, "value"), None)
|
||||
|
||||
d.allow_copy = 1
|
||||
d.run_method("save_customization")
|
||||
self.assertEquals(frappe.db.get_value("Property Setter",
|
||||
self.assertEqual(frappe.db.get_value("Property Setter",
|
||||
{"doc_type": "Event", "property": "allow_copy"}, "value"), '1')
|
||||
|
||||
d.allow_copy = 0
|
||||
d.run_method("save_customization")
|
||||
self.assertEquals(frappe.db.get_value("Property Setter",
|
||||
self.assertEqual(frappe.db.get_value("Property Setter",
|
||||
{"doc_type": "Event", "property": "allow_copy"}, "value"), None)
|
||||
|
||||
def test_save_customization_field_property(self):
|
||||
d = self.get_customize_form("Event")
|
||||
self.assertEquals(frappe.db.get_value("Property Setter",
|
||||
self.assertEqual(frappe.db.get_value("Property Setter",
|
||||
{"doc_type": "Event", "property": "reqd", "field_name": "repeat_this_event"}, "value"), None)
|
||||
|
||||
repeat_this_event_field = d.get("fields", {"fieldname": "repeat_this_event"})[0]
|
||||
repeat_this_event_field.reqd = 1
|
||||
d.run_method("save_customization")
|
||||
self.assertEquals(frappe.db.get_value("Property Setter",
|
||||
self.assertEqual(frappe.db.get_value("Property Setter",
|
||||
{"doc_type": "Event", "property": "reqd", "field_name": "repeat_this_event"}, "value"), '1')
|
||||
|
||||
repeat_this_event_field = d.get("fields", {"fieldname": "repeat_this_event"})[0]
|
||||
repeat_this_event_field.reqd = 0
|
||||
d.run_method("save_customization")
|
||||
self.assertEquals(frappe.db.get_value("Property Setter",
|
||||
self.assertEqual(frappe.db.get_value("Property Setter",
|
||||
{"doc_type": "Event", "property": "reqd", "field_name": "repeat_this_event"}, "value"), None)
|
||||
|
||||
def test_save_customization_custom_field_property(self):
|
||||
d = self.get_customize_form("Event")
|
||||
self.assertEquals(frappe.db.get_value("Custom Field", "Event-test_custom_field", "reqd"), 0)
|
||||
self.assertEqual(frappe.db.get_value("Custom Field", "Event-test_custom_field", "reqd"), 0)
|
||||
|
||||
custom_field = d.get("fields", {"fieldname": "test_custom_field"})[0]
|
||||
custom_field.reqd = 1
|
||||
d.run_method("save_customization")
|
||||
self.assertEquals(frappe.db.get_value("Custom Field", "Event-test_custom_field", "reqd"), 1)
|
||||
self.assertEqual(frappe.db.get_value("Custom Field", "Event-test_custom_field", "reqd"), 1)
|
||||
|
||||
custom_field = d.get("fields", {"is_custom_field": True})[0]
|
||||
custom_field.reqd = 0
|
||||
d.run_method("save_customization")
|
||||
self.assertEquals(frappe.db.get_value("Custom Field", "Event-test_custom_field", "reqd"), 0)
|
||||
self.assertEqual(frappe.db.get_value("Custom Field", "Event-test_custom_field", "reqd"), 0)
|
||||
|
||||
def test_save_customization_new_field(self):
|
||||
d = self.get_customize_form("Event")
|
||||
|
|
@ -115,14 +115,14 @@ class TestCustomizeForm(unittest.TestCase):
|
|||
"is_custom_field": 1
|
||||
})
|
||||
d.run_method("save_customization")
|
||||
self.assertEquals(frappe.db.get_value("Custom Field",
|
||||
self.assertEqual(frappe.db.get_value("Custom Field",
|
||||
"Event-test_add_custom_field_via_customize_form", "fieldtype"), "Data")
|
||||
|
||||
self.assertEquals(frappe.db.get_value("Custom Field",
|
||||
self.assertEqual(frappe.db.get_value("Custom Field",
|
||||
"Event-test_add_custom_field_via_customize_form", 'insert_after'), last_fieldname)
|
||||
|
||||
frappe.delete_doc("Custom Field", "Event-test_add_custom_field_via_customize_form")
|
||||
self.assertEquals(frappe.db.get_value("Custom Field",
|
||||
self.assertEqual(frappe.db.get_value("Custom Field",
|
||||
"Event-test_add_custom_field_via_customize_form"), None)
|
||||
|
||||
|
||||
|
|
@ -142,7 +142,7 @@ class TestCustomizeForm(unittest.TestCase):
|
|||
d.doc_type = "Event"
|
||||
d.run_method('reset_to_defaults')
|
||||
|
||||
self.assertEquals(d.get("fields", {"fieldname": "repeat_this_event"})[0].in_list_view, 0)
|
||||
self.assertEqual(d.get("fields", {"fieldname": "repeat_this_event"})[0].in_list_view, 0)
|
||||
|
||||
frappe.local.test_objects["Property Setter"] = []
|
||||
make_test_records_for_doctype("Property Setter")
|
||||
|
|
@ -156,7 +156,7 @@ class TestCustomizeForm(unittest.TestCase):
|
|||
d = self.get_customize_form("Event")
|
||||
|
||||
# don't allow for standard fields
|
||||
self.assertEquals(d.get("fields", {"fieldname": "subject"})[0].allow_on_submit or 0, 0)
|
||||
self.assertEqual(d.get("fields", {"fieldname": "subject"})[0].allow_on_submit or 0, 0)
|
||||
|
||||
# allow for custom field
|
||||
self.assertEqual(d.get("fields", {"fieldname": "test_custom_field"})[0].allow_on_submit, 1)
|
||||
|
|
|
|||
|
|
@ -858,7 +858,7 @@ class Database(object):
|
|||
if not datetime:
|
||||
return '0001-01-01 00:00:00.000000'
|
||||
|
||||
if isinstance(datetime, frappe.string_types):
|
||||
if isinstance(datetime, str):
|
||||
if ':' not in datetime:
|
||||
datetime = datetime + ' 00:00:00.000000'
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import warnings
|
||||
|
||||
import pymysql
|
||||
from pymysql.constants import ER, FIELD_TYPE
|
||||
from pymysql.converters import conversions, escape_string
|
||||
|
|
@ -55,7 +53,6 @@ class MariaDBDatabase(Database):
|
|||
}
|
||||
|
||||
def get_connection(self):
|
||||
warnings.filterwarnings('ignore', category=pymysql.Warning)
|
||||
usessl = 0
|
||||
if frappe.conf.db_ssl_ca and frappe.conf.db_ssl_cert and frappe.conf.db_ssl_key:
|
||||
usessl = 1
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import frappe
|
||||
import psycopg2
|
||||
|
|
@ -13,9 +11,9 @@ from frappe.database.postgres.schema import PostgresTable
|
|||
|
||||
# cast decimals as floats
|
||||
DEC2FLOAT = psycopg2.extensions.new_type(
|
||||
psycopg2.extensions.DECIMAL.values,
|
||||
'DEC2FLOAT',
|
||||
lambda value, curs: float(value) if value is not None else None)
|
||||
psycopg2.extensions.DECIMAL.values,
|
||||
'DEC2FLOAT',
|
||||
lambda value, curs: float(value) if value is not None else None)
|
||||
|
||||
psycopg2.extensions.register_type(DEC2FLOAT)
|
||||
|
||||
|
|
@ -65,7 +63,6 @@ class PostgresDatabase(Database):
|
|||
}
|
||||
|
||||
def get_connection(self):
|
||||
# warnings.filterwarnings('ignore', category=psycopg2.Warning)
|
||||
conn = psycopg2.connect("host='{}' dbname='{}' user='{}' password='{}' port={}".format(
|
||||
self.host, self.user, self.user, self.password, self.port
|
||||
))
|
||||
|
|
@ -114,7 +111,7 @@ class PostgresDatabase(Database):
|
|||
if not date:
|
||||
return '0001-01-01'
|
||||
|
||||
if not isinstance(date, frappe.string_types):
|
||||
if not isinstance(date, str):
|
||||
date = date.strftime('%Y-%m-%d')
|
||||
|
||||
return date
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ def enqueue_create_notification(users, doc):
|
|||
|
||||
doc = frappe._dict(doc)
|
||||
|
||||
if isinstance(users, frappe.string_types):
|
||||
if isinstance(users, str):
|
||||
users = [user.strip() for user in users.split(',') if user.strip()]
|
||||
users = list(set(users))
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
.download-backup-card {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
margin-bottom: var(--margin-lg);
|
||||
}
|
||||
|
||||
.download-backup-card:hover {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
frappe.pages['backups'].on_page_load = function(wrapper) {
|
||||
var page = frappe.ui.make_app_page({
|
||||
parent: wrapper,
|
||||
title: 'Download Backups',
|
||||
title: __('Download Backups'),
|
||||
single_column: true
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ def handle_setup_exception(args):
|
|||
frappe.db.rollback()
|
||||
if args:
|
||||
traceback = frappe.get_traceback()
|
||||
print(traceback)
|
||||
for hook in frappe.get_hooks("setup_wizard_exception"):
|
||||
frappe.get_attr(hook)(traceback, args)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
frappe.pages['translation-tool'].on_page_load = function(wrapper) {
|
||||
var page = frappe.ui.make_app_page({
|
||||
parent: wrapper,
|
||||
title: 'Translation Tool',
|
||||
title: __('Translation Tool'),
|
||||
single_column: true,
|
||||
card_layout: true,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
</div>
|
||||
<div class="chart-wrapper performance-heatmap">
|
||||
<div class="null-state">
|
||||
<span>No Data to Show</span>
|
||||
<span>{%=__("No Data to Show") %}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
</div>
|
||||
<div class="chart-wrapper performance-percentage-chart">
|
||||
<div class="null-state">
|
||||
<span>No Data to Show</span>
|
||||
<span>{%=__("No Data to Show") %}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
</div>
|
||||
<div class="chart-wrapper performance-line-chart">
|
||||
<div class="null-state">
|
||||
<span>No Data to Show</span>
|
||||
<span>{%=__("No Data to Show") %}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -41,4 +41,4 @@
|
|||
<div class="recent-activity-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -377,10 +377,17 @@ def handle_duration_fieldtype_values(result, columns):
|
|||
|
||||
if fieldtype == "Duration":
|
||||
for entry in range(0, len(result)):
|
||||
val_in_seconds = result[entry][i]
|
||||
if val_in_seconds:
|
||||
duration_val = format_duration(val_in_seconds)
|
||||
result[entry][i] = duration_val
|
||||
row = result[entry]
|
||||
if isinstance(row, dict):
|
||||
val_in_seconds = row[col.fieldname]
|
||||
if val_in_seconds:
|
||||
duration_val = format_duration(val_in_seconds)
|
||||
row[col.fieldname] = duration_val
|
||||
else:
|
||||
val_in_seconds = row[i]
|
||||
if val_in_seconds:
|
||||
duration_val = format_duration(val_in_seconds)
|
||||
row[i] = duration_val
|
||||
|
||||
return result
|
||||
|
||||
|
|
|
|||
|
|
@ -221,3 +221,37 @@ def validate_and_sanitize_search_inputs(fn, instance, args, kwargs):
|
|||
return []
|
||||
|
||||
return fn(**kwargs)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_names_for_mentions(search_term):
|
||||
users_for_mentions = frappe.cache().get_value('users_for_mentions', get_users_for_mentions)
|
||||
user_groups = frappe.cache().get_value('user_groups', get_user_groups)
|
||||
|
||||
filtered_mentions = []
|
||||
for mention_data in users_for_mentions + user_groups:
|
||||
if search_term.lower() not in mention_data.value.lower():
|
||||
continue
|
||||
|
||||
mention_data['link'] = frappe.utils.get_url_to_form(
|
||||
'User Group' if mention_data.get('is_group') else 'User Profile',
|
||||
mention_data['id']
|
||||
)
|
||||
|
||||
filtered_mentions.append(mention_data)
|
||||
|
||||
return sorted(filtered_mentions, key=lambda d: d['value'])
|
||||
|
||||
def get_users_for_mentions():
|
||||
return frappe.get_all('User',
|
||||
fields=['name as id', 'full_name as value'],
|
||||
filters={
|
||||
'name': ['not in', ('Administrator', 'Guest')],
|
||||
'allowed_in_mentions': True,
|
||||
'user_type': 'System User',
|
||||
})
|
||||
|
||||
def get_user_groups():
|
||||
return frappe.get_all('User Group', fields=['name as id', 'name as value'], update={
|
||||
'is_group': True
|
||||
})
|
||||
|
|
|
|||
|
|
@ -17,14 +17,14 @@ class TestDocumentFollow(unittest.TestCase):
|
|||
|
||||
document_follow.unfollow_document("Event", event_doc.name, user.name)
|
||||
doc = document_follow.follow_document("Event", event_doc.name, user.name)
|
||||
self.assertEquals(doc.user, user.name)
|
||||
self.assertEqual(doc.user, user.name)
|
||||
|
||||
document_follow.send_hourly_updates()
|
||||
|
||||
email_queue_entry_name = frappe.get_all("Email Queue", limit=1)[0].name
|
||||
email_queue_entry_doc = frappe.get_doc("Email Queue", email_queue_entry_name)
|
||||
|
||||
self.assertEquals((email_queue_entry_doc.recipients[0].recipient), user.name)
|
||||
self.assertEqual((email_queue_entry_doc.recipients[0].recipient), user.name)
|
||||
|
||||
self.assertIn(event_doc.doctype, email_queue_entry_doc.message)
|
||||
self.assertIn(event_doc.name, email_queue_entry_doc.message)
|
||||
|
|
|
|||
|
|
@ -8,9 +8,14 @@ import re
|
|||
import json
|
||||
import socket
|
||||
import time
|
||||
from frappe import _
|
||||
import functools
|
||||
|
||||
import email.utils
|
||||
|
||||
from frappe import _, are_emails_muted
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import validate_email_address, cint, cstr, get_datetime, DATE_FORMAT, strip, comma_or, sanitize_html, add_days
|
||||
from frappe.utils import (validate_email_address, cint, cstr, get_datetime,
|
||||
DATE_FORMAT, strip, comma_or, sanitize_html, add_days, parse_addr)
|
||||
from frappe.utils.user import is_system_user
|
||||
from frappe.utils.jinja import render_template
|
||||
from frappe.email.smtp import SMTPServer
|
||||
|
|
@ -21,17 +26,37 @@ from datetime import datetime, timedelta
|
|||
from frappe.desk.form import assign_to
|
||||
from frappe.utils.user import get_system_managers
|
||||
from frappe.utils.background_jobs import enqueue, get_jobs
|
||||
from frappe.core.doctype.communication.email import set_incoming_outgoing_accounts
|
||||
from frappe.utils.html_utils import clean_email_html
|
||||
from frappe.utils.error import raise_error_on_no_output
|
||||
from frappe.email.utils import get_port
|
||||
|
||||
OUTGOING_EMAIL_ACCOUNT_MISSING = _("Please setup default Email Account from Setup > Email > Email Account")
|
||||
|
||||
class SentEmailInInbox(Exception):
|
||||
pass
|
||||
|
||||
class InvalidEmailCredentials(frappe.ValidationError):
|
||||
pass
|
||||
def cache_email_account(cache_name):
|
||||
def decorator_cache_email_account(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper_cache_email_account(*args, **kwargs):
|
||||
if not hasattr(frappe.local, cache_name):
|
||||
setattr(frappe.local, cache_name, {})
|
||||
|
||||
cached_accounts = getattr(frappe.local, cache_name)
|
||||
match_by = list(kwargs.values()) + ['default']
|
||||
matched_accounts = list(filter(None, [cached_accounts.get(key) for key in match_by]))
|
||||
if matched_accounts:
|
||||
return matched_accounts[0]
|
||||
|
||||
matched_accounts = func(*args, **kwargs)
|
||||
cached_accounts.update(matched_accounts or {})
|
||||
return matched_accounts and list(matched_accounts.values())[0]
|
||||
return wrapper_cache_email_account
|
||||
return decorator_cache_email_account
|
||||
|
||||
class EmailAccount(Document):
|
||||
DOCTYPE = 'Email Account'
|
||||
|
||||
def autoname(self):
|
||||
"""Set name as `email_account_name` or make title from Email Address."""
|
||||
if not self.email_account_name:
|
||||
|
|
@ -72,9 +97,8 @@ class EmailAccount(Document):
|
|||
self.get_incoming_server()
|
||||
self.no_failed = 0
|
||||
|
||||
|
||||
if self.enable_outgoing:
|
||||
self.check_smtp()
|
||||
self.validate_smtp_conn()
|
||||
else:
|
||||
if self.enable_incoming or (self.enable_outgoing and not self.no_smtp_authentication):
|
||||
frappe.throw(_("Password is required or select Awaiting Password"))
|
||||
|
|
@ -90,6 +114,13 @@ 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 validate_smtp_conn(self):
|
||||
if not self.smtp_server:
|
||||
frappe.throw(_("SMTP Server is required"))
|
||||
|
||||
server = self.get_smtp_server()
|
||||
return server.session
|
||||
|
||||
def before_save(self):
|
||||
messages = []
|
||||
as_list = 1
|
||||
|
|
@ -151,24 +182,6 @@ class EmailAccount(Document):
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
def check_smtp(self):
|
||||
"""Checks SMTP settings."""
|
||||
if self.enable_outgoing:
|
||||
if not self.smtp_server:
|
||||
frappe.throw(_("{0} is required").format("SMTP Server"))
|
||||
|
||||
server = SMTPServer(
|
||||
login = getattr(self, "login_id", None) or self.email_id,
|
||||
server=self.smtp_server,
|
||||
port=cint(self.smtp_port),
|
||||
use_tls=cint(self.use_tls),
|
||||
use_ssl=cint(self.use_ssl_for_outgoing)
|
||||
)
|
||||
if self.password and not self.no_smtp_authentication:
|
||||
server.password = self.get_password()
|
||||
|
||||
server.sess
|
||||
|
||||
def get_incoming_server(self, in_receive=False, email_sync_rule="UNSEEN"):
|
||||
"""Returns logged in POP3/IMAP connection object."""
|
||||
if frappe.cache().get_value("workers:no-internet") == True:
|
||||
|
|
@ -231,7 +244,7 @@ class EmailAccount(Document):
|
|||
return None
|
||||
|
||||
elif not in_receive and any(map(lambda t: t in message, auth_error_codes)):
|
||||
self.throw_invalid_credentials_exception()
|
||||
SMTPServer.throw_invalid_credentials_exception()
|
||||
else:
|
||||
frappe.throw(cstr(e))
|
||||
|
||||
|
|
@ -249,13 +262,142 @@ class EmailAccount(Document):
|
|||
else:
|
||||
raise
|
||||
|
||||
@property
|
||||
def _password(self):
|
||||
raise_exception = not (self.no_smtp_authentication or frappe.flags.in_test)
|
||||
return self.get_password(raise_exception=raise_exception)
|
||||
|
||||
@property
|
||||
def default_sender(self):
|
||||
return email.utils.formataddr((self.name, self.get("email_id")))
|
||||
|
||||
def is_exists_in_db(self):
|
||||
"""Some of the Email Accounts we create from configs and those doesn't exists in DB.
|
||||
This is is to check the specific email account exists in DB or not.
|
||||
"""
|
||||
return self.find_one_by_filters(name=self.name)
|
||||
|
||||
@classmethod
|
||||
def throw_invalid_credentials_exception(cls):
|
||||
frappe.throw(
|
||||
_("Incorrect email or password. Please check your login credentials."),
|
||||
exc=InvalidEmailCredentials,
|
||||
title=_("Invalid Credentials")
|
||||
)
|
||||
def from_record(cls, record):
|
||||
email_account = frappe.new_doc(cls.DOCTYPE)
|
||||
email_account.update(record)
|
||||
return email_account
|
||||
|
||||
@classmethod
|
||||
def find(cls, name):
|
||||
return frappe.get_doc(cls.DOCTYPE, name)
|
||||
|
||||
@classmethod
|
||||
def find_one_by_filters(cls, **kwargs):
|
||||
name = frappe.db.get_value(cls.DOCTYPE, kwargs)
|
||||
return cls.find(name) if name else None
|
||||
|
||||
@classmethod
|
||||
def find_from_config(cls):
|
||||
config = cls.get_account_details_from_site_config()
|
||||
return cls.from_record(config) if config else None
|
||||
|
||||
@classmethod
|
||||
def create_dummy(cls):
|
||||
return cls.from_record({"sender": "notifications@example.com"})
|
||||
|
||||
@classmethod
|
||||
@raise_error_on_no_output(
|
||||
keep_quiet = lambda: not cint(frappe.get_system_settings('setup_complete')),
|
||||
error_message = OUTGOING_EMAIL_ACCOUNT_MISSING, error_type = frappe.OutgoingEmailError) # noqa
|
||||
@cache_email_account('outgoing_email_account')
|
||||
def find_outgoing(cls, match_by_email=None, match_by_doctype=None, _raise_error=False):
|
||||
"""Find the outgoing Email account to use.
|
||||
|
||||
:param match_by_email: Find account using emailID
|
||||
:param match_by_doctype: Find account by matching `Append To` doctype
|
||||
:param _raise_error: This is used by raise_error_on_no_output decorator to raise error.
|
||||
"""
|
||||
if match_by_email:
|
||||
match_by_email = parse_addr(match_by_email)[1]
|
||||
doc = cls.find_one_by_filters(enable_outgoing=1, email_id=match_by_email)
|
||||
if doc:
|
||||
return {match_by_email: doc}
|
||||
|
||||
if match_by_doctype:
|
||||
doc = cls.find_one_by_filters(enable_outgoing=1, enable_incoming=1, append_to=match_by_doctype)
|
||||
if doc:
|
||||
return {match_by_doctype: doc}
|
||||
|
||||
doc = cls.find_default_outgoing()
|
||||
if doc:
|
||||
return {'default': doc}
|
||||
|
||||
@classmethod
|
||||
def find_default_outgoing(cls):
|
||||
""" Find default outgoing account.
|
||||
"""
|
||||
doc = cls.find_one_by_filters(enable_outgoing=1, default_outgoing=1)
|
||||
doc = doc or cls.find_from_config()
|
||||
return doc or (are_emails_muted() and cls.create_dummy())
|
||||
|
||||
@classmethod
|
||||
def find_incoming(cls, match_by_email=None, match_by_doctype=None):
|
||||
"""Find the incoming Email account to use.
|
||||
:param match_by_email: Find account using emailID
|
||||
:param match_by_doctype: Find account by matching `Append To` doctype
|
||||
"""
|
||||
doc = cls.find_one_by_filters(enable_incoming=1, email_id=match_by_email)
|
||||
if doc:
|
||||
return doc
|
||||
|
||||
doc = cls.find_one_by_filters(enable_incoming=1, append_to=match_by_doctype)
|
||||
if doc:
|
||||
return doc
|
||||
|
||||
doc = cls.find_default_incoming()
|
||||
return doc
|
||||
|
||||
@classmethod
|
||||
def find_default_incoming(cls):
|
||||
doc = cls.find_one_by_filters(enable_incoming=1, default_incoming=1)
|
||||
return doc
|
||||
|
||||
@classmethod
|
||||
def get_account_details_from_site_config(cls):
|
||||
if not frappe.conf.get("mail_server"):
|
||||
return {}
|
||||
|
||||
field_to_conf_name_map = {
|
||||
'smtp_server': {'conf_names': ('mail_server',)},
|
||||
'smtp_port': {'conf_names': ('mail_port',)},
|
||||
'use_tls': {'conf_names': ('use_tls', 'mail_login')},
|
||||
'login_id': {'conf_names': ('mail_login',)},
|
||||
'email_id': {'conf_names': ('auto_email_id', 'mail_login'), 'default': 'notifications@example.com'},
|
||||
'password': {'conf_names': ('mail_password',)},
|
||||
'always_use_account_email_id_as_sender':
|
||||
{'conf_names': ('always_use_account_email_id_as_sender',), 'default': 0},
|
||||
'always_use_account_name_as_sender_name':
|
||||
{'conf_names': ('always_use_account_name_as_sender_name',), 'default': 0},
|
||||
'name': {'conf_names': ('email_sender_name',), 'default': 'Frappe'},
|
||||
'from_site_config': {'default': True}
|
||||
}
|
||||
|
||||
account_details = {}
|
||||
for doc_field_name, d in field_to_conf_name_map.items():
|
||||
conf_names, default = d.get('conf_names') or [], d.get('default')
|
||||
value = [frappe.conf.get(k) for k in conf_names if frappe.conf.get(k)]
|
||||
account_details[doc_field_name] = (value and value[0]) or default
|
||||
return account_details
|
||||
|
||||
def sendmail_config(self):
|
||||
return {
|
||||
'server': self.smtp_server,
|
||||
'port': cint(self.smtp_port),
|
||||
'login': getattr(self, "login_id", None) or self.email_id,
|
||||
'password': self._password,
|
||||
'use_ssl': cint(self.use_ssl_for_outgoing),
|
||||
'use_tls': cint(self.use_tls)
|
||||
}
|
||||
|
||||
def get_smtp_server(self):
|
||||
config = self.sendmail_config()
|
||||
return SMTPServer(**config)
|
||||
|
||||
def handle_incoming_connect_error(self, description):
|
||||
if test_internet():
|
||||
|
|
@ -642,6 +784,8 @@ class EmailAccount(Document):
|
|||
|
||||
def send_auto_reply(self, communication, email):
|
||||
"""Send auto reply if set."""
|
||||
from frappe.core.doctype.communication.email import set_incoming_outgoing_accounts
|
||||
|
||||
if self.enable_auto_reply:
|
||||
set_incoming_outgoing_accounts(communication)
|
||||
|
||||
|
|
@ -653,7 +797,7 @@ class EmailAccount(Document):
|
|||
frappe.sendmail(recipients = [email.from_email],
|
||||
sender = self.email_id,
|
||||
reply_to = communication.incoming_email_account,
|
||||
subject = _("Re: ") + communication.subject,
|
||||
subject = " ".join([_("Re:"), communication.subject]),
|
||||
content = render_template(self.auto_reply_message or "", communication.as_dict()) or \
|
||||
frappe.get_template("templates/emails/auto_reply.html").render(communication.as_dict()),
|
||||
reference_doctype = communication.reference_doctype,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@
|
|||
"incoming_port": "993",
|
||||
"attachment_limit": "1",
|
||||
"smtp_server": "smtp.test.com",
|
||||
"smtp_port": "587"
|
||||
"smtp_port": "587",
|
||||
"password": "password"
|
||||
},
|
||||
{
|
||||
"doctype": "Email Account",
|
||||
|
|
@ -25,6 +26,7 @@
|
|||
"incoming_port": "143",
|
||||
"attachment_limit": "1",
|
||||
"smtp_server": "smtp.test.com",
|
||||
"smtp_port": "587"
|
||||
"smtp_port": "587",
|
||||
"password": "password"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@
|
|||
"unsubscribe_method",
|
||||
"expose_recipients",
|
||||
"attachments",
|
||||
"retry"
|
||||
"retry",
|
||||
"email_account"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -139,13 +140,19 @@
|
|||
"fieldtype": "Int",
|
||||
"label": "Retry",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "email_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Email Account",
|
||||
"options": "Email Account"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-envelope",
|
||||
"idx": 1,
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2020-07-17 15:58:15.369419",
|
||||
"modified": "2021-04-29 06:33:25.191729",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Email Queue",
|
||||
|
|
|
|||
|
|
@ -2,15 +2,26 @@
|
|||
# Copyright (c) 2015, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import traceback
|
||||
import json
|
||||
|
||||
from rq.timeouts import JobTimeoutException
|
||||
import smtplib
|
||||
import quopri
|
||||
from email.parser import Parser
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, safe_encode, task
|
||||
from frappe.model.document import Document
|
||||
from frappe.email.queue import send_one
|
||||
from frappe.utils import now_datetime
|
||||
|
||||
from frappe.email.queue import get_unsubcribed_url
|
||||
from frappe.email.email_body import add_attachment
|
||||
from frappe.utils import cint
|
||||
from email.policy import SMTPUTF8
|
||||
|
||||
MAX_RETRY_COUNT = 3
|
||||
class EmailQueue(Document):
|
||||
DOCTYPE = 'Email Queue'
|
||||
|
||||
def set_recipients(self, recipients):
|
||||
self.set("recipients", [])
|
||||
for r in recipients:
|
||||
|
|
@ -30,6 +41,241 @@ class EmailQueue(Document):
|
|||
duplicate.set_recipients(recipients)
|
||||
return duplicate
|
||||
|
||||
@classmethod
|
||||
def find(cls, name):
|
||||
return frappe.get_doc(cls.DOCTYPE, name)
|
||||
|
||||
def update_db(self, commit=False, **kwargs):
|
||||
frappe.db.set_value(self.DOCTYPE, self.name, kwargs)
|
||||
if commit:
|
||||
frappe.db.commit()
|
||||
|
||||
def update_status(self, status, commit=False, **kwargs):
|
||||
self.update_db(status = status, commit = commit, **kwargs)
|
||||
if self.communication:
|
||||
communication_doc = frappe.get_doc('Communication', self.communication)
|
||||
communication_doc.set_delivery_status(commit=commit)
|
||||
|
||||
@property
|
||||
def cc(self):
|
||||
return (self.show_as_cc and self.show_as_cc.split(",")) or []
|
||||
|
||||
@property
|
||||
def to(self):
|
||||
return [r.recipient for r in self.recipients if r.recipient not in self.cc]
|
||||
|
||||
@property
|
||||
def attachments_list(self):
|
||||
return json.loads(self.attachments) if self.attachments else []
|
||||
|
||||
def get_email_account(self):
|
||||
from frappe.email.doctype.email_account.email_account import EmailAccount
|
||||
|
||||
if self.email_account:
|
||||
return frappe.get_doc('Email Account', self.email_account)
|
||||
|
||||
return EmailAccount.find_outgoing(
|
||||
match_by_email = self.sender, match_by_doctype = self.reference_doctype)
|
||||
|
||||
def is_to_be_sent(self):
|
||||
return self.status in ['Not Sent','Partially Sent']
|
||||
|
||||
def can_send_now(self):
|
||||
hold_queue = (cint(frappe.defaults.get_defaults().get("hold_queue"))==1)
|
||||
if frappe.are_emails_muted() or not self.is_to_be_sent() or hold_queue:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def send(self, is_background_task=False):
|
||||
""" Send emails to recipients.
|
||||
"""
|
||||
if not self.can_send_now():
|
||||
frappe.db.rollback()
|
||||
return
|
||||
|
||||
with SendMailContext(self, is_background_task) as ctx:
|
||||
message = None
|
||||
for recipient in self.recipients:
|
||||
if not recipient.is_mail_to_be_sent():
|
||||
continue
|
||||
|
||||
message = ctx.build_message(recipient.recipient)
|
||||
if not frappe.flags.in_test:
|
||||
ctx.smtp_session.sendmail(recipient.recipient, self.sender, message)
|
||||
ctx.add_to_sent_list(recipient)
|
||||
|
||||
if frappe.flags.in_test:
|
||||
frappe.flags.sent_mail = message
|
||||
return
|
||||
|
||||
if ctx.email_account_doc.append_emails_to_sent_folder and ctx.sent_to:
|
||||
ctx.email_account_doc.append_email_to_sent_folder(message)
|
||||
|
||||
|
||||
@task(queue = 'short')
|
||||
def send_mail(email_queue_name, is_background_task=False):
|
||||
"""This is equalent to EmqilQueue.send.
|
||||
|
||||
This provides a way to make sending mail as a background job.
|
||||
"""
|
||||
record = EmailQueue.find(email_queue_name)
|
||||
record.send(is_background_task=is_background_task)
|
||||
|
||||
class SendMailContext:
|
||||
def __init__(self, queue_doc: Document, is_background_task: bool = False):
|
||||
self.queue_doc = queue_doc
|
||||
self.is_background_task = is_background_task
|
||||
self.email_account_doc = queue_doc.get_email_account()
|
||||
self.smtp_server = self.email_account_doc.get_smtp_server()
|
||||
self.sent_to = [rec.recipient for rec in self.queue_doc.recipients if rec.is_main_sent()]
|
||||
|
||||
def __enter__(self):
|
||||
self.queue_doc.update_status(status='Sending', commit=True)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
exceptions = [
|
||||
smtplib.SMTPServerDisconnected,
|
||||
smtplib.SMTPAuthenticationError,
|
||||
smtplib.SMTPRecipientsRefused,
|
||||
smtplib.SMTPConnectError,
|
||||
smtplib.SMTPHeloError,
|
||||
JobTimeoutException
|
||||
]
|
||||
|
||||
self.smtp_server.quit()
|
||||
self.log_exception(exc_type, exc_val, exc_tb)
|
||||
|
||||
if exc_type in exceptions:
|
||||
email_status = (self.sent_to and 'Partially Sent') or 'Not Sent'
|
||||
self.queue_doc.update_status(status = email_status, commit = True)
|
||||
elif exc_type:
|
||||
if self.queue_doc.retry < MAX_RETRY_COUNT:
|
||||
update_fields = {'status': 'Not Sent', 'retry': self.queue_doc.retry + 1}
|
||||
else:
|
||||
update_fields = {'status': (self.sent_to and 'Partially Errored') or 'Error'}
|
||||
self.queue_doc.update_status(**update_fields, commit = True)
|
||||
else:
|
||||
email_status = self.is_mail_sent_to_all() and 'Sent'
|
||||
email_status = email_status or (self.sent_to and 'Partially Sent') or 'Not Sent'
|
||||
self.queue_doc.update_status(status = email_status, commit = True)
|
||||
|
||||
def log_exception(self, exc_type, exc_val, exc_tb):
|
||||
if exc_type:
|
||||
traceback_string = "".join(traceback.format_tb(exc_tb))
|
||||
traceback_string += f"\n Queue Name: {self.queue_doc.name}"
|
||||
|
||||
if self.is_background_task:
|
||||
frappe.log_error(title = 'frappe.email.queue.flush', message = traceback_string)
|
||||
else:
|
||||
frappe.log_error(message = traceback_string)
|
||||
|
||||
@property
|
||||
def smtp_session(self):
|
||||
if frappe.flags.in_test:
|
||||
return
|
||||
return self.smtp_server.session
|
||||
|
||||
def add_to_sent_list(self, recipient):
|
||||
# Update recipient status
|
||||
recipient.update_db(status='Sent', commit=True)
|
||||
self.sent_to.append(recipient.recipient)
|
||||
|
||||
def is_mail_sent_to_all(self):
|
||||
return sorted(self.sent_to) == sorted([rec.recipient for rec in self.queue_doc.recipients])
|
||||
|
||||
def get_message_object(self, message):
|
||||
return Parser(policy=SMTPUTF8).parsestr(message)
|
||||
|
||||
def message_placeholder(self, placeholder_key):
|
||||
map = {
|
||||
'tracker': '<!--email open check-->',
|
||||
'unsubscribe_url': '<!--unsubscribe url-->',
|
||||
'cc': '<!--cc message-->',
|
||||
'recipient': '<!--recipient-->',
|
||||
}
|
||||
return map.get(placeholder_key)
|
||||
|
||||
def build_message(self, recipient_email):
|
||||
"""Build message specific to the recipient.
|
||||
"""
|
||||
message = self.queue_doc.message
|
||||
if not message:
|
||||
return ""
|
||||
|
||||
message = message.replace(self.message_placeholder('tracker'), self.get_tracker_str())
|
||||
message = message.replace(self.message_placeholder('unsubscribe_url'),
|
||||
self.get_unsubscribe_str(recipient_email))
|
||||
message = message.replace(self.message_placeholder('cc'), self.get_receivers_str())
|
||||
message = message.replace(self.message_placeholder('recipient'),
|
||||
self.get_receipient_str(recipient_email))
|
||||
message = self.include_attachments(message)
|
||||
return message
|
||||
|
||||
def get_tracker_str(self):
|
||||
tracker_url_html = \
|
||||
'<img src="https://{}/api/method/frappe.core.doctype.communication.email.mark_email_as_seen?name={}"/>'
|
||||
|
||||
message = ''
|
||||
if frappe.conf.use_ssl and self.queue_doc.track_email_status:
|
||||
message = quopri.encodestring(
|
||||
tracker_url_html.format(frappe.local.site, self.queue_doc.communication).encode()
|
||||
).decode()
|
||||
return message
|
||||
|
||||
def get_unsubscribe_str(self, recipient_email):
|
||||
unsubscribe_url = ''
|
||||
if self.queue_doc.add_unsubscribe_link and self.queue_doc.reference_doctype:
|
||||
doctype, doc_name = self.queue_doc.reference_doctype, self.queue_doc.reference_name
|
||||
unsubscribe_url = get_unsubcribed_url(doctype, doc_name, recipient_email,
|
||||
self.queue_doc.unsubscribe_method, self.queue_doc.unsubscribe_param)
|
||||
|
||||
return quopri.encodestring(unsubscribe_url.encode()).decode()
|
||||
|
||||
def get_receivers_str(self):
|
||||
message = ''
|
||||
if self.queue_doc.expose_recipients == "footer":
|
||||
to_str = ', '.join(self.queue_doc.to)
|
||||
cc_str = ', '.join(self.queue_doc.cc)
|
||||
message = f"This email was sent to {to_str}"
|
||||
message = message + f" and copied to {cc_str}" if cc_str else message
|
||||
return message
|
||||
|
||||
def get_receipient_str(self, recipient_email):
|
||||
message = ''
|
||||
if self.queue_doc.expose_recipients != "header":
|
||||
message = recipient_email
|
||||
return message
|
||||
|
||||
def include_attachments(self, message):
|
||||
message_obj = self.get_message_object(message)
|
||||
attachments = self.queue_doc.attachments_list
|
||||
|
||||
for attachment in attachments:
|
||||
if attachment.get('fcontent'):
|
||||
continue
|
||||
|
||||
fid = attachment.get("fid")
|
||||
if fid:
|
||||
_file = frappe.get_doc("File", fid)
|
||||
fcontent = _file.get_content()
|
||||
attachment.update({
|
||||
'fname': _file.file_name,
|
||||
'fcontent': fcontent,
|
||||
'parent': message_obj
|
||||
})
|
||||
attachment.pop("fid", None)
|
||||
add_attachment(**attachment)
|
||||
|
||||
elif attachment.get("print_format_attachment") == 1:
|
||||
attachment.pop("print_format_attachment", None)
|
||||
print_format_file = frappe.attach_print(**attachment)
|
||||
print_format_file.update({"parent": message_obj})
|
||||
add_attachment(**print_format_file)
|
||||
|
||||
return safe_encode(message_obj.as_string())
|
||||
|
||||
@frappe.whitelist()
|
||||
def retry_sending(name):
|
||||
doc = frappe.get_doc("Email Queue", name)
|
||||
|
|
@ -42,7 +288,9 @@ def retry_sending(name):
|
|||
|
||||
@frappe.whitelist()
|
||||
def send_now(name):
|
||||
send_one(name, now=True)
|
||||
record = EmailQueue.find(name)
|
||||
if record:
|
||||
record.send()
|
||||
|
||||
def on_doctype_update():
|
||||
"""Add index in `tabCommunication` for `(reference_doctype, reference_name)`"""
|
||||
|
|
|
|||
|
|
@ -7,4 +7,16 @@ import frappe
|
|||
from frappe.model.document import Document
|
||||
|
||||
class EmailQueueRecipient(Document):
|
||||
pass
|
||||
DOCTYPE = 'Email Queue Recipient'
|
||||
|
||||
def is_mail_to_be_sent(self):
|
||||
return self.status == 'Not Sent'
|
||||
|
||||
def is_main_sent(self):
|
||||
return self.status == 'Sent'
|
||||
|
||||
def update_db(self, commit=False, **kwargs):
|
||||
frappe.db.set_value(self.DOCTYPE, self.name, kwargs)
|
||||
if commit:
|
||||
frappe.db.commit()
|
||||
|
||||
|
|
|
|||
|
|
@ -102,7 +102,8 @@
|
|||
"default": "0",
|
||||
"fieldname": "is_standard",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Standard"
|
||||
"label": "Is Standard",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "is_standard",
|
||||
|
|
@ -281,7 +282,7 @@
|
|||
"icon": "fa fa-envelope",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-24 14:25:43.245677",
|
||||
"modified": "2021-05-04 11:17:11.882314",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Notification",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
from __future__ import unicode_literals
|
||||
import frappe, re, os
|
||||
from frappe.utils.pdf import get_pdf
|
||||
from frappe.email.smtp import get_outgoing_email_account
|
||||
from frappe.email.doctype.email_account.email_account import EmailAccount
|
||||
from frappe.utils import (get_url, scrub_urls, strip, expand_relative_urls, cint,
|
||||
split_emails, to_markdown, markdown, random_string, parse_addr)
|
||||
import email.utils
|
||||
|
|
@ -75,7 +75,8 @@ class EMail:
|
|||
self.bcc = bcc or []
|
||||
self.html_set = False
|
||||
|
||||
self.email_account = email_account or get_outgoing_email_account(sender=sender)
|
||||
self.email_account = email_account or \
|
||||
EmailAccount.find_outgoing(match_by_email=sender, _raise_error=True)
|
||||
|
||||
def set_html(self, message, text_content = None, footer=None, print_html=None,
|
||||
formatted=None, inline_images=None, header=None):
|
||||
|
|
@ -249,8 +250,8 @@ class EMail:
|
|||
|
||||
def get_formatted_html(subject, message, footer=None, print_html=None,
|
||||
email_account=None, header=None, unsubscribe_link=None, sender=None, with_container=False):
|
||||
if not email_account:
|
||||
email_account = get_outgoing_email_account(False, sender=sender)
|
||||
|
||||
email_account = email_account or EmailAccount.find_outgoing(match_by_email=sender)
|
||||
|
||||
signature = None
|
||||
if "<!-- signature-included -->" not in message:
|
||||
|
|
@ -480,4 +481,4 @@ def sanitize_email_header(str):
|
|||
return str.replace('\r', '').replace('\n', '')
|
||||
|
||||
def get_brand_logo(email_account):
|
||||
return email_account.get('brand_logo')
|
||||
return email_account.get('brand_logo')
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ import sys
|
|||
from six.moves import html_parser as HTMLParser
|
||||
import smtplib, quopri, json
|
||||
from frappe import msgprint, _, safe_decode, safe_encode, enqueue
|
||||
from frappe.email.smtp import SMTPServer, get_outgoing_email_account
|
||||
from frappe.email.smtp import SMTPServer
|
||||
from frappe.email.doctype.email_account.email_account import EmailAccount
|
||||
from frappe.email.email_body import get_email, get_formatted_html, add_attachment
|
||||
from frappe.utils.verified_command import get_signed_params, verify_request
|
||||
from html2text import html2text
|
||||
|
|
@ -73,7 +74,9 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content=
|
|||
if isinstance(send_after, int):
|
||||
send_after = add_days(nowdate(), send_after)
|
||||
|
||||
email_account = get_outgoing_email_account(True, append_to=reference_doctype, sender=sender)
|
||||
email_account = EmailAccount.find_outgoing(
|
||||
match_by_doctype=reference_doctype, match_by_email=sender, _raise_error=True)
|
||||
|
||||
if not sender or sender == "Administrator":
|
||||
sender = email_account.default_sender
|
||||
|
||||
|
|
@ -170,19 +173,19 @@ def add(recipients, sender, subject, **kwargs):
|
|||
if not email_queue:
|
||||
email_queue = get_email_queue([r], sender, subject, **kwargs)
|
||||
if kwargs.get('now'):
|
||||
send_one(email_queue.name, now=True)
|
||||
email_queue.send()
|
||||
else:
|
||||
duplicate = email_queue.get_duplicate([r])
|
||||
duplicate.insert(ignore_permissions=True)
|
||||
|
||||
if kwargs.get('now'):
|
||||
send_one(duplicate.name, now=True)
|
||||
duplicate.send()
|
||||
|
||||
frappe.db.commit()
|
||||
else:
|
||||
email_queue = get_email_queue(recipients, sender, subject, **kwargs)
|
||||
if kwargs.get('now'):
|
||||
send_one(email_queue.name, now=True)
|
||||
email_queue.send()
|
||||
|
||||
def get_email_queue(recipients, sender, subject, **kwargs):
|
||||
'''Make Email Queue object'''
|
||||
|
|
@ -234,6 +237,9 @@ def get_email_queue(recipients, sender, subject, **kwargs):
|
|||
', '.join(mail.recipients), traceback.format_exc()), 'Email Not Sent')
|
||||
|
||||
recipients = list(set(recipients + kwargs.get('cc', []) + kwargs.get('bcc', [])))
|
||||
email_account = kwargs.get('email_account')
|
||||
email_account_name = email_account and email_account.is_exists_in_db() and email_account.name
|
||||
|
||||
e.set_recipients(recipients)
|
||||
e.reference_doctype = kwargs.get('reference_doctype')
|
||||
e.reference_name = kwargs.get('reference_name')
|
||||
|
|
@ -245,8 +251,8 @@ def get_email_queue(recipients, sender, subject, **kwargs):
|
|||
e.send_after = kwargs.get('send_after')
|
||||
e.show_as_cc = ",".join(kwargs.get('cc', []))
|
||||
e.show_as_bcc = ",".join(kwargs.get('bcc', []))
|
||||
e.email_account = email_account_name or None
|
||||
e.insert(ignore_permissions=True)
|
||||
|
||||
return e
|
||||
|
||||
def get_emails_sent_this_month():
|
||||
|
|
@ -328,44 +334,25 @@ def return_unsubscribed_page(email, doctype, name):
|
|||
indicator_color='green')
|
||||
|
||||
def flush(from_test=False):
|
||||
"""flush email queue, every time: called from scheduler"""
|
||||
# additional check
|
||||
|
||||
auto_commit = not from_test
|
||||
"""flush email queue, every time: called from scheduler
|
||||
"""
|
||||
from frappe.email.doctype.email_queue.email_queue import send_mail
|
||||
# To avoid running jobs inside unit tests
|
||||
if frappe.are_emails_muted():
|
||||
msgprint(_("Emails are muted"))
|
||||
from_test = True
|
||||
|
||||
smtpserver_dict = frappe._dict()
|
||||
if cint(frappe.defaults.get_defaults().get("hold_queue"))==1:
|
||||
return
|
||||
|
||||
for email in get_queue():
|
||||
for row in get_queue():
|
||||
try:
|
||||
func = send_mail if from_test else send_mail.enqueue
|
||||
is_background_task = not from_test
|
||||
func(email_queue_name = row.name, is_background_task = is_background_task)
|
||||
except Exception:
|
||||
frappe.log_error()
|
||||
|
||||
if cint(frappe.defaults.get_defaults().get("hold_queue"))==1:
|
||||
break
|
||||
|
||||
if email.name:
|
||||
smtpserver = smtpserver_dict.get(email.sender)
|
||||
if not smtpserver:
|
||||
smtpserver = SMTPServer()
|
||||
smtpserver_dict[email.sender] = smtpserver
|
||||
|
||||
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:
|
||||
# frappe.db.commit()
|
||||
def get_queue():
|
||||
return frappe.db.sql('''select
|
||||
name, sender
|
||||
|
|
@ -378,213 +365,6 @@ def get_queue():
|
|||
by priority desc, creation asc
|
||||
limit 500''', { 'now': now_datetime() }, as_dict=True)
|
||||
|
||||
|
||||
def send_one(email, smtpserver=None, auto_commit=True, now=False):
|
||||
'''Send Email Queue with given smtpserver'''
|
||||
|
||||
email = frappe.db.sql('''select
|
||||
name, status, communication, message, sender, reference_doctype,
|
||||
reference_name, unsubscribe_param, unsubscribe_method, expose_recipients,
|
||||
show_as_cc, add_unsubscribe_link, attachments, retry
|
||||
from
|
||||
`tabEmail Queue`
|
||||
where
|
||||
name=%s
|
||||
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)
|
||||
|
||||
if frappe.are_emails_muted():
|
||||
frappe.msgprint(_("Emails are muted"))
|
||||
return
|
||||
|
||||
if cint(frappe.defaults.get_defaults().get("hold_queue"))==1 :
|
||||
return
|
||||
|
||||
if email.status not in ('Not Sent','Partially Sent') :
|
||||
# rollback to release lock and return
|
||||
frappe.db.rollback()
|
||||
return
|
||||
|
||||
frappe.db.sql("""update `tabEmail Queue` set status='Sending', modified=%s where name=%s""",
|
||||
(now_datetime(), email.name), auto_commit=auto_commit)
|
||||
|
||||
if email.communication:
|
||||
frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit)
|
||||
|
||||
email_sent_to_any_recipient = None
|
||||
|
||||
try:
|
||||
message = None
|
||||
|
||||
if not frappe.flags.in_test:
|
||||
if not smtpserver:
|
||||
smtpserver = SMTPServer()
|
||||
|
||||
# to avoid always using default email account for outgoing
|
||||
if getattr(frappe.local, "outgoing_email_account", None):
|
||||
frappe.local.outgoing_email_account = {}
|
||||
|
||||
smtpserver.setup_email_account(email.reference_doctype, sender=email.sender)
|
||||
|
||||
for recipient in recipients_list:
|
||||
if recipient.status != "Not Sent":
|
||||
continue
|
||||
|
||||
message = prepare_message(email, recipient.recipient, recipients_list)
|
||||
if not frappe.flags.in_test:
|
||||
smtpserver.sess.sendmail(email.sender, recipient.recipient, message)
|
||||
|
||||
recipient.status = "Sent"
|
||||
frappe.db.sql("""update `tabEmail Queue Recipient` set status='Sent', modified=%s where name=%s""",
|
||||
(now_datetime(), recipient.name), auto_commit=auto_commit)
|
||||
|
||||
email_sent_to_any_recipient = any("Sent" == s.status for s in recipients_list)
|
||||
|
||||
#if all are sent set status
|
||||
if email_sent_to_any_recipient:
|
||||
frappe.db.sql("""update `tabEmail Queue` set status='Sent', modified=%s where name=%s""",
|
||||
(now_datetime(), email.name), auto_commit=auto_commit)
|
||||
else:
|
||||
frappe.db.sql("""update `tabEmail Queue` set status='Error', error=%s
|
||||
where name=%s""", ("No recipients to send to", email.name), auto_commit=auto_commit)
|
||||
if frappe.flags.in_test:
|
||||
frappe.flags.sent_mail = message
|
||||
return
|
||||
if email.communication:
|
||||
frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit)
|
||||
|
||||
if smtpserver.append_emails_to_sent_folder and email_sent_to_any_recipient:
|
||||
smtpserver.email_account.append_email_to_sent_folder(message)
|
||||
|
||||
except (smtplib.SMTPServerDisconnected,
|
||||
smtplib.SMTPConnectError,
|
||||
smtplib.SMTPHeloError,
|
||||
smtplib.SMTPAuthenticationError,
|
||||
smtplib.SMTPRecipientsRefused,
|
||||
JobTimeoutException):
|
||||
|
||||
# bad connection/timeout, retry later
|
||||
|
||||
if email_sent_to_any_recipient:
|
||||
frappe.db.sql("""update `tabEmail Queue` set status='Partially Sent', modified=%s where name=%s""",
|
||||
(now_datetime(), email.name), auto_commit=auto_commit)
|
||||
else:
|
||||
frappe.db.sql("""update `tabEmail Queue` set status='Not Sent', modified=%s where name=%s""",
|
||||
(now_datetime(), email.name), auto_commit=auto_commit)
|
||||
|
||||
if email.communication:
|
||||
frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit)
|
||||
|
||||
# no need to attempt further
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
|
||||
if email.retry < 3:
|
||||
frappe.db.sql("""update `tabEmail Queue` set status='Not Sent', modified=%s, retry=retry+1 where name=%s""",
|
||||
(now_datetime(), email.name), auto_commit=auto_commit)
|
||||
else:
|
||||
if email_sent_to_any_recipient:
|
||||
frappe.db.sql("""update `tabEmail Queue` set status='Partially Errored', error=%s where name=%s""",
|
||||
(text_type(e), email.name), auto_commit=auto_commit)
|
||||
else:
|
||||
frappe.db.sql("""update `tabEmail Queue` set status='Error', error=%s
|
||||
where name=%s""", (text_type(e), email.name), auto_commit=auto_commit)
|
||||
|
||||
if email.communication:
|
||||
frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit)
|
||||
|
||||
if now:
|
||||
print(frappe.get_traceback())
|
||||
raise e
|
||||
|
||||
else:
|
||||
# log to Error Log
|
||||
frappe.log_error('frappe.email.queue.flush')
|
||||
|
||||
def prepare_message(email, recipient, recipients_list):
|
||||
message = email.message
|
||||
if not message:
|
||||
return ""
|
||||
|
||||
# Parse "Email Account" from "Email Sender"
|
||||
email_account = get_outgoing_email_account(raise_exception_not_set=False, sender=email.sender)
|
||||
if frappe.conf.use_ssl and email_account.track_email_status:
|
||||
# Using SSL => Publically available domain => Email Read Reciept Possible
|
||||
message = message.replace("<!--email open check-->", quopri.encodestring('<img src="https://{}/api/method/frappe.core.doctype.communication.email.mark_email_as_seen?name={}"/>'.format(frappe.local.site, email.communication).encode()).decode())
|
||||
else:
|
||||
# No SSL => No Email Read Reciept
|
||||
message = message.replace("<!--email open check-->", quopri.encodestring("".encode()).decode())
|
||||
|
||||
if email.add_unsubscribe_link and email.reference_doctype: # is missing the check for unsubscribe message but will not add as there will be no unsubscribe url
|
||||
unsubscribe_url = get_unsubcribed_url(email.reference_doctype, email.reference_name, recipient,
|
||||
email.unsubscribe_method, email.unsubscribe_params)
|
||||
message = message.replace("<!--unsubscribe url-->", quopri.encodestring(unsubscribe_url.encode()).decode())
|
||||
|
||||
if email.expose_recipients == "header":
|
||||
pass
|
||||
else:
|
||||
if email.expose_recipients == "footer":
|
||||
if isinstance(email.show_as_cc, string_types):
|
||||
email.show_as_cc = email.show_as_cc.split(",")
|
||||
email_sent_to = [r.recipient for r in recipients_list]
|
||||
email_sent_cc = ", ".join([e for e in email_sent_to if e in email.show_as_cc])
|
||||
email_sent_to = ", ".join([e for e in email_sent_to if e not in email.show_as_cc])
|
||||
|
||||
if email_sent_cc:
|
||||
email_sent_message = _("This email was sent to {0} and copied to {1}").format(email_sent_to,email_sent_cc)
|
||||
else:
|
||||
email_sent_message = _("This email was sent to {0}").format(email_sent_to)
|
||||
message = message.replace("<!--cc message-->", quopri.encodestring(email_sent_message.encode()).decode())
|
||||
|
||||
message = message.replace("<!--recipient-->", recipient)
|
||||
|
||||
message = (message and message.encode('utf8')) or ''
|
||||
message = safe_decode(message)
|
||||
|
||||
if PY3:
|
||||
from email.policy import SMTPUTF8
|
||||
message = Parser(policy=SMTPUTF8).parsestr(message)
|
||||
else:
|
||||
message = Parser().parsestr(message)
|
||||
|
||||
if email.attachments:
|
||||
# On-demand attachments
|
||||
|
||||
attachments = json.loads(email.attachments)
|
||||
|
||||
for attachment in attachments:
|
||||
if attachment.get('fcontent'):
|
||||
continue
|
||||
|
||||
fid = attachment.get("fid")
|
||||
if fid:
|
||||
_file = frappe.get_doc("File", fid)
|
||||
fcontent = _file.get_content()
|
||||
attachment.update({
|
||||
'fname': _file.file_name,
|
||||
'fcontent': fcontent,
|
||||
'parent': message
|
||||
})
|
||||
attachment.pop("fid", None)
|
||||
add_attachment(**attachment)
|
||||
|
||||
elif attachment.get("print_format_attachment") == 1:
|
||||
attachment.pop("print_format_attachment", None)
|
||||
print_format_file = frappe.attach_print(**attachment)
|
||||
print_format_file.update({"parent": message})
|
||||
add_attachment(**print_format_file)
|
||||
|
||||
return safe_encode(message.as_string())
|
||||
|
||||
def clear_outbox(days=None):
|
||||
"""Remove low priority older than 31 days in Outbox or configured in Log Settings.
|
||||
Note: Used separate query to avoid deadlock
|
||||
|
|
|
|||
|
|
@ -9,11 +9,24 @@ import _socket, sys
|
|||
from frappe import _
|
||||
from frappe.utils import cint, cstr, parse_addr
|
||||
|
||||
CONNECTION_FAILED = _('Could not connect to outgoing email server')
|
||||
AUTH_ERROR_TITLE = _("Invalid Credentials")
|
||||
AUTH_ERROR = _("Incorrect email or password. Please check your login credentials.")
|
||||
SOCKET_ERROR_TITLE = _("Incorrect Configuration")
|
||||
SOCKET_ERROR = _("Invalid Outgoing Mail Server or Port")
|
||||
SEND_MAIL_FAILED = _("Unable to send emails at this time")
|
||||
EMAIL_ACCOUNT_MISSING = _('Email Account not setup. Please create a new Email Account from Setup > Email > Email Account')
|
||||
|
||||
class InvalidEmailCredentials(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
def send(email, append_to=None, retry=1):
|
||||
"""Deprecated: Send the message or add it to Outbox Email"""
|
||||
def _send(retry):
|
||||
from frappe.email.doctype.email_account.email_account import EmailAccount
|
||||
try:
|
||||
smtpserver = SMTPServer(append_to=append_to)
|
||||
email_account = EmailAccount.find_outgoing(match_by_doctype=append_to)
|
||||
smtpserver = email_account.get_smtp_server()
|
||||
|
||||
# validate is called in as_string
|
||||
email_body = email.as_string()
|
||||
|
|
@ -34,224 +47,80 @@ def send(email, append_to=None, retry=1):
|
|||
|
||||
_send(retry)
|
||||
|
||||
def get_outgoing_email_account(raise_exception_not_set=True, append_to=None, sender=None):
|
||||
"""Returns outgoing email account based on `append_to` or the default
|
||||
outgoing account. If default outgoing account is not found, it will
|
||||
try getting settings from `site_config.json`."""
|
||||
|
||||
sender_email_id = None
|
||||
_email_account = None
|
||||
|
||||
if sender:
|
||||
sender_email_id = parse_addr(sender)[1]
|
||||
|
||||
if not getattr(frappe.local, "outgoing_email_account", None):
|
||||
frappe.local.outgoing_email_account = {}
|
||||
|
||||
if not (frappe.local.outgoing_email_account.get(append_to)
|
||||
or frappe.local.outgoing_email_account.get(sender_email_id)
|
||||
or frappe.local.outgoing_email_account.get("default")):
|
||||
email_account = None
|
||||
|
||||
if sender_email_id:
|
||||
# check if the sender has an email account with enable_outgoing
|
||||
email_account = _get_email_account({"enable_outgoing": 1,
|
||||
"email_id": sender_email_id})
|
||||
|
||||
if not email_account and append_to:
|
||||
# append_to is only valid when enable_incoming is checked
|
||||
email_accounts = frappe.db.get_values("Email Account", {
|
||||
"enable_outgoing": 1,
|
||||
"enable_incoming": 1,
|
||||
"append_to": append_to,
|
||||
}, cache=True)
|
||||
|
||||
if email_accounts:
|
||||
_email_account = email_accounts[0]
|
||||
|
||||
else:
|
||||
email_account = _get_email_account({
|
||||
"enable_outgoing": 1,
|
||||
"enable_incoming": 1,
|
||||
"append_to": append_to
|
||||
})
|
||||
|
||||
if not email_account:
|
||||
# sender don't have the outging email account
|
||||
sender_email_id = None
|
||||
email_account = get_default_outgoing_email_account(raise_exception_not_set=raise_exception_not_set)
|
||||
|
||||
if not email_account and _email_account:
|
||||
# if default email account is not configured then setup first email account based on append to
|
||||
email_account = _email_account
|
||||
|
||||
if not email_account and raise_exception_not_set and cint(frappe.db.get_single_value('System Settings', 'setup_complete')):
|
||||
frappe.throw(_("Please setup default Email Account from Setup > Email > Email Account"),
|
||||
frappe.OutgoingEmailError)
|
||||
|
||||
if email_account:
|
||||
if email_account.enable_outgoing and not getattr(email_account, 'from_site_config', False):
|
||||
raise_exception = True
|
||||
if email_account.smtp_server in ['localhost','127.0.0.1'] or email_account.no_smtp_authentication:
|
||||
raise_exception = False
|
||||
email_account.password = email_account.get_password(raise_exception=raise_exception)
|
||||
email_account.default_sender = email.utils.formataddr((email_account.name, email_account.get("email_id")))
|
||||
|
||||
frappe.local.outgoing_email_account[append_to or sender_email_id or "default"] = email_account
|
||||
|
||||
return frappe.local.outgoing_email_account.get(append_to) \
|
||||
or frappe.local.outgoing_email_account.get(sender_email_id) \
|
||||
or frappe.local.outgoing_email_account.get("default")
|
||||
|
||||
def get_default_outgoing_email_account(raise_exception_not_set=True):
|
||||
'''conf should be like:
|
||||
{
|
||||
"mail_server": "smtp.example.com",
|
||||
"mail_port": 587,
|
||||
"use_tls": 1,
|
||||
"mail_login": "emails@example.com",
|
||||
"mail_password": "Super.Secret.Password",
|
||||
"auto_email_id": "emails@example.com",
|
||||
"email_sender_name": "Example Notifications",
|
||||
"always_use_account_email_id_as_sender": 0,
|
||||
"always_use_account_name_as_sender_name": 0
|
||||
}
|
||||
'''
|
||||
email_account = _get_email_account({"enable_outgoing": 1, "default_outgoing": 1})
|
||||
if email_account:
|
||||
email_account.password = email_account.get_password(raise_exception=False)
|
||||
|
||||
if not email_account and frappe.conf.get("mail_server"):
|
||||
# from site_config.json
|
||||
email_account = frappe.new_doc("Email Account")
|
||||
email_account.update({
|
||||
"smtp_server": frappe.conf.get("mail_server"),
|
||||
"smtp_port": frappe.conf.get("mail_port"),
|
||||
|
||||
# legacy: use_ssl was used in site_config instead of use_tls, but meant the same thing
|
||||
"use_tls": cint(frappe.conf.get("use_tls") or 0) or cint(frappe.conf.get("use_ssl") or 0),
|
||||
"login_id": frappe.conf.get("mail_login"),
|
||||
"email_id": frappe.conf.get("auto_email_id") or frappe.conf.get("mail_login") or 'notifications@example.com',
|
||||
"password": frappe.conf.get("mail_password"),
|
||||
"always_use_account_email_id_as_sender": frappe.conf.get("always_use_account_email_id_as_sender", 0),
|
||||
"always_use_account_name_as_sender_name": frappe.conf.get("always_use_account_name_as_sender_name", 0)
|
||||
})
|
||||
email_account.from_site_config = True
|
||||
email_account.name = frappe.conf.get("email_sender_name") or "Frappe"
|
||||
|
||||
if not email_account and not raise_exception_not_set:
|
||||
return None
|
||||
|
||||
if frappe.are_emails_muted():
|
||||
# create a stub
|
||||
email_account = frappe.new_doc("Email Account")
|
||||
email_account.update({
|
||||
"email_id": "notifications@example.com"
|
||||
})
|
||||
|
||||
return email_account
|
||||
|
||||
def _get_email_account(filters):
|
||||
name = frappe.db.get_value("Email Account", filters)
|
||||
return frappe.get_doc("Email Account", name) if name else None
|
||||
|
||||
class SMTPServer:
|
||||
def __init__(self, login=None, password=None, server=None, port=None, use_tls=None, use_ssl=None, append_to=None):
|
||||
# get defaults from mail settings
|
||||
def __init__(self, server, login=None, password=None, port=None, use_tls=None, use_ssl=None):
|
||||
self.login = login
|
||||
self.password = password
|
||||
self._server = server
|
||||
self._port = port
|
||||
self.use_tls = use_tls
|
||||
self.use_ssl = use_ssl
|
||||
self._session = None
|
||||
|
||||
self._sess = None
|
||||
self.email_account = None
|
||||
self.server = None
|
||||
self.append_emails_to_sent_folder = None
|
||||
|
||||
if server:
|
||||
self.server = server
|
||||
self.port = port
|
||||
self.use_tls = cint(use_tls)
|
||||
self.use_ssl = cint(use_ssl)
|
||||
self.login = login
|
||||
self.password = password
|
||||
|
||||
else:
|
||||
self.setup_email_account(append_to)
|
||||
|
||||
def setup_email_account(self, append_to=None, sender=None):
|
||||
self.email_account = get_outgoing_email_account(raise_exception_not_set=False, append_to=append_to, sender=sender)
|
||||
if self.email_account:
|
||||
self.server = self.email_account.smtp_server
|
||||
self.login = (getattr(self.email_account, "login_id", None) or self.email_account.email_id)
|
||||
if not self.email_account.no_smtp_authentication:
|
||||
if self.email_account.ascii_encode_password:
|
||||
self.password = frappe.safe_encode(self.email_account.password, 'ascii')
|
||||
else:
|
||||
self.password = self.email_account.password
|
||||
else:
|
||||
self.password = None
|
||||
self.port = self.email_account.smtp_port
|
||||
self.use_tls = self.email_account.use_tls
|
||||
self.sender = self.email_account.email_id
|
||||
self.use_ssl = self.email_account.use_ssl_for_outgoing
|
||||
self.append_emails_to_sent_folder = self.email_account.append_emails_to_sent_folder
|
||||
self.always_use_account_email_id_as_sender = cint(self.email_account.get("always_use_account_email_id_as_sender"))
|
||||
self.always_use_account_name_as_sender_name = cint(self.email_account.get("always_use_account_name_as_sender_name"))
|
||||
if not self.server:
|
||||
frappe.msgprint(EMAIL_ACCOUNT_MISSING, raise_exception=frappe.OutgoingEmailError)
|
||||
|
||||
@property
|
||||
def sess(self):
|
||||
"""get session"""
|
||||
if self._sess:
|
||||
return self._sess
|
||||
def port(self):
|
||||
port = self._port or (self.use_ssl and 465) or (self.use_tls and 587)
|
||||
return cint(port)
|
||||
|
||||
# check if email server specified
|
||||
if not getattr(self, 'server'):
|
||||
err_msg = _('Email Account not setup. Please create a new Email Account from Setup > Email > Email Account')
|
||||
frappe.msgprint(err_msg)
|
||||
raise frappe.OutgoingEmailError(err_msg)
|
||||
@property
|
||||
def server(self):
|
||||
return cstr(self._server or "")
|
||||
|
||||
def secure_session(self, conn):
|
||||
"""Secure the connection incase of TLS.
|
||||
"""
|
||||
if self.use_tls:
|
||||
conn.ehlo()
|
||||
conn.starttls()
|
||||
conn.ehlo()
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
if self.is_session_active():
|
||||
return self._session
|
||||
|
||||
SMTP = smtplib.SMTP_SSL if self.use_ssl else smtplib.SMTP
|
||||
|
||||
try:
|
||||
if self.use_ssl:
|
||||
if not self.port:
|
||||
self.port = 465
|
||||
|
||||
self._sess = smtplib.SMTP_SSL((self.server or ""), cint(self.port))
|
||||
else:
|
||||
if self.use_tls and not self.port:
|
||||
self.port = 587
|
||||
|
||||
self._sess = smtplib.SMTP(cstr(self.server or ""),
|
||||
cint(self.port) or None)
|
||||
|
||||
if not self._sess:
|
||||
err_msg = _('Could not connect to outgoing email server')
|
||||
frappe.msgprint(err_msg)
|
||||
raise frappe.OutgoingEmailError(err_msg)
|
||||
|
||||
if self.use_tls:
|
||||
self._sess.ehlo()
|
||||
self._sess.starttls()
|
||||
self._sess.ehlo()
|
||||
self._session = SMTP(self.server, self.port)
|
||||
if not self._session:
|
||||
frappe.msgprint(CONNECTION_FAILED, raise_exception=frappe.OutgoingEmailError)
|
||||
|
||||
self.secure_session(self._session)
|
||||
if self.login and self.password:
|
||||
ret = self._sess.login(str(self.login or ""), str(self.password or ""))
|
||||
res = self._session.login(str(self.login or ""), str(self.password or ""))
|
||||
|
||||
# check if logged correctly
|
||||
if ret[0]!=235:
|
||||
frappe.msgprint(ret[1])
|
||||
raise frappe.OutgoingEmailError(ret[1])
|
||||
if res[0]!=235:
|
||||
frappe.msgprint(res[1], raise_exception=frappe.OutgoingEmailError)
|
||||
|
||||
return self._sess
|
||||
return self._session
|
||||
|
||||
except smtplib.SMTPAuthenticationError as e:
|
||||
from frappe.email.doctype.email_account.email_account import EmailAccount
|
||||
EmailAccount.throw_invalid_credentials_exception()
|
||||
self.throw_invalid_credentials_exception()
|
||||
|
||||
except _socket.error as e:
|
||||
# Invalid mail server -- due to refusing connection
|
||||
frappe.throw(
|
||||
_("Invalid Outgoing Mail Server or Port"),
|
||||
exc=frappe.ValidationError,
|
||||
title=_("Incorrect Configuration")
|
||||
)
|
||||
frappe.throw(SOCKET_ERROR, title=SOCKET_ERROR_TITLE)
|
||||
|
||||
except smtplib.SMTPException:
|
||||
frappe.msgprint(_('Unable to send emails at this time'))
|
||||
frappe.msgprint(SEND_MAIL_FAILED)
|
||||
raise
|
||||
|
||||
def is_session_active(self):
|
||||
if self._session:
|
||||
try:
|
||||
return self._session.noop()[0] == 250
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def quit(self):
|
||||
if self.is_session_active():
|
||||
self._session.quit()
|
||||
|
||||
@classmethod
|
||||
def throw_invalid_credentials_exception(cls):
|
||||
frappe.throw(AUTH_ERROR, title=AUTH_ERROR_TITLE, exc=InvalidEmailCredentials)
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ from frappe import safe_decode
|
|||
from frappe.email.receive import Email
|
||||
from frappe.email.email_body import (replace_filename_with_cid,
|
||||
get_email, inline_style_in_html, get_header)
|
||||
from frappe.email.queue import prepare_message, get_email_queue
|
||||
from frappe.email.queue import get_email_queue
|
||||
from frappe.email.doctype.email_queue.email_queue import SendMailContext
|
||||
from six import PY3
|
||||
|
||||
|
||||
class TestEmailBody(unittest.TestCase):
|
||||
def setUp(self):
|
||||
email_html = '''
|
||||
|
|
@ -57,7 +57,8 @@ This is the text version of this email
|
|||
content='<h1>' + uni_chr1 + 'abcd' + uni_chr2 + '</h1>',
|
||||
formatted='<h1>' + uni_chr1 + 'abcd' + uni_chr2 + '</h1>',
|
||||
text_content='whatever')
|
||||
result = prepare_message(email=email, recipient='test@test.com', recipients_list=[])
|
||||
mail_ctx = SendMailContext(queue_doc = email)
|
||||
result = mail_ctx.build_message(recipient_email = 'test@test.com')
|
||||
self.assertTrue(b"<h1>=EA=80=80abcd=DE=B4</h1>" in result)
|
||||
|
||||
def test_prepare_message_returns_cr_lf(self):
|
||||
|
|
@ -68,8 +69,10 @@ This is the text version of this email
|
|||
content='<h1>\n this is a test of newlines\n' + '</h1>',
|
||||
formatted='<h1>\n this is a test of newlines\n' + '</h1>',
|
||||
text_content='whatever')
|
||||
result = safe_decode(prepare_message(email=email,
|
||||
recipient='test@test.com', recipients_list=[]))
|
||||
|
||||
mail_ctx = SendMailContext(queue_doc = email)
|
||||
result = safe_decode(mail_ctx.build_message(recipient_email='test@test.com'))
|
||||
|
||||
if PY3:
|
||||
self.assertTrue(result.count('\n') == result.count("\r"))
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import unittest
|
||||
import frappe
|
||||
from frappe.email.smtp import SMTPServer
|
||||
from frappe.email.smtp import get_outgoing_email_account
|
||||
from frappe.email.doctype.email_account.email_account import EmailAccount
|
||||
|
||||
class TestSMTP(unittest.TestCase):
|
||||
def test_smtp_ssl_session(self):
|
||||
|
|
@ -33,13 +33,13 @@ class TestSMTP(unittest.TestCase):
|
|||
|
||||
frappe.local.outgoing_email_account = {}
|
||||
# lowest preference given to email account with default incoming enabled
|
||||
create_email_account(email_id="default_outgoing_enabled@gmail.com", password="***", enable_outgoing = 1, default_outgoing=1)
|
||||
self.assertEqual(get_outgoing_email_account().email_id, "default_outgoing_enabled@gmail.com")
|
||||
create_email_account(email_id="default_outgoing_enabled@gmail.com", password="password", enable_outgoing = 1, default_outgoing=1)
|
||||
self.assertEqual(EmailAccount.find_outgoing().email_id, "default_outgoing_enabled@gmail.com")
|
||||
|
||||
frappe.local.outgoing_email_account = {}
|
||||
# highest preference given to email account with append_to matching
|
||||
create_email_account(email_id="append_to@gmail.com", password="***", enable_outgoing = 1, default_outgoing=1, append_to="Blog Post")
|
||||
self.assertEqual(get_outgoing_email_account(append_to="Blog Post").email_id, "append_to@gmail.com")
|
||||
create_email_account(email_id="append_to@gmail.com", password="password", enable_outgoing = 1, default_outgoing=1, append_to="Blog Post")
|
||||
self.assertEqual(EmailAccount.find_outgoing(match_by_doctype="Blog Post").email_id, "append_to@gmail.com")
|
||||
|
||||
# add back the mail_server
|
||||
frappe.conf['mail_server'] = mail_server
|
||||
|
|
@ -75,4 +75,4 @@ def make_server(port, ssl, tls):
|
|||
use_tls = tls
|
||||
)
|
||||
|
||||
server.sess
|
||||
server.session
|
||||
|
|
|
|||
|
|
@ -55,8 +55,8 @@ class EventProducer(Document):
|
|||
self.reload()
|
||||
|
||||
def check_url(self):
|
||||
if not frappe.utils.validate_url(self.producer_url):
|
||||
frappe.throw(_('Invalid URL'))
|
||||
valid_url_schemes = ("http", "https")
|
||||
frappe.utils.validate_url(self.producer_url, throw=True, valid_schemes=valid_url_schemes)
|
||||
|
||||
# remove '/' from the end of the url like http://test_site.com/
|
||||
# to prevent mismatch in get_url() results
|
||||
|
|
|
|||
|
|
@ -228,10 +228,7 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
|
|||
is_whitelisted(fn)
|
||||
is_valid_http_method(fn)
|
||||
|
||||
try:
|
||||
fnargs = inspect.getargspec(method_obj)[0]
|
||||
except ValueError:
|
||||
fnargs = inspect.getfullargspec(method_obj).args
|
||||
fnargs = inspect.getfullargspec(method_obj).args
|
||||
|
||||
if not fnargs or (len(fnargs)==1 and fnargs[0]=="self"):
|
||||
response = doc.run_method(method)
|
||||
|
|
|
|||
|
|
@ -71,7 +71,8 @@ numeric_fieldtypes = (
|
|||
data_field_options = (
|
||||
'Email',
|
||||
'Name',
|
||||
'Phone'
|
||||
'Phone',
|
||||
'URL'
|
||||
)
|
||||
|
||||
default_fields = (
|
||||
|
|
|
|||
|
|
@ -34,8 +34,9 @@ def get_controller(doctype):
|
|||
from frappe.model.document import Document
|
||||
from frappe.utils.nestedset import NestedSet
|
||||
|
||||
module_name, custom = frappe.db.get_value("DocType", doctype, ("module", "custom"), cache=True) \
|
||||
or ["Core", False]
|
||||
module_name, custom = frappe.db.get_value(
|
||||
"DocType", doctype, ("module", "custom"), cache=True
|
||||
) or ["Core", False]
|
||||
|
||||
if custom:
|
||||
if frappe.db.field_exists("DocType", "is_tree"):
|
||||
|
|
@ -666,6 +667,12 @@ class BaseDocument(object):
|
|||
if data_field_options == "Phone":
|
||||
frappe.utils.validate_phone_number(data, throw=True)
|
||||
|
||||
if data_field_options == "URL":
|
||||
if not data:
|
||||
continue
|
||||
|
||||
frappe.utils.validate_url(data, throw=True)
|
||||
|
||||
def _validate_constants(self):
|
||||
if frappe.flags.in_import or self.is_new() or self.flags.ignore_validate_constants:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -465,7 +465,7 @@ class DatabaseQuery(object):
|
|||
|
||||
elif f.operator.lower() in ('in', 'not in'):
|
||||
values = f.value or ''
|
||||
if isinstance(values, frappe.string_types):
|
||||
if isinstance(values, str):
|
||||
values = values.split(",")
|
||||
|
||||
fallback = "''"
|
||||
|
|
|
|||
|
|
@ -1347,6 +1347,22 @@ class Document(BaseDocument):
|
|||
from frappe.desk.doctype.tag.tag import DocTags
|
||||
return DocTags(self.doctype).get_tags(self.name).split(",")[1:]
|
||||
|
||||
def __repr__(self):
|
||||
name = self.name or "unsaved"
|
||||
doctype = self.__class__.__name__
|
||||
|
||||
docstatus = f" docstatus={self.docstatus}" if self.docstatus else ""
|
||||
parent = f" parent={self.parent}" if self.parent else ""
|
||||
|
||||
return f"<{doctype}: {name}{docstatus}{parent}>"
|
||||
|
||||
def __str__(self):
|
||||
name = self.name or "unsaved"
|
||||
doctype = self.__class__.__name__
|
||||
|
||||
return f"{doctype}({name})"
|
||||
|
||||
|
||||
def execute_action(doctype, name, action, **kwargs):
|
||||
"""Execute an action on a document (called by background worker)"""
|
||||
doc = frappe.get_doc(doctype, name)
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ class Meta(Document):
|
|||
# non standard list object, skip
|
||||
continue
|
||||
|
||||
if (isinstance(value, (frappe.text_type, int, float, datetime, list, tuple))
|
||||
if (isinstance(value, (str, int, float, datetime, list, tuple))
|
||||
or (not no_nulls and value is None)):
|
||||
out[key] = value
|
||||
|
||||
|
|
|
|||
|
|
@ -199,10 +199,39 @@ def getseries(key, digits):
|
|||
|
||||
|
||||
def revert_series_if_last(key, name, doc=None):
|
||||
if ".#" in key:
|
||||
"""
|
||||
Reverts the series for particular naming series:
|
||||
* key is naming series - SINV-.YYYY-.####
|
||||
* name is actual name - SINV-2021-0001
|
||||
|
||||
1. This function split the key into two parts prefix (SINV-YYYY) & hashes (####).
|
||||
2. Use prefix to get the current index of that naming series from Series table
|
||||
3. Then revert the current index.
|
||||
|
||||
*For custom naming series:*
|
||||
1. hash can exist anywhere, if it exist in hashes then it take normal flow.
|
||||
2. If hash doesn't exit in hashes, we get the hash from prefix, then update name and prefix accordingly.
|
||||
|
||||
*Example:*
|
||||
1. key = SINV-.YYYY.-
|
||||
* If key doesn't have hash it will add hash at the end
|
||||
* prefix will be SINV-YYYY based on this will get current index from Series table.
|
||||
2. key = SINV-.####.-2021
|
||||
* now prefix = SINV-#### and hashes = 2021 (hash doesn't exist)
|
||||
* will search hash in key then accordingly get prefix = SINV-
|
||||
3. key = ####.-2021
|
||||
* prefix = #### and hashes = 2021 (hash doesn't exist)
|
||||
* will search hash in key then accordingly get prefix = ""
|
||||
"""
|
||||
if ".#" in key:
|
||||
prefix, hashes = key.rsplit(".", 1)
|
||||
if "#" not in hashes:
|
||||
return
|
||||
# get the hash part from the key
|
||||
hash = re.search("#+", key)
|
||||
if not hash:
|
||||
return
|
||||
name = name.replace(hashes, "")
|
||||
prefix = prefix.replace(hash.group(), "")
|
||||
else:
|
||||
prefix = key
|
||||
|
||||
|
|
|
|||
|
|
@ -599,9 +599,10 @@ def get_client_scopes(client_id):
|
|||
def get_userinfo(user):
|
||||
picture = None
|
||||
frappe_server_url = get_server_url()
|
||||
valid_url_schemes = ("http", "https", "ftp", "ftps")
|
||||
|
||||
if user.user_image:
|
||||
if frappe.utils.validate_url(user.user_image):
|
||||
if frappe.utils.validate_url(user.user_image, valid_schemes=valid_url_schemes):
|
||||
picture = user.user_image
|
||||
else:
|
||||
picture = frappe_server_url + "/" + user.user_image
|
||||
|
|
|
|||
|
|
@ -336,3 +336,4 @@ frappe.patches.v13_0.remove_twilio_settings
|
|||
frappe.patches.v12_0.rename_uploaded_files_with_proper_name
|
||||
frappe.patches.v13_0.queryreport_columns
|
||||
frappe.patches.v13_0.jinja_hook
|
||||
frappe.patches.v13_0.update_notification_channel_if_empty
|
||||
|
|
|
|||
15
frappe/patches/v13_0/update_notification_channel_if_empty.py
Normal file
15
frappe/patches/v13_0/update_notification_channel_if_empty.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
|
||||
def execute():
|
||||
|
||||
frappe.reload_doc("Email", "doctype", "Notification")
|
||||
|
||||
notifications = frappe.get_all('Notification', {'is_standard': 1}, {'name', 'channel'})
|
||||
for notification in notifications:
|
||||
if not notification.channel:
|
||||
frappe.db.set_value("Notification", notification.name, "channel", "Email", update_modified=False)
|
||||
frappe.db.commit()
|
||||
|
|
@ -114,8 +114,6 @@ frappe.Application = Class.extend({
|
|||
dialog.get_close_btn().toggle(false);
|
||||
});
|
||||
|
||||
this.setup_user_group_listeners();
|
||||
|
||||
// listen to build errors
|
||||
this.setup_build_error_listener();
|
||||
|
||||
|
|
@ -476,14 +474,19 @@ frappe.Application = Class.extend({
|
|||
$('<link rel="icon" href="' + link + '" type="image/x-icon">').appendTo("head");
|
||||
},
|
||||
trigger_primary_action: function() {
|
||||
if(window.cur_dialog && cur_dialog.display) {
|
||||
// trigger primary
|
||||
cur_dialog.get_primary_btn().trigger("click");
|
||||
} else if(cur_frm && cur_frm.page.btn_primary.is(':visible')) {
|
||||
cur_frm.page.btn_primary.trigger('click');
|
||||
} else if(frappe.container.page.save_action) {
|
||||
frappe.container.page.save_action();
|
||||
}
|
||||
// to trigger change event on active input before triggering primary action
|
||||
$(document.activeElement).blur();
|
||||
// wait for possible JS validations triggered after blur (it might change primary button)
|
||||
setTimeout(() => {
|
||||
if (window.cur_dialog && cur_dialog.display) {
|
||||
// trigger primary
|
||||
cur_dialog.get_primary_btn().trigger("click");
|
||||
} else if (cur_frm && cur_frm.page.btn_primary.is(':visible')) {
|
||||
cur_frm.page.btn_primary.trigger('click');
|
||||
} else if (frappe.container.page.save_action) {
|
||||
frappe.container.page.save_action();
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
|
||||
set_rtl: function() {
|
||||
|
|
@ -593,15 +596,6 @@ frappe.Application = Class.extend({
|
|||
}
|
||||
},
|
||||
|
||||
setup_user_group_listeners() {
|
||||
frappe.realtime.on('user_group_added', (user_group) => {
|
||||
frappe.boot.user_groups && frappe.boot.user_groups.push(user_group);
|
||||
});
|
||||
frappe.realtime.on('user_group_deleted', (user_group) => {
|
||||
frappe.boot.user_groups = (frappe.boot.user_groups || []).filter(el => el !== user_group);
|
||||
});
|
||||
},
|
||||
|
||||
setup_energy_point_listeners() {
|
||||
frappe.realtime.on('energy_point_alert', (message) => {
|
||||
frappe.show_alert(message);
|
||||
|
|
@ -611,8 +605,7 @@ frappe.Application = Class.extend({
|
|||
setup_copy_doc_listener() {
|
||||
$('body').on('paste', (e) => {
|
||||
try {
|
||||
let clipboard_data = e.clipboardData || window.clipboardData || e.originalEvent.clipboardData;
|
||||
let pasted_data = clipboard_data.getData('Text');
|
||||
let pasted_data = frappe.utils.get_clipboard_data(e);
|
||||
let doc = JSON.parse(pasted_data);
|
||||
if (doc.doctype) {
|
||||
e.preventDefault();
|
||||
|
|
@ -627,6 +620,7 @@ frappe.Application = Class.extend({
|
|||
let res = frappe.model.with_doctype(doc.doctype, () => {
|
||||
let newdoc = frappe.model.copy_doc(doc);
|
||||
newdoc.__newname = doc.name;
|
||||
delete doc.name;
|
||||
newdoc.idx = null;
|
||||
newdoc.__run_link_triggers = false;
|
||||
frappe.set_route('Form', newdoc.doctype, newdoc.name);
|
||||
|
|
|
|||
|
|
@ -319,7 +319,7 @@ frappe.get_data_pill = (label, target_id=null, remove_action=null, image=null) =
|
|||
|
||||
frappe.get_modal = function(title, content) {
|
||||
return $(`<div class="modal fade" style="overflow: auto;" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-scrollable">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="fill-width flex title-section">
|
||||
|
|
|
|||
|
|
@ -90,16 +90,10 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({
|
|||
});
|
||||
|
||||
this.$input.on("awesomplete-open", () => {
|
||||
this.toggle_container_scroll('.modal-dialog', 'modal-dialog-scrollable');
|
||||
this.toggle_container_scroll('.grid-form-body .form-area', 'scrollable');
|
||||
|
||||
this.autocomplete_open = true;
|
||||
});
|
||||
|
||||
this.$input.on("awesomplete-close", () => {
|
||||
this.toggle_container_scroll('.modal-dialog', 'modal-dialog-scrollable', true);
|
||||
this.toggle_container_scroll('.grid-form-body .form-area', 'scrollable', true);
|
||||
|
||||
this.autocomplete_open = false;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import Picker from '../../color_picker/color_picker';
|
|||
|
||||
frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({
|
||||
make_input: function () {
|
||||
this.df.placeholder = this.df.placeholder || __('Choose a color');
|
||||
this._super();
|
||||
this.make_color_input();
|
||||
},
|
||||
|
|
@ -48,7 +49,16 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({
|
|||
$(window).off('hashchange.color-popover');
|
||||
});
|
||||
|
||||
this.$wrapper.find('.control-input').on('click', (e) => {
|
||||
this.picker.on_change = (color) => {
|
||||
this.set_value(color);
|
||||
};
|
||||
|
||||
if (!this.selected_color) {
|
||||
this.selected_color = $(`<div class="selected-color"></div>`);
|
||||
this.selected_color.insertAfter(this.$input);
|
||||
}
|
||||
|
||||
this.$wrapper.find('.selected-color').parent().on('click', (e) => {
|
||||
this.$wrapper.popover('toggle');
|
||||
if (!this.get_color()) {
|
||||
this.$input.val('');
|
||||
|
|
@ -63,15 +73,6 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({
|
|||
this.$wrapper.popover('hide');
|
||||
});
|
||||
});
|
||||
|
||||
this.picker.on_change = (color) => {
|
||||
this.set_value(color);
|
||||
};
|
||||
|
||||
if (!this.selected_color) {
|
||||
this.selected_color = $(`<div class="selected-color"></div>`);
|
||||
this.selected_color.insertAfter(this.$input);
|
||||
}
|
||||
},
|
||||
refresh() {
|
||||
this._super();
|
||||
|
|
@ -83,8 +84,7 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({
|
|||
},
|
||||
set_formatted_input: function(value) {
|
||||
this._super(value);
|
||||
|
||||
this.$input.val(value || __('Choose a color'));
|
||||
this.$input.val(value);
|
||||
this.selected_color.css({
|
||||
"background-color": value || 'transparent',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -78,35 +78,25 @@ frappe.ui.form.ControlComment = frappe.ui.form.ControlTextEditor.extend({
|
|||
},
|
||||
|
||||
get_mention_options() {
|
||||
if (!(this.mentions && this.mentions.length)) {
|
||||
if (!this.enable_mentions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const at_values = this.mentions.slice();
|
||||
|
||||
let me = this;
|
||||
return {
|
||||
allowedChars: /^[A-Za-z0-9_]*$/,
|
||||
mentionDenotationChars: ["@"],
|
||||
isolateCharacter: true,
|
||||
source: function (searchTerm, renderList, mentionChar) {
|
||||
let values;
|
||||
|
||||
if (mentionChar === "@") {
|
||||
values = at_values;
|
||||
}
|
||||
|
||||
if (searchTerm.length === 0) {
|
||||
renderList(values, searchTerm);
|
||||
} else {
|
||||
const matches = [];
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
if (~values[i].value.toLowerCase().indexOf(searchTerm.toLowerCase())) {
|
||||
matches.push(values[i]);
|
||||
}
|
||||
}
|
||||
renderList(matches, searchTerm);
|
||||
}
|
||||
},
|
||||
source: frappe.utils.debounce(async function(search_term, renderList) {
|
||||
let method = me.mention_search_method || 'frappe.desk.search.get_names_for_mentions';
|
||||
let values = await frappe.xcall(method, {
|
||||
search_term
|
||||
});
|
||||
renderList(values, search_term);
|
||||
}, 300),
|
||||
renderItem(item) {
|
||||
let value = item.value;
|
||||
return `${value} ${item.is_group ? frappe.utils.icon('users') : ''}`;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
frappe.ui.form.ControlCurrency = frappe.ui.form.ControlFloat.extend({
|
||||
format_for_input: function(value) {
|
||||
var formatted_value = format_number(value, this.get_number_format(), this.get_precision());
|
||||
return isNaN(parseFloat(value)) ? "" : formatted_value;
|
||||
return isNaN(Number(value)) ? "" : formatted_value;
|
||||
},
|
||||
|
||||
get_precision: function() {
|
||||
|
|
|
|||
|
|
@ -18,11 +18,96 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({
|
|||
this.$input.attr("maxlength", this.df.length || 140);
|
||||
}
|
||||
|
||||
this.$input.on('paste', (e) => {
|
||||
let pasted_data = frappe.utils.get_clipboard_data(e);
|
||||
let maxlength = this.$input.attr('maxlength');
|
||||
if (maxlength && pasted_data.length > maxlength) {
|
||||
let warning_message = __('The value you pasted was {0} characters long. Max allowed characters is {1}.', [
|
||||
cstr(pasted_data.length).bold(),
|
||||
cstr(maxlength).bold()
|
||||
]);
|
||||
|
||||
// Only show edit link to users who can update the doctype
|
||||
if (this.frm && frappe.model.can_write(this.frm.doctype)) {
|
||||
let doctype_edit_link = null;
|
||||
if (this.frm.meta.custom) {
|
||||
doctype_edit_link = frappe.utils.get_form_link(
|
||||
'DocType',
|
||||
this.frm.doctype, true,
|
||||
__('this form')
|
||||
);
|
||||
} else {
|
||||
doctype_edit_link = frappe.utils.get_form_link('Customize Form', 'Customize Form', true, null, {
|
||||
doc_type: this.frm.doctype
|
||||
});
|
||||
}
|
||||
let edit_note = __('{0}: You can increase the limit for the field if required via {1}', [
|
||||
__('Note').bold(),
|
||||
doctype_edit_link
|
||||
]);
|
||||
warning_message += `<br><br><span class="text-muted text-small">${edit_note}</span>`;
|
||||
}
|
||||
|
||||
frappe.msgprint({
|
||||
message: warning_message,
|
||||
indicator: 'orange',
|
||||
title: __('Data Clipped')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.set_input_attributes();
|
||||
this.input = this.$input.get(0);
|
||||
this.has_input = true;
|
||||
this.bind_change_event();
|
||||
this.setup_autoname_check();
|
||||
|
||||
if (this.df.options == 'URL') {
|
||||
this.setup_url_field();
|
||||
}
|
||||
},
|
||||
setup_url_field: function() {
|
||||
this.$wrapper.find('.control-input').append(
|
||||
`<span class="link-btn">
|
||||
<a class="btn-open no-decoration" title="${__("Open Link")}" target="_blank">
|
||||
${frappe.utils.icon('link-url', 'sm')}
|
||||
</a>
|
||||
</span>`
|
||||
);
|
||||
|
||||
this.$link = this.$wrapper.find('.link-btn');
|
||||
this.$link_open = this.$link.find('.btn-open');
|
||||
|
||||
this.$input.on("focus", () => {
|
||||
setTimeout(() => {
|
||||
let inputValue = this.get_input_value();
|
||||
|
||||
if (inputValue && validate_url(inputValue)) {
|
||||
this.$link.toggle(true);
|
||||
this.$link_open.attr('href', this.get_input_value());
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
|
||||
|
||||
this.$input.bind("input", () => {
|
||||
let inputValue = this.get_input_value();
|
||||
|
||||
if (inputValue && validate_url(inputValue)) {
|
||||
this.$link.toggle(true);
|
||||
this.$link_open.attr('href', this.get_input_value());
|
||||
} else {
|
||||
this.$link.toggle(false);
|
||||
}
|
||||
});
|
||||
|
||||
this.$input.on("blur", () => {
|
||||
// if this disappears immediately, the user's click
|
||||
// does not register, hence timeout
|
||||
setTimeout(() => {
|
||||
this.$link.toggle(false);
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
bind_change_event: function() {
|
||||
const change_handler = e => {
|
||||
|
|
@ -126,6 +211,9 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({
|
|||
this.df.invalid = email_invalid;
|
||||
return v;
|
||||
}
|
||||
} else if (this.df.options == 'URL') {
|
||||
this.df.invalid = !validate_url(v);
|
||||
return v;
|
||||
} else {
|
||||
return v;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ frappe.ui.form.ControlFloat = frappe.ui.form.ControlInt.extend({
|
|||
number_format = this.get_number_format();
|
||||
}
|
||||
var formatted_value = format_number(value, number_format, this.get_precision());
|
||||
return isNaN(parseFloat(value)) ? "" : formatted_value;
|
||||
return isNaN(Number(value)) ? "" : formatted_value;
|
||||
},
|
||||
|
||||
get_number_format: function() {
|
||||
|
|
|
|||
|
|
@ -241,16 +241,10 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
|
|||
});
|
||||
|
||||
this.$input.on("awesomplete-open", () => {
|
||||
this.toggle_container_scroll('.modal-dialog', 'modal-dialog-scrollable');
|
||||
this.toggle_container_scroll('.grid-form-body .form-area', 'scrollable');
|
||||
|
||||
this.autocomplete_open = true;
|
||||
});
|
||||
|
||||
this.$input.on("awesomplete-close", () => {
|
||||
this.toggle_container_scroll('.modal-dialog', 'modal-dialog-scrollable', true);
|
||||
this.toggle_container_scroll('.grid-form-body .form-area', 'scrollable', true);
|
||||
|
||||
this.autocomplete_open = false;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class MentionBlot extends Embed {
|
|||
denotationChar.innerHTML = data.denotationChar;
|
||||
node.appendChild(denotationChar);
|
||||
node.innerHTML += data.value;
|
||||
node.innerHTML += `${data.isGroup === 'true' ? frappe.utils.icon('users') : ''}`;
|
||||
node.dataset.id = data.id;
|
||||
node.dataset.value = data.value;
|
||||
node.dataset.denotationChar = data.denotationChar;
|
||||
|
|
|
|||
|
|
@ -26,8 +26,7 @@ frappe.ui.form.ControlTable = frappe.ui.form.Control.extend({
|
|||
const row_docname = $(e.target).closest('.grid-row').data('name');
|
||||
const in_grid_form = $(e.target).closest('.form-in-grid').length;
|
||||
|
||||
let clipboard_data = e.clipboardData || window.clipboardData || e.originalEvent.clipboardData;
|
||||
let pasted_data = clipboard_data.getData('Text');
|
||||
let pasted_data = frappe.utils.get_clipboard_data(e);
|
||||
|
||||
if (!pasted_data || in_grid_form) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ frappe.ui.form.Footer = Class.extend({
|
|||
parent: this.wrapper.find(".comment-box"),
|
||||
render_input: true,
|
||||
only_input: true,
|
||||
mentions: frappe.utils.get_names_for_mentions(),
|
||||
enable_mentions: true,
|
||||
df: {
|
||||
fieldtype: 'Comment',
|
||||
fieldname: 'comment'
|
||||
|
|
|
|||
|
|
@ -492,7 +492,7 @@ class FormTimeline extends BaseTimeline {
|
|||
fieldname: 'comment',
|
||||
label: 'Comment'
|
||||
},
|
||||
mentions: frappe.utils.get_names_for_mentions(),
|
||||
enable_mentions: true,
|
||||
render_input: true,
|
||||
only_input: true,
|
||||
no_wrapper: true
|
||||
|
|
|
|||
|
|
@ -15,7 +15,10 @@ frappe.form.formatters = {
|
|||
return "<div style='text-align: right'>" + value + "</div>";
|
||||
}
|
||||
},
|
||||
Data: function(value) {
|
||||
Data: function(value, df) {
|
||||
if (df && df.options == "URL") {
|
||||
return `<a href="${value}" title="Open Link" target="_blank">${value}</a>`;
|
||||
}
|
||||
return value==null ? "" : value;
|
||||
},
|
||||
Select: function(value) {
|
||||
|
|
@ -156,7 +159,7 @@ frappe.form.formatters = {
|
|||
return value || "";
|
||||
},
|
||||
DateRange: function(value) {
|
||||
if($.isArray(value)) {
|
||||
if (Array.isArray(value)) {
|
||||
return __("{0} to {1}", [frappe.datetime.str_to_user(value[0]), frappe.datetime.str_to_user(value[1])]);
|
||||
} else {
|
||||
return value || "";
|
||||
|
|
@ -292,12 +295,12 @@ frappe.form.formatters = {
|
|||
return formatted_values.join(', ');
|
||||
},
|
||||
Color: (value) => {
|
||||
return `<div>
|
||||
return value ? `<div>
|
||||
<div class="selected-color" style="background-color: ${value}"></div>
|
||||
<span class="color-value">${value}</span>
|
||||
</div>`;
|
||||
</div>` : '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
frappe.form.get_formatter = function(fieldtype) {
|
||||
if(!fieldtype)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ export default class GridRow {
|
|||
$.extend(this, opts);
|
||||
if (this.doc && this.parent_df.options) {
|
||||
frappe.meta.make_docfield_copy_for(this.parent_df.options, this.doc.name, this.docfields);
|
||||
this.docfields = frappe.meta.get_docfields(this.parent_df.options, this.doc.name);
|
||||
const docfields = frappe.meta.get_docfields(this.parent_df.options, this.doc.name);
|
||||
this.docfields = docfields.length ? docfields : opts.docfields;
|
||||
}
|
||||
this.columns = {};
|
||||
this.columns_list = [];
|
||||
|
|
@ -422,7 +423,7 @@ export default class GridRow {
|
|||
field.$input
|
||||
.addClass('input-sm')
|
||||
.attr('data-col-idx', column.column_index)
|
||||
.attr('placeholder', __(df.label));
|
||||
.attr('placeholder', __(df.placeholder || df.label));
|
||||
// flag list input
|
||||
if (this.columns_list && this.columns_list.slice(-1)[0]===column) {
|
||||
field.$input.attr('data-last-input', 1);
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export default class GridRowForm {
|
|||
</div>
|
||||
</div>
|
||||
<div class="grid-form-body">
|
||||
<div class="form-area scrollable"></div>
|
||||
<div class="form-area"></div>
|
||||
<div class="grid-footer-toolbar hidden-xs flex justify-between">
|
||||
<div class="grid-shortcuts">
|
||||
<span> ${frappe.utils.icon("keyboard", "md")} </span>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
}
|
||||
set_title() {
|
||||
if (this.frm.is_new()) {
|
||||
var title = __('New {0}', [this.frm.meta.name]);
|
||||
var title = __('New {0}', [__(this.frm.meta.name)]);
|
||||
} else if (this.frm.meta.title_field) {
|
||||
let title_field = (this.frm.doc[this.frm.meta.title_field] || "").toString().trim();
|
||||
var title = strip_html(title_field || this.frm.docname);
|
||||
|
|
@ -551,7 +551,7 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
|
||||
let fields = this.frm.fields
|
||||
.filter(visible_fields_filter)
|
||||
.map(f => ({ label: f.df.label, value: f.df.fieldname }));
|
||||
.map(f => ({ label: __(f.df.label), value: f.df.fieldname }));
|
||||
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __('Jump to field'),
|
||||
|
|
|
|||
|
|
@ -150,13 +150,13 @@ frappe.views.ListViewSelect = class ListViewSelect {
|
|||
const views_wrapper = this.sidebar.sidebar.find(".views-section");
|
||||
views_wrapper.find(".sidebar-label").html(`${__(view)}`);
|
||||
const $dropdown = views_wrapper.find(".views-dropdown");
|
||||
|
||||
let placeholder = `Select ${view}`;
|
||||
|
||||
let placeholder = `${__("Select {0}", [__(view)])}`;
|
||||
let html = ``;
|
||||
|
||||
if (!items || !items.length) {
|
||||
html = `<div class="empty-state">
|
||||
${__("No {} Found", [view])}
|
||||
${__("No {0} Found", [__(view)])}
|
||||
</div>`;
|
||||
} else {
|
||||
const page_name = this.get_page_name();
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<div class="tag-filters-area">
|
||||
<div class="active-tag-filters">
|
||||
<button class="btn btn-default btn-xs add-filter text-muted">
|
||||
Add Filter
|
||||
{{ __("Add Filter") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -71,12 +71,12 @@
|
|||
</div>
|
||||
<div v-if="requests.length == 0" class="no-result text-muted flex justify-center align-center" style="">
|
||||
<div class="msg-box no-border" v-if="status.status == 'Inactive'" >
|
||||
<p>Recorder is Inactive</p>
|
||||
<p><button class="btn btn-primary btn-sm btn-new-doc" @click="start()">Start Recording</button></p>
|
||||
<p>{{ __("Recorder is Inactive") }}</p>
|
||||
<p><button class="btn btn-primary btn-sm btn-new-doc" @click="start()">{{ __("Start Recording") }}</button></p>
|
||||
</div>
|
||||
<div class="msg-box no-border" v-if="status.status == 'Active'" >
|
||||
<p>No Requests found</p>
|
||||
<p>Go make some noise</p>
|
||||
<p>{{ __("No Requests found") }}</p>
|
||||
<p>{{ __("Go make some noise") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="requests.length != 0" class="list-paging-area">
|
||||
|
|
@ -108,12 +108,12 @@ export default {
|
|||
return {
|
||||
requests: [],
|
||||
columns: [
|
||||
{label: "Path", slug: "path"},
|
||||
{label: "Duration (ms)", slug: "duration", sortable: true, number: true},
|
||||
{label: "Time in Queries (ms)", slug: "time_queries", sortable: true, number: true},
|
||||
{label: "Queries", slug: "queries", sortable: true, number: true},
|
||||
{label: "Method", slug: "method"},
|
||||
{label: "Time", slug: "time", sortable: true},
|
||||
{label: __("Path"), slug: "path"},
|
||||
{label: __("Duration (ms)"), slug: "duration", sortable: true, number: true},
|
||||
{label: __("Time in Queries (ms)"), slug: "time_queries", sortable: true, number: true},
|
||||
{label: __("Queries"), slug: "queries", sortable: true, number: true},
|
||||
{label: __("Method"), slug: "method"},
|
||||
{label: __("Time"), slug: "time", sortable: true},
|
||||
],
|
||||
query: {
|
||||
sort: "duration",
|
||||
|
|
@ -140,7 +140,7 @@ export default {
|
|||
mounted() {
|
||||
this.fetch_status();
|
||||
this.refresh();
|
||||
this.$root.page.set_secondary_action("Clear", () => {
|
||||
this.$root.page.set_secondary_action(__("Clear"), () => {
|
||||
frappe.set_route("recorder");
|
||||
this.clear();
|
||||
});
|
||||
|
|
@ -151,11 +151,11 @@ export default {
|
|||
const current_page = this.query.pagination.page;
|
||||
const total_pages = this.query.pagination.total;
|
||||
return [{
|
||||
label: "First",
|
||||
label: __("First"),
|
||||
number: 1,
|
||||
status: (current_page == 1) ? "disabled" : "",
|
||||
},{
|
||||
label: "Previous",
|
||||
label: __("Previous"),
|
||||
number: Math.max(current_page - 1, 1),
|
||||
status: (current_page == 1) ? "disabled" : "",
|
||||
}, {
|
||||
|
|
@ -163,11 +163,11 @@ export default {
|
|||
number: current_page,
|
||||
status: "btn-info",
|
||||
}, {
|
||||
label: "Next",
|
||||
label: __("Next"),
|
||||
number: Math.min(current_page + 1, total_pages),
|
||||
status: (current_page == total_pages) ? "disabled" : "",
|
||||
}, {
|
||||
label: "Last",
|
||||
label: __("Last"),
|
||||
number: total_pages,
|
||||
status: (current_page == total_pages) ? "disabled" : "",
|
||||
}];
|
||||
|
|
@ -230,11 +230,11 @@ export default {
|
|||
},
|
||||
update_buttons: function() {
|
||||
if(this.status.status == "Active") {
|
||||
this.$root.page.set_primary_action("Stop", () => {
|
||||
this.$root.page.set_primary_action(__("Stop"), () => {
|
||||
this.stop();
|
||||
});
|
||||
} else {
|
||||
this.$root.page.set_primary_action("Start", () => {
|
||||
this.$root.page.set_primary_action(__("Start"), () => {
|
||||
this.start();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
</div>
|
||||
<div class="row form-section visible-section">
|
||||
<div class="col-sm-10">
|
||||
<h6 class="form-section-heading uppercase">SQL Queries</h6>
|
||||
<h6 class="form-section-heading uppercase">{{ __("SQL Queries") }}</h6>
|
||||
</div>
|
||||
<div class="col-sm-2 filter-list">
|
||||
<div class="sort-selector">
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
<div class="checkbox">
|
||||
<label>
|
||||
<span class="input-area"><input type="checkbox" class="input-with-feedback bold" data-fieldtype="Check" v-model="group_duplicates"></span>
|
||||
<span class="label-area small">Group Duplicate Queries</span>
|
||||
<span class="label-area small">{{ __("Group Duplicate Queries") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -48,15 +48,15 @@
|
|||
<div class="grid-row">
|
||||
<div class="data-row row">
|
||||
<div class="row-index col col-xs-1">
|
||||
<span>Index</span></div>
|
||||
<span>{{ __("Index") }}</span></div>
|
||||
<div class="col grid-static-col col-xs-6">
|
||||
<div class="static-area ellipsis">Query</div>
|
||||
<div class="static-area ellipsis">{{ __("Query") }}</div>
|
||||
</div>
|
||||
<div class="col grid-static-col col-xs-2">
|
||||
<div class="static-area ellipsis text-right">Duration (ms)</div>
|
||||
<div class="static-area ellipsis text-right">{{ __("Duration (ms)") }}</div>
|
||||
</div>
|
||||
<div class="col grid-static-col col-xs-2">
|
||||
<div class="static-area ellipsis text-right">Exact Copies</div>
|
||||
<div class="static-area ellipsis text-right">{{ __("Exact Copies") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -82,7 +82,7 @@
|
|||
<div class="recorder-form-in-grid" v-if="showing == call.index">
|
||||
<div class="grid-form-heading" @click="showing = null">
|
||||
<div class="toolbar grid-header-toolbar">
|
||||
<span class="panel-title">SQL Query #<span class="grid-form-row-index">{{ call.index }}</span></span>
|
||||
<span class="panel-title">{{ __("SQL Query") }} #<span class="grid-form-row-index">{{ call.index }}</span></span>
|
||||
<div class="btn btn-default btn-xs pull-right" style="margin-left: 7px;">
|
||||
<span class="hidden-xs octicon octicon-triangle-up"></span>
|
||||
</div>
|
||||
|
|
@ -98,25 +98,25 @@
|
|||
<form>
|
||||
<div class="frappe-control">
|
||||
<div class="form-group">
|
||||
<div class="clearfix"><label class="control-label">Query</label></div>
|
||||
<div class="clearfix"><label class="control-label">{{ __("Query") }}</label></div>
|
||||
<div class="control-value like-disabled-input for-description"><pre>{{ call.query }}</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="frappe-control input-max-width">
|
||||
<div class="form-group">
|
||||
<div class="clearfix"><label class="control-label">Duration (ms)</label></div>
|
||||
<div class="clearfix"><label class="control-label">{{ __("Duration (ms)") }}</label></div>
|
||||
<div class="control-value like-disabled-input">{{ call.duration }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="frappe-control input-max-width">
|
||||
<div class="form-group">
|
||||
<div class="clearfix"><label class="control-label">Exact Copies</label></div>
|
||||
<div class="clearfix"><label class="control-label">{{ __("Exact Copies") }}</label></div>
|
||||
<div class="control-value like-disabled-input">{{ call.exact_copies }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="frappe-control">
|
||||
<div class="form-group">
|
||||
<div class="clearfix"><label class="control-label">Stack Trace</label></div>
|
||||
<div class="clearfix"><label class="control-label"{{ __("Stack Trace") }}</label></div>
|
||||
<div class="control-value like-disabled-input for-description" style="overflow:auto">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
|
|
@ -137,7 +137,7 @@
|
|||
</div>
|
||||
<div class="frappe-control" v-if="call.explain_result[0]">
|
||||
<div class="form-group">
|
||||
<div class="clearfix"><label class="control-label">SQL Explain</label></div>
|
||||
<div class="clearfix"><label class="control-label">{{ __("SQL Explain") }}</label></div>
|
||||
<div class="control-value like-disabled-input for-description" style="overflow:auto">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
|
|
@ -165,7 +165,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="request.calls.length == 0" class="grid-empty text-center">No Data</div>
|
||||
<div v-if="request.calls.length == 0" class="grid-empty text-center">{{ __("No Data") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -201,19 +201,19 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
columns: [
|
||||
{label: "Path", slug: "path", type: "Data", class: "col-sm-6"},
|
||||
{label: "CMD", slug: "cmd", type: "Data", class: "col-sm-6"},
|
||||
{label: "Time", slug: "time", type: "Time", class: "col-sm-6"},
|
||||
{label: "Duration (ms)", slug: "duration", type: "Float", class: "col-sm-6"},
|
||||
{label: "Number of Queries", slug: "queries", type: "Int", class: "col-sm-6"},
|
||||
{label: "Time in Queries (ms)", slug: "time_queries", type: "Float", class: "col-sm-6"},
|
||||
{label: "Request Headers", slug: "headers", type: "Small Text", formatter: value => `<pre class="for-description like-disabled-input">${JSON.stringify(value, null, 4)}</pre>`, class: "col-sm-12"},
|
||||
{label: "Form Dict", slug: "form_dict", type: "Small Text", formatter: value => `<pre class="for-description like-disabled-input">${JSON.stringify(value, null, 4)}</pre>`, class: "col-sm-12"},
|
||||
{label: __("Path"), slug: "path", type: "Data", class: "col-sm-6"},
|
||||
{label: __("CMD"), slug: "cmd", type: "Data", class: "col-sm-6"},
|
||||
{label: __("Time"), slug: "time", type: "Time", class: "col-sm-6"},
|
||||
{label: __("Duration (ms)"), slug: "duration", type: "Float", class: "col-sm-6"},
|
||||
{label: __("Number of Queries"), slug: "queries", type: "Int", class: "col-sm-6"},
|
||||
{label: __("Time in Queries (ms)"), slug: "time_queries", type: "Float", class: "col-sm-6"},
|
||||
{label: __("Request Headers"), slug: "headers", type: "Small Text", formatter: value => `<pre class="for-description like-disabled-input">${JSON.stringify(value, null, 4)}</pre>`, class: "col-sm-12"},
|
||||
{label: __("Form Dict"), slug: "form_dict", type: "Small Text", formatter: value => `<pre class="for-description like-disabled-input">${JSON.stringify(value, null, 4)}</pre>`, class: "col-sm-12"},
|
||||
],
|
||||
table_columns: [
|
||||
{label: "Execution Order", slug: "index", sortable: true},
|
||||
{label: "Duration (ms)", slug: "duration", sortable: true},
|
||||
{label: "Exact Copies", slug: "exact_copies", sortable: true},
|
||||
{label: __("Execution Order"), slug: "index", sortable: true},
|
||||
{label: __("Duration (ms)"), slug: "duration", sortable: true},
|
||||
{label: __("Exact Copies"), slug: "exact_copies", sortable: true},
|
||||
],
|
||||
query: {
|
||||
sort: "duration",
|
||||
|
|
@ -236,11 +236,11 @@ export default {
|
|||
const current_page = this.query.pagination.page;
|
||||
const total_pages = this.query.pagination.total;
|
||||
return [{
|
||||
label: "First",
|
||||
label: __("First"),
|
||||
number: 1,
|
||||
status: (current_page == 1) ? "disabled" : "",
|
||||
},{
|
||||
label: "Previous",
|
||||
label: __("Previous"),
|
||||
number: Math.max(current_page - 1, 1),
|
||||
status: (current_page == 1) ? "disabled" : "",
|
||||
}, {
|
||||
|
|
@ -248,11 +248,11 @@ export default {
|
|||
number: current_page,
|
||||
status: "btn-info",
|
||||
}, {
|
||||
label: "Next",
|
||||
label: __("Next"),
|
||||
number: Math.min(current_page + 1, total_pages),
|
||||
status: (current_page == total_pages) ? "disabled" : "",
|
||||
}, {
|
||||
label: "Last",
|
||||
label: __("Last"),
|
||||
number: total_pages,
|
||||
status: (current_page == total_pages) ? "disabled" : "",
|
||||
}];
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import RecorderRoot from "./RecorderRoot.vue";
|
|||
import RecorderDetail from "./RecorderDetail.vue";
|
||||
import RequestDetail from "./RequestDetail.vue";
|
||||
|
||||
Vue.prototype.__ = window.__;
|
||||
Vue.prototype.frappe = window.frappe;
|
||||
|
||||
Vue.use(VueRouter);
|
||||
const routes = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -36,18 +36,6 @@ frappe.ui.FieldSelect = Class.extend({
|
|||
var item = me.awesomplete.get_item(value);
|
||||
me.$input.val(item.label);
|
||||
});
|
||||
this.$input.on("awesomplete-open", () => {
|
||||
let modal = this.$input.parents('.modal-dialog')[0];
|
||||
if (modal) {
|
||||
$(modal).removeClass("modal-dialog-scrollable");
|
||||
}
|
||||
});
|
||||
this.$input.on("awesomplete-close", () => {
|
||||
let modal = this.$input.parents('.modal-dialog')[0];
|
||||
if (modal) {
|
||||
$(modal).addClass("modal-dialog-scrollable");
|
||||
}
|
||||
});
|
||||
|
||||
if(this.filter_fields) {
|
||||
for(var i in this.filter_fields)
|
||||
|
|
|
|||
|
|
@ -312,7 +312,7 @@ class NotificationsView extends BaseNotificationsView {
|
|||
this.container.append($(`<div class="notification-null-state">
|
||||
<div class="text-center">
|
||||
<img src="/assets/frappe/images/ui-states/notification-empty-state.svg" alt="Generic Empty State" class="null-state">
|
||||
<div class="title">No New notifications</div>
|
||||
<div class="title">${__('No New notifications')}</div>
|
||||
<div class="subtitle">
|
||||
${__('Looks like you haven’t received any notifications.')}
|
||||
</div></div></div>`));
|
||||
|
|
@ -430,7 +430,7 @@ class EventsView extends BaseNotificationsView {
|
|||
<div class="notification-null-state">
|
||||
<div class="text-center">
|
||||
<img src="/assets/frappe/images/ui-states/event-empty-state.svg" alt="Generic Empty State" class="null-state">
|
||||
<div class="title">No Upcoming Events</div>
|
||||
<div class="title">${__('No Upcoming Events')}</div>
|
||||
<div class="subtitle">
|
||||
${__('There are no upcoming events for you.')}
|
||||
</div></div></div>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,34 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
|
|||
title: __("Switch Theme")
|
||||
});
|
||||
this.body = $(`<div class="theme-grid"></div>`).appendTo(this.dialog.$body);
|
||||
this.bind_events();
|
||||
}
|
||||
|
||||
bind_events() {
|
||||
this.dialog.$wrapper.on('keydown', (e) => {
|
||||
if (!this.themes) return;
|
||||
|
||||
const key = frappe.ui.keys.get_key(e);
|
||||
let increment_by;
|
||||
|
||||
if (key === "right") {
|
||||
increment_by = 1;
|
||||
} else if (key === "left") {
|
||||
increment_by = -1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const current_index = this.themes.findIndex(theme => {
|
||||
return theme.name === this.current_theme;
|
||||
});
|
||||
|
||||
const new_theme = this.themes[current_index + increment_by];
|
||||
if (!new_theme) return;
|
||||
|
||||
new_theme.$html.click();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
refresh() {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ window.validate_name = function(txt) {
|
|||
return frappe.utils.validate_type(txt, "name");
|
||||
};
|
||||
|
||||
window.validate_url = function(txt) {
|
||||
return frappe.utils.validate_type(txt, "url");
|
||||
};
|
||||
|
||||
window.nth = function(number) {
|
||||
number = cint(number);
|
||||
var s = 'th';
|
||||
|
|
|
|||
|
|
@ -405,7 +405,7 @@ Object.assign(frappe.utils, {
|
|||
regExp = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
break;
|
||||
case "url":
|
||||
regExp = /^(https?|s?ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!$&'()*+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[$&'()*+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!$&'()*+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!$&'()*+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!$&'()*+,;=]|:|@)|\/|\?)*)?$/i;
|
||||
regExp = /^((([A-Za-z0-9.+-]+:(?:\/\/)?)(?:[-;:&=\+\,\w]@)?[A-Za-z0-9.-]+(:[0-9]+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/i;
|
||||
break;
|
||||
case "dateIso":
|
||||
regExp = /^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])$/;
|
||||
|
|
@ -831,10 +831,13 @@ Object.assign(frappe.utils, {
|
|||
if (callNow) func.apply(context, args);
|
||||
};
|
||||
},
|
||||
get_form_link: function(doctype, name, html = false, display_text = null) {
|
||||
get_form_link: function(doctype, name, html=false, display_text=null, query_params_obj=null) {
|
||||
display_text = display_text || name;
|
||||
name = encodeURIComponent(name);
|
||||
const route = `/app/${encodeURIComponent(doctype.toLowerCase().replace(/ /g, '-'))}/${name}`;
|
||||
let route = `/app/${encodeURIComponent(doctype.toLowerCase().replace(/ /g, '-'))}/${name}`;
|
||||
if (query_params_obj) {
|
||||
route += frappe.utils.make_query_string(query_params_obj);
|
||||
}
|
||||
if (html) {
|
||||
return `<a href="${route}">${display_text}</a>`;
|
||||
}
|
||||
|
|
@ -1272,31 +1275,6 @@ Object.assign(frappe.utils, {
|
|||
</div>`);
|
||||
},
|
||||
|
||||
get_names_for_mentions() {
|
||||
let names_for_mentions = Object.keys(frappe.boot.user_info || [])
|
||||
.filter(user => {
|
||||
return !["Administrator", "Guest"].includes(user)
|
||||
&& frappe.boot.user_info[user].allowed_in_mentions
|
||||
&& frappe.boot.user_info[user].user_type === 'System User';
|
||||
})
|
||||
.map(user => {
|
||||
return {
|
||||
id: frappe.boot.user_info[user].name,
|
||||
value: frappe.boot.user_info[user].fullname,
|
||||
};
|
||||
});
|
||||
|
||||
frappe.boot.user_groups && frappe.boot.user_groups.map(group => {
|
||||
names_for_mentions.push({
|
||||
id: group,
|
||||
value: group,
|
||||
is_group: true,
|
||||
link: frappe.utils.get_form_link('User Group', group)
|
||||
});
|
||||
});
|
||||
|
||||
return names_for_mentions;
|
||||
},
|
||||
print(doctype, docname, print_format, letterhead, lang_code) {
|
||||
let w = window.open(
|
||||
frappe.urllib.get_full_url(
|
||||
|
|
@ -1319,5 +1297,11 @@ Object.assign(frappe.utils, {
|
|||
frappe.msgprint(__('Please enable pop-ups'));
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
get_clipboard_data(clipboard_paste_event) {
|
||||
let e = clipboard_paste_event;
|
||||
let clipboard_data = e.clipboardData || window.clipboardData || e.originalEvent.clipboardData;
|
||||
return clipboard_data.getData('Text');
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ frappe.breadcrumbs = {
|
|||
if (breadcrumbs.doctype && ["print", "form"].includes(view)) {
|
||||
this.set_list_breadcrumb(breadcrumbs);
|
||||
this.set_form_breadcrumb(breadcrumbs, view);
|
||||
} else if (breadcrumbs.doctype && view === 'list') {
|
||||
this.set_list_breadcrumb(breadcrumbs);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
|
|||
setup_defaults() {
|
||||
return super.setup_defaults()
|
||||
.then(() => {
|
||||
this.page_title = __('{0} Dashboard', [this.doctype]);
|
||||
this.page_title = __('{0} Dashboard', [__(this.doctype)]);
|
||||
this.dashboard_settings = frappe.get_user_settings(this.doctype)['dashboard_settings'] || null;
|
||||
});
|
||||
}
|
||||
|
|
@ -271,7 +271,7 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
|
|||
show_add_chart_dialog() {
|
||||
let fields = this.get_field_options();
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Add a {0} Chart", [this.doctype]),
|
||||
title: __("Add a {0} Chart", [__(this.doctype)]),
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'new_or_existing',
|
||||
|
|
|
|||
|
|
@ -335,12 +335,12 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
let message;
|
||||
if (dashboard_name) {
|
||||
let dashboard_route_html = `<a href="#dashboard-view/${dashboard_name}">${dashboard_name}</a>`;
|
||||
message = __("New {0} {1} added to Dashboard {2}", [doctype, name, dashboard_route_html]);
|
||||
message = __("New {0} {1} added to Dashboard {2}", [__(doctype), name, dashboard_route_html]);
|
||||
} else {
|
||||
message = __("New {0} {1} created", [doctype, name]);
|
||||
message = __("New {0} {1} created", [__(doctype), name]);
|
||||
}
|
||||
|
||||
frappe.msgprint(message, __("New {0} Created", [doctype]));
|
||||
frappe.msgprint(message, __("New {0} Created", [__(doctype)]));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -937,7 +937,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
else {
|
||||
wrapper[0].innerHTML =
|
||||
`<div class="flex justify-center align-center text-muted" style="height: 120px; display: flex;">
|
||||
<div>Please select X and Y fields</div>
|
||||
<div>${__("Please select X and Y fields")}</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
|
@ -1094,7 +1094,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
|
||||
return Object.assign(column, {
|
||||
id: column.fieldname,
|
||||
name: __(column.label),
|
||||
name: __(column.label, null, `Column of report '${this.report_name}'`), // context has to match context in get_messages_from_report in translate.py
|
||||
width: parseInt(column.width) || null,
|
||||
editable: false,
|
||||
compareValue: compareFn,
|
||||
|
|
@ -1343,7 +1343,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
|
||||
open_url_post(frappe.request.url, args);
|
||||
}
|
||||
}, __('Export Report: '+ this.report_name), __('Download'));
|
||||
}, __('Export Report: {0}', [this.report_name]), __('Download'));
|
||||
}
|
||||
|
||||
get_data_for_csv(include_indentation) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import Widget from "./base_widget.js";
|
|||
|
||||
frappe.provide("frappe.utils");
|
||||
|
||||
const INDICATOR_COLORS = ["Grey", "Green", "Red", "Orange", "Pink", "Yellow", "Blue", "Cyan", "Teal"];
|
||||
export default class ShortcutWidget extends Widget {
|
||||
constructor(opts) {
|
||||
opts.shadow = true;
|
||||
|
|
@ -79,7 +78,7 @@ export default class ShortcutWidget extends Widget {
|
|||
|
||||
this.action_area.empty();
|
||||
const label = get_label();
|
||||
let color = INDICATOR_COLORS.includes(this.color) && count ? this.color.toLowerCase() : 'gray';
|
||||
let color = this.color && count ? this.color.toLowerCase() : 'gray';
|
||||
$(`<div class="indicator-pill ellipsis ${color}">${label}</div>`).appendTo(this.action_area);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -237,9 +237,19 @@ class ShortcutDialog extends WidgetDialog {
|
|||
hidden: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Color",
|
||||
fieldtype: "Select",
|
||||
fieldname: "color",
|
||||
label: __("Color"),
|
||||
options: ["Grey", "Green", "Red", "Orange", "Pink", "Yellow", "Blue", "Cyan"],
|
||||
default: "Grey",
|
||||
onchange: () => {
|
||||
let color = this.dialog.fields_dict.color.value.toLowerCase();
|
||||
let $select = this.dialog.fields_dict.color.$input;
|
||||
if (!$select.parent().find('.color-box').get(0)) {
|
||||
$(`<div class="color-box"></div>`).insertBefore($select.get(0));
|
||||
}
|
||||
$select.parent().find('.color-box').get(0).style.backgroundColor = `var(--text-on-${color})`;
|
||||
}
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
|
|
|
|||
|
|
@ -121,3 +121,10 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.data-row.row {
|
||||
.selected-color {
|
||||
top: calc(50% - 11px);
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,17 @@
|
|||
--blue-100: #D3E9FC;
|
||||
--blue-50 : #F0F8FE;
|
||||
|
||||
--cyan-900: #006464;
|
||||
--cyan-800: #007272;
|
||||
--cyan-700: #008b8b;
|
||||
--cyan-600: #02c5c5;
|
||||
--cyan-500: #00ffff;
|
||||
--cyan-400: #2ef8f8;
|
||||
--cyan-300: #6efcfc;
|
||||
--cyan-200: #a0f8f8;
|
||||
--cyan-100: #c7fcfc;
|
||||
--cyan-50 : #dafafa;
|
||||
|
||||
--green-900: #2D401D;
|
||||
--green-800: #44622A;
|
||||
--green-700: #518B21;
|
||||
|
|
@ -151,6 +162,8 @@
|
|||
--bg-gray: var(--gray-200);
|
||||
--bg-light-gray: var(--gray-100);
|
||||
--bg-purple: var(--purple-100);
|
||||
--bg-pink: var(--pink-50);
|
||||
--bg-cyan: var(--cyan-50);
|
||||
|
||||
--text-on-blue: var(--blue-600);
|
||||
--text-on-light-blue: var(--blue-500);
|
||||
|
|
@ -163,6 +176,8 @@
|
|||
--text-on-gray: var(--gray-600);
|
||||
--text-on-light-gray: var(--gray-800);
|
||||
--text-on-purple: var(--purple-500);
|
||||
--text-on-pink: var(--pink-500);
|
||||
--text-on-cyan: var(--cyan-600);
|
||||
|
||||
--awesomplete-hover-bg: var(--control-bg);
|
||||
|
||||
|
|
|
|||
|
|
@ -16,73 +16,82 @@
|
|||
}
|
||||
}
|
||||
|
||||
$check-icon: url("data:image/svg+xml, <svg viewBox='0 0 8 7' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M1 4.00001L2.66667 5.80001L7 1.20001' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
|
||||
|
||||
input[type="checkbox"] {
|
||||
position: relative;
|
||||
width: 0 !important;
|
||||
height: var(--custom-checkbox-size);
|
||||
margin-right: calc(var(--custom-checkbox-size) + var(--checkbox-right-margin)) !important;
|
||||
font-size: calc(var(--custom-checkbox-size) - 1px);
|
||||
width: var(--checkbox-size) !important;
|
||||
height: var(--checkbox-size);
|
||||
margin-right: var(--checkbox-right-margin) !important;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
border: 1px solid var(--gray-400);
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
|
||||
&:before {
|
||||
width: var(--custom-checkbox-size);
|
||||
height: var(--custom-checkbox-size);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
content: ' ';
|
||||
border: 1px solid var(--gray-400);
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
// Reset browser behavior
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
|
||||
-webkit-print-color-adjust: exact;
|
||||
color-adjust: exact;
|
||||
|
||||
.grid-static-col & {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
&:checked:before {
|
||||
content: url("data: image/svg+xml;utf8, <svg width='8' height='7' viewBox='0 0 8 7' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M1 4.00001L2.66667 5.80001L7 1.20001' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
|
||||
background: linear-gradient(180deg, #4AC3F8 -124.51%, #2490EF 100%);
|
||||
&:checked {
|
||||
background-color: #2490EF;
|
||||
background-image: $check-icon, linear-gradient(180deg, #4AC3F8 -124.51%, #2490EF 100%);
|
||||
background-size: 57%, 100%;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none; // Prevent browser behavior
|
||||
box-shadow: var(--checkbox-focus-shadow);
|
||||
}
|
||||
|
||||
&.disabled-deselected:before, &:disabled:not([checked])::before {
|
||||
background: var(--disabled-control-bg);
|
||||
border: 0.5px solid var(--gray-300);
|
||||
box-sizing: border-box;
|
||||
&.disabled-deselected, &:disabled {
|
||||
background-color: var(--disabled-control-bg);
|
||||
box-shadow: inset 0px 1px 7px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
border: 0.5px solid var(--gray-300);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.disabled-selected:before, &:disabled:checked::before {
|
||||
content: url("data: image/svg+xml;utf8, <svg width='8' height='7' viewBox='0 0 8 7' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M1 4.00001L2.66667 5.80001L7 1.20001' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
|
||||
background: var(--gray-500);
|
||||
box-sizing: border-box;
|
||||
&.disabled-selected, &:disabled:checked {
|
||||
background-color: var(--gray-500);
|
||||
background-image: $check-icon;
|
||||
background-size: 57%;
|
||||
box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
line-height: 10px;
|
||||
border: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Firefox doesn't support
|
||||
// pseudo elements on checkbox
|
||||
html.firefox, html.safari {
|
||||
:root {
|
||||
--custom-checkbox-size: 0px;
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
width: var(--base-checkbox-size) !important;
|
||||
height: var(--base-checkbox-size);
|
||||
margin-right: var(--checkbox-right-margin) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.frappe-card {
|
||||
@include card();
|
||||
}
|
||||
|
||||
.frappe-control[data-fieldtype="Select"].frappe-control[data-fieldname="color"] {
|
||||
select {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.color-box {
|
||||
position: absolute;
|
||||
top: calc(50% - 11px);
|
||||
left: 8px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 5px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.frappe-control[data-fieldtype="Select"] .control-input,
|
||||
.frappe-control[data-fieldtype="Select"].form-group {
|
||||
position: relative;
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@
|
|||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.grid-static-col[data-fieldtype="Code"] {
|
||||
.grid-static-col[data-fieldtype="Code"], .grid-static-col[data-fieldtype="HTML Editor"] {
|
||||
overflow: hidden;
|
||||
|
||||
.static-area {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue