Merge remote-tracking branch 'origin/develop' into ci/reports

Signed-off-by: mathieu.brunot <mathieu.brunot@monogramm.io>
This commit is contained in:
mathieu.brunot 2019-12-14 14:38:43 +01:00
commit d38e9855be
No known key found for this signature in database
GPG key ID: 81584BEAF692D7E0
197 changed files with 3682 additions and 4750 deletions

2
.codacy.yml Normal file
View file

@ -0,0 +1,2 @@
exclude_paths:
- '**.sql'

2
.pylintrc Normal file
View file

@ -0,0 +1,2 @@
disable=access-member-before-definition
disable=no-member

View file

@ -1,6 +1,5 @@
language: python
dist: trusty
sudo: required
addons:
hosts:
@ -54,6 +53,16 @@ matrix:
env: DB=mariadb TYPE=server
script: bench --site test_site run-tests --coverage --junit-xml-output frappe_unit_tests.xml
before_install:
# install wkhtmltopdf
- wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
- tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
- sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
- sudo chmod o+x /usr/local/bin/wkhtmltopdf
# install cups
- sudo apt-get install libcups2-dev
install:
- cd ~
- source ./.nvm/nvm.sh
@ -67,23 +76,20 @@ install:
- mkdir ~/frappe-bench/sites/test_site
- cp $TRAVIS_BUILD_DIR/.travis/$DB.json ~/frappe-bench/sites/test_site/site_config.json
- mysql -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"
- mysql -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
- if [ $DB == "mariadb" ];then
mysql -u root -e "SET GLOBAL character_set_server = 'utf8mb4'";
mysql -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'";
mysql -u root -e "CREATE DATABASE test_frappe";
mysql -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'";
mysql -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'";
mysql -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'";
mysql -u root -e "FLUSH PRIVILEGES";
fi
- mysql -u root -e "CREATE DATABASE test_frappe"
- mysql -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
- mysql -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
- mysql -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"
- mysql -u root -e "FLUSH PRIVILEGES"
- psql -c "CREATE DATABASE test_frappe" -U postgres
- psql -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres
- wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
- tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
- sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
- sudo chmod o+x /usr/local/bin/wkhtmltopdf
- if [ $DB == "postgres" ];then
psql -c "CREATE DATABASE test_frappe" -U postgres;
psql -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres;
fi
- cd ./frappe-bench

View file

@ -1,9 +1,15 @@
context('Form', () => {
before(() => {
beforeEach(() => {
cy.login();
cy.visit('/desk');
});
before(() => {
cy.login();
cy.visit('/desk');
cy.window().its('frappe').then(frappe => {
frappe.call("frappe.tests.ui_test_helpers.create_contact_records");
});
});
it('create a new form', () => {
cy.visit('/desk#Form/ToDo/New ToDo 1');
cy.fill_field('description', 'this is a test todo', 'Text Editor').blur();
@ -13,4 +19,21 @@ context('Form', () => {
cy.location('hash').should('eq', '#List/ToDo/List');
cy.get('.list-row').should('contain', 'this is a test todo');
});
it('navigates between documents with child table list filters applied', () => {
cy.visit('/desk#List/Contact');
cy.get('.tag-filters-area .btn:contains("Add Filter")').click();
cy.get('.fieldname-select-area').should('exist');
cy.get('.fieldname-select-area input').type('Number{enter}', { force: true });
cy.get('.filter-field .input-with-feedback.form-control').type('123', { force: true });
cy.get('.filter-box .btn:contains("Apply")').click({ force: true });
cy.visit('/desk#Form/Contact/Test Form Contact 3');
cy.get('.prev-doc').click();
cy.get('.msgprint-dialog .modal-body').contains('No further records').should('be.visible');
cy.get('.modal-backdrop').click();
cy.get('.next-doc').click();
cy.contains('Test Form Contact 2').should('not.exist');
cy.get('.page-title .title-text').should('contain', 'Test Form Contact 1');
cy.visit('/desk#List/Contact');
cy.get('.clear-filters.btn').click();
});
});

View file

@ -0,0 +1,51 @@
context('Grid Pagination', () => {
beforeEach(() => {
cy.login();
cy.visit('/desk');
});
before(() => {
cy.login();
cy.visit('/desk');
cy.window().its('frappe').then(frappe => {
frappe.call("frappe.tests.ui_test_helpers.create_contact_phone_nos_records");
});
});
it('creates pages for child table', () => {
cy.visit('/desk#Form/Contact/Test Contact');
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
cy.get('@table').find('.current-page-number').should('contain', '1');
cy.get('@table').find('.total-page-number').should('contain', '20');
cy.get('@table').find('.grid-body .grid-row').should('have.length', 50);
});
it('goes to the next and previous page', () => {
cy.visit('/desk#Form/Contact/Test Contact');
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
cy.get('@table').find('.next-page').click();
cy.get('@table').find('.current-page-number').should('contain', '2');
cy.get('@table').find('.grid-body .grid-row').first().should('have.attr', 'data-idx', '51');
cy.get('@table').find('.prev-page').click();
cy.get('@table').find('.current-page-number').should('contain', '1');
cy.get('@table').find('.grid-body .grid-row').first().should('have.attr', 'data-idx', '1');
});
it('adds and deletes rows and changes page', ()=> {
cy.visit('/desk#Form/Contact/Test Contact');
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
cy.get('@table').find('button.grid-add-row').click();
cy.get('@table').find('.grid-body .row-index').should('contain', 1001);
cy.get('@table').find('.current-page-number').should('contain', '21');
cy.get('@table').find('.total-page-number').should('contain', '21');
cy.get('@table').find('.grid-body .grid-row .grid-row-check').click({force: true});
cy.get('@table').find('button.grid-remove-rows').click();
cy.get('@table').find('.grid-body .row-index').last().should('contain', 1000);
cy.get('@table').find('.current-page-number').should('contain', '20');
cy.get('@table').find('.total-page-number').should('contain', '20');
});
it('deletes all rows', ()=> {
cy.visit('/desk#Form/Contact/Test Contact');
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
cy.get('@table').find('.grid-heading-row .grid-row-check').click({force: true});
cy.get('@table').find('button.grid-remove-all-rows').click();
cy.get('.modal-dialog .btn-primary').contains('Yes').click();
cy.get('@table').find('.grid-body .grid-row').should('have.length', 0);
});
});

View file

@ -2,10 +2,9 @@ context('List View', () => {
before(() => {
cy.login();
cy.visit('/desk');
cy.window().its('frappe').then(frappe => {
frappe.call("frappe.tests.ui_test_helpers.setup_workflow");
return cy.window().its('frappe').then(frappe => {
return frappe.xcall("frappe.tests.ui_test_helpers.setup_workflow");
});
cy.clear_cache();
});
it('enables "Actions" button', () => {
const actions = ['Approve', 'Reject', 'Edit', 'Assign To', 'Apply Assignment Rule', 'Print', 'Delete'];

View file

@ -23,7 +23,7 @@ if sys.version[0] == '2':
reload(sys)
sys.setdefaultencoding("utf-8")
__version__ = '12.0.17'
__version__ = '12.0.20'
__title__ = "Frappe Framework"
local = Local()
@ -123,7 +123,6 @@ def init(site, sites_path=None, new_site=False):
local.debug_log = []
local.realtime_log = []
local.flags = _dict({
"ran_schedulers": [],
"currently_saving": [],
"redirect_location": "",
"in_install_db": False,
@ -1508,7 +1507,22 @@ def logger(module=None, with_more_info=True):
def log_error(message=None, title=None):
'''Log error to Error Log'''
return get_doc(dict(doctype='Error Log', error=as_unicode(message or get_traceback()),
# AI ALERT:
# the title and message may be swapped
# the better API for this is log_error(title, message), and used in many cases this way
# this hack tries to be smart about whats a title (single line ;-)) and fixes it
if message:
if '\n' not in message:
title = message
error = get_traceback()
else:
error = message
else:
error = get_traceback()
return get_doc(dict(doctype='Error Log', error=as_unicode(error),
method=title)).insert(ignore_permissions=True)
def get_desk_link(doctype, name):

View file

@ -82,7 +82,7 @@ def handle():
if frappe.local.request.method=="PUT":
if frappe.local.form_dict.data is None:
data = json.loads(frappe.local.request.get_data())
data = json.loads(frappe.safe_decode(frappe.local.request.get_data()))
else:
data = json.loads(frappe.local.form_dict.data)
doc = frappe.get_doc(doctype, name)
@ -117,7 +117,7 @@ def handle():
if frappe.local.request.method=="POST":
if frappe.local.form_dict.data is None:
data = json.loads(frappe.local.request.get_data())
data = json.loads(frappe.safe_decode(frappe.local.request.get_data()))
else:
data = json.loads(frappe.local.form_dict.data)
data.update({

View file

@ -105,10 +105,6 @@ class AutoRepeat(Document):
schedule_details = []
start_date = getdate(self.start_date)
end_date = getdate(self.end_date)
today = frappe.utils.datetime.date.today()
if start_date < today:
start_date = today
if not self.end_date:
start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day)
@ -121,7 +117,8 @@ class AutoRepeat(Document):
start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day)
if self.end_date:
start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day)
start_date = start_date = get_next_schedule_date(
start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, end_date, for_full_schedule=True)
while (getdate(start_date) < getdate(end_date)):
row = {
"reference_document" : self.reference_document,
@ -129,7 +126,8 @@ class AutoRepeat(Document):
"next_scheduled_date" : start_date
}
schedule_details.append(row)
start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, end_date)
start_date = start_date = get_next_schedule_date(
start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, end_date, for_full_schedule=True)
return schedule_details
@ -271,18 +269,28 @@ class AutoRepeat(Document):
)
def get_next_schedule_date(start_date, frequency, repeat_on_day, repeat_on_last_day = False, end_date = None):
def get_next_schedule_date(start_date, frequency, repeat_on_day, repeat_on_last_day=False, end_date=None, for_full_schedule=False):
month_count = month_map.get(frequency)
day_count = 0
if month_count and repeat_on_last_day:
next_date = get_next_date(start_date, month_count, 31)
day_count = 31
next_date = get_next_date(start_date, month_count, day_count)
elif month_count and repeat_on_day:
next_date = get_next_date(start_date, month_count, repeat_on_day)
day_count = repeat_on_day
next_date = get_next_date(start_date, month_count, day_count)
elif month_count:
next_date = get_next_date(start_date, month_count)
else:
days = 7 if frequency == 'Weekly' else 1
next_date = add_days(start_date, days)
# next schedule date should be after or on current date
if not for_full_schedule:
while getdate(next_date) < getdate(today()):
next_date = get_next_date(next_date, month_count, day_count)
return next_date
def get_next_date(dt, mcount, day=None):
@ -307,7 +315,7 @@ def create_repeated_entries(data):
current_date = getdate(today())
schedule_date = getdate(doc.next_schedule_date)
while schedule_date <= current_date and not doc.disabled:
if schedule_date == current_date and not doc.disabled:
doc.create_documents()
schedule_date = get_next_schedule_date(schedule_date, doc.frequency, doc.repeat_on_day, doc.repeat_on_last_day, doc.end_date)

View file

@ -96,6 +96,21 @@ class TestAutoRepeat(unittest.TestCase):
linked_comm = frappe.db.exists("Communication", dict(reference_doctype="ToDo", reference_name=new_todo))
self.assertTrue(linked_comm)
def test_next_schedule_date(self):
current_date = getdate(today())
todo = frappe.get_doc(
dict(doctype='ToDo', description='test next schedule date todo', assigned_by='Administrator')).insert()
doc = make_auto_repeat(frequency='Monthly', reference_document=todo.name, start_date=add_months(today(), -2))
#check next_schedule_date is set as per current date
#it should not be a previous month's date
self.assertEqual(doc.next_schedule_date, current_date)
data = get_auto_repeat_entries(current_date)
create_repeated_entries(data)
docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name})
#the original doc + the repeated doc
self.assertEqual(len(docnames), 2)
def make_auto_repeat(**args):
args = frappe._dict(args)

View file

@ -13,6 +13,8 @@ from six import text_type
@click.argument('site')
@click.option('--db-name', help='Database name')
@click.option('--db-type', default='mariadb', type=click.Choice(['mariadb', 'postgres']), help='Optional "postgres" or "mariadb". Default is "mariadb"')
@click.option('--db-host', help='Database Host')
@click.option('--db-port', type=int, help='Database Port')
@click.option('--mariadb-root-username', default='root', help='Root username for MariaDB')
@click.option('--mariadb-root-password', help='Root password for MariaDB')
@click.option('--admin-password', help='Administrator password for new site', default=None)
@ -21,22 +23,22 @@ from six import text_type
@click.option('--source_sql', help='Initiate database with a SQL file')
@click.option('--install-app', multiple=True, help='Install app after installation')
def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin_password=None,
verbose=False, install_apps=None, source_sql=None, force=None, install_app=None,
db_name=None, db_type=None):
verbose=False, install_apps=None, source_sql=None, force=None, install_app=None,
db_name=None, db_type=None, db_host=None, db_port=None):
"Create a new site"
frappe.init(site=site, new_site=True)
_new_site(db_name, site, mariadb_root_username=mariadb_root_username,
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force,
db_type=db_type)
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force,
db_type=db_type, db_host=db_host, db_port=db_port)
if len(frappe.utils.get_sites()) == 1:
use(site)
def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=None,
admin_password=None, verbose=False, install_apps=None, source_sql=None, force=False,
reinstall=False, db_type=None):
admin_password=None, verbose=False, install_apps=None, source_sql=None, force=False,
reinstall=False, db_type=None, db_host=None, db_port=None):
"""Install a new Frappe site"""
if not force and os.path.exists(site):
@ -65,8 +67,8 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
installing = touch_file(get_site_path('locks', 'installing.lock'))
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password,
db_name=db_name, admin_password=admin_password, verbose=verbose,
source_sql=source_sql, force=force, reinstall=reinstall, db_type=db_type)
db_name=db_name, admin_password=admin_password, verbose=verbose,
source_sql=source_sql, force=force, reinstall=reinstall, db_type=db_type, db_host=db_host, db_port=db_port)
apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
for app in apps_to_install:

View file

@ -507,26 +507,6 @@ def run_ui_tests(context, app, headless=False):
formatted_command = command.format(site_env=site_env, password_env=password_env, run_or_open=run_or_open)
frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True)
@click.command('run-setup-wizard-ui-test')
@click.option('--app', help="App to run tests on, leave blank for all apps")
@click.option('--profile', is_flag=True, default=False)
@pass_context
def run_setup_wizard_ui_test(context, app=None, profile=False):
"Run setup wizard UI test"
import frappe.test_runner
site = get_site(context)
frappe.init(site=site)
frappe.connect()
ret = frappe.test_runner.run_setup_wizard_ui_test(app=app, verbose=context.verbose,
profile=profile)
if len(ret.failures) == 0 and len(ret.errors) == 0:
ret = 0
if os.environ.get('CI'):
sys.exit(ret)
@click.command('serve')
@click.option('--port', default=8000)
@click.option('--profile', is_flag=True, default=False)
@ -752,7 +732,6 @@ commands = [
reset_perms,
run_tests,
run_ui_tests,
run_setup_wizard_ui_test,
serve,
set_config,
show_config,

View file

@ -1,4 +1,5 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.desk.moduleview import add_setup_section
@ -88,7 +89,7 @@ def get_data():
]
},
{
"label": _("Email"),
"label": _("Email / Notifications"),
"icon": "fa fa-envelope",
"items": [
{
@ -120,6 +121,12 @@ def get_data():
"type": "doctype",
"name": "Newsletter",
"description": _("Create and manage newsletter")
},
{
"type": "doctype",
"route": "Form/Notification Settings/{}".format(frappe.session.user),
"name": "Notification Settings",
"description": _("Configure notifications for mentions, assignments, energy points and more.")
}
]
},

