Merge branch 'develop' of https://github.com/frappe/frappe into pref_global_search
This commit is contained in:
commit
86e1d0367b
134 changed files with 2227 additions and 3512 deletions
2
.codacy.yml
Normal file
2
.codacy.yml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
exclude_paths:
|
||||
- '**.sql'
|
||||
2
.pylintrc
Normal file
2
.pylintrc
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
disable=access-member-before-definition
|
||||
disable=no-member
|
||||
40
.travis.yml
40
.travis.yml
|
|
@ -1,6 +1,5 @@
|
|||
language: python
|
||||
dist: trusty
|
||||
sudo: required
|
||||
|
||||
addons:
|
||||
hosts:
|
||||
|
|
@ -39,6 +38,16 @@ matrix:
|
|||
env: DB=mariadb TYPE=server
|
||||
script: bench --site test_site run-tests --coverage
|
||||
|
||||
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
|
||||
|
|
@ -52,23 +61,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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'];
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -509,17 +508,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):
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
57
frappe/core/doctype/doctype_action/doctype_action.json
Normal file
57
frappe/core/doctype/doctype_action/doctype_action.json
Normal 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
|
||||
}
|
||||
10
frappe/core/doctype/doctype_action/doctype_action.py
Normal file
10
frappe/core/doctype/doctype_action/doctype_action.py
Normal 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
|
||||
46
frappe/core/doctype/doctype_link/doctype_link.json
Normal file
46
frappe/core/doctype/doctype_link/doctype_link.json
Normal 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
|
||||
}
|
||||
10
frappe/core/doctype/doctype_link/doctype_link.py
Normal file
10
frappe/core/doctype/doctype_link/doctype_link.py
Normal 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
|
||||
0
frappe/core/doctype/scheduled_job_log/__init__.py
Normal file
0
frappe/core/doctype/scheduled_job_log/__init__.py
Normal 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) {
|
||||
|
||||
// }
|
||||
});
|
||||
64
frappe/core/doctype/scheduled_job_log/scheduled_job_log.json
Normal file
64
frappe/core/doctype/scheduled_job_log/scheduled_job_log.json
Normal 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
|
||||
}
|
||||
10
frappe/core/doctype/scheduled_job_log/scheduled_job_log.py
Normal file
10
frappe/core/doctype/scheduled_job_log/scheduled_job_log.py
Normal 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
|
||||
|
|
@ -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
|
||||
0
frappe/core/doctype/scheduled_job_type/__init__.py
Normal file
0
frappe/core/doctype/scheduled_job_type/__init__.py
Normal 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) {
|
||||
|
||||
// }
|
||||
});
|
||||
|
|
@ -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\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-09-27 12:19:23.259989",
|
||||
"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
|
||||
}
|
||||
159
frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
Normal file
159
frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
# -*- 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():
|
||||
self.update_last_execution()
|
||||
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'''
|
||||
# save last execution in expected execution time as per cron
|
||||
self.last_execution = self.get_next_execution()
|
||||
|
||||
# if the next scheduled event is before NOW, then its due!
|
||||
return self.last_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)).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())
|
||||
frappe.db.commit()
|
||||
|
||||
def update_last_execution(self):
|
||||
self.db_set('last_execution', self.last_execution, update_modified=False)
|
||||
frappe.db.commit()
|
||||
|
||||
def get_queue_name(self):
|
||||
return 'long' if ('Long' in self.frequency) else 'default'
|
||||
|
||||
@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)
|
||||
|
|
@ -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')))
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
]);
|
||||
|
||||
});
|
||||
|
|
@ -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'));
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
--
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
--
|
||||
|
|
|
|||
0
frappe/desk/doctype/setup_wizard_help_link/__init__.py
Normal file
0
frappe/desk/doctype/setup_wizard_help_link/__init__.py
Normal 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": "Setup Wizard Help Link",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -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 SetupWizardHelpLink(Document):
|
||||
pass
|
||||
0
frappe/desk/doctype/setup_wizard_slide/__init__.py
Normal file
0
frappe/desk/doctype/setup_wizard_slide/__init__.py
Normal file
45
frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.js
Normal file
45
frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright (c) 2019, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Setup Wizard Slide', {
|
||||
refresh: function(frm) {
|
||||
frm.toggle_reqd('ref_doctype', frm.doc.slide_type!='Information');
|
||||
frm.toggle_reqd('slide_module', frm.doc.slide_type=='Information');
|
||||
},
|
||||
|
||||
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, 'Setup Wizard Slide', 'slide_fields');
|
||||
row.label = data.label;
|
||||
row.fieldtype = data.fieldtype;
|
||||
row.fieldname = data.fieldname;
|
||||
row.options = data.options;
|
||||
});
|
||||
refresh_field('slide_fields');
|
||||
}
|
||||
}
|
||||
});
|
||||
182
frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.json
Normal file
182
frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.json
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
{
|
||||
"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",
|
||||
"submit_method",
|
||||
"column_break_6",
|
||||
"max_count",
|
||||
"add_more_button",
|
||||
"section_break_18",
|
||||
"ref_doctype",
|
||||
"slide_fields",
|
||||
"section_break_10",
|
||||
"domains",
|
||||
"column_break_12",
|
||||
"help_links"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "slide_title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Slide Title",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.slide_type!='Information'",
|
||||
"description": "By default the code inside `create_onboarding_docs` method of the `Reference Document Type` is executed. If your method is not on the doctype level, place this method in {app_name}.utilities.onboarding_utils.{method_name} and specify the method name here",
|
||||
"fieldname": "submit_method",
|
||||
"fieldtype": "Data",
|
||||
"label": "Submit Method"
|
||||
},
|
||||
{
|
||||
"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!='Information'",
|
||||
"fieldname": "add_more_button",
|
||||
"fieldtype": "Check",
|
||||
"label": "Add More Button"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.slide_type!='Information'",
|
||||
"fieldname": "slide_fields",
|
||||
"fieldtype": "Table",
|
||||
"label": "Slide Fields",
|
||||
"options": "Setup Wizard 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": "Setup Wizard Help Link"
|
||||
},
|
||||
{
|
||||
"fieldname": "action_section_break",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Action Settings"
|
||||
},
|
||||
{
|
||||
"description": "If slide type is Action there should be a submit method bound to be executed after the slide is completed.",
|
||||
"fieldname": "slide_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Slide Type",
|
||||
"options": "Information\nCreate\nSettings",
|
||||
"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!='Information'",
|
||||
"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'",
|
||||
"fieldname": "slide_module",
|
||||
"fieldtype": "Link",
|
||||
"label": "Module",
|
||||
"options": "Module Def"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_18",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Fields"
|
||||
}
|
||||
],
|
||||
"modified": "2019-11-26 17:33:34.553367",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Setup Wizard 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
|
||||
}
|
||||
97
frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.py
Normal file
97
frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# -*- 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.model.document import Document
|
||||
from frappe.modules.export_file import export_to_files
|
||||
|
||||
class SetupWizardSlide(Document):
|
||||
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=[['Setup Wizard Slide', self.name]], record_module=module)
|
||||
|
||||
def get_onboarding_slides_as_list():
|
||||
slides = []
|
||||
slide_docs = frappe.get_all('Setup Wizard Slide',
|
||||
filters={'slide_order': ('!=', 0)},
|
||||
order_by='slide_order')
|
||||
for entry in slide_docs:
|
||||
# using get_doc because child table fields are not fetched in get_all
|
||||
slide_doc = frappe.get_doc('Setup Wizard Slide', entry.name)
|
||||
if frappe.scrub(slide_doc.app) in frappe.get_installed_apps():
|
||||
slides.append(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,
|
||||
submit_method=slide_doc.submit_method,
|
||||
image_src=get_slide_image(slide_doc),
|
||||
ref_doctype=slide_doc.ref_doctype,
|
||||
app=slide_doc.app
|
||||
))
|
||||
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
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_onboarding_docs(values, doctype=None, submit_method=None, app=None, slide_type=None):
|
||||
data = json.loads(values)
|
||||
if submit_method:
|
||||
try:
|
||||
method = frappe.scrub(app) + '.utilities.onboarding_utils.' + submit_method
|
||||
frappe.call(method, data)
|
||||
except AttributeError:
|
||||
create_generic_onboarding_doc(data, doctype, slide_type)
|
||||
else:
|
||||
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()
|
||||
|
|
@ -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 TestSetupWizardSlide(unittest.TestCase):
|
||||
pass
|
||||
0
frappe/desk/doctype/setup_wizard_slide_field/__init__.py
Normal file
0
frappe/desk/doctype/setup_wizard_slide_field/__init__.py
Normal 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": "options",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Options"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"modified": "2019-11-25 16:50:53.994656",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Setup Wizard Slide Field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
|
|
@ -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 SetupWizardSlideField(Document):
|
||||
pass
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ 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,
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -513,6 +513,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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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"}),
|
||||
]
|
||||
|
||||
#######
|
||||
|
|
|
|||
|
|
@ -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", "setup_wizard_slide_field"),
|
||||
("desk", "setup_wizard_help_link"),
|
||||
("desk", "setup_wizard_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', 'setup_wizard_slide']
|
||||
for doctype in document_types:
|
||||
doctype_path = os.path.join(start_path, doctype)
|
||||
if os.path.exists(doctype_path):
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ frappe.patches.v7_2.remove_in_filter
|
|||
frappe.patches.v11_0.drop_column_apply_user_permissions
|
||||
execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) #2017-09-22
|
||||
execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2018-02-20
|
||||
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', 'custom_docperm')
|
||||
execute:frappe.reload_doc('core', 'doctype', 'docperm') #2018-05-29
|
||||
execute:frappe.reload_doc('core', 'doctype', 'comment')
|
||||
|
|
@ -258,3 +260,4 @@ 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")
|
||||
|
|
@ -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
|
|
@ -695,7 +695,8 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
{
|
||||
fieldname: "content",
|
||||
fieldtype: "Code",
|
||||
label: label
|
||||
label: label,
|
||||
options: "HTML"
|
||||
},
|
||||
{
|
||||
fieldname: "help",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"add_more_button": 0,
|
||||
"app": "ERPNext",
|
||||
"creation": "2019-11-22 13:25:42.892593",
|
||||
"docstatus": 0,
|
||||
"doctype": "Setup Wizard Slide",
|
||||
"domains": [],
|
||||
"help_links": [
|
||||
{
|
||||
"label": "Know more about printing and branding through letterhead",
|
||||
"video_id": "cKZHcx1znMc"
|
||||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"image_src": "/assets/erpnext/images/illustrations/letterhead.png",
|
||||
"max_count": 0,
|
||||
"modified": "2019-11-27 11:39:56.213373",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Company Letter Head",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Letter Head",
|
||||
"slide_desc": "Attach Letterhead: (Keep it web friendly as 1024px by 128px)",
|
||||
"slide_fields": [
|
||||
{
|
||||
"align": "center",
|
||||
"fieldname": "letterhead",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Attach Letterhead",
|
||||
"options": "image",
|
||||
"reqd": 0
|
||||
}
|
||||
],
|
||||
"slide_order": 20,
|
||||
"slide_title": "Company Letter Head",
|
||||
"slide_type": "Create",
|
||||
"submit_method": ""
|
||||
}
|
||||
|
|
@ -148,6 +148,7 @@
|
|||
"public/js/frappe/ui/page.html",
|
||||
"public/js/frappe/ui/page.js",
|
||||
"public/js/frappe/ui/slides.js",
|
||||
"public/js/frappe/ui/onboarding_dialog.js",
|
||||
"public/js/frappe/ui/find.js",
|
||||
"public/js/frappe/ui/iconbar.js",
|
||||
"public/js/frappe/form/layout.js",
|
||||
|
|
|
|||
|
|
@ -1158,16 +1158,6 @@ input[type="checkbox"]:focus {
|
|||
color: #fff;
|
||||
border-color: #b1bdca;
|
||||
}
|
||||
.user-progress-dialog .slides-progress {
|
||||
.onboarding-dialog .slides-progress {
|
||||
margin-top: 15px;
|
||||
}
|
||||
.user-progress-dialog .done-state .check-container {
|
||||
font-size: 64px;
|
||||
margin: 40px;
|
||||
}
|
||||
.user-progress-dialog .done-state .title {
|
||||
font-weight: normal;
|
||||
}
|
||||
.user-progress-dialog .done-state .help-links a {
|
||||
margin: 0px 10px;
|
||||
}
|
||||
|
|
|
|||
BIN
frappe/public/images/ui-states/empty.png
Normal file
BIN
frappe/public/images/ui-states/empty.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
|
|
@ -88,6 +88,9 @@ frappe.Application = Class.extend({
|
|||
}
|
||||
|
||||
this.show_update_available();
|
||||
if (frappe.boot.is_first_startup) {
|
||||
this.setup_onboarding_wizard();
|
||||
}
|
||||
|
||||
if(frappe.ui.startup_setup_dialog && !frappe.boot.setup_complete) {
|
||||
frappe.ui.startup_setup_dialog.pre_show();
|
||||
|
|
@ -482,6 +485,28 @@ frappe.Application = Class.extend({
|
|||
});
|
||||
},
|
||||
|
||||
setup_onboarding_wizard: () => {
|
||||
var me = this;
|
||||
frappe.call('frappe.desk.doctype.setup_wizard_slide.setup_wizard_slide.get_onboarding_slides').then(res => {
|
||||
if (res.message) {
|
||||
let slides = res.message;
|
||||
if (slides.length) {
|
||||
frappe.require("assets/frappe/js/frappe/ui/onboarding_dialog.js", () => {
|
||||
me.progress_dialog = new frappe.setup.OnboardingDialog({
|
||||
slides: slides
|
||||
});
|
||||
me.progress_dialog.show();
|
||||
frappe.call({
|
||||
method: "frappe.desk.page.setup_wizard.setup_wizard.reset_is_first_startup",
|
||||
args: {},
|
||||
callback: () => {}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setup_analytics: function() {
|
||||
if(window.mixpanel) {
|
||||
window.mixpanel.identify(frappe.session.user);
|
||||
|
|
|
|||
|
|
@ -1,21 +1,6 @@
|
|||
frappe.ui.form.ControlCurrency = frappe.ui.form.ControlFloat.extend({
|
||||
eval_expression: function(value) {
|
||||
if (typeof value ==='string' && value.match(/^[0-9+-/* ]+$/)) {
|
||||
// Removes seperator
|
||||
value = strip_number_groups(value, this.get_number_format());
|
||||
|
||||
try {
|
||||
return eval(value);
|
||||
} catch (e) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
// If not string
|
||||
return value;
|
||||
},
|
||||
|
||||
format_for_input: function(value) {
|
||||
var formatted_value = format_number(parseFloat(value), this.get_number_format(), this.get_precision());
|
||||
var formatted_value = format_number(value, this.get_number_format(), this.get_precision());
|
||||
return isNaN(parseFloat(value)) ? "" : formatted_value;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
frappe.ui.form.ControlFloat = frappe.ui.form.ControlInt.extend({
|
||||
parse: function(value) {
|
||||
value = this.eval_expression(value);
|
||||
return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision(),
|
||||
return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision(),
|
||||
// While parsing currency, get_number_format passes currency's number_format
|
||||
// In case of parsing float, it passes global number_format
|
||||
this.get_number_format());
|
||||
|
|
@ -12,7 +12,7 @@ frappe.ui.form.ControlFloat = frappe.ui.form.ControlInt.extend({
|
|||
if (this.df.fieldtype==="Float" && this.df.options && this.df.options.trim()) {
|
||||
number_format = this.get_number_format();
|
||||
}
|
||||
var formatted_value = format_number(parseFloat(value), number_format, this.get_precision());
|
||||
var formatted_value = format_number(value, number_format, this.get_precision());
|
||||
return isNaN(parseFloat(value)) ? "" : formatted_value;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -20,20 +20,18 @@ frappe.ui.form.ControlInt = frappe.ui.form.ControlData.extend({
|
|||
});
|
||||
},
|
||||
eval_expression: function(value) {
|
||||
if (typeof value==='string'
|
||||
&& value.match(/^[0-9+-/* ]+$/)
|
||||
// strings with commas are evaluated incorrectly
|
||||
// for e.g 47,186.00 -> 186
|
||||
&& !value.includes(',')) {
|
||||
try {
|
||||
return eval(value);
|
||||
} catch (e) {
|
||||
// bad expression
|
||||
return value;
|
||||
if (typeof value === 'string') {
|
||||
if (value.match(/^[0-9\+\-\/\* ]+$/)) {
|
||||
// If it is a string containing operators
|
||||
try {
|
||||
return eval(value);
|
||||
} catch (e) {
|
||||
// bad expression
|
||||
return value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
parse: function(value) {
|
||||
return cint(this.eval_expression(value), null);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({
|
|||
|
||||
this.$list_wrapper = $(template);
|
||||
this.$input = $('<input>');
|
||||
this.input = this.$input.get(0);
|
||||
this.$list_wrapper.prependTo(this.input_area);
|
||||
this.$filter_input = this.$list_wrapper.find('input');
|
||||
this.$list_wrapper.on('click', '.dropdown-menu', e => {
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
$("body").attr("data-sidebar", 1);
|
||||
}
|
||||
this.setup_file_drop();
|
||||
this.setup_doctype_actions();
|
||||
|
||||
this.setup_done = true;
|
||||
}
|
||||
|
|
@ -319,6 +320,29 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
}
|
||||
}
|
||||
|
||||
// sets up the refresh event for custom buttons
|
||||
// added via configuration
|
||||
setup_doctype_actions() {
|
||||
if (this.meta.actions) {
|
||||
for (let action of this.meta.actions) {
|
||||
frappe.ui.form.on(this.doctype, 'refresh', () => {
|
||||
if (!this.is_new()) {
|
||||
this.add_custom_button(action.label, () => {
|
||||
if (action.action_type==='Server Action') {
|
||||
frappe.xcall(action.action, {doc: this.doc}).then(() => {
|
||||
frappe.msgprint({
|
||||
message: __('{} Complete', [action.label]),
|
||||
alert: true
|
||||
});
|
||||
});
|
||||
}
|
||||
}, action.group);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch_doc(docname) {
|
||||
// record switch
|
||||
if(this.docname != docname && (!this.meta.in_dialog || this.in_form) && !this.meta.istable) {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,13 @@ frappe.ui.form.PrintPreview = Class.extend({
|
|||
me.multilingual_preview();
|
||||
});
|
||||
|
||||
this.wrapper
|
||||
.find(".print-preview-refresh")
|
||||
.on("click", function () {
|
||||
me.set_default_print_language();
|
||||
me.multilingual_preview();
|
||||
});
|
||||
|
||||
//On selection of language get code and pass it to preview method
|
||||
this.language_sel = this.wrapper
|
||||
.find(".languages")
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
<div class="form-print-wrapper">
|
||||
<div class="print-toolbar row">
|
||||
<div class="col-xs-2">
|
||||
<select class="print-preview-select input-sm form-control"></select></div>
|
||||
<div class="col-xs-3">
|
||||
<div class="input-group">
|
||||
<select class="print-preview-select input-sm form-control"></select>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default btn-sm border print-preview-refresh" type="button">{%= __("Refresh") %}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-2">
|
||||
<select class="languages input-sm form-control"
|
||||
placeholder="{{ __("Language") }}"></select></div>
|
||||
|
|
@ -12,21 +18,30 @@
|
|||
{%= __("Letter Head") %}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<div class="col-xs-5 text-right">
|
||||
<!-- <a class="close btn-print-close" style="margin-top: 2px; margin-left: 10px;">×</a> -->
|
||||
<div class="btn-group">
|
||||
<a class="btn-print-print btn-sm btn btn-default">
|
||||
<strong>{%= __("Print") %}</strong></a>
|
||||
<a class="btn-sm btn btn-default" href="#Form/Print Settings">
|
||||
{%= __("Settings...") %}</a>
|
||||
<a class="btn-printer-setting btn-sm btn btn-default" style="display: none;">
|
||||
{%= __("Printer Settings...") %}</a>
|
||||
<a class="btn-print-edit btn-sm btn btn-default">
|
||||
{%= __("Customize...") %}</a>
|
||||
<a class="dropdown-toggle btn btn-default btn-sm"
|
||||
style="border-top-right-radius:0px; border-bottom-right-radius: 0px;"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span>{%= __("Settings") %}</span>
|
||||
<span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu print-format-dropdown" style="max-height: 300px;
|
||||
overflow-y: auto; left: auto;">
|
||||
<li><a href="#Form/Print Settings">
|
||||
{%= __("Print Settings...") %}</a></li>
|
||||
<li><a class="btn-printer-setting" style="display: none;">
|
||||
{%= __("Raw Printing Settings...") %}</a></li>
|
||||
<li><a class="btn-print-edit">
|
||||
{%= __("Customize...") %}</a></li>
|
||||
</ul>
|
||||
<a class="btn-print-preview btn-sm btn btn-default">
|
||||
{%= __("Full Page") %}</a>
|
||||
<a class="btn-download-pdf btn-sm btn btn-default">
|
||||
{%= __("PDF") %}</a>
|
||||
<a class="btn-print-print btn-sm btn btn-default">
|
||||
<strong>{%= __("Print") %}</strong></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -301,15 +301,32 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
this.columns = this.columns.slice(0, column_count);
|
||||
}
|
||||
|
||||
get_documentation_link() {
|
||||
if (this.meta.documentation) {
|
||||
return `<a href="${this.meta.documentation}" target="blank" class="meta-description small text-muted">Need Help?</a>`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
get_no_result_message() {
|
||||
let help_link = this.get_documentation_link();
|
||||
let filters = this.filter_area.get();
|
||||
let no_result_message = filters.length ? __('No {0} found', [__(this.doctype)]) : __('You haven\'t created a {0} yet', [__(this.doctype)]);
|
||||
let new_button_label = filters.length ? __('Create a new {0}', [__(this.doctype)]) : __('Create your first {0}', [__(this.doctype)]);
|
||||
let empty_state_image = this.settings.empty_state_image || '/assets/frappe/images/ui-states/empty.png';
|
||||
|
||||
const new_button = this.can_create ?
|
||||
`<p><button class="btn btn-primary btn-sm btn-new-doc">
|
||||
${__('Create a new {0}', [__(this.doctype)])}
|
||||
${new_button_label}
|
||||
</button></p>` : '';
|
||||
|
||||
return `<div class="msg-box no-border">
|
||||
<p>${__('No {0} found', [__(this.doctype)])}</p>
|
||||
<div>
|
||||
<img src="${empty_state_image}" alt="Generic Empty State" class="null-state">
|
||||
</div>
|
||||
<p>${no_result_message}</p>
|
||||
${new_button}
|
||||
${help_link}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
|
@ -391,6 +408,12 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
}
|
||||
|
||||
after_render() {
|
||||
this.$no_result.html(`
|
||||
<div class="no-result text-muted flex justify-center align-center">
|
||||
${this.get_no_result_message()}
|
||||
</div>
|
||||
`);
|
||||
this.setup_new_doc_event();
|
||||
this.list_sidebar.reload_stats();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,13 +47,6 @@
|
|||
@click="toggle_log(user.name)"
|
||||
>{{ user[key] }}</span>
|
||||
</div>
|
||||
<energy-point-history
|
||||
v-show="show_log_for===user.name"
|
||||
class="energy-point-history"
|
||||
:user="user.name"
|
||||
:from_date="from_date"
|
||||
:key="user.name + user.energy_points"
|
||||
></energy-point-history>
|
||||
</li>
|
||||
<li class="user-card text-muted" v-if="!filtered_users.length">{{__('No user found')}}</li>
|
||||
</ul>
|
||||
|
|
|
|||
139
frappe/public/js/frappe/ui/onboarding_dialog.js
Normal file
139
frappe/public/js/frappe/ui/onboarding_dialog.js
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.provide("frappe.setup");
|
||||
frappe.provide("frappe.ui");
|
||||
|
||||
frappe.setup.OnboardingSlide = class OnboardingSlide extends frappe.ui.Slide {
|
||||
constructor(slide = null) {
|
||||
super(slide);
|
||||
}
|
||||
|
||||
make() {
|
||||
super.make();
|
||||
this.$next_btn = this.slides_footer.find('.next-btn');
|
||||
this.$complete_btn = this.slides_footer.find('.complete-btn');
|
||||
this.$action_button = this.slides_footer.find('.next-btn');
|
||||
if (this.help_links) {
|
||||
this.$help_links = $(`<div class="text-center">
|
||||
<div class="help-links"></div>
|
||||
</div>`).appendTo(this.$body);
|
||||
this.setup_help_links();
|
||||
}
|
||||
}
|
||||
|
||||
before_show() {
|
||||
(this.id === 0) ?
|
||||
this.$next_btn.text(__('Start')) : this.$next_btn.text(__('Next'));
|
||||
//last slide
|
||||
if (this.id === this.parent[0].children.length-1) {
|
||||
this.$complete_btn.removeClass('hide').addClass('action primary');
|
||||
this.$next_btn.removeClass('action primary');
|
||||
this.$action_button = this.$complete_btn;
|
||||
}
|
||||
this.setup_action_button();
|
||||
}
|
||||
|
||||
primary_action() {
|
||||
let me = this;
|
||||
if (this.set_values()) {
|
||||
this.$action_button.addClass('disabled');
|
||||
if (me.add_more) me.values.max_count = me.max_count;
|
||||
frappe.call({
|
||||
method: 'frappe.desk.doctype.setup_wizard_slide.setup_wizard_slide.create_onboarding_docs',
|
||||
args: {
|
||||
values: me.values,
|
||||
doctype: me.ref_doctype,
|
||||
submit_method: me.submit_method,
|
||||
app: me.app,
|
||||
slide_type: me.slide_type
|
||||
},
|
||||
callback: function() {
|
||||
if (me.id === me.parent[0].children.length-1) {
|
||||
$('.onboarding-dialog').modal('toggle');
|
||||
frappe.msgprint({
|
||||
message: __('You are all set up!'),
|
||||
indicator: 'green',
|
||||
title: __('Success')
|
||||
});
|
||||
}
|
||||
},
|
||||
onerror: function() {
|
||||
me.slides_footer.find('.primary').removeClass('disabled');
|
||||
},
|
||||
freeze: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
unbind_primary_action() {
|
||||
// unbind only action method as next button is same as create button in this setup wizard
|
||||
this.$action_button.off('click.primary_action');
|
||||
}
|
||||
|
||||
setup_help_links() {
|
||||
this.help_links.map(link => {
|
||||
let $link = $(
|
||||
`<a target="_blank" class="small text-muted">${link.label}</a>
|
||||
<span class="small">
|
||||
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
|
||||
</span>`
|
||||
);
|
||||
if (link.video_id) {
|
||||
$link.on('click', () => {
|
||||
frappe.help.show_video(link.video_id, link.label);
|
||||
});
|
||||
}
|
||||
this.$help_links.append($link);
|
||||
});
|
||||
}
|
||||
|
||||
setup_action_button() {
|
||||
if (this.slide_type !== 'Information') {
|
||||
this.$action_button.addClass('primary');
|
||||
} else {
|
||||
this.$action_button.removeClass('primary');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
frappe.setup.OnboardingDialog = class OnboardingDialog {
|
||||
constructor({
|
||||
slides = []
|
||||
}) {
|
||||
this.slides = slides;
|
||||
this.setup();
|
||||
}
|
||||
|
||||
setup() {
|
||||
this.dialog = new frappe.ui.Dialog({
|
||||
static: true,
|
||||
minimizable: false,
|
||||
title: __("Let's Onboard!")
|
||||
});
|
||||
this.$wrapper = $(this.dialog.$wrapper).addClass('onboarding-dialog');
|
||||
this.slide_container = new frappe.ui.Slides({
|
||||
parent: this.dialog.body,
|
||||
slides: this.slides,
|
||||
slide_class: frappe.setup.OnboardingSlide,
|
||||
unidirectional: 1,
|
||||
before_load: ($footer) => {
|
||||
$footer.find('.prev-btn').addClass('hide');
|
||||
$footer.find('.next-btn').removeClass('btn-default').addClass('btn-primary action');
|
||||
$footer.find('.text-right').prepend(
|
||||
$(`<a class="complete-btn btn btn-primary btn-sm hide">
|
||||
${__("Complete")}</a>`));
|
||||
}
|
||||
});
|
||||
|
||||
this.$wrapper.find('.modal-title').prepend(
|
||||
`<span class="onboarding-icon">
|
||||
<i class="fa fa-rocket" aria-hidden="true"></i>
|
||||
</span>`
|
||||
);
|
||||
}
|
||||
|
||||
show() {
|
||||
this.dialog.show();
|
||||
}
|
||||
};
|
||||
|
|
@ -26,7 +26,9 @@ frappe.ui.Slide = class Slide {
|
|||
<div class="form-wrapper">
|
||||
<div class="form"></div>
|
||||
<div class="add-more text-center" style="margin-top: 5px;">
|
||||
<a class="form-more-btn hide btn btn-default btn-xs">${__("Add More")}</a>
|
||||
<a class="form-more-btn hide btn btn-default btn-xs">
|
||||
<span><i class="fa fa-plus small" aria-hidden="true"></i></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>`).appendTo(this.$wrapper);
|
||||
|
|
@ -35,9 +37,9 @@ frappe.ui.Slide = class Slide {
|
|||
this.$form = this.$body.find(".form");
|
||||
this.$primary_btn = this.slides_footer.find('.primary');
|
||||
|
||||
if(this.help) this.$content.append($(`<p class="slide-help">${this.help}</p>`));
|
||||
if(this.image_src) this.$content.append(
|
||||
$(`<img src="${this.image_src}" style="margin: 20px;">`));
|
||||
if(this.help) this.$content.append($(`<p class="slide-help">${this.help}</p>`));
|
||||
|
||||
this.reqd_fields = [];
|
||||
|
||||
|
|
@ -69,6 +71,8 @@ frappe.ui.Slide = class Slide {
|
|||
this.set_reqd_fields();
|
||||
}
|
||||
|
||||
setup_done_state() {}
|
||||
|
||||
// Form methods
|
||||
get_atomic_fields() {
|
||||
var fields = JSON.parse(JSON.stringify(this.fields));
|
||||
|
|
@ -122,6 +126,7 @@ frappe.ui.Slide = class Slide {
|
|||
if(!field.static) {
|
||||
if(field.label) field.label += ' ' + this.count;
|
||||
}
|
||||
field.reqd = 0;
|
||||
return field;
|
||||
}));
|
||||
if(this.count === this.max_count) {
|
||||
|
|
@ -163,7 +168,7 @@ frappe.ui.Slide = class Slide {
|
|||
}
|
||||
|
||||
bind_primary_action() {
|
||||
this.slides_footer.find(".primary").on('click', () => {
|
||||
this.slides_footer.find(".primary").on('click.primary_action', () => {
|
||||
this.primary_action();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -302,10 +302,16 @@ frappe.search.SearchDialog = Class.extend({
|
|||
return link;
|
||||
}
|
||||
|
||||
if(result.image) {
|
||||
$result.append('<a '+ get_link(result) +
|
||||
'><div class="result-image"><img data-name="' + result.label
|
||||
+ '" src="'+ result.image +'" alt="' + result.label + '"></div></a>');
|
||||
if (result.image) {
|
||||
$result.append(`<a ${get_link(result)}>
|
||||
<div class="result-image">
|
||||
<img
|
||||
data-name="${result.label}"
|
||||
src="${result.image}"
|
||||
alt="${result.label}"
|
||||
onerror="this.src='/assets/frappe/images/fallback-thumbnail.jpg'">
|
||||
</div>
|
||||
</a>`);
|
||||
} else if (result.image === null) {
|
||||
$result.append('<a '+ get_link(result) +
|
||||
'><div class="result-image"><div class="flex-text"><span>'
|
||||
|
|
|
|||
|
|
@ -1,209 +0,0 @@
|
|||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.provide("frappe.setup");
|
||||
frappe.provide("frappe.ui");
|
||||
|
||||
frappe.setup.UserProgressSlide = class UserProgressSlide extends frappe.ui.Slide {
|
||||
constructor(slide = null) {
|
||||
super(slide);
|
||||
}
|
||||
|
||||
make() {
|
||||
super.make();
|
||||
}
|
||||
|
||||
setup_done_state() {
|
||||
this.$body.find(".slide-help").hide();
|
||||
this.$body.find(".form-wrapper").hide();
|
||||
this.slides_footer.find('.next-btn').addClass('btn-primary');
|
||||
this.slides_footer.find('.done-btn').hide();
|
||||
this.$primary_btn.hide();
|
||||
this.make_done_state();
|
||||
}
|
||||
|
||||
make_done_state() {
|
||||
this.$done_state = $(`<div class="done-state text-center">
|
||||
<div class="check-container"><i class="check fa fa-fw fa-check-circle text-success"></i></div>
|
||||
<h4 class="title"><a></a></h4>
|
||||
<div class="help-links"></div>
|
||||
</div>`).appendTo(this.$body);
|
||||
|
||||
this.$done_state_title = this.$done_state.find('.title');
|
||||
this.$check = this.$done_state.find('.check');
|
||||
this.$help_links = this.$done_state.find('.help-links');
|
||||
|
||||
if(this.done_state_title) {
|
||||
$("<a>" + this.done_state_title + "</a>").appendTo(this.$done_state_title);
|
||||
this.$done_state_title.on('click', () => {
|
||||
frappe.set_route(this.done_state_title_route);
|
||||
});
|
||||
}
|
||||
|
||||
if(this.help_links) {
|
||||
this.help_links.map(link => {
|
||||
let $link = $(`<a target="_blank" class="small text-muted">${link.label}</a>`);
|
||||
if(link.url) {
|
||||
$link.attr({"href": link.url});
|
||||
} else if(link.video_id) {
|
||||
$link.on('click', () => {
|
||||
frappe.help.show_video(link.video_id, link.label);
|
||||
})
|
||||
}
|
||||
this.$help_links.append($link);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
before_show() {
|
||||
if(this.done) {
|
||||
this.slides_footer.find('.next-btn').addClass('btn-primary');
|
||||
this.slides_footer.find('.done-btn').hide();
|
||||
} else {
|
||||
this.slides_footer.find('.next-btn').removeClass('btn-primary');
|
||||
this.slides_footer.find('.done-btn').show();
|
||||
}
|
||||
if(this.dialog_dismissed) {
|
||||
this.slides_footer.find('.next-btn').removeClass('btn-primary');
|
||||
}
|
||||
}
|
||||
|
||||
primary_action() {
|
||||
var me = this;
|
||||
if(this.set_values()) {
|
||||
this.slides_footer.find('.make-btn').addClass('disabled');
|
||||
frappe.call({
|
||||
method: me.submit_method,
|
||||
args: {args_data: me.values},
|
||||
callback: function() {
|
||||
me.done = 1;
|
||||
me.refresh();
|
||||
},
|
||||
onerror: function() {
|
||||
me.slides_footer.find('.make-btn').removeClass('disabled');
|
||||
},
|
||||
freeze: true
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
frappe.setup.UserProgressDialog = class UserProgressDialog {
|
||||
constructor({
|
||||
slides = []
|
||||
}) {
|
||||
this.slides = slides;
|
||||
this.progress_state_dict = {};
|
||||
this.slides.map(slide => {
|
||||
this.progress_state_dict[slide.action_name] = slide.done || 0;
|
||||
});
|
||||
this.progress_percent = 0;
|
||||
this.setup();
|
||||
}
|
||||
|
||||
setup() {
|
||||
this.dialog = new frappe.ui.Dialog({title: __("Complete Setup")});
|
||||
this.$wrapper = $(this.dialog.$wrapper).addClass('user-progress-dialog');
|
||||
this.slide_container = new frappe.ui.Slides({
|
||||
parent: this.dialog.body,
|
||||
slides: this.slides,
|
||||
slide_class: frappe.setup.UserProgressSlide,
|
||||
done_state: 1,
|
||||
before_load: ($footer) => {
|
||||
$footer.find('.text-right')
|
||||
.prepend($(`<a class="done-btn btn btn-default btn-sm">
|
||||
${__("Mark as Done")}</a>`))
|
||||
.append($(`<a class="make-btn btn btn-primary btn-sm primary action">
|
||||
${__('Create')}</a>`));
|
||||
},
|
||||
on_update: (completed, total) => {
|
||||
let percent = completed * 100 / total;
|
||||
$('.user-progress .progress-bar').css({'width': percent + '%'});
|
||||
if(percent === 100) {
|
||||
this.dismiss_progress();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.$wrapper.find('.done-btn').on('click', () => {
|
||||
this.mark_as_done();
|
||||
});
|
||||
|
||||
this.get_and_update_progress_state();
|
||||
this.check_for_updates();
|
||||
}
|
||||
|
||||
mark_as_done() {
|
||||
let me = this;
|
||||
let current_slide = this.slide_container.current_slide;
|
||||
frappe.call({
|
||||
method: current_slide.mark_as_done_method,
|
||||
args: {action_name: current_slide.action_name},
|
||||
callback: function() {
|
||||
current_slide.done = 1;
|
||||
current_slide.refresh();
|
||||
},
|
||||
freeze: true
|
||||
});
|
||||
}
|
||||
|
||||
check_for_updates() {
|
||||
this.updater = setInterval(() => {
|
||||
this.get_and_update_progress_state();
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
get_and_update_progress_state() {
|
||||
var me = this;
|
||||
frappe.call({
|
||||
method: "frappe.desk.user_progress.update_and_get_user_progress",
|
||||
callback: function(r) {
|
||||
let states = r.message;
|
||||
let changed = 0;
|
||||
let completed = 0;
|
||||
Object.keys(states).map(action_name => {
|
||||
if(states[action_name]) {
|
||||
completed ++;
|
||||
}
|
||||
if(me.progress_state_dict[action_name] != states[action_name]) {
|
||||
changed = 1;
|
||||
me.progress_state_dict[action_name] = states[action_name];
|
||||
}
|
||||
});
|
||||
|
||||
if(changed) {
|
||||
Object.keys(me.slide_container.slide_dict).map((id) => {
|
||||
let slide = me.slide_container.slide_dict[id];
|
||||
if(me.progress_state_dict[slide.action_name]) {
|
||||
if(!slide.done) {
|
||||
slide.done = 1;
|
||||
slide.refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
me.progress_percent = completed / Object.keys(states).length * 100;
|
||||
me.update_progress();
|
||||
},
|
||||
freeze: false
|
||||
});
|
||||
}
|
||||
|
||||
update_progress() {
|
||||
$('.user-progress .progress-bar').css({'width': this.progress_percent + '%'});
|
||||
if(this.progress_percent === 100) {
|
||||
this.dismiss_progress();
|
||||
}
|
||||
}
|
||||
|
||||
dismiss_progress() {
|
||||
$('.user-progress').addClass('hide');
|
||||
clearInterval(this.updater);
|
||||
}
|
||||
|
||||
show() {
|
||||
this.dialog.show();
|
||||
}
|
||||
};
|
||||
|
|
@ -264,11 +264,11 @@ Object.assign(frappe.utils, {
|
|||
if(has_words(["Pending", "Review", "Medium", "Not Approved"], text)) {
|
||||
style = "warning";
|
||||
colour = "orange";
|
||||
} else if(has_words(["Open", "Urgent", "High"], text)) {
|
||||
} else if(has_words(["Open", "Urgent", "High", "Failed"], text)) {
|
||||
style = "danger";
|
||||
colour = "red";
|
||||
} else if(has_words(["Closed", "Finished", "Converted", "Completed", "Confirmed",
|
||||
"Approved", "Yes", "Active", "Available", "Paid"], text)) {
|
||||
} else if(has_words(["Closed", "Finished", "Converted", "Completed", "Complete", "Confirmed",
|
||||
"Approved", "Yes", "Active", "Available", "Paid", "Success"], text)) {
|
||||
style = "success";
|
||||
colour = "green";
|
||||
} else if(has_words(["Submitted"], text)) {
|
||||
|
|
|
|||
|
|
@ -293,6 +293,12 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
this.last_ajax.abort();
|
||||
}
|
||||
|
||||
const query_params = this.get_query_params();
|
||||
|
||||
if (query_params.prepared_report_name) {
|
||||
filters.prepared_report_name = query_params.prepared_report_name;
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
this.last_ajax = frappe.call({
|
||||
method: 'frappe.desk.query_report.run',
|
||||
|
|
@ -303,7 +309,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
},
|
||||
callback: resolve,
|
||||
always: () => this.page.btn_secondary.prop('disabled', false)
|
||||
})
|
||||
});
|
||||
}).then(r => {
|
||||
let data = r.message;
|
||||
this.hide_status();
|
||||
|
|
@ -313,18 +319,18 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
|
||||
if (data.prepared_report) {
|
||||
this.prepared_report = true;
|
||||
const query_string = frappe.utils.get_query_string(frappe.get_route_str());
|
||||
const query_params = frappe.utils.get_query_params(query_string);
|
||||
// If query_string contains prepared_report_name then set filters
|
||||
// to match the mentioned prepared report doc and disable editing
|
||||
if(query_params.prepared_report_name) {
|
||||
if (query_params.prepared_report_name) {
|
||||
this.prepared_report_action = "Edit";
|
||||
const filters_from_report = JSON.parse(data.doc.filters);
|
||||
Object.values(this.filters).forEach(function(field) {
|
||||
if (filters_from_report[field.fieldname]) {
|
||||
field.set_input(filters_from_report[field.fieldname]);
|
||||
}
|
||||
field.input.disabled = true;
|
||||
if (field.input) {
|
||||
field.input.disabled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
this.add_prepared_report_buttons(data.doc);
|
||||
|
|
@ -357,6 +363,12 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
});
|
||||
}
|
||||
|
||||
get_query_params() {
|
||||
const query_string = frappe.utils.get_query_string(frappe.get_route_str());
|
||||
const query_params = frappe.utils.get_query_params(query_string);
|
||||
return query_params;
|
||||
}
|
||||
|
||||
add_prepared_report_buttons(doc) {
|
||||
if(doc){
|
||||
this.page.add_inner_button(__("Download Report"), function (){
|
||||
|
|
|
|||
|
|
@ -959,28 +959,33 @@ input[type="checkbox"] {
|
|||
}
|
||||
}
|
||||
|
||||
// User Progress Dialog
|
||||
.user-progress-dialog {
|
||||
.slides-progress {
|
||||
margin-top: 15px;
|
||||
// Onboarding Dialog
|
||||
.onboarding-dialog {
|
||||
|
||||
.modal-dialog {
|
||||
width: 50%;
|
||||
height: 80%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.done-state {
|
||||
.check-container {
|
||||
font-size: 64px;
|
||||
margin: 40px;
|
||||
}
|
||||
.onboarding-icon {
|
||||
color: #3246F5;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: normal;
|
||||
}
|
||||
.modal-content .slide-container {
|
||||
height: auto;
|
||||
min-height: 100%;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.help-links {
|
||||
img {
|
||||
max-width: 128px;
|
||||
max-height: 128px;
|
||||
}
|
||||
|
||||
a {
|
||||
margin: 0px 10px;
|
||||
}
|
||||
}
|
||||
.slides-progress {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,22 @@
|
|||
.result, .no-result, .freeze {
|
||||
min-height: ~"calc(100vh - 284px)";
|
||||
}
|
||||
|
||||
.msg-box {
|
||||
margin-bottom: 8em;
|
||||
// To compensate for percieved centering
|
||||
|
||||
.null-state {
|
||||
height: 12em !important;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.meta-description {
|
||||
width: 45%;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.freeze-row {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
}
|
||||
|
||||
.category-list[data-category="Open Documents"] li a:hover {
|
||||
background-color: #f0f4f7;
|
||||
background-color: #f0f4f7;
|
||||
}
|
||||
|
||||
.open-doc-count {
|
||||
|
|
@ -70,7 +70,7 @@
|
|||
}
|
||||
|
||||
a.recent-item:hover {
|
||||
background-color: #f0f4f7;
|
||||
background-color: #f0f4f7;
|
||||
}
|
||||
|
||||
a.unread:hover .mark-read {
|
||||
|
|
@ -102,17 +102,19 @@ a.unread:hover .mark-read {
|
|||
background: @light-yellow;
|
||||
}
|
||||
|
||||
.notification-timestamp {
|
||||
margin-top: 5px;
|
||||
font-size: 11px;
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.recent-item {
|
||||
.notification-timestamp {
|
||||
margin-top: 5px;
|
||||
font-size: 11px;
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 30px;
|
||||
.user-avatar {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.event-time {
|
||||
|
|
|
|||
|
|
@ -450,7 +450,7 @@ body[data-route^="Module"] .main-menu {
|
|||
flex-basis: 50%;
|
||||
text-align: center;
|
||||
align-self: center;
|
||||
font-size: @text-small;
|
||||
font-size: 9px;
|
||||
}
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ def get():
|
|||
bootinfo["disable_async"] = frappe.conf.disable_async
|
||||
|
||||
bootinfo["setup_complete"] = cint(frappe.db.get_single_value('System Settings', 'setup_complete'))
|
||||
|
||||
bootinfo["is_first_startup"] = cint(frappe.db.get_single_value('System Settings', 'is_first_startup'))
|
||||
|
||||
return bootinfo
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
{% endif %}
|
||||
|
||||
{% for section in page %}
|
||||
<div class="row section-break">
|
||||
<div class="row section-break" data-label="{{ section.label or '' | e }}">
|
||||
{%- if doc.print_line_breaks and loop.index != 1 -%}<hr>{%- endif -%}
|
||||
{%- if doc.print_section_headings and section.label and section.has_data -%}
|
||||
<h4 class='col-sm-12'>{{ _(section.label) }}</h4>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(),
|
|||
with open(frappe.get_app_path(app, doctype_list_path), 'r') as f:
|
||||
doctype = f.read().strip().splitlines()
|
||||
|
||||
if ui_tests:
|
||||
print("Selenium testing has been deprecated\nUse bench --site {site_name} run-ui-tests for Cypress tests")
|
||||
|
||||
xmloutput_fh = None
|
||||
if junit_xml_output:
|
||||
xmloutput_fh = open(junit_xml_output, 'w')
|
||||
|
|
@ -71,7 +74,7 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(),
|
|||
else:
|
||||
ret = run_all_tests(app, verbose, profile, ui_tests, failfast=failfast)
|
||||
|
||||
frappe.db.commit()
|
||||
if frappe.db: frappe.db.commit()
|
||||
|
||||
# workaround! since there is no separate test db
|
||||
frappe.clear_cache()
|
||||
|
|
@ -170,21 +173,6 @@ def run_tests_for_module(module, verbose=False, tests=(), profile=False):
|
|||
|
||||
return _run_unittest(module, verbose=verbose, tests=tests, profile=profile)
|
||||
|
||||
def run_setup_wizard_ui_test(app=None, verbose=False, profile=False):
|
||||
'''Run setup wizard UI test using test_test_runner'''
|
||||
frappe.flags.run_setup_wizard_ui_test = 1
|
||||
return run_ui_tests(app=app, test=None, verbose=verbose, profile=profile)
|
||||
|
||||
def run_ui_tests(app=None, test=None, test_list=None, verbose=False, profile=False):
|
||||
'''Run a single unit test for UI using test_test_runner'''
|
||||
module = importlib.import_module('frappe.tests.ui.test_test_runner')
|
||||
frappe.flags.ui_test_app = app
|
||||
if test_list:
|
||||
frappe.flags.ui_test_list = test_list
|
||||
else:
|
||||
frappe.flags.ui_test_path = test
|
||||
return _run_unittest(module, verbose=verbose, tests=(), profile=profile)
|
||||
|
||||
def _run_unittest(modules, verbose=False, tests=(), profile=False):
|
||||
test_suite = unittest.TestSuite()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import frappe
|
||||
from werkzeug.wrappers import Request
|
||||
from werkzeug.test import EnvironBuilder
|
||||
|
||||
def set_request(**kwargs):
|
||||
builder = EnvironBuilder(**kwargs)
|
||||
frappe.local.request = Request(builder.get_environ())
|
||||
|
||||
def insert_test_data(doctype, sort_fn=None):
|
||||
import frappe.model
|
||||
data = get_test_doclist(doctype)
|
||||
if sort_fn:
|
||||
data = sorted(data, key=sort_fn)
|
||||
|
||||
for doclist in data:
|
||||
frappe.insert(doclist)
|
||||
|
||||
def get_test_doclist(doctype, name=None):
|
||||
"""get test doclist, collection of doclists"""
|
||||
import os
|
||||
from frappe import conf
|
||||
from frappe.modules.utils import peval_doclist
|
||||
from frappe.modules import scrub
|
||||
|
||||
doctype = scrub(doctype)
|
||||
doctype_path = os.path.join(os.path.dirname(os.path.abspath(conf.__file__)),
|
||||
conf.test_data_path, doctype)
|
||||
|
||||
if name:
|
||||
with open(os.path.join(doctype_path, scrub(name) + ".json"), 'r') as txtfile:
|
||||
doclist = peval_doclist(txtfile.read())
|
||||
|
||||
return doclist
|
||||
|
||||
else:
|
||||
all_doclists = []
|
||||
for fname in filter(lambda n: n.endswith(".json"), os.listdir(doctype_path)):
|
||||
with open(os.path.join(doctype_path, scrub(fname)), 'r') as txtfile:
|
||||
all_doclists.append(peval_doclist(txtfile.read()))
|
||||
|
||||
return all_doclists
|
||||
|
|
@ -5,13 +5,14 @@ from __future__ import unicode_literals
|
|||
import unittest, frappe, os
|
||||
from frappe.core.doctype.user.user import generate_keys
|
||||
from frappe.frappeclient import FrappeClient
|
||||
from frappe.utils.data import get_url
|
||||
|
||||
import requests
|
||||
import base64
|
||||
|
||||
class TestAPI(unittest.TestCase):
|
||||
def test_insert_many(self):
|
||||
server = FrappeClient(frappe.get_site_config().host_name, "Administrator", "admin", verify=False)
|
||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False)
|
||||
frappe.db.sql("delete from `tabNote` where title in ('Sing','a','song','of','sixpence')")
|
||||
frappe.db.commit()
|
||||
|
||||
|
|
@ -30,7 +31,7 @@ class TestAPI(unittest.TestCase):
|
|||
self.assertTrue(frappe.db.get_value('Note', {'title': 'sixpence'}))
|
||||
|
||||
def test_create_doc(self):
|
||||
server = FrappeClient(frappe.get_site_config().host_name, "Administrator", "admin", verify=False)
|
||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False)
|
||||
frappe.db.sql("delete from `tabNote` where title = 'test_create'")
|
||||
frappe.db.commit()
|
||||
|
||||
|
|
@ -39,13 +40,13 @@ class TestAPI(unittest.TestCase):
|
|||
self.assertTrue(frappe.db.get_value('Note', {'title': 'test_create'}))
|
||||
|
||||
def test_list_docs(self):
|
||||
server = FrappeClient(frappe.get_site_config().host_name, "Administrator", "admin", verify=False)
|
||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False)
|
||||
doc_list = server.get_list("Note")
|
||||
|
||||
self.assertTrue(len(doc_list))
|
||||
|
||||
def test_get_doc(self):
|
||||
server = FrappeClient(frappe.get_site_config().host_name, "Administrator", "admin", verify=False)
|
||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False)
|
||||
frappe.db.sql("delete from `tabNote` where title = 'get_this'")
|
||||
frappe.db.commit()
|
||||
|
||||
|
|
@ -56,7 +57,7 @@ class TestAPI(unittest.TestCase):
|
|||
self.assertTrue(doc)
|
||||
|
||||
def test_update_doc(self):
|
||||
server = FrappeClient(frappe.get_site_config().host_name, "Administrator", "admin", verify=False)
|
||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False)
|
||||
frappe.db.sql("delete from `tabNote` where title in ('Sing','sing')")
|
||||
frappe.db.commit()
|
||||
|
||||
|
|
@ -68,7 +69,7 @@ class TestAPI(unittest.TestCase):
|
|||
self.assertTrue(doc["title"] == changed_title)
|
||||
|
||||
def test_delete_doc(self):
|
||||
server = FrappeClient(frappe.get_site_config().host_name, "Administrator", "admin", verify=False)
|
||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False)
|
||||
frappe.db.sql("delete from `tabNote` where title = 'delete'")
|
||||
frappe.db.commit()
|
||||
|
||||
|
|
@ -90,21 +91,21 @@ class TestAPI(unittest.TestCase):
|
|||
|
||||
api_key = frappe.db.get_value("User", "Administrator", "api_key")
|
||||
header = {"Authorization": "token {}:{}".format(api_key, generated_secret)}
|
||||
res = requests.post(frappe.get_site_config().host_name + "/api/method/frappe.auth.get_logged_user", headers=header)
|
||||
res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header)
|
||||
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual("Administrator", res.json()["message"])
|
||||
self.assertEqual(keys['api_secret'], generated_secret)
|
||||
|
||||
header = {"Authorization": "Basic {}".format(base64.b64encode(frappe.safe_encode("{}:{}".format(api_key, generated_secret))).decode())}
|
||||
res = requests.post(frappe.get_site_config().host_name + "/api/method/frappe.auth.get_logged_user", headers=header)
|
||||
res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual("Administrator", res.json()["message"])
|
||||
|
||||
# Valid api key, invalid api secret
|
||||
api_secret = "ksk&93nxoe3os"
|
||||
header = {"Authorization": "token {}:{}".format(api_key, api_secret)}
|
||||
res = requests.post(frappe.get_site_config().host_name + "/api/method/frappe.auth.get_logged_user", headers=header)
|
||||
res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header)
|
||||
self.assertEqual(res.status_code, 403)
|
||||
|
||||
|
||||
|
|
@ -112,5 +113,5 @@ class TestAPI(unittest.TestCase):
|
|||
api_key = "@3djdk3kld"
|
||||
api_secret = "ksk&93nxoe3os"
|
||||
header = {"Authorization": "token {}:{}".format(api_key, api_secret)}
|
||||
res = requests.post(frappe.get_site_config().host_name + "/api/method/frappe.auth.get_logged_user", headers=header)
|
||||
res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header)
|
||||
self.assertEqual(res.status_code, 401)
|
||||
|
|
|
|||
|
|
@ -1,85 +0,0 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import unittest, frappe, requests, time
|
||||
from frappe.test_runner import make_test_records
|
||||
from frappe.utils.selenium_testdriver import TestDriver
|
||||
from six.moves.urllib.parse import urlparse
|
||||
from frappe.frappeclient import FrappeOAuth2Client
|
||||
|
||||
class TestFrappeOAuth2Client(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.driver = TestDriver()
|
||||
make_test_records("OAuth Client")
|
||||
make_test_records("User")
|
||||
self.client_id = frappe.get_all("OAuth Client", fields=["*"])[0].get("client_id")
|
||||
|
||||
# Set Frappe server URL reqired for id_token generation
|
||||
try:
|
||||
frappe_login_key = frappe.get_doc("Social Login Key", "frappe")
|
||||
except frappe.DoesNotExistError:
|
||||
frappe_login_key = frappe.new_doc("Social Login Key")
|
||||
frappe_login_key.get_social_login_provider("Frappe", initialize=True)
|
||||
frappe_login_key.base_url = "http://localhost:8000"
|
||||
frappe_login_key.save()
|
||||
|
||||
def test_insert_note(self):
|
||||
|
||||
# Go to Authorize url
|
||||
self.driver.get(
|
||||
"api/method/frappe.integrations.oauth2.authorize?client_id=" +
|
||||
self.client_id +
|
||||
"&scope=all%20openid&response_type=code&redirect_uri=http%3A%2F%2Flocalhost"
|
||||
)
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# Login
|
||||
username = self.driver.find("#login_email")[0]
|
||||
username.send_keys("test@example.com")
|
||||
|
||||
password = self.driver.find("#login_password")[0]
|
||||
password.send_keys("Eastern_43A1W")
|
||||
|
||||
sign_in = self.driver.find(".btn-login")[0]
|
||||
sign_in.submit()
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# Allow access to resource
|
||||
allow = self.driver.find("#allow")[0]
|
||||
allow.click()
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# Get authorization code from redirected URL
|
||||
auth_code = urlparse(self.driver.driver.current_url).query.split("=")[1]
|
||||
|
||||
payload = "grant_type=authorization_code&code="
|
||||
payload += auth_code
|
||||
payload += "&redirect_uri=http%3A%2F%2Flocalhost&client_id="
|
||||
payload += self.client_id
|
||||
|
||||
headers = {'content-type':'application/x-www-form-urlencoded'}
|
||||
|
||||
# Request for bearer token
|
||||
token_response = requests.post( frappe.get_site_config().host_name +
|
||||
"/api/method/frappe.integrations.oauth2.get_token", data=payload, headers=headers)
|
||||
|
||||
# Parse bearer token json
|
||||
bearer_token = token_response.json()
|
||||
client = FrappeOAuth2Client(frappe.get_site_config().host_name, bearer_token.get("access_token"))
|
||||
|
||||
notes = [
|
||||
{"doctype": "Note", "title": "Sing", "public": True},
|
||||
{"doctype": "Note", "title": "a", "public": True},
|
||||
{"doctype": "Note", "title": "Song", "public": True},
|
||||
{"doctype": "Note", "title": "of", "public": True},
|
||||
{"doctype": "Note", "title": "sixpence", "public": True}
|
||||
]
|
||||
|
||||
for note in notes:
|
||||
client.insert(note)
|
||||
|
||||
self.assertTrue(len(frappe.get_all("Note")) == 5)
|
||||
|
|
@ -7,7 +7,7 @@ from __future__ import unicode_literals
|
|||
import unittest
|
||||
import frappe
|
||||
import frappe.recorder
|
||||
from .test_website import set_request
|
||||
from frappe.utils import set_request
|
||||
|
||||
import sqlparse
|
||||
|
||||
|
|
|
|||
|
|
@ -2,75 +2,81 @@ from __future__ import unicode_literals
|
|||
|
||||
from unittest import TestCase
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from frappe.utils.scheduler import (enqueue_applicable_events, restrict_scheduler_events_if_dormant,
|
||||
get_enabled_scheduler_events)
|
||||
from frappe import _dict
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils import now_datetime, today, add_days, add_to_date
|
||||
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
|
||||
from frappe.utils.background_jobs import enqueue, get_jobs
|
||||
from frappe.utils.scheduler import enqueue_events, is_dormant, schedule_jobs_based_on_activity
|
||||
from frappe.utils import add_days, get_datetime
|
||||
|
||||
import frappe
|
||||
import time
|
||||
|
||||
def test_timeout():
|
||||
'''This function needs to be pickleable'''
|
||||
time.sleep(100)
|
||||
|
||||
def test_timeout_10():
|
||||
time.sleep(10)
|
||||
|
||||
def test_method():
|
||||
pass
|
||||
|
||||
class TestScheduler(TestCase):
|
||||
def setUp(self):
|
||||
frappe.db.set_global('enabled_scheduler_events', "")
|
||||
frappe.flags.ran_schedulers = []
|
||||
if not frappe.get_all('Scheduled Job Type', limit=1):
|
||||
sync_jobs()
|
||||
|
||||
def test_all_events(self):
|
||||
last = now_datetime() - relativedelta(hours=2)
|
||||
enqueue_applicable_events(frappe.local.site, now_datetime(), last)
|
||||
self.assertTrue("all" in frappe.flags.ran_schedulers)
|
||||
def test_enqueue_jobs(self):
|
||||
frappe.db.sql("update `tabScheduled Job Type` set last_execution = '2010-01-01 00:00:00'")
|
||||
|
||||
def test_enabled_events(self):
|
||||
frappe.flags.enabled_events = ["hourly", "hourly_long", "daily", "daily_long",
|
||||
"weekly", "weekly_long", "monthly", "monthly_long"]
|
||||
frappe.flags.execute_job = True
|
||||
enqueue_events(site = frappe.local.site)
|
||||
frappe.flags.execute_job = False
|
||||
|
||||
# maintain last_event and next_event on the same day
|
||||
last_event = now_datetime().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
next_event = last_event + relativedelta(minutes=30)
|
||||
self.assertTrue('frappe.email.queue.clear_outbox', frappe.flags.enqueued_jobs)
|
||||
self.assertTrue('frappe.utils.change_log.check_for_update', frappe.flags.enqueued_jobs)
|
||||
self.assertTrue('frappe.email.doctype.auto_email_report.auto_email_report.send_monthly', frappe.flags.enqueued_jobs)
|
||||
|
||||
enqueue_applicable_events(frappe.local.site, next_event, last_event)
|
||||
self.assertFalse("cron" in frappe.flags.ran_schedulers)
|
||||
def test_queue_peeking(self):
|
||||
job = get_test_job()
|
||||
|
||||
# maintain last_event and next_event on the same day
|
||||
last_event = now_datetime().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
next_event = last_event + relativedelta(hours=2)
|
||||
self.assertTrue(job.enqueue())
|
||||
job.db_set('last_execution', '2010-01-01 00:00:00')
|
||||
frappe.db.commit()
|
||||
|
||||
frappe.flags.ran_schedulers = []
|
||||
enqueue_applicable_events(frappe.local.site, next_event, last_event)
|
||||
self.assertTrue("all" in frappe.flags.ran_schedulers)
|
||||
self.assertTrue("hourly" in frappe.flags.ran_schedulers)
|
||||
# 1 job in queue
|
||||
self.assertTrue(job.enqueue())
|
||||
job.db_set('last_execution', '2010-01-01 00:00:00')
|
||||
frappe.db.commit()
|
||||
|
||||
frappe.flags.enabled_events = None
|
||||
# 2nd job not loaded
|
||||
self.assertFalse(job.enqueue())
|
||||
job.delete()
|
||||
|
||||
def test_enabled_events_day_change(self):
|
||||
def test_is_dormant(self):
|
||||
self.assertTrue(is_dormant(check_time= get_datetime('2100-01-01 00:00:00')))
|
||||
self.assertTrue(is_dormant(check_time = add_days(frappe.db.get_last_created('Activity Log'), 5)))
|
||||
self.assertFalse(is_dormant(check_time = frappe.db.get_last_created('Activity Log')))
|
||||
|
||||
# use flags instead of globals as this test fails intermittently
|
||||
# the root cause has not been identified but the culprit seems cache
|
||||
# since cache is mutable, it maybe be changed by a parallel process
|
||||
frappe.flags.enabled_events = ["daily", "daily_long", "weekly", "weekly_long",
|
||||
"monthly", "monthly_long"]
|
||||
|
||||
# maintain last_event and next_event on different days
|
||||
next_event = now_datetime().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
last_event = next_event - relativedelta(hours=2)
|
||||
|
||||
frappe.flags.ran_schedulers = []
|
||||
enqueue_applicable_events(frappe.local.site, next_event, last_event)
|
||||
self.assertTrue("all" in frappe.flags.ran_schedulers)
|
||||
self.assertFalse("hourly" in frappe.flags.ran_schedulers)
|
||||
|
||||
frappe.flags.enabled_events = None
|
||||
def test_once_a_day_for_dormant(self):
|
||||
frappe.db.clear_table('Scheduled Job Log')
|
||||
self.assertTrue(schedule_jobs_based_on_activity(check_time= get_datetime('2100-01-01 00:00:00')))
|
||||
self.assertTrue(schedule_jobs_based_on_activity(check_time = add_days(frappe.db.get_last_created('Activity Log'), 5)))
|
||||
|
||||
# create a fake job executed 5 days from now
|
||||
job = get_test_job(method='frappe.tests.test_scheduler.test_method', frequency='Daily')
|
||||
job.execute()
|
||||
job_log = frappe.get_doc('Scheduled Job Log', dict(scheduled_job_type=job.name))
|
||||
job_log.db_set('creation', add_days(frappe.db.get_last_created('Activity Log'), 5))
|
||||
|
||||
# inactive site with recent job, don't run
|
||||
self.assertFalse(schedule_jobs_based_on_activity(check_time = add_days(frappe.db.get_last_created('Activity Log'), 5)))
|
||||
|
||||
# one more day has passed
|
||||
self.assertTrue(schedule_jobs_based_on_activity(check_time = add_days(frappe.db.get_last_created('Activity Log'), 6)))
|
||||
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_job_timeout(self):
|
||||
return
|
||||
job = enqueue(test_timeout, timeout=10)
|
||||
count = 5
|
||||
while count > 0:
|
||||
|
|
@ -81,5 +87,19 @@ class TestScheduler(TestCase):
|
|||
|
||||
self.assertTrue(job.is_failed)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.flags.ran_schedulers = []
|
||||
def get_test_job(method='frappe.tests.test_scheduler.test_timeout_10', frequency='All'):
|
||||
if not frappe.db.exists('Scheduled Job Type', dict(method=method)):
|
||||
job = frappe.get_doc(dict(
|
||||
doctype = 'Scheduled Job Type',
|
||||
method = method,
|
||||
last_execution = '2010-01-01 00:00:00',
|
||||
frequency = frequency
|
||||
)).insert()
|
||||
else:
|
||||
job = frappe.get_doc('Scheduled Job Type', dict(method=method))
|
||||
job.db_set('last_execution', '2010-01-01 00:00:00')
|
||||
job.db_set('frequency', frequency)
|
||||
frappe.db.commit()
|
||||
|
||||
return job
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import frappe, unittest
|
||||
from frappe.tests.test_website import get_html_for_route
|
||||
from frappe.utils import get_html_for_route
|
||||
|
||||
class TestSitemap(unittest.TestCase):
|
||||
def test_sitemap(self):
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue