Merge branch 'develop' of https://github.com/frappe/frappe into pref_global_search

This commit is contained in:
Himanshu Warekar 2019-12-03 14:25:04 +05:30
commit 86e1d0367b
134 changed files with 2227 additions and 3512 deletions

2
.codacy.yml Normal file
View file

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

2
.pylintrc Normal file
View file

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

View file

@ -1,6 +1,5 @@
language: python
dist: trusty
sudo: required
addons:
hosts:
@ -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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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):

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,98 @@
{
"actions": [
{
"action": "frappe.core.doctype.scheduled_job_type.scheduled_job_type.execute_event",
"action_type": "Server Action",
"label": "Execute"
}
],
"creation": "2019-09-23 14:34:09.205368",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"stopped",
"method",
"frequency",
"cron_format",
"last_execution",
"create_log"
],
"fields": [
{
"fieldname": "method",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Method",
"read_only": 1,
"reqd": 1
},
{
"default": "0",
"fieldname": "stopped",
"fieldtype": "Check",
"label": "Stopped"
},
{
"default": "0",
"depends_on": "eval:doc.queue==='All'",
"fieldname": "create_log",
"fieldtype": "Check",
"label": "Create Log"
},
{
"fieldname": "last_execution",
"fieldtype": "Datetime",
"label": "Last Execution",
"read_only": 1
},
{
"allow_in_quick_entry": 1,
"depends_on": "eval:doc.queue==='Cron'",
"fieldname": "cron_format",
"fieldtype": "Data",
"label": "Cron Format",
"read_only": 1
},
{
"fieldname": "frequency",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Frequency",
"options": "All\nHourly\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
}

View 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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
}

View file

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

View 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');
}
}
});

View 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
}

View 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()

View file

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

View file

@ -0,0 +1,74 @@
{
"creation": "2019-11-13 13:35:08.617909",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"label",
"fieldtype",
"fieldname",
"align",
"placeholder",
"reqd",
"column_break_4",
"options"
],
"fields": [
{
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label"
},
{
"fieldname": "fieldtype",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Fieldtype",
"options": "Attach\nAttach Image\nCheck\nCurrency\nData\nDate\nDatetime\nFloat\nHTML\nInt\nRating\nSelect\nLink\nSmall Text\nText\nText Editor\nSection Break\nColumn Break"
},
{
"fieldname": "fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Fieldname"
},
{
"fieldname": "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"
}

View file

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

View file

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

View file

@ -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

View file

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

View file

@ -21,7 +21,6 @@ from frappe.desk.form import assign_to
from frappe.utils.user import get_system_managers
from frappe.utils.background_jobs import enqueue, get_jobs
from frappe.core.doctype.communication.email import set_incoming_outgoing_accounts
from frappe.utils.scheduler import log
from frappe.utils.html_utils import clean_email_html
from frappe.email.utils import get_port
@ -284,7 +283,7 @@ class EmailAccount(Document):
except Exception:
frappe.db.rollback()
log('email_account.receive')
frappe.log_error('email_account.receive')
if self.use_imap:
self.handle_bad_emails(email_server, uid, msg, frappe.get_traceback())
exceptions.append(frappe.get_traceback())

View file

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

View file

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

View file

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

View file

@ -12,7 +12,6 @@ import frappe
from frappe import _, safe_decode, safe_encode
from frappe.utils import (extract_email_id, convert_utc_to_user_timezone, now,
cint, cstr, strip, markdown, parse_addr)
from frappe.utils.scheduler import log
from frappe.core.doctype.file.file import get_random_filename, MaxFileSizeReachedError
class EmailSizeExceededError(frappe.ValidationError): pass
@ -80,7 +79,7 @@ class EmailServer:
except _socket.error:
# log performs rollback and logs error in Error Log
log("receive.connect_pop")
frappe.log_error("receive.connect_pop")
# Invalid mail server -- due to refusing connection
frappe.msgprint(_('Invalid Mail Server. Please rectify and try again.'))
@ -255,7 +254,7 @@ class EmailServer:
else:
# log performs rollback and logs error in Error Log
log("receive.get_messages", self.make_error_msg(msg_num, incoming_mail))
frappe.log_error("receive.get_messages", self.make_error_msg(msg_num, incoming_mail))
self.errors = True
frappe.db.rollback()

View file

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

View file

@ -18,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)

View file

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

View file

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

View file

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

View file

@ -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)

View file

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

View file

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

View file

@ -29,6 +29,8 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe
# these need to go first at time of install
for d in (("core", "docfield"),
("core", "docperm"),
("core", "doctype_action"),
("core", "doctype_link"),
("core", "role"),
("core", "has_role"),
("core", "doctype"),
@ -41,7 +43,10 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe
("data_migration", "data_migration_mapping_detail"),
("data_migration", "data_migration_mapping"),
("data_migration", "data_migration_plan_mapping"),
("data_migration", "data_migration_plan")):
("data_migration", "data_migration_plan"),
("desk", "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):

View file

@ -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")

View file

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

File diff suppressed because it is too large Load diff

View file

@ -695,7 +695,8 @@ frappe.PrintFormatBuilder = Class.extend({
{
fieldname: "content",
fieldtype: "Code",
label: label
label: label,
options: "HTML"
},
{
fieldname: "help",

View file

@ -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": ""
}

View file

@ -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",

View file

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View file

@ -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);

View file

@ -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;
},

View file

@ -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;
},

View file

@ -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);

View file

@ -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 => {

View file

@ -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) {

View file

@ -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")

View file

@ -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;">&times;</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>

View file

@ -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();
}

View file

@ -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>

View 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();
}
};

View file

@ -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();
});
}

View file

@ -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>'

View file

@ -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();
}
};

View file

@ -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)) {

View file

@ -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 (){

View file

@ -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;
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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;
}

View file

@ -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

View file

@ -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>

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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