View file

@ -60,10 +60,6 @@ class Address(Document):
if not [row for row in self.links if row.link_doctype == "Company"]:
frappe.throw(_("Company is mandatory, as it is your company address"))
# removing other links
to_remove = [row for row in self.links if row.link_doctype != "Company"]
[ self.remove(row) for row in to_remove ]
def get_display(self):
return get_address_display(self.as_dict())

View file

@ -10,7 +10,6 @@ from email.utils import formataddr
from frappe.core.utils import get_parent_doc
from frappe.utils import (get_url, get_formatted_email, cint,
validate_email_address, split_emails, time_diff_in_seconds, parse_addr, get_datetime)
from frappe.utils.scheduler import log
from frappe.email.email_body import get_message_id
import frappe.email.smtp
import time
@ -399,7 +398,7 @@ def get_bcc(doc, recipients=None, fetched_from_email_account=False):
return bcc
def add_attachments(name, attachments):
'''Add attachments to the given Communiction'''
'''Add attachments to the given Communication'''
# loop through attachments
for a in attachments:
if isinstance(a, string_types):
@ -412,7 +411,9 @@ def add_attachments(name, attachments):
"file_url": attach.file_url,
"attached_to_doctype": "Communication",
"attached_to_name": name,
"folder": "Home/Attachments"})
"folder": "Home/Attachments",
"is_private": attach.is_private
})
_file.save(ignore_permissions=True)
def filter_email_list(doc, email_list, exclude, is_cc=False, is_bcc=False):
@ -509,17 +510,7 @@ def sendmail(communication_name, print_html=None, print_format=None, attachments
break
except:
traceback = log("frappe.core.doctype.communication.email.sendmail", frappe.as_json({
"communication_name": communication_name,
"print_html": print_html,
"print_format": print_format,
"attachments": attachments,
"recipients": recipients,
"cc": cc,
"bcc": bcc,
"lang": lang
}))
frappe.logger(__name__).error(traceback)
traceback = frappe.log_error("frappe.core.doctype.communication.email.sendmail")
raise
def update_mins_to_first_communication(parent, communication):

View file

@ -54,8 +54,10 @@ class Importer:
extension = None
if self.data_import and self.data_import.import_file:
file_doc = frappe.get_doc("File", {"file_url": self.data_import.import_file})
parts = file_doc.get_extension()
extension = parts[1]
content = file_doc.get_content()
extension = file_doc.file_name.split(".")[1]
extension = extension.lstrip(".")
if file_path:
content, extension = self.read_file(file_path)
@ -79,6 +81,12 @@ class Importer:
return file_content, extn
def read_content(self, content, extension):
error_title = _("Template Error")
if extension not in ("csv", "xlsx", "xls"):
frappe.throw(
_("Import template should be of type .csv, .xlsx or .xls"), title=error_title
)
if extension == "csv":
data = read_csv_content(content)
elif extension == "xlsx":
@ -86,6 +94,11 @@ class Importer:
elif extension == "xls":
data = read_xls_file_from_attached_file(content)
if len(data) <= 1:
frappe.throw(
_("Import template should contain a Header and atleast one row."), title=error_title
)
self.header_row = data[0]
self.data = data[1:]
@ -862,15 +875,15 @@ class Importer:
if failed_records:
print("Failed to import {0} records".format(len(failed_records)))
file_name = '{0}_import_on_{1}.txt'.format(self.doctype, frappe.utils.now())
print('Check {0} for errors'.format(os.path.join('sites', file_name)))
file_name = "{0}_import_on_{1}.txt".format(self.doctype, frappe.utils.now())
print("Check {0} for errors".format(os.path.join("sites", file_name)))
text = ""
for w in failed_records:
text += "Row Indexes: {0}\n".format(str(w.get('row_indexes', [])))
text += "Messages:\n{0}\n".format('\n'.join(w.get('messages', [])))
text += "Traceback:\n{0}\n\n".format(w.get('exception'))
text += "Row Indexes: {0}\n".format(str(w.get("row_indexes", [])))
text += "Messages:\n{0}\n".format("\n".join(w.get("messages", [])))
text += "Traceback:\n{0}\n\n".format(w.get("exception"))
with open(file_name, 'w') as f:
with open(file_name, "w") as f:
f.write(text)

View file

@ -1,4 +1,5 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "Prompt",
"creation": "2013-02-18 13:36:19",
@ -28,6 +29,7 @@
"name_case",
"column_break_15",
"description",
"documentation",
"form_settings_section",
"image_field",
"timeline_field",
@ -57,6 +59,10 @@
"restrict_to_domain",
"read_only",
"in_create",
"actions_section",
"actions",
"links_section",
"links",
"web_view",
"has_web_view",
"allow_guest_to_view",
@ -454,11 +460,39 @@
"fieldname": "nsm_parent_field",
"fieldtype": "Data",
"label": "Parent Field (Tree)"
},
{
"description": "URL for documentation or help",
"fieldname": "documentation",
"fieldtype": "Data",
"label": "Documentation Link"
},
{
"fieldname": "actions_section",
"fieldtype": "Section Break",
"label": "Actions"
},
{
"fieldname": "actions",
"fieldtype": "Table",
"label": "Actions",
"options": "DocType Action"
},
{
"fieldname": "links_section",
"fieldtype": "Section Break",
"label": "Links Section"
},
{
"fieldname": "links",
"fieldtype": "Table",
"label": "Links",
"options": "DocType Link"
}
],
"icon": "fa fa-bolt",
"idx": 6,
"modified": "2019-09-07 14:28:05.392490",
"modified": "2019-11-25 17:24:03.690192",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",

View file

@ -0,0 +1,57 @@
{
"actions": [],
"creation": "2019-09-23 16:28:13.953520",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"label",
"action_type",
"action",
"group"
],
"fields": [
{
"columns": 2,
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label",
"reqd": 1
},
{
"fieldname": "group",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Group"
},
{
"columns": 2,
"fieldname": "action_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Action Type",
"options": "Server Action",
"reqd": 1
},
{
"columns": 4,
"fieldname": "action",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Action",
"reqd": 1
}
],
"istable": 1,
"modified": "2019-09-24 09:11:39.860100",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType Action",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

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

View file

@ -0,0 +1,46 @@
{
"actions": [],
"creation": "2019-09-24 11:41:25.291377",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"link_doctype",
"link_fieldname",
"group"
],
"fields": [
{
"fieldname": "link_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Link DocType",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "link_fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Link Fieldname",
"reqd": 1
},
{
"fieldname": "group",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Group"
}
],
"istable": 1,
"modified": "2019-09-24 11:41:25.291377",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType Link",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

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

View file

@ -89,8 +89,9 @@ class File(Document):
def validate(self):
if self.is_new():
self.set_is_private()
self.set_file_name()
self.validate_duplicate_entry()
self.validate_file_name()
self.validate_folder()
if not self.file_url and not self.flags.ignore_file_validate:
@ -133,6 +134,9 @@ class File(Document):
frappe.db.set_value(self.attached_to_doctype, self.attached_to_name,
self.attached_to_field, self.file_url)
if self.file_url and (self.is_private != self.file_url.startswith('/private')):
frappe.throw(_('Invalid file URL. Please contact System Administrator.'))
def set_folder_name(self):
"""Make parent folders if not exists based on reference doctype and name"""
if self.attached_to_doctype and not self.folder:
@ -157,9 +161,11 @@ class File(Document):
def validate_duplicate_entry(self):
if not self.flags.ignore_duplicate_entry_error and not self.is_folder:
# check duplicate name
if not self.content_hash:
self.generate_content_hash()
# check duplicate assignement
# check duplicate name
# check duplicate assignment
filters = {
'content_hash': self.content_hash,
'is_private': self.is_private,
@ -184,21 +190,20 @@ class File(Document):
else:
self.file_url = duplicate_file.file_url
def validate_file_name(self):
def set_file_name(self):
if not self.file_name and self.file_url:
self.file_name = self.file_url.split('/')[-1]
def generate_content_hash(self):
if self.content_hash or not self.file_url:
if self.content_hash or not self.file_url or self.file_url.startswith('http'):
return
if self.file_url.startswith("/files/"):
try:
with open(get_files_path(self.file_name.lstrip("/")), "rb") as f:
self.content_hash = get_content_hash(f.read())
except IOError:
frappe.msgprint(_("File {0} does not exist").format(self.file_url))
raise
try:
with open(get_files_path(self.file_name.lstrip("/"), is_private=self.is_private), "rb") as f:
self.content_hash = get_content_hash(f.read())
except IOError:
frappe.msgprint(_("File {0} does not exist").format(self.file_url))
raise
def on_trash(self):
if self.is_home_folder or self.is_attachments_folder:
@ -563,6 +568,9 @@ class File(Document):
except frappe.DoesNotExistError:
frappe.clear_messages()
def set_is_private(self):
if self.file_url:
self.is_private = cint(self.file_url.startswith('/private'))
def on_doctype_update():
frappe.db.add_index("File", ["attached_to_doctype", "attached_to_name"])

View file

@ -25,7 +25,7 @@ class PreparedReport(Document):
enqueue(run_background, prepared_report=self.name, timeout=6000)
def on_trash(self):
remove_all("PreparedReport", self.name, from_delete=True)
remove_all("Prepared Report", self.name)
def run_background(prepared_report):
@ -85,7 +85,8 @@ def create_json_gz_file(data, dt, dn):
"file_name": json_filename,
"attached_to_doctype": dt,
"attached_to_name": dn,
"content": compressed_content
"content": compressed_content,
"is_private": 1
})
_file.save(ignore_permissions=True)

View file

@ -0,0 +1,8 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Scheduled Job Log', {
// refresh: function(frm) {
// }
});

View file

@ -0,0 +1,64 @@
{
"actions": [],
"creation": "2019-09-23 14:36:36.935869",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"status",
"scheduled_job_type",
"details"
],
"fields": [
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"options": "Scheduled\nSuccess\nFailed",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "details",
"fieldtype": "Code",
"label": "Details",
"read_only": 1
},
{
"fieldname": "scheduled_job_type",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Scheduled Job",
"options": "Scheduled Job Type",
"read_only": 1,
"reqd": 1
}
],
"links": [],
"modified": "2019-09-25 11:55:10.646458",
"modified_by": "Administrator",
"module": "Core",
"name": "Scheduled Job Log",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

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

View file

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

View file

@ -0,0 +1,8 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Scheduled Job Type', {
// refresh: function(frm) {
// }
});

View file

@ -0,0 +1,98 @@
{
"actions": [
{
"action": "frappe.core.doctype.scheduled_job_type.scheduled_job_type.execute_event",
"action_type": "Server Action",
"label": "Execute"
}
],
"creation": "2019-09-23 14:34:09.205368",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"stopped",
"method",
"frequency",
"cron_format",
"last_execution",
"create_log"
],
"fields": [
{
"fieldname": "method",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Method",
"read_only": 1,
"reqd": 1
},
{
"default": "0",
"fieldname": "stopped",
"fieldtype": "Check",
"label": "Stopped"
},
{
"default": "0",
"depends_on": "eval:doc.queue==='All'",
"fieldname": "create_log",
"fieldtype": "Check",
"label": "Create Log"
},
{
"fieldname": "last_execution",
"fieldtype": "Datetime",
"label": "Last Execution",
"read_only": 1
},
{
"allow_in_quick_entry": 1,
"depends_on": "eval:doc.queue==='Cron'",
"fieldname": "cron_format",
"fieldtype": "Data",
"label": "Cron Format",
"read_only": 1
},
{
"fieldname": "frequency",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Frequency",
"options": "All\nHourly\nHourly Long\nDaily\nDaily Long\nWeekly\nWeekly Long\nMonthly\nMonthly Long\nCron\nYearly\nAnnual",
"read_only": 1,
"reqd": 1
}
],
"in_create": 1,
"links": [
{
"link_doctype": "Scheduled Job Log",
"link_fieldname": "scheduled_job_type"
}
],
"modified": "2019-12-09 11:10:21.259929",
"modified_by": "Administrator",
"module": "Core",
"name": "Scheduled Job Type",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,156 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, json
from frappe.model.document import Document
from frappe.utils import now_datetime, get_datetime
from datetime import datetime
from croniter import croniter
from frappe.utils.background_jobs import enqueue, get_jobs
class ScheduledJobType(Document):
def autoname(self):
self.name = '.'.join(self.method.split('.')[-2:])
def validate(self):
if self.frequency != 'All':
# force logging for all events other than continuous ones (ALL)
self.create_log = 1
def enqueue(self):
# enqueue event if last execution is done
if self.is_event_due():
if frappe.flags.enqueued_jobs:
frappe.flags.enqueued_jobs.append(self.method)
if frappe.flags.execute_job:
self.execute()
else:
if not self.is_job_in_queue():
enqueue('frappe.core.doctype.scheduled_job_type.scheduled_job_type.run_scheduled_job',
queue = self.get_queue_name(), job_type=self.method)
return True
return False
def is_event_due(self, current_time = None):
'''Return true if event is due based on time lapsed since last execution'''
# if the next scheduled event is before NOW, then its due!
return self.get_next_execution() <= (current_time or now_datetime())
def is_job_in_queue(self):
queued_jobs = get_jobs(site=frappe.local.site, key='job_type')[frappe.local.site]
return self.method in queued_jobs
def get_next_execution(self):
CRON_MAP = {
"Yearly": "0 0 1 1 *",
"Annual": "0 0 1 1 *",
"Monthly": "0 0 1 * *",
"Monthly Long": "0 0 1 * *",
"Weekly": "0 0 * * 0",
"Weekly Long": "0 0 * * 0",
"Daily": "0 0 * * *",
"Daily Long": "0 0 * * *",
"Hourly": "0 * * * *",
"Hourly Long": "0 * * * *",
"All": "0/" + str((frappe.get_conf().scheduler_interval or 240) // 60) + " * * * *",
}
if not self.cron_format:
self.cron_format = CRON_MAP[self.frequency]
return croniter(self.cron_format,
get_datetime(self.last_execution or datetime(2000, 1, 1))).get_next(datetime)
def execute(self):
self.scheduler_log = None
try:
self.log_status('Start')
frappe.get_attr(self.method)()
frappe.db.commit()
self.log_status('Complete')
except Exception:
frappe.db.rollback()
self.log_status('Failed')
def log_status(self, status):
# log file
frappe.logger(__name__).info('Scheduled Job {0}: {1} for {2}'.format(status, self.method, frappe.local.site))
self.update_scheduler_log(status)
def update_scheduler_log(self, status):
if not self.create_log:
return
if not self.scheduler_log:
self.scheduler_log = frappe.get_doc(dict(doctype = 'Scheduled Job Log', scheduled_job_type=self.name)).insert(ignore_permissions=True)
self.scheduler_log.db_set('status', status)
if status == 'Failed':
self.scheduler_log.db_set('details', frappe.get_traceback())
if status == 'Start':
self.db_set('last_execution', now_datetime(), update_modified=False)
frappe.db.commit()
def get_queue_name(self):
return 'long' if ('Long' in self.frequency) else 'default'
def on_trash(self):
frappe.db.sql('delete from `tabScheduled Job Log` where scheduled_job_type=%s', self.name)
@frappe.whitelist()
def execute_event(doc):
frappe.only_for('System Manager')
doc = json.loads(doc)
frappe.get_doc('Scheduled Job Type', doc.get('name')).enqueue()
def run_scheduled_job(job_type):
'''This is a wrapper function that runs a hooks.scheduler_events method'''
try:
frappe.get_doc('Scheduled Job Type', dict(method=job_type)).execute()
except Exception:
print(frappe.get_traceback())
def sync_jobs():
frappe.reload_doc('core', 'doctype', 'scheduled_job_type')
all_events = []
scheduler_events = frappe.get_hooks("scheduler_events")
insert_events(all_events, scheduler_events)
clear_events(all_events, scheduler_events)
def insert_events(all_events, scheduler_events):
for event_type in scheduler_events:
events = scheduler_events.get(event_type)
if isinstance(events, dict):
insert_cron_event(events, all_events)
else:
# hourly, daily etc
insert_event_list(events, event_type, all_events)
def insert_cron_event(events, all_events):
for cron_format in events:
for event in events.get(cron_format):
all_events.append(event)
insert_single_event('Cron', event, cron_format)
def insert_event_list(events, event_type, all_events):
for event in events:
all_events.append(event)
frequency = event_type.replace('_', ' ').title()
insert_single_event(frequency, event)
def insert_single_event(frequency, event, cron_format = None):
if not frappe.db.exists('Scheduled Job Type', dict(method=event)):
frappe.get_doc(dict(
doctype = 'Scheduled Job Type',
method = event,
cron_format = cron_format,
frequency = frequency
)).insert()
def clear_events(all_events, scheduler_events):
for event in frappe.get_all('Scheduled Job Type', ('name', 'method')):
if event.method not in all_events:
frappe.delete_doc('Scheduled Job Type', event.name)

View file

@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from frappe.utils import get_datetime
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
class TestScheduledJobType(unittest.TestCase):
def setUp(self):
if not frappe.get_all('Scheduled Job Type', limit=1):
frappe.db.rollback()
frappe.db.sql('truncate `tabScheduled Job Type`')
sync_jobs()
frappe.db.commit()
def test_sync_jobs(self):
all_job = frappe.get_doc('Scheduled Job Type',
dict(method='frappe.email.queue.flush'))
self.assertEqual(all_job.frequency, 'All')
daily_job = frappe.get_doc('Scheduled Job Type',
dict(method='frappe.email.queue.clear_outbox'))
self.assertEqual(daily_job.frequency, 'Daily')
# check if cron jobs are synced
cron_job = frappe.get_doc('Scheduled Job Type',
dict(method='frappe.oauth.delete_oauth2_data'))
self.assertEqual(cron_job.frequency, 'Cron')
self.assertEqual(cron_job.cron_format, '0/15 * * * *')
def test_daily_job(self):
job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.email.queue.clear_outbox'))
job.db_set('last_execution', '2019-01-01 00:00:00')
self.assertTrue(job.is_event_due(get_datetime('2019-01-02 00:00:06')))
self.assertFalse(job.is_event_due(get_datetime('2019-01-01 00:00:06')))
self.assertFalse(job.is_event_due(get_datetime('2019-01-01 23:59:59')))
def test_weekly_job(self):
job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.social.doctype.energy_point_log.energy_point_log.send_weekly_summary'))
job.db_set('last_execution', '2019-01-01 00:00:00')
self.assertTrue(job.is_event_due(get_datetime('2019-01-06 00:00:01')))
self.assertFalse(job.is_event_due(get_datetime('2019-01-02 00:00:06')))
self.assertFalse(job.is_event_due(get_datetime('2019-01-05 23:59:59')))
def test_monthly_job(self):
job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.email.doctype.auto_email_report.auto_email_report.send_monthly'))
job.db_set('last_execution', '2019-01-01 00:00:00')
self.assertTrue(job.is_event_due(get_datetime('2019-02-01 00:00:01')))
self.assertFalse(job.is_event_due(get_datetime('2019-01-15 00:00:06')))
self.assertFalse(job.is_event_due(get_datetime('2019-01-31 23:59:59')))
def test_cron_job(self):
# runs every 15 mins
job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.oauth.delete_oauth2_data'))
job.db_set('last_execution', '2019-01-01 00:00:00')
self.assertTrue(job.is_event_due(get_datetime('2019-01-01 00:15:01')))
self.assertFalse(job.is_event_due(get_datetime('2019-01-01 00:05:06')))
self.assertFalse(job.is_event_due(get_datetime('2019-01-01 00:14:59')))

View file

@ -1,4 +1,5 @@
{
"actions": [],
"creation": "2014-04-17 16:53:52.640856",
"doctype": "DocType",
"document_type": "System",
@ -21,7 +22,7 @@
"backup_limit",
"background_workers",
"enable_scheduler",
"scheduler_last_event",
"dormant_days",
"permissions",
"apply_strict_user_permissions",
"column_break_21",
@ -168,13 +169,6 @@
"hidden": 1,
"label": "Enable Scheduled Jobs"
},
{
"fieldname": "scheduler_last_event",
"fieldtype": "Data",
"hidden": 1,
"label": "Scheduler Last Event",
"report_hide": 1
},
{
"collapsible": 1,
"fieldname": "permissions",
@ -397,11 +391,18 @@
"fieldname": "allow_guests_to_upload_files",
"fieldtype": "Check",
"label": "Allow Guests to Upload Files"
},
{
"default": "4",
"description": "Will run scheduled jobs only once a day for inactive sites. Default 4 days if set to 0.",
"fieldname": "dormant_days",
"fieldtype": "Int",
"label": "Run Jobs only Daily if Inactive For (Days)"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"modified": "2019-08-16 08:26:45.936626",
"modified": "2019-09-24 10:04:28.807388",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",

View file

@ -1,23 +0,0 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Test Runner", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially('Test Runner', [
// insert a new Test Runner
() => frappe.tests.make([
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);
});

View file

@ -1,87 +0,0 @@
// Copyright (c) 2017, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Test Runner', {
refresh: (frm) => {
frm.disable_save();
frm.page.set_primary_action(__("Run Tests"), () => {
return new Promise(resolve => {
let wrapper = $(frm.fields_dict.output.wrapper).empty();
$("<p>Loading...</p>").appendTo(wrapper);
// all tests
frappe.call({
method: 'frappe.core.doctype.test_runner.test_runner.get_test_js',
args: { test_path: frm.doc.module_path }
}).always((data) => {
$("<div id='qunit'></div>").appendTo(wrapper.empty());
frm.events.run_tests(frm, data.message);
resolve();
});
});
});
},
run_tests: function(frm, files) {
frappe.flags.in_test = true;
let require_list = [
"assets/frappe/js/lib/jquery/qunit.js",
"assets/frappe/js/lib/jquery/qunit.css"
].concat();
frappe.require(require_list, () => {
files.forEach((f) => {
frappe.dom.eval(f.script);
});
QUnit.config.notrycatch = true;
window.onerror = function(msg, url, lineNo, columnNo, error) {
console.log(error.stack); // eslint-disable-line
$('<div id="frappe-qunit-done"></div>').appendTo($('body'));
};
QUnit.testDone(function(details) {
// var result = {
// "Module name": details.module,
// "Test name": details.name,
// "Assertions": {
// "Total": details.total,
// "Passed": details.passed,
// "Failed": details.failed
// },
// "Skipped": details.skipped,
// "Todo": details.todo,
// "Runtime": details.runtime
// };
// eslint-disable-next-line
// console.log(JSON.stringify(result, null, 2));
details.assertions.map(a => {
// eslint-disable-next-line
console.log(`${a.result ? '✔' : '✗'} ${a.message}`);
});
});
QUnit.load();
QUnit.done(({ total, failed, passed, runtime }) => {
// flag for selenium that test is done
console.log( `Total: ${total}, Failed: ${failed}, Passed: ${passed}, Runtime: ${runtime}` ); // eslint-disable-line
if(failed) {
console.log('Tests Failed'); // eslint-disable-line
} else {
console.log('Tests Passed'); // eslint-disable-line
}
frappe.set_route('Form', 'Test Runner', 'Test Runner');
$('<div id="frappe-qunit-done"></div>').appendTo($('body'));
});
});
}
});

View file

@ -1,152 +0,0 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2017-06-26 10:57:19.976624",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "module_path",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Module Path",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "app",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "App",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "output",
"fieldtype": "HTML",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Output",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2017-07-19 03:22:33.221169",
"modified_by": "Administrator",
"module": "Core",
"name": "Test Runner",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 0,
"role": "Administrator",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}

View file

@ -1,36 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, os
from frappe.model.document import Document
class TestRunner(Document):
pass
@frappe.whitelist()
def get_test_js(test_path=None):
'''Get test + data for app, example: app/tests/ui/test_name.js'''
if not test_path:
test_path = frappe.db.get_single_value('Test Runner', 'module_path')
test_js = []
# split
app, test_path = test_path.split(os.path.sep, 1)
# now full path
test_path = frappe.get_app_path(app, test_path)
def add_file(path):
with open(path, 'r') as fileobj:
test_js.append(dict(
script = fileobj.read()
))
# add test_lib.js
add_file(frappe.get_app_path('frappe', 'tests', 'ui', 'data', 'test_lib.js'))
add_file(test_path)
return test_js

View file

@ -97,7 +97,9 @@ class User(Document):
self.share_with_self()
clear_notifications(user=self.name)
frappe.clear_cache(user=self.name)
self.send_password_notification(self.__new_password)
if self.__new_password:
self.send_password_notification(self.__new_password)
self.reset_password_key = ''
create_contact(self, ignore_mandatory=True)
if self.name not in ('Administrator', 'Guest') and not self.user_image:
frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name)
@ -1071,4 +1073,4 @@ def generate_keys(user):
user_details.save()
return {"api_secret": api_secret}
frappe.throw(frappe._("Not Permitted"), frappe.PermissionError)
frappe.throw(frappe._("Not Permitted"), frappe.PermissionError)

View file

@ -11,7 +11,7 @@
<tbody>
{% for j in jobs %}
<tr>
<td><span class="indicator {{ j.color }}" title="{{ j.status }}">{{ j.queue.split(".").slice(-1)[0] }}</span></td>
<td><span class="indicator {{ j.color }}" title="{{ j.get_status() }}">{{ j.queue.split(".").slice(-1)[0] }}</span></td>
<td style="overflow: auto;">
<div>
{{ frappe.utils.encode_tags(j.job_name) }}

View file

@ -29,9 +29,9 @@ def get_info(show_failed=False):
jobs.append({
'job_name': j.kwargs.get('kwargs', {}).get('playbook_method') \
or str(j.kwargs.get('job_name')),
'status': j.status, 'queue': name,
'status': j.get_status(), 'queue': name,
'creation': format_datetime(convert_utc_to_user_timezone(j.created_at)),
'color': colors[j.status]
'color': colors[j.get_status()]
})
if j.exc_info:
jobs[-1]['exc_info'] = j.exc_info

View file

@ -36,13 +36,15 @@ def generate_and_cache_results(chart, chart_name, function, cache_key):
def get_from_date_from_timespan(to_date, timespan):
days = months = years = 0
if "Last Week" == timespan:
if timespan == "Last Week":
days = -7
if "Last Month" == timespan:
if timespan == "Last Month":
months = -1
elif "Last Quarter" == timespan:
elif timespan == "Last Quarter":
months = -3
elif "Last Year" == timespan:
elif timespan == "Last Year":
years = -1
elif timespan == "All Time":
years = -50
return add_to_date(to_date, years=years, months=months, days=days,
as_datetime=True)

View file

@ -15,7 +15,7 @@ import frappe.model.meta
from frappe import _
from time import time
from frappe.utils import now, getdate, cast_fieldtype
from frappe.utils import now, getdate, cast_fieldtype, get_datetime
from frappe.utils.background_jobs import execute_job, get_queue
from frappe.model.utils.link_count import flush_local_link_count
from frappe.utils import cint
@ -941,6 +941,16 @@ class Database(object):
else:
frappe.throw(_('No conditions provided'))
def get_last_created(self, doctype):
last_record = self.get_all(doctype, ('creation'), limit=1, order_by='creation desc')
if last_record:
return get_datetime(last_record[0].creation)
else:
return None
def clear_table(self, doctype):
self.sql('truncate `tab{}`'.format(doctype))
def log_touched_tables(self, query, values=None):
if values:
query = frappe.safe_decode(self._cursor.mogrify(query, values))

View file

@ -80,12 +80,14 @@ class DbManager:
if pipe:
print('Creating Database...')
command = '{pipe} mysql -u {user} -p{password} -h{host} {target} {source}'.format(
command = '{pipe} mysql -u {user} -p{password} -h{host} ' + ('-P{port}' if frappe.db.port else '') + ' {target} {source}'
command = command.format(
pipe=pipe,
user=esc(user),
password=esc(password),
host=esc(frappe.db.host),
target=esc(target),
source=source
source=source,
port=frappe.db.port
)
os.system(command)

View file

@ -105,6 +105,53 @@ CREATE TABLE `tabDocPerm` (
KEY `parent` (`parent`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Table structure for table `tabDocType Action`
--
CREATE TABLE `tabDocType Action` (
`name` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL,
`creation` datetime(6) DEFAULT NULL,
`modified` datetime(6) DEFAULT NULL,
`modified_by` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`owner` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`docstatus` int(1) NOT NULL DEFAULT 0,
`parent` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`parentfield` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`parenttype` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`idx` int(8) NOT NULL DEFAULT 0,
`label` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`group` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`action_type` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`action` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`name`),
KEY `parent` (`parent`),
KEY `modified` (`modified`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED;
--
-- Table structure for table `tabDocType Action`
--
CREATE TABLE `tabDocType Link` (
`name` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL,
`creation` datetime(6) DEFAULT NULL,
`modified` datetime(6) DEFAULT NULL,
`modified_by` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`owner` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`docstatus` int(1) NOT NULL DEFAULT 0,
`parent` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`parentfield` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`parenttype` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`idx` int(8) NOT NULL DEFAULT 0,
`group` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`link_doctype` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`link_fieldname` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`name`),
KEY `parent` (`parent`),
KEY `modified` (`modified`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED;
--
-- Table structure for table `tabDocType`
--

View file

@ -106,6 +106,57 @@ CREATE TABLE "tabDocPerm" (
create index on "tabDocPerm" ("parent");
--
-- Table structure for table "tabDocType Action"
--
DROP TABLE IF EXISTS "tabDocType Action";
CREATE TABLE "tabDocType Action" (
"name" varchar(255) NOT NULL,
"creation" timestamp(6) DEFAULT NULL,
"modified" timestamp(6) DEFAULT NULL,
"modified_by" varchar(255) DEFAULT NULL,
"owner" varchar(255) DEFAULT NULL,
"docstatus" smallint NOT NULL DEFAULT 0,
"parent" varchar(255) DEFAULT NULL,
"parentfield" varchar(255) DEFAULT NULL,
"parenttype" varchar(255) DEFAULT NULL,
"idx" bigint NOT NULL DEFAULT 0,
"label" varchar(140) NOT NULL,
"group" varchar(140) DEFAULT NULL,
"action_type" varchar(140) NOT NULL,
"action" varchar(140) NOT NULL,
PRIMARY KEY ("name")
) ;
create index on "tabDocType Action" ("parent");
--
-- Table structure for table "tabDocType Link"
--
DROP TABLE IF EXISTS "tabDocType Link";
CREATE TABLE "tabDocType Link" (
"name" varchar(255) NOT NULL,
"creation" timestamp(6) DEFAULT NULL,
"modified" timestamp(6) DEFAULT NULL,
"modified_by" varchar(255) DEFAULT NULL,
"owner" varchar(255) DEFAULT NULL,
"docstatus" smallint NOT NULL DEFAULT 0,
"parent" varchar(255) DEFAULT NULL,
"parentfield" varchar(255) DEFAULT NULL,
"parenttype" varchar(255) DEFAULT NULL,
"idx" bigint NOT NULL DEFAULT 0,
"label" varchar(140) DEFAULT NULL,
"group" varchar(140) DEFAULT NULL,
"link_doctype" varchar(140) NOT NULL,
"link_fieldname" varchar(140) NOT NULL,
PRIMARY KEY ("name")
) ;
create index on "tabDocType Link" ("parent");
--
-- Table structure for table "tabDocType"
--

View file

@ -3,6 +3,15 @@
frappe.ui.form.on('Bulk Update', {
refresh: function(frm) {
frm.set_query("document_type", function() {
return {
filters: [
['DocType', 'issingle', '=', 0],
['DocType', 'name', 'not in', frappe.model.core_doctypes_list]
]
};
});
frm.page.set_primary_action(__('Update'), function() {
if (!frm.doc.update_value) {
frappe.throw(__('Field "value" is mandatory. Please specify value to be updated'));

View file

@ -41,6 +41,7 @@ frappe.ui.form.on('Dashboard Chart', {
timespan: function(frm) {
const time_interval_options = {
"Select Date Range": ["Quarterly", "Monthly", "Weekly", "Daily"],
"All Time": ["Yearly", "Monthly"],
"Last Year": ["Quarterly", "Monthly", "Weekly", "Daily"],
"Last Quarter": ["Monthly", "Weekly", "Daily"],
"Last Month": ["Weekly", "Daily"],

View file

@ -82,14 +82,14 @@
"fieldname": "timespan",
"fieldtype": "Select",
"label": "Timespan",
"options": "Last Year\nLast Quarter\nLast Month\nLast Week\nSelect Date Range"
"options": "All Time\nLast Year\nLast Quarter\nLast Month\nLast Week\nSelect Date Range"
},
{
"depends_on": "timeseries",
"fieldname": "time_interval",
"fieldtype": "Select",
"label": "Time Interval",
"options": "Quarterly\nMonthly\nWeekly\nDaily"
"options": "Yearly\nQuarterly\nMonthly\nWeekly\nDaily"
},
{
"default": "0",
@ -187,7 +187,7 @@
"label": "To Date"
}
],
"modified": "2019-11-04 12:32:14.525409",
"modified": "2019-11-18 16:20:11.529496",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",

View file

@ -74,8 +74,7 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
result = convert_to_dates(data, timegrain)
# add missing data points for periods where there was no result
result = add_missing_values(result, timegrain, from_date, to_date)
result = add_missing_values(result, timegrain, timespan, from_date, to_date)
chart_config = {
"labels": [formatdate(r[0].strftime('%Y-%m-%d')) for r in result],
"datasets": [{
@ -133,7 +132,9 @@ def get_aggregate_function(chart_type):
"Average": "AVG",
}[chart_type]
def convert_to_dates(data, timegrain):
""" Converts individual dates within data to the end of period """
result = []
for d in data:
if timegrain == 'Daily':
@ -141,10 +142,11 @@ def convert_to_dates(data, timegrain):
elif timegrain == 'Weekly':
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), weeks = d[1] + 1), days = -1), d[2]])
elif timegrain == 'Monthly':
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months = d[1]), days = -1), d[2]])
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=d[1]), days = -1), d[2]])
elif timegrain == 'Quarterly':
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months = d[1] * 3), days = -1), d[2]])
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=d[1] * 3), days = -1), d[2]])
elif timegrain == 'Yearly':
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=12), days = -1), d[2]])
result[-1][0] = getdate(result[-1][0])
return result
@ -164,17 +166,17 @@ def get_unit_function(datefield, timegrain):
return unit_function
def add_missing_values(data, timegrain, from_date, to_date):
def add_missing_values(data, timegrain, timespan, from_date, to_date):
# add missing intervals
result = []
first_expected_date = get_period_ending(from_date, timegrain)
# fill out data before the first data point
first_data_point_date = data[0][0] if data else getdate(add_to_date(to_date, days=1))
while first_data_point_date > first_expected_date:
result.append([first_expected_date, 0.0])
first_expected_date = get_next_expected_date(first_expected_date, timegrain)
if timespan != 'All Time':
first_expected_date = get_period_ending(from_date, timegrain)
# fill out data before the first data point
first_data_point_date = data[0][0] if data else getdate(add_to_date(to_date, days=1))
while first_data_point_date > first_expected_date:
result.append([first_expected_date, 0.0])
first_expected_date = get_next_expected_date(first_expected_date, timegrain)
# fill data points and missing points
for i, d in enumerate(data):
@ -212,14 +214,16 @@ def get_next_expected_date(date, timegrain):
def get_period_ending(date, timegrain):
date = getdate(date)
if timegrain=='Daily':
if timegrain == 'Daily':
pass
elif timegrain=='Weekly':
elif timegrain == 'Weekly':
date = get_week_ending(date)
elif timegrain=='Monthly':
elif timegrain == 'Monthly':
date = get_month_ending(date)
elif timegrain=='Quarterly':
elif timegrain == 'Quarterly':
date = get_quarter_ending(date)
elif timegrain == 'Yearly':
date = get_year_ending(date)
return getdate(date)
@ -231,7 +235,7 @@ def get_week_ending(date):
# first day of next week
date = add_to_date('{}-01-01'.format(date.year), weeks = week_of_the_year + 1)
# last day of this week
return add_to_date(date, days = -1)
return add_to_date(date, days=-1)
def get_month_ending(date):
month_of_the_year = int(date.strftime('%m'))
@ -239,7 +243,7 @@ def get_month_ending(date):
date = add_to_date('{}-01-01'.format(date.year), months = month_of_the_year)
# last day of this month
return add_to_date(date, days = -1)
return add_to_date(date, days=-1)
def get_quarter_ending(date):
date = getdate(date)
@ -255,8 +259,17 @@ def get_quarter_ending(date):
return date
def get_year_ending(date):
''' returns year ending of the given date '''
# first day of next year (note year starts from 1)
date = add_to_date('{}-01-01'.format(date.year), months = 12)
# last day of this month
return add_to_date(date, days=-1)
class DashboardChart(Document):
def on_update(self):
frappe.cache().delete_key('chart-data:{}'.format(self.name))

View file

@ -36,6 +36,9 @@ class TestDashboardChart(unittest.TestCase):
self.assertEqual(get_period_ending('2019-10-01', 'Quarterly'),
getdate('2019-12-31'))
self.assertEqual(get_period_ending('2019-10-01', 'Yearly'),
getdate('2019-12-31'))
def test_dashboard_chart(self):
if frappe.db.exists('Dashboard Chart', 'Test Dashboard Chart'):
frappe.delete_doc('Dashboard Chart', 'Test Dashboard Chart')

View file

@ -29,12 +29,16 @@ class GlobalSearchSettings(Document):
repeated_dts = (", ".join([frappe.bold(dt) for dt in repeated_dts]))
frappe.throw(_("Document Type {0} has been repeated.").format(repeated_dts))
def get_doctypes_for_global_search():
doctypes = frappe.get_list("Global Search DocType", fields=["document_type"], order_by="idx ASC")
if not doctypes:
return []
# reset cache
frappe.cache().hdel('global_search', 'search_priorities')
def get_doctypes_for_global_search():
def get_from_db():
doctypes = frappe.get_list("Global Search DocType", fields=["document_type"], order_by="idx ASC")
return [d.document_type for d in doctypes] or []
return frappe.cache().hget("global_search", "search_priorities", get_from_db)
return [d.document_type for d in doctypes]
@frappe.whitelist()
def reset_global_search_settings_doctypes():
@ -57,7 +61,7 @@ def update_global_search_doctypes():
if search_doctypes.get(domain):
global_search_doctypes.extend(search_doctypes.get(domain))
doctype_list = set([dt.name for dt in frappe.get_list("DocType")])
doctype_list = set([dt.name for dt in frappe.get_all("DocType")])
allowed_in_global_search = []
for dt in global_search_doctypes:

View file

@ -42,7 +42,6 @@ def enqueue_create_notification(users, doc):
This breaks new site creation if Redis server is not running.
We do not need any notifications in fresh installation
'''
if frappe.flags.in_install:
return

View file

@ -0,0 +1,12 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Notification Settings', {
onload: () => {
frappe.breadcrumbs.add({
label: __('Settings'),
route: '#modules/Settings',
type: 'Custom'
});
}
});

View file

@ -0,0 +1,45 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Onboarding Slide', {
refresh: function(frm) {
frm.toggle_reqd('ref_doctype', (frm.doc.slide_type=='Create' || frm.doc.slide_type=='Settings'));
frm.toggle_reqd('slide_module', (frm.doc.slide_type=='Information' || frm.doc.slide_type=='Continue'));
},
ref_doctype: function(frm) {
frm.set_query('ref_doctype', function() {
if (frm.doc.slide_type === 'Create') {
return {
filters: {
'issingle': 0,
'istable': 0
}
};
} else if (frm.doc.slide_type === 'Settings') {
return {
filters: {
'issingle': 1,
'istable': 0
}
};
}
});
//fetch mandatory fields automatically
if (frm.doc.ref_doctype) {
frappe.model.clear_table(frm.doc, 'slide_fields');
let fields = frappe.meta.get_docfields(frm.doc.ref_doctype, null, {
reqd: 1
});
$.each(fields, function(_i, data) {
let row = frappe.model.add_child(frm.doc, 'Onboarding Slide', 'slide_fields');
row.label = data.label;
row.fieldtype = data.fieldtype;
row.fieldname = data.fieldname;
row.options = data.options;
});
refresh_field('slide_fields');
}
}
});

View file

@ -0,0 +1,184 @@
{
"autoname": "field:slide_title",
"creation": "2019-11-13 14:39:56.834658",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"slide_title",
"app",
"slide_order",
"column_break_4",
"image_src",
"slide_module",
"description_section_break",
"slide_desc",
"action_section_break",
"slide_type",
"column_break_6",
"max_count",
"add_more_button",
"section_break_18",
"ref_doctype",
"slide_fields",
"section_break_10",
"domains",
"column_break_12",
"help_links",
"is_completed"
],
"fields": [
{
"fieldname": "slide_title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Slide Title",
"reqd": 1,
"unique": 1
},
{
"fieldname": "slide_desc",
"fieldtype": "HTML Editor",
"label": "Slide Description"
},
{
"default": "3",
"depends_on": "add_more_button",
"description": "The amount of times you want to repeat the set of fields (eg: if you want 3 customers in the slide, set this field to 3. Only the first set of fields is shown as mandatory in the slide)",
"fieldname": "max_count",
"fieldtype": "Int",
"label": "Max Count"
},
{
"default": "0",
"depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'",
"fieldname": "add_more_button",
"fieldtype": "Check",
"label": "Add More Button"
},
{
"depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'",
"fieldname": "slide_fields",
"fieldtype": "Table",
"label": "Slide Fields",
"options": "Onboarding Slide Field"
},
{
"fieldname": "section_break_10",
"fieldtype": "Section Break"
},
{
"description": "Specify in what all domains should the slides show up. If nothing is specified the slide is shown in all domains by default.",
"fieldname": "domains",
"fieldtype": "Table",
"label": "Domains",
"options": "Has Domain"
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"description": "Add a help video link just in case user has no idea about what to fill in the slide.",
"fieldname": "help_links",
"fieldtype": "Table",
"label": "Help Links",
"options": "Onboarding Slide Help Link"
},
{
"fieldname": "action_section_break",
"fieldtype": "Section Break",
"label": "Action Settings"
},
{
"description": "If Slide Type is Create or Settings there should be a 'create_onboarding_docs' method in the {ref_doctype}.py file bound to be executed after the slide is completed.",
"fieldname": "slide_type",
"fieldtype": "Select",
"label": "Slide Type",
"options": "Information\nCreate\nSettings\nContinue",
"reqd": 1
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"fieldname": "app",
"fieldtype": "Select",
"label": "App",
"options": "Frappe\nERPNext",
"reqd": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "image_src",
"fieldtype": "Data",
"label": "Slide Image Source"
},
{
"fieldname": "description_section_break",
"fieldtype": "Section Break",
"label": "Description"
},
{
"depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'",
"fieldname": "ref_doctype",
"fieldtype": "Link",
"label": "Reference Document Type",
"options": "DocType"
},
{
"default": "0",
"description": "Determines the order of the slide in the wizard. If the slide is not to be displayed, priority should be set to 0.",
"fieldname": "slide_order",
"fieldtype": "Int",
"label": "Slide Order"
},
{
"depends_on": "eval:doc.slide_type=='Information' || doc.slide_type=='Continue'",
"fieldname": "slide_module",
"fieldtype": "Link",
"label": "Module",
"options": "Module Def"
},
{
"collapsible_depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'",
"fieldname": "section_break_18",
"fieldtype": "Section Break",
"label": "Fields"
},
{
"default": "0",
"fieldname": "is_completed",
"fieldtype": "Check",
"hidden": 1,
"label": "Is Completed",
"print_hide": 1
}
],
"modified": "2019-12-04 10:50:43.528901",
"modified_by": "Administrator",
"module": "Desk",
"name": "Onboarding Slide",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import json
from frappe import _
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
class OnboardingSlide(Document):
def validate(self):
if self.slide_type == 'Continue' and frappe.db.exists('Onboarding Slide', {'slide_type': 'Continue', 'name': ('!=', self.name)}):
frappe.throw(_('An Onboarding Slide of Slide Type Continue already exists.'))
if self.slide_order:
same_order_slide = frappe.db.exists('Onboarding Slide', {'slide_order': self.slide_order, 'name': ('!=', self.name)})
if same_order_slide:
frappe.throw(_('An Onboarding Slide <b>{0}</b> with the same slide order already exists').format(same_order_slide))
def on_update(self):
if self.ref_doctype:
module = frappe.db.get_value('DocType', self.ref_doctype, 'module')
else:
module = self.slide_module
export_to_files(record_list=[['Onboarding Slide', self.name]], record_module=module)
def get_onboarding_slides_as_list():
slides = []
slide_docs = frappe.db.get_all('Onboarding Slide',
filters={'is_completed': 0},
or_filters={'slide_order': ('!=', 0), 'slide_type': 'Continue'},
order_by='slide_order')
# to check if continue slide is required
first_slide = get_first_slide()
for entry in slide_docs:
# using get_doc because child table fields are not fetched in get_all
slide_doc = frappe.get_doc('Onboarding Slide', entry.name)
if frappe.scrub(slide_doc.app) in frappe.get_installed_apps():
slide = frappe._dict(
slide_type=slide_doc.slide_type,
title=slide_doc.slide_title,
help=slide_doc.slide_desc,
fields=slide_doc.slide_fields,
help_links=get_help_links(slide_doc),
add_more=slide_doc.add_more_button,
max_count=slide_doc.max_count,
image_src=get_slide_image(slide_doc),
ref_doctype=slide_doc.ref_doctype,
app=slide_doc.app
)
if slide.slide_type == 'Continue':
if is_continue_slide_required(first_slide):
slides.insert(0, slide)
else:
slides.append(slide)
return slides
@frappe.whitelist()
def get_onboarding_slides():
slides = []
slide_list = get_onboarding_slides_as_list()
active_domains = frappe.get_active_domains()
for slide in slide_list:
if not slide.domains or any(domain in active_domains for domain in slide.domains):
slides.append(slide)
return slides
def get_help_links(slide_doc):
links=[]
for link in slide_doc.help_links:
links.append({
'label': link.label,
'video_id': link.video_id
})
return links
def get_slide_image(slide_doc):
if slide_doc.image_src:
return slide_doc.image_src
return None
def is_continue_slide_required(first_slide):
# check if first slide itself is not completed
if not first_slide.is_completed:
return False
# check if there is any active slide which is not completed
return frappe.db.exists('Onboarding Slide', {
'is_completed': 0,
'slide_order': ('!=', 0),
'slide_type': ('!=', 'Continue')
})
@frappe.whitelist()
def create_onboarding_docs(values, doctype=None, app=None, slide_type=None):
data = json.loads(values)
doc = frappe.new_doc(doctype)
if hasattr(doc, 'create_onboarding_docs'):
doc.create_onboarding_docs(data)
else:
create_generic_onboarding_doc(data, doctype, slide_type)
def create_generic_onboarding_doc(data, doctype, slide_type):
if slide_type == 'Settings':
doc = frappe.get_single(doctype)
for entry in data:
doc.set(entry, data.get(entry))
doc.save()
elif slide_type == 'Create':
doc = frappe.new_doc(doctype)
for entry in data:
doc.set(entry, data.get(entry))
doc.flags.ignore_mandatory = True
doc.flags.ignore_links = True
doc.insert()
@frappe.whitelist()
def mark_slide_as_completed(slide_title):
frappe.db.set_value('Onboarding Slide', slide_title, 'is_completed', 1)
def get_first_slide():
slides = frappe.db.get_all('Onboarding Slide',
filters={'slide_order': ('!=', 0), 'slide_type': ('!=', 'Continue')},
order_by='slide_order',
fields=['name', 'is_completed']
)
return slides[0]

View file

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

View file

@ -0,0 +1,74 @@
{
"creation": "2019-11-13 13:35:08.617909",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"label",
"fieldtype",
"fieldname",
"align",
"placeholder",
"reqd",
"column_break_4",
"options"
],
"fields": [
{
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label"
},
{
"fieldname": "fieldtype",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Fieldtype",
"options": "Attach\nAttach Image\nCheck\nCurrency\nData\nDate\nDatetime\nFloat\nHTML\nInt\nRating\nSelect\nLink\nSmall Text\nText\nText Editor\nSection Break\nColumn Break"
},
{
"fieldname": "fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Fieldname"
},
{
"fieldname": "align",
"fieldtype": "Select",
"label": "Align",
"options": "\ncenter\nleft\nright"
},
{
"fieldname": "placeholder",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Placeholder"
},
{
"default": "0",
"fieldname": "reqd",
"fieldtype": "Check",
"label": "Mandatory"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "options",
"fieldtype": "Text",
"in_list_view": 1,
"label": "Options"
}
],
"istable": 1,
"modified": "2019-12-02 16:43:51.930018",
"modified_by": "Administrator",
"module": "Desk",
"name": "Onboarding Slide Field",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
}

View file

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

View file

@ -0,0 +1,35 @@
{
"creation": "2019-11-19 12:22:42.805741",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"label",
"video_id"
],
"fields": [
{
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label"
},
{
"fieldname": "video_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Video"
}
],
"istable": 1,
"modified": "2019-11-19 13:39:57.716248",
"modified_by": "Administrator",
"module": "Desk",
"name": "Onboarding Slide Help Link",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

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

View file

@ -105,7 +105,7 @@ def get_next(doctype, value, prev, filters, sort_order, sort_field):
res = frappe.get_list(doctype,
fields = ["name"],
filters = filters,
order_by = sort_field + " " + sort_order,
order_by = "`tab{0}`.{1}".format(doctype, sort_field) + " " + sort_order,
limit_start=0, limit_page_length=1, as_list=True)
if not res:

View file

@ -48,7 +48,7 @@ def get_group_by_count(doctype, current_filters, field):
else:
return frappe.db.get_list(doctype,
filters=current_filters,
group_by=field,
group_by='`tab{0}`.{1}'.format(doctype, field),
fields=['count(*) as count', '`{}` as name'.format(field)],
order_by='count desc',
limit=50,

View file

@ -234,8 +234,11 @@ def get_config(app, module):
for item in section["items"]:
if item["type"]=="report" and item["name"] in disabled_reports:
continue
# some module links might not have name
if not item.get("name"):
item["name"] = item.get("label")
if not item.get("label"):
item["label"] = _(item["name"])
item["label"] = _(item.get("name"))
items.append(item)
section['items'] = items
@ -297,7 +300,7 @@ def get_onboard_items(app, module):
@frappe.whitelist()
def get_links_for_module(app, module):
return [l.get('label') for l in get_links(app, module)]
return [{'value': l.get('name'), 'label': l.get('label')} for l in get_links(app, module)]
def get_links(app, module):
try:
@ -330,13 +333,13 @@ def get_desktop_settings():
def apply_user_saved_links(module):
module = frappe._dict(module)
all_links = get_links(module.app, module.module_name)
module_links_by_label = {}
module_links_by_name = {}
for link in all_links:
module_links_by_label[link['label']] = link
module_links_by_name[link['name']] = link
if module.module_name in user_saved_links_by_module:
user_links = frappe.parse_json(user_saved_links_by_module[module.module_name])
module.links = [module_links_by_label[l] for l in user_links if l in module_links_by_label]
module.links = [module_links_by_name[l] for l in user_links if l in module_links_by_name]
return module

View file

@ -11,15 +11,19 @@ import json
@frappe.whitelist()
@frappe.read_only()
def get_notifications():
out = {
"open_count_doctype": {},
"targets": {},
}
if (frappe.flags.in_install or
not frappe.db.get_single_value('System Settings', 'setup_complete')):
return {
"open_count_doctype": {},
"targets": {},
}
return out
config = get_notification_config()
if not config:
return out
groups = list(config.get("for_doctype")) + list(config.get("for_module"))
cache = frappe.cache()
@ -31,10 +35,10 @@ def get_notifications():
if count is not None:
notification_count[name] = count
return {
"open_count_doctype": get_notifications_for_doctypes(config, notification_count),
"targets": get_notifications_for_targets(config, notification_percent),
}
out['open_count_doctype'] = get_notifications_for_doctypes(config, notification_count)
out['targets'] = get_notifications_for_targets(config, notification_percent)
return out
def get_notifications_for_doctypes(config, notification_count):
"""Notifications for DocTypes"""
@ -118,6 +122,10 @@ def clear_notifications(user=None):
return
cache = frappe.cache()
config = get_notification_config()
if not config:
return
for_doctype = list(config.get('for_doctype')) if config.get('for_doctype') else []
for_module = list(config.get('for_module')) if config.get('for_module') else []
groups = for_doctype + for_module
@ -139,6 +147,8 @@ def delete_notification_count_for(doctype):
def clear_doctype_notifications(doc, method=None, *args, **kwargs):
config = get_notification_config()
if not config:
return
if isinstance(doc, string_types):
doctype = doc # assuming doctype name was passed directly
else:

View file

@ -19,6 +19,14 @@
background: #f0f4f7;
}
.from-date-field .clearfix{
display: none;
}
.from-date-field {
margin-left: 10px;
}
.select-time:focus, .select-doctype:focus, .select-filter:focus, .select-sort:focus {
background: #f0f4f7;
}

View file

@ -41,7 +41,11 @@ class Leaderboard {
return field;
});
}
this.timespans = ["Week", "Month", "Quarter", "Year", "All Time"];
this.timespans = [
"This Week", "This Month", "This Quarter", "This Year",
"Last Week", "Last Month", "Last Quarter", "Last Year",
"All Time", "Select From Date"
];
// for saving current selected filters
const _initial_doctype = frappe.get_route()[1] || this.doctypes[0];
@ -103,7 +107,8 @@ class Leaderboard {
this.timespans.map(d => {
return {"label": __(d), value: d };
})
);
);
this.create_from_date_field();
this.type_select = this.page.add_select(__("Field"),
this.options.selected_filter.map(d => {
@ -113,7 +118,12 @@ class Leaderboard {
this.timespan_select.on("change", (e) => {
this.options.selected_timespan = e.currentTarget.value;
this.make_request();
if (this.options.selected_timespan === 'Select From Date') {
this.from_date_field.show();
} else {
this.from_date_field.hide();
this.make_request();
}
});
this.type_select.on("change", (e) => {
@ -122,6 +132,28 @@ class Leaderboard {
});
}
create_from_date_field() {
let timespan_field = $(this.parent).find(`.frappe-control[data-original-title='Timespan']`);
this.from_date_field = $(`<div class="from-date-field"></div>`).insertAfter(timespan_field).hide();
let date_field = frappe.ui.form.make_control({
df: {
fieldtype: 'Date',
fieldname: 'selected_from_date',
placeholder: frappe.datetime.month_start(),
default: frappe.datetime.month_start(),
input_class: 'input-sm',
reqd: 1,
change: () => {
this.selected_from_date = date_field.get_value();
if (this.selected_from_date) this.make_request();
}
},
parent: $(this.parent).find('.from-date-field'),
render_input: 1
});
}
render_selected_doctype() {
this.$sidebar_list.on("click", "li", (e)=> {
@ -207,7 +239,6 @@ class Leaderboard {
this.leaderboard_config[this.options.selected_doctype].method,
{
'from_date': this.get_from_date(),
'timespan': this.options.selected_timespan,
'company': this.options.selected_company,
'field': this.options.selected_filter_item,
'limit': this.leaderboard_limit,
@ -360,17 +391,20 @@ class Leaderboard {
get_from_date() {
let timespan = this.options.selected_timespan.toLowerCase();
let current_date = frappe.datetime.now_date();
let date = '';
if (timespan === "month") {
date = frappe.datetime.add_months(current_date, -1);
} else if (timespan === "quarter") {
date = frappe.datetime.add_months(current_date, -3);
} else if (timespan === "year") {
date = frappe.datetime.add_months(current_date, -12);
} else if (timespan === "week") {
date = frappe.datetime.add_days(current_date, -7);
let get_from_date = {
"this week": frappe.datetime.week_start(),
"this month": frappe.datetime.month_start(),
"this quarter": frappe.datetime.quarter_start(),
"this year": frappe.datetime.year_start(),
"last week": frappe.datetime.add_days(current_date, -7),
"last month": frappe.datetime.add_months(current_date, -1),
"last quarter": frappe.datetime.add_months(current_date, -3),
"last year": frappe.datetime.add_months(current_date, -12),
"all time": "",
"select from date": this.selected_from_date || frappe.datetime.month_start()
}
return date;
return get_from_date[timespan];
}
}

View file

@ -10,6 +10,7 @@ from frappe.desk.doctype.global_search_settings.global_search_settings import up
def install():
update_genders_and_salutations()
update_global_search_doctypes()
setup_email_linking()
@frappe.whitelist()
def update_genders_and_salutations():
@ -20,13 +21,10 @@ def update_genders_and_salutations():
for record in records:
doc = frappe.new_doc(record.get("doctype"))
doc.update(record)
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
try:
doc.insert(ignore_permissions=True)
except frappe.DuplicateEntryError as e:
# pass DuplicateEntryError and continue
if e.args and e.args[0]==doc.doctype and e.args[1]==doc.name:
# make sure DuplicateEntryError is for the exact same doc and not a related doc
pass
else:
raise
def setup_email_linking():
doc = frappe.get_doc({
"doctype": "Email Account",
"email_id": "email_linking@example.com",
}).insert(ignore_permissions=True, ignore_if_duplicate=True)

View file

@ -9,8 +9,10 @@ from six.moves import range
import frappe.permissions
from frappe.model.db_query import DatabaseQuery
from frappe import _
from six import text_type, string_types, StringIO
from six import string_types, StringIO
from frappe.core.doctype.access_log.access_log import make_access_log
from frappe.utils import cstr
@frappe.whitelist()
@frappe.read_only()
@ -170,11 +172,11 @@ def export_query():
writer = csv.writer(f)
for r in data:
# encode only unicode type strings and not int, floats etc.
writer.writerow([handle_html(frappe.as_unicode(v)).encode('utf-8') \
writer.writerow([handle_html(frappe.as_unicode(v)) \
if isinstance(v, string_types) else v for v in r])
f.seek(0)
frappe.response['result'] = text_type(f.read(), 'utf-8')
frappe.response['result'] = cstr(f.read())
frappe.response['type'] = 'csv'
frappe.response['doctype'] = title
@ -261,17 +263,24 @@ def delete_bulk(doctype, items):
@frappe.whitelist()
@frappe.read_only()
def get_sidebar_stats(stats, doctype, filters=[]):
_user_tags = []
data = frappe._dict(frappe.local.form_dict)
filters = json.loads(data["filters"])
if not frappe.cache().hget("tags_count", doctype):
tags = [tag.name for tag in frappe.get_list("Tag")]
_user_tags = []
for tag in tags:
count = frappe.db.count("Tag Link", filters={"document_type": doctype, "tag": tag})
if count > 0:
_user_tags.append([tag, count])
frappe.cache().hset("tags_count", doctype, _user_tags)
if not frappe.cache().hget("Tags", doctype):
tags = set([tag.tag for tag in frappe.get_list("Tag Link", filters={"document_type": doctype}, fields=["tag"])])
frappe.cache().hset("Tags", doctype, tags)
return {"stats": {"_user_tags": frappe.cache().hget("tags_count", doctype)}}
for tag in list(frappe.cache().hget("Tags", doctype)):
tag_filters = []
tag_filters.extend(filters)
tag_filters.extend([['Tag Link', 'tag', '=', tag]])
count = frappe.get_all(doctype, filters=tag_filters, fields=["count(*)"])
if count[0].get("count(*)") > 0:
_user_tags.append([tag, count[0].get("count(*)")])
return {"stats": {"_user_tags": _user_tags}}
@frappe.whitelist()
@frappe.read_only()

View file

@ -50,7 +50,7 @@ def sanitize_searchfield(searchfield):
# this is called by the Link Field
@frappe.whitelist()
def search_link(doctype, txt, query=None, filters=None, page_length=20, searchfield=None, reference_doctype=None, ignore_user_permissions=False):
search_widget(doctype, txt, query, searchfield=searchfield, page_length=page_length, filters=filters, reference_doctype=reference_doctype, ignore_user_permissions=ignore_user_permissions)
search_widget(doctype, txt.strip(), query, searchfield=searchfield, page_length=page_length, filters=filters, reference_doctype=reference_doctype, ignore_user_permissions=ignore_user_permissions)
frappe.response['results'] = build_for_autosuggest(frappe.response["values"])
del frappe.response["values"]

View file

@ -1,30 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe
from frappe.utils import cint
@frappe.whitelist()
def get_user_progress_slides():
'''
Return user progress slides for the desktop (called via `get_user_progress_slides` hook)
'''
slides = []
if cint(frappe.db.get_single_value('System Settings', 'setup_complete')):
for fn in frappe.get_hooks('get_user_progress_slides'):
slides += frappe.get_attr(fn)()
return slides
@frappe.whitelist()
def update_and_get_user_progress():
'''
Return setup progress action states (called via `update_and_get_user_progress` hook)
'''
states = {}
for fn in frappe.get_hooks('update_and_get_user_progress'):
states.update(frappe.get_attr(fn)())
return states

View file

@ -21,7 +21,6 @@ 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.scheduler import log
from frappe.utils.html_utils import clean_email_html
from frappe.email.utils import get_port
@ -284,7 +283,7 @@ class EmailAccount(Document):
except Exception:
frappe.db.rollback()
log('email_account.receive')
frappe.log_error('email_account.receive')
if self.use_imap:
self.handle_bad_emails(email_server, uid, msg, frappe.get_traceback())
exceptions.append(frappe.get_traceback())

View file

@ -9,7 +9,6 @@ from frappe import throw, _
from frappe.website.website_generator import WebsiteGenerator
from frappe.utils.verified_command import get_signed_params, verify_request
from frappe.utils.background_jobs import enqueue
from frappe.utils.scheduler import log
from frappe.email.queue import send
from frappe.email.doctype.email_group.email_group import add_subscribers
from frappe.utils import parse_addr
@ -213,7 +212,7 @@ def send_newsletter(newsletter):
doc.db_set("email_sent", 0)
frappe.db.commit()
log("send_newsletter")
frappe.log_error("send_newsletter")
raise

View file

@ -136,7 +136,7 @@ class TestNotification(unittest.TestCase):
"reference_name": event.name, "status": "Not Sent"}))
frappe.set_user('Administrator')
frappe.utils.scheduler.trigger(frappe.local.site, "daily", now=True)
frappe.get_doc('Scheduled Job Type', dict(method='frappe.email.doctype.notification.notification.trigger_daily_alerts')).execute()
# not today, so no alert
self.assertFalse(frappe.db.get_value("Email Queue", {"reference_doctype": "Event",
@ -150,7 +150,7 @@ class TestNotification(unittest.TestCase):
self.assertFalse(frappe.db.get_value("Email Queue", {"reference_doctype": "Event",
"reference_name": event.name, "status": "Not Sent"}))
frappe.utils.scheduler.trigger(frappe.local.site, "daily", now=True)
frappe.get_doc('Scheduled Job Type', dict(method='frappe.email.doctype.notification.notification.trigger_daily_alerts')).execute()
# today so show alert
self.assertTrue(frappe.db.get_value("Email Queue", {"reference_doctype": "Event",

View file

@ -12,7 +12,6 @@ from frappe.utils.verified_command import get_signed_params, verify_request
from html2text import html2text
from frappe.utils import get_url, nowdate, encode, now_datetime, add_days, split_emails, cstr, cint
from rq.timeouts import JobTimeoutException
from frappe.utils.scheduler import log
from six import text_type, string_types
class EmailLimitCrossedError(frappe.ValidationError): pass
@ -469,7 +468,7 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals
else:
# log to Error Log
log('frappe.email.queue.flush', text_type(e))
frappe.log_error('frappe.email.queue.flush')
def prepare_message(email, recipient, recipients_list):
message = email.message

View file

@ -12,7 +12,6 @@ import frappe
from frappe import _, safe_decode, safe_encode
from frappe.utils import (extract_email_id, convert_utc_to_user_timezone, now,
cint, cstr, strip, markdown, parse_addr)
from frappe.utils.scheduler import log
from frappe.core.doctype.file.file import get_random_filename, MaxFileSizeReachedError
class EmailSizeExceededError(frappe.ValidationError): pass
@ -80,7 +79,7 @@ class EmailServer:
except _socket.error:
# log performs rollback and logs error in Error Log
log("receive.connect_pop")
frappe.log_error("receive.connect_pop")
# Invalid mail server -- due to refusing connection
frappe.msgprint(_('Invalid Mail Server. Please rectify and try again.'))
@ -255,7 +254,7 @@ class EmailServer:
else:
# log performs rollback and logs error in Error Log
log("receive.get_messages", self.make_error_msg(msg_num, incoming_mail))
frappe.log_error("receive.get_messages", self.make_error_msg(msg_num, incoming_mail))
self.errors = True
frappe.db.rollback()
@ -457,9 +456,9 @@ class Email:
def show_attached_email_headers_in_content(self, part):
# get the multipart/alternative message
try:
from html import escape # python 3.x
from html import escape # python 3.x
except ImportError:
from cgi import escape # python 2.x
from cgi import escape # python 2.x
message = list(part.walk())[1]
headers = []
@ -481,7 +480,7 @@ class Email:
"""Detect chartset."""
charset = part.get_content_charset()
if not charset:
charset = chardet.detect(frappe.safe_encode(part))['encoding']
charset = chardet.detect(cstr(part))['encoding']
return charset
@ -515,7 +514,7 @@ class Email:
'fcontent': fcontent,
})
cid = (part.get("Content-Id") or "").strip("><")
cid = (cstr(part.get("Content-Id")) or "").strip("><")
if cid:
self.cid_map[fname] = cid

View file

@ -7,10 +7,5 @@ frappe.ui.form.on('Currency', {
if(!frm.doc.enabled) {
frm.set_intro(__("This Currency is disabled. Enable to use in transactions"));
}
},
after_save(frm) {
if (frm.doc.enabled)
locals[':Currency'][frm.doc.name] = Object.assign(frm.doc, { doctype: ':Currency' });
}
});

View file

@ -76,8 +76,7 @@ leaderboards = "frappe.desk.leaderboard.get_leaderboards"
on_session_creation = [
"frappe.core.doctype.activity_log.feed.login_feed",
"frappe.core.doctype.user.user.notify_admin_access_to_system_manager",
"frappe.utils.scheduler.reset_enabled_scheduler_events",
"frappe.core.doctype.user.user.notify_admin_access_to_system_manager"
]
on_logout = "frappe.core.doctype.session_default_settings.session_default_settings.clear_session_defaults"
@ -153,14 +152,18 @@ doc_events = {
}
scheduler_events = {
"cron": {
"0/15 * * * *": [
"frappe.oauth.delete_oauth2_data",
"frappe.website.doctype.web_page.web_page.check_publish_status",
"frappe.twofactor.delete_all_barcodes_for_users"
]
},
"all": [
"frappe.email.queue.flush",
"frappe.email.doctype.email_account.email_account.pull",
"frappe.email.doctype.email_account.email_account.notify_unreplied",
"frappe.oauth.delete_oauth2_data",
"frappe.integrations.doctype.razorpay_settings.razorpay_settings.capture_payment",
"frappe.twofactor.delete_all_barcodes_for_users",
"frappe.website.doctype.web_page.web_page.check_publish_status",
'frappe.utils.global_search.sync_global_search'
],
"hourly": [

View file

@ -18,15 +18,16 @@ from frappe.utils.fixtures import sync_fixtures
from frappe.website import render
from frappe.modules.utils import sync_customizations
from frappe.database import setup_database
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
def install_db(root_login="root", root_password=None, db_name=None, source_sql=None,
admin_password=None, verbose=True, force=0, site_config=None, reinstall=False,
db_type=None):
admin_password=None, verbose=True, force=0, site_config=None, reinstall=False,
db_type=None, db_host=None, db_port=None):
if not db_type:
db_type = frappe.conf.db_type or 'mariadb'
make_conf(db_name, site_config=site_config, db_type=db_type)
make_conf(db_name, site_config=site_config, db_type=db_type, db_host=db_host, db_port=db_port)
frappe.flags.in_install_db = True
frappe.flags.root_login = root_login
@ -91,6 +92,7 @@ def install_app(name, verbose=False, set_as_patched=True):
for after_install in app_hooks.after_install or []:
frappe.get_attr(after_install)()
sync_jobs()
sync_fixtures(name)
sync_customizations(name)
@ -189,14 +191,14 @@ def init_singles():
doc.flags.ignore_validate=True
doc.save()
def make_conf(db_name=None, db_password=None, site_config=None, db_type=None):
def make_conf(db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None):
site = frappe.local.site
make_site_config(db_name, db_password, site_config, db_type=db_type)
make_site_config(db_name, db_password, site_config, db_type=db_type, db_host=db_host, db_port=db_port)
sites_path = frappe.local.sites_path
frappe.destroy()
frappe.init(site, sites_path=sites_path)
def make_site_config(db_name=None, db_password=None, site_config=None, db_type=None):
def make_site_config(db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None):
frappe.create_folder(os.path.join(frappe.local.site_path))
site_file = get_site_config_path()
@ -207,6 +209,12 @@ def make_site_config(db_name=None, db_password=None, site_config=None, db_type=N
if db_type:
site_config['db_type'] = db_type
if db_host:
site_config['db_host'] = db_host
if db_port:
site_config['db_port'] = db_port
with open(site_file, "w") as f:
f.write(json.dumps(site_config, indent=1, sort_keys=True))

View file

@ -1,700 +1,183 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 0,
"beta": 0,
"creation": "2017-11-18 15:36:09.676722",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"allow_import": 1,
"creation": "2017-11-18 15:36:09.676722",
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enable_social_login",
"client_credentials",
"social_login_provider",
"client_id",
"column_break_0",
"provider_name",
"client_secret",
"sb_identity_details",
"icon",
"column_break_1",
"base_url",
"client_urls",
"authorize_url",
"access_token_url",
"column_break_3",
"redirect_url",
"api_endpoint",
"custom_base_url",
"client_information",
"api_endpoint_args",
"auth_url_data",
"user_id_property"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "enable_social_login",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Enable Social Login",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"default": "0",
"fieldname": "enable_social_login",
"fieldtype": "Check",
"label": "Enable Social Login"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"collapsible_depends_on": "eval:doc.enable_social_login",
"columns": 0,
"fieldname": "client_credentials",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Client Credentials",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"collapsible": 1,
"collapsible_depends_on": "eval:doc.enable_social_login",
"fieldname": "client_credentials",
"fieldtype": "Section Break",
"label": "Client Credentials"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Custom",
"depends_on": "eval:doc.custom!=1",
"fieldname": "social_login_provider",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Social Login Provider",
"length": 0,
"no_copy": 0,
"options": "Custom\nFacebook\nFrappe\nGitHub\nGoogle\nOffice 365\nSalesforce\nfairlogin",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 1,
"unique": 0
},
"default": "Custom",
"depends_on": "eval:doc.custom!=1",
"fieldname": "social_login_provider",
"fieldtype": "Select",
"label": "Social Login Provider",
"options": "Custom\nFacebook\nFrappe\nGitHub\nGoogle\nOffice 365\nSalesforce\nfairlogin",
"set_only_once": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "client_id",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Client ID",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "client_id",
"fieldtype": "Data",
"label": "Client ID"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_0",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "column_break_0",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "provider_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Provider Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 1,
"unique": 0
},
"fieldname": "provider_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Provider Name",
"reqd": 1,
"set_only_once": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "client_secret",
"fieldtype": "Password",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Client Secret",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "client_secret",
"fieldtype": "Password",
"label": "Client Secret"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"collapsible_depends_on": "eval:doc.custom_base_url",
"columns": 0,
"depends_on": "",
"fieldname": "sb_identity_details",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Identity Details",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"collapsible": 1,
"collapsible_depends_on": "eval:doc.custom_base_url",
"fieldname": "sb_identity_details",
"fieldtype": "Section Break",
"label": "Identity Details"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "icon",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Icon",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "icon",
"fieldtype": "Data",
"label": "Icon"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_1",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "base_url",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Base URL",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "base_url",
"fieldtype": "Data",
"label": "Base URL"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"collapsible_depends_on": "eval:doc.social_login_provider===\"Custom\"",
"columns": 0,
"depends_on": "",
"fieldname": "client_urls",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Client URLs",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"collapsible": 1,
"collapsible_depends_on": "eval:doc.social_login_provider===\"Custom\"",
"fieldname": "client_urls",
"fieldtype": "Section Break",
"label": "Client URLs"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "authorize_url",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Authorize URL",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "authorize_url",
"fieldtype": "Data",
"label": "Authorize URL"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "access_token_url",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Access Token URL",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "access_token_url",
"fieldtype": "Data",
"label": "Access Token URL"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "redirect_url",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Redirect URL",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "redirect_url",
"fieldtype": "Data",
"label": "Redirect URL"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "api_endpoint",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "API Endpoint",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "api_endpoint",
"fieldtype": "Data",
"label": "API Endpoint"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "custom_base_url",
"fieldtype": "Check",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Custom Base URL",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"default": "0",
"fieldname": "custom_base_url",
"fieldtype": "Check",
"hidden": 1,
"label": "Custom Base URL"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"collapsible_depends_on": "eval:doc.social_login_provider===\"Custom\"",
"columns": 0,
"depends_on": "",
"fieldname": "client_information",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Client Information",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"collapsible": 1,
"collapsible_depends_on": "eval:doc.social_login_provider===\"Custom\"",
"fieldname": "client_information",
"fieldtype": "Section Break",
"label": "Client Information"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "api_endpoint_args",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "API Endpoint Args",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "api_endpoint_args",
"fieldtype": "Code",
"label": "API Endpoint Args"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "auth_url_data",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Auth URL Data",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"fieldname": "auth_url_data",
"fieldtype": "Code",
"label": "Auth URL Data"
},
{
"depends_on": "eval:doc.social_login_provider===\"Custom\"",
"fieldname": "user_id_property",
"fieldtype": "Data",
"label": "User ID Property"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-09-15 09:00:00.000000",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Social Login Key",
"name_case": "",
"owner": "Administrator",
],
"modified": "2019-12-03 12:35:55.115260",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Social Login Key",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "provider_name",
"track_changes": 1,
"track_seen": 0
}
],
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "provider_name",
"track_changes": 1
}

View file

@ -15,6 +15,7 @@ from frappe.desk.notifications import clear_notifications
from frappe.website import render
from frappe.core.doctype.language.language import sync_languages
from frappe.modules.utils import sync_customizations
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
from frappe.utils import global_search
def migrate(verbose=True, rebuild_website=False, skip_failing=False):
@ -46,9 +47,11 @@ def migrate(verbose=True, rebuild_website=False, skip_failing=False):
# run patches
frappe.modules.patch_handler.run_all(skip_failing)
# sync
frappe.model.sync.sync_all(verbose=verbose)
frappe.translate.clear_cache()
sync_jobs()
sync_fixtures()
sync_customizations()
sync_languages()

View file

@ -45,7 +45,7 @@ default_fields = ('doctype','name','owner','creation','modified','modified_by',
'parent','parentfield','parenttype','idx','docstatus')
optional_fields = ("_user_tags", "_comments", "_assign", "_liked_by", "_seen")
table_fields = ('Table', 'Table MultiSelect')
core_doctypes_list = ('DocType', 'DocField', 'DocPerm', 'User', 'Role', 'Has Role',
core_doctypes_list = ('DocType', 'DocField', 'DocPerm', 'DocType Action', 'DocType Link', 'User', 'Role', 'Has Role',
'Page', 'Module Def', 'Print Format', 'Report', 'Customize Form',
'Customize Form Field', 'Property Setter', 'Custom Field', 'Custom Script')

View file

@ -22,6 +22,8 @@ max_positive_value = {
'bigint': 2 ** 63
}
DOCTYPES_FOR_DOCTYPE = ('DocType', 'DocField', 'DocPerm', 'DocType Action', 'DocType Link')
_classes = {}
def get_controller(doctype):
@ -255,7 +257,7 @@ class BaseDocument(object):
def get_valid_columns(self):
if self.doctype not in frappe.local.valid_columns:
if self.doctype in ("DocField", "DocPerm") and self.parent in ("DocType", "DocField", "DocPerm"):
if self.doctype in DOCTYPES_FOR_DOCTYPE:
from frappe.model.meta import get_table_columns
valid = get_table_columns(self.doctype)
else:
@ -312,7 +314,7 @@ class BaseDocument(object):
self.created_by = self.modified_by = frappe.session.user
# if doctype is "DocType", don't insert null values as we don't know who is valid yet
d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in ('DocType', 'DocField', 'DocPerm'))
d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in DOCTYPES_FOR_DOCTYPE)
columns = list(d)
try:
@ -347,7 +349,7 @@ class BaseDocument(object):
self.db_insert()
return
d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in ('DocType', 'DocField', 'DocPerm'))
d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in DOCTYPES_FOR_DOCTYPE)
# don't update name, as case might've been changed
name = d['name']

View file

@ -501,6 +501,10 @@ class DatabaseQuery(object):
value = f.value or "''"
fallback = "''"
elif f.fieldname == 'name':
value = f.value or "''"
fallback = "''"
else:
value = flt(f.value)
fallback = 0
@ -513,6 +517,8 @@ class DatabaseQuery(object):
or not can_be_null
or (f.value and f.operator.lower() in ('=', 'like'))
or 'ifnull(' in column_name.lower()):
if f.operator.lower() == 'like' and frappe.conf.get('db_type') == 'postgres':
f.operator = 'ilike'
condition = '{column_name} {operator} {value}'.format(
column_name=column_name, operator=f.operator,
value=value)

View file

@ -80,8 +80,8 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa
if not (for_reload or frappe.flags.in_migrate or frappe.flags.in_install or frappe.flags.in_test):
try:
delete_controllers(name, doc.module)
except (FileNotFoundError, OSError):
# in case a doctype doesnt have any controller code
except (FileNotFoundError, OSError, KeyError):
# in case a doctype doesnt have any controller code nor any app and module
pass
else:
@ -332,8 +332,8 @@ def clear_references(doctype, reference_doctype, reference_name,
(reference_doctype, reference_name))
def clear_timeline_references(link_doctype, link_name):
frappe.db.sql("""delete from `tabCommunication Link`
where `tabCommunication Link`.link_doctype='{0}' and `tabCommunication Link`.link_name='{1}'""".format(link_doctype, link_name)) # nosec
frappe.db.sql("""DELETE FROM `tabCommunication Link`
WHERE `tabCommunication Link`.link_doctype=%s AND `tabCommunication Link`.link_name=%s""", (link_doctype, link_name))
def insert_feed(doc):
from frappe.utils import get_fullname

View file

@ -150,8 +150,8 @@ class Document(BaseDocument):
super(Document, self).__init__(d)
if self.name=="DocType" and self.doctype=="DocType":
from frappe.model.meta import doctype_table_fields
table_fields = doctype_table_fields
from frappe.model.meta import DOCTYPE_TABLE_FIELDS
table_fields = DOCTYPE_TABLE_FIELDS
else:
table_fields = self.meta.get_table_fields()

View file

@ -151,7 +151,7 @@ class Meta(Document):
if self.name!="DocType":
self._table_fields = self.get('fields', {"fieldtype": ['in', table_fields]})
else:
self._table_fields = doctype_table_fields
self._table_fields = DOCTYPE_TABLE_FIELDS
return self._table_fields
@ -165,7 +165,7 @@ class Meta(Document):
def get_valid_columns(self):
if not hasattr(self, "_valid_columns"):
if self.name in ("DocType", "DocField", "DocPerm", "Property Setter"):
if self.name in ("DocType", "DocField", "DocPerm", 'DocType Action', 'DocType Link', "Property Setter"):
self._valid_columns = get_table_columns(self.name)
else:
self._valid_columns = self.default_fields + \
@ -174,7 +174,12 @@ class Meta(Document):
return self._valid_columns
def get_table_field_doctype(self, fieldname):
return { "fields": "DocField", "permissions": "DocPerm"}.get(fieldname)
return {
"fields": "DocField",
"permissions": "DocPerm",
"actions": "DocType Action",
'links': 'DocType Link'
}.get(fieldname)
def get_field(self, fieldname):
'''Return docfield from meta'''
@ -419,11 +424,44 @@ class Meta(Document):
except ImportError:
pass
self.add_doctype_links(data)
for hook in frappe.get_hooks("override_doctype_dashboards", {}).get(self.name, []):
data = frappe.get_attr(hook)(data=data)
return data
def add_doctype_links(self, data):
'''add `links` child table in standard link dashboard format'''
if hasattr(self, 'links') and self.links:
if not data.transactions:
# init groups
data.transactions = []
data.non_standard_fieldnames = {}
for link in self.links:
link.added = False
for group in data.transactions:
# group found
if group.label == link.label:
if not link.link_doctype in group.items:
group.items.append(link.link_doctype)
link.added = True
if not link.added:
# group not found, make a new group
data.transactions.append(dict(
label = link.group,
items = [link.link_doctype]
))
if link.link_fieldname != data.fieldname:
if data.fieldname:
data.non_standard_fieldnames[link.link_doctype] = link.link_fieldname
else:
data.fieldname = link.link_fieldname
def get_row_template(self):
return self.get_web_template(suffix='_row')
@ -441,9 +479,11 @@ class Meta(Document):
def is_nested_set(self):
return self.has_field('lft') and self.has_field('rgt')
doctype_table_fields = [
DOCTYPE_TABLE_FIELDS = [
frappe._dict({"fieldname": "fields", "options": "DocField"}),
frappe._dict({"fieldname": "permissions", "options": "DocPerm"})
frappe._dict({"fieldname": "permissions", "options": "DocPerm"}),
frappe._dict({"fieldname": "actions", "options": "DocType Action"}),
frappe._dict({"fieldname": "links", "options": "DocType Link"}),
]
#######

View file

@ -29,6 +29,8 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe
# these need to go first at time of install
for d in (("core", "docfield"),
("core", "docperm"),
("core", "doctype_action"),
("core", "doctype_link"),
("core", "role"),
("core", "has_role"),
("core", "doctype"),
@ -41,7 +43,10 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe
("data_migration", "data_migration_mapping_detail"),
("data_migration", "data_migration_mapping"),
("data_migration", "data_migration_plan_mapping"),
("data_migration", "data_migration_plan")):
("data_migration", "data_migration_plan"),
("desk", "onboarding_slide_field"),
("desk", "onboarding_slide_help_link"),
("desk", "onboarding_slide")):
files.append(os.path.join(frappe.get_app_path("frappe"), d[0],
"doctype", d[1], d[1] + ".json"))
@ -70,7 +75,7 @@ def get_doc_files(files, start_path, force=0, sync_everything = False, verbose=F
# load in sequence - warning for devs
document_types = ['doctype', 'page', 'report', 'dashboard_chart_source', 'print_format',
'website_theme', 'web_form', 'notification', 'print_style',
'data_migration_mapping', 'data_migration_plan']
'data_migration_mapping', 'data_migration_plan', 'onboarding_slide']
for doctype in document_types:
doctype_path = os.path.join(start_path, doctype)
if os.path.exists(doctype_path):

View file

@ -6,9 +6,11 @@ frappe.patches.v8_0.update_global_search_table
frappe.patches.v7_0.update_auth
frappe.patches.v8_0.drop_in_dialog #2017-09-22
frappe.patches.v7_2.remove_in_filter
frappe.patches.v11_0.drop_column_apply_user_permissions
execute:frappe.reload_doc('core', 'doctype', 'doctype_action', force=True) #2019-09-23
execute:frappe.reload_doc('core', 'doctype', 'doctype_link', force=True) #2019-09-23
execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) #2017-09-22
execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2018-02-20
frappe.patches.v11_0.drop_column_apply_user_permissions
execute:frappe.reload_doc('core', 'doctype', 'custom_docperm')
execute:frappe.reload_doc('core', 'doctype', 'docperm') #2018-05-29
execute:frappe.reload_doc('core', 'doctype', 'comment')
@ -257,3 +259,7 @@ frappe.patches.v12_0.setup_tags
frappe.patches.v12_0.update_auto_repeat_status_and_not_submittable
frappe.patches.v12_0.copy_to_parent_for_tags
frappe.patches.v12_0.create_notification_settings_for_user
frappe.patches.v11_0.make_all_prepared_report_attachments_private #2019-11-26
execute:frappe.delete_doc("Test Runner")
frappe.patches.v12_0.setup_email_linking
frappe.patches.v12_0.fix_home_settings_for_all_users

View file

@ -0,0 +1,26 @@
from __future__ import unicode_literals
import frappe
def execute():
if frappe.db.count("File", filters={"attached_to_doctype": "Prepared Report", "is_private": 0}) > 10000:
frappe.db.auto_commit_on_many_writes = True
files = frappe.get_all("File", fields=["name", "attached_to_name"], filters={"attached_to_doctype": "Prepared Report", "is_private": 0})
for file_dict in files:
# For some reason Prepared Report doc might not exist, check if it exists first
if frappe.db.exists("Prepared Report", file_dict.attached_to_name):
try:
file_doc = frappe.get_doc("File", file_dict.name)
file_doc.is_private = 1
file_doc.save()
except Exception:
# File might not exist on the file system in that case delete both Prepared Report and File doc
frappe.delete_doc("Prepared Report", file_dict.attached_to_name)
else:
# If Prepared Report doc doesn't exist then the file doc is useless. Delete it.
frappe.delete_doc("File", file_dict.name)
if frappe.db.auto_commit_on_many_writes:
frappe.db.auto_commit_on_many_writes = False

View file

@ -0,0 +1,41 @@
import frappe
from frappe.config import get_modules_from_all_apps_for_user
import json
def execute():
users = frappe.get_all('User', fields=['name', 'home_settings'])
for user in users:
if not user.home_settings:
continue
home_settings = json.loads(user.home_settings)
modules_by_category = home_settings.get('modules_by_category')
if not modules_by_category:
continue
visible_modules = []
category_to_check = []
for category, modules in modules_by_category.items():
visible_modules += modules
category_to_check.append(category)
all_modules = get_modules_from_all_apps_for_user(user.name)
all_modules = set([m.get('name') or m.get('module_name') or m.get('label') \
for m in all_modules if m.get('category') in category_to_check])
hidden_modules = home_settings['hidden_modules']
modules_in_home_settings = set(visible_modules + hidden_modules)
all_modules = all_modules.union(modules_in_home_settings)
missing_modules = all_modules - modules_in_home_settings
if missing_modules:
home_settings['hidden_modules'] += missing_modules
home_settings = json.dumps(home_settings)
frappe.set_value('User', user.name, 'home_settings', home_settings)
frappe.cache().delete_key('home_settings')

View file

@ -0,0 +1,6 @@
from __future__ import unicode_literals
from frappe.desk.page.setup_wizard.install_fixtures import setup_email_linking
def execute():
setup_email_linking()

View file

@ -4,8 +4,7 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import is_image
from frappe import _
from frappe.model.document import Document
class LetterHead(Document):
@ -43,3 +42,16 @@ class LetterHead(Document):
# update control panel - so it loads new letter directly
frappe.db.set_default("default_letter_head_content", self.content)
def create_onboarding_docs(self, args):
letterhead = args.get('letterhead')
if letterhead:
try:
frappe.get_doc({
'doctype': self.doctype,
'image': letterhead,
'letter_head_name': _('Standard'),
'is_default': 1
}).insert()
except frappe.NameError:
pass

File diff suppressed because it is too large Load diff

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