From 528d04213e5123d89ae7979cbbb182ca9520736c Mon Sep 17 00:00:00 2001
From: Rohan Bansal
Date: Mon, 5 Oct 2020 17:16:23 +0530
Subject: [PATCH 01/83] feat: clear failed jobs queue
---
.../page/background_jobs/background_jobs.js | 22 +++++++++---
.../page/background_jobs/background_jobs.py | 34 ++++++++++++-------
.../background_jobs_outer.html | 5 ++-
3 files changed, 43 insertions(+), 18 deletions(-)
diff --git a/frappe/core/page/background_jobs/background_jobs.js b/frappe/core/page/background_jobs/background_jobs.js
index bbc8bf049b..9dd2c10b05 100644
--- a/frappe/core/page/background_jobs/background_jobs.js
+++ b/frappe/core/page/background_jobs/background_jobs.js
@@ -1,5 +1,5 @@
frappe.pages['background_jobs'].on_page_load = function(wrapper) {
- var page = frappe.ui.make_app_page({
+ let page = frappe.ui.make_app_page({
parent: wrapper,
title: __('Background Jobs'),
single_column: true
@@ -9,6 +9,9 @@ frappe.pages['background_jobs'].on_page_load = function(wrapper) {
page.content = $(page.body).find('.table-area');
frappe.pages.background_jobs.page = page;
+
+ let $remove_failed_btn = page.body.find('.remove-failed');
+ $remove_failed_btn.click(remove_failed_jobs);
}
frappe.pages['background_jobs'].on_page_show = function(wrapper) {
@@ -22,10 +25,10 @@ frappe.pages['background_jobs'].on_page_show = function(wrapper) {
}
frappe.pages.background_jobs.refresh_jobs = function() {
- var page = frappe.pages.background_jobs.page;
+ let page = frappe.pages.background_jobs.page;
// don't call if already waiting for a response
- if(page.called) return;
+ if (page.called) return;
page.called = true;
frappe.call({
method: 'frappe.core.page.background_jobs.background_jobs.get_info',
@@ -35,11 +38,20 @@ frappe.pages.background_jobs.refresh_jobs = function() {
callback: function(r) {
page.called = false;
page.body.find('.list-jobs').remove();
- $(frappe.render_template('background_jobs', {jobs:r.message || []})).appendTo(page.content);
+ $(frappe.render_template('background_jobs', { jobs: r.message || [] })).appendTo(page.content);
- if(frappe.get_route()[0]==='background_jobs') {
+ if (frappe.get_route()[0] === 'background_jobs') {
frappe.background_jobs_timeout = setTimeout(frappe.pages.background_jobs.refresh_jobs, 2000);
}
}
});
}
+
+const remove_failed_jobs = function() {
+ frappe.call({
+ method: 'frappe.core.page.background_jobs.background_jobs.remove_failed_jobs',
+ callback: function (r) {
+ frappe.pages.background_jobs.refresh_jobs();
+ }
+ });
+}
diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py
index 4a94de4ace..2185149024 100644
--- a/frappe/core/page/background_jobs/background_jobs.py
+++ b/frappe/core/page/background_jobs/background_jobs.py
@@ -1,14 +1,13 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
-import frappe
-
from rq import Queue, Worker
-from frappe.utils.background_jobs import get_redis_conn
-from frappe.utils import format_datetime, cint, convert_utc_to_user_timezone
-from frappe.utils.scheduler import is_scheduler_inactive
+
+import frappe
from frappe import _
+from frappe.utils import cint, convert_utc_to_user_timezone, format_datetime
+from frappe.utils.background_jobs import get_redis_conn
+from frappe.utils.scheduler import is_scheduler_inactive
colors = {
'queued': 'orange',
@@ -17,6 +16,7 @@ colors = {
'finished': 'green'
}
+
@frappe.whitelist()
def get_info(show_failed=False):
conn = get_redis_conn()
@@ -25,11 +25,9 @@ def get_info(show_failed=False):
jobs = []
def add_job(j, name):
- if j.kwargs.get('site')==frappe.local.site:
+ if j.kwargs.get('site') == frappe.local.site:
jobs.append({
- 'job_name': j.kwargs.get('kwargs', {}).get('playbook_method') \
- or j.kwargs.get('kwargs', {}).get('job_type') \
- or str(j.kwargs.get('job_name')),
+ 'job_name': j.kwargs.get('kwargs', {}).get('playbook_method') or j.kwargs.get('kwargs', {}).get('job_type') or str(j.kwargs.get('job_name')),
'status': j.get_status(), 'queue': name,
'creation': format_datetime(convert_utc_to_user_timezone(j.created_at)),
'color': colors[j.get_status()]
@@ -44,15 +42,27 @@ def get_info(show_failed=False):
for q in queues:
if q.name != 'failed':
- for j in q.get_jobs(): add_job(j, q.name)
+ for j in q.get_jobs():
+ add_job(j, q.name)
if cint(show_failed):
for q in queues:
if q.name == 'failed':
- for j in q.get_jobs()[:10]: add_job(j, q.name)
+ for j in q.get_jobs()[:10]:
+ add_job(j, q.name)
return jobs
+
+@frappe.whitelist()
+def remove_failed_jobs():
+ conn = get_redis_conn()
+ queues = Queue.all(conn)
+ for q in queues:
+ if q.name == 'failed':
+ q.empty()
+
+
@frappe.whitelist()
def get_scheduler_status():
if is_scheduler_inactive():
diff --git a/frappe/core/page/background_jobs/background_jobs_outer.html b/frappe/core/page/background_jobs/background_jobs_outer.html
index 4da4498304..8c79806005 100644
--- a/frappe/core/page/background_jobs/background_jobs_outer.html
+++ b/frappe/core/page/background_jobs/background_jobs_outer.html
@@ -2,9 +2,12 @@
- {{ __("Show failed jobs") }}
+ {{ __("Show failed jobs") }}
+
+ {{ __("Remove failed jobs") }}
+
From 09886799e8badea0bb55d2024ab292a13e419632 Mon Sep 17 00:00:00 2001
From: Hendy Irawan
Date: Mon, 5 Oct 2020 08:20:18 +0000
Subject: [PATCH 02/83] fix(form): Form UI wrong filename and URL
Form UI gives wrong file name and URL of Attach field if filename contains ","
---
frappe/public/js/frappe/form/controls/attach.js | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/frappe/public/js/frappe/form/controls/attach.js b/frappe/public/js/frappe/form/controls/attach.js
index fe662c1ada..ba058ccf8e 100644
--- a/frappe/public/js/frappe/form/controls/attach.js
+++ b/frappe/public/js/frappe/form/controls/attach.js
@@ -1,6 +1,6 @@
frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({
make_input: function() {
- var me = this;
+ let me = this;
this.$input = $('')
.html(__("Attach"))
.prependTo(me.input_area)
@@ -28,7 +28,7 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({
this.toggle_reload_button();
},
clear_attachment: function() {
- var me = this;
+ let me = this;
if(this.frm) {
me.parse_validate_and_set_in_model(null);
me.refresh();
@@ -79,10 +79,12 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({
this.value = value;
if(this.value) {
this.$input.toggle(false);
- if(this.value.indexOf(",")!==-1) {
- var parts = this.value.split(",");
- var filename = parts[0];
- dataurl = parts[1];
+ // value can also be using this format: FILENAME,DATA_URL
+ // Important: We have to be careful because normal filenames may also contain ","
+ let two_part_matches = this.value.match(/^([^:]+),(.+):(.+)$/);
+ if (two_part_matches !== null) {
+ let filename = two_part_matches[1];
+ dataurl = two_part_matches[2] + ':' + two_part_matches[3];
}
this.$value.toggle(true).find(".attached-file-link")
.html(filename || this.value)
From d6775a03c030daa62cb11a226d206b16f0e7aef5 Mon Sep 17 00:00:00 2001
From: Rohan Bansal
Date: Fri, 6 Nov 2020 17:10:04 +0530
Subject: [PATCH 03/83] fix: use inbuilt RQ functions to clear job
---
.../page/background_jobs/background_jobs.js | 4 +-
.../page/background_jobs/background_jobs.py | 74 +++++++++++--------
2 files changed, 47 insertions(+), 31 deletions(-)
diff --git a/frappe/core/page/background_jobs/background_jobs.js b/frappe/core/page/background_jobs/background_jobs.js
index 9dd2c10b05..75d97b9411 100644
--- a/frappe/core/page/background_jobs/background_jobs.js
+++ b/frappe/core/page/background_jobs/background_jobs.js
@@ -50,8 +50,8 @@ frappe.pages.background_jobs.refresh_jobs = function() {
const remove_failed_jobs = function() {
frappe.call({
method: 'frappe.core.page.background_jobs.background_jobs.remove_failed_jobs',
- callback: function (r) {
+ callback: function () {
frappe.pages.background_jobs.refresh_jobs();
}
});
-}
+};
diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py
index 2185149024..a8e6c91212 100644
--- a/frappe/core/page/background_jobs/background_jobs.py
+++ b/frappe/core/page/background_jobs/background_jobs.py
@@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
+from typing import TYPE_CHECKING, Dict, List
from rq import Queue, Worker
import frappe
@@ -9,7 +10,10 @@ from frappe.utils import cint, convert_utc_to_user_timezone, format_datetime
from frappe.utils.background_jobs import get_redis_conn
from frappe.utils.scheduler import is_scheduler_inactive
-colors = {
+if TYPE_CHECKING:
+ from rq.job import Job
+
+COLORS = {
'queued': 'orange',
'failed': 'red',
'started': 'blue',
@@ -18,53 +22,65 @@ colors = {
@frappe.whitelist()
-def get_info(show_failed=False):
+def get_info(show_failed: bool = False) -> List[Dict]:
conn = get_redis_conn()
queues = Queue.all(conn)
workers = Worker.all(conn)
jobs = []
- def add_job(j, name):
- if j.kwargs.get('site') == frappe.local.site:
- jobs.append({
- 'job_name': j.kwargs.get('kwargs', {}).get('playbook_method') or j.kwargs.get('kwargs', {}).get('job_type') or str(j.kwargs.get('job_name')),
- 'status': j.get_status(), 'queue': name,
- 'creation': format_datetime(convert_utc_to_user_timezone(j.created_at)),
- 'color': colors[j.get_status()]
- })
- if j.exc_info:
- jobs[-1]['exc_info'] = j.exc_info
+ def add_job(job: Job, name: str) -> None:
+ if job.kwargs.get('site') == frappe.local.site:
+ job_info = {
+ 'job_name': job.kwargs.get('kwargs', {}).get('playbook_method')
+ or job.kwargs.get('kwargs', {}).get('job_type')
+ or str(job.kwargs.get('job_name')),
+ 'status': job.get_status(),
+ 'queue': name,
+ 'creation': format_datetime(convert_utc_to_user_timezone(job.created_at)),
+ 'color': COLORS[job.get_status()]
+ }
- for w in workers:
- j = w.get_current_job()
- if j:
- add_job(j, w.name)
+ if job.exc_info:
+ job_info['exc_info'] = job.exc_info
- for q in queues:
- if q.name != 'failed':
- for j in q.get_jobs():
- add_job(j, q.name)
+ jobs.append(job_info)
+ # show worker jobs
+ for worker in workers:
+ job = worker.get_current_job()
+ if job:
+ add_job(job, worker.name)
+
+ # show active queued jobs
+ for queue in queues:
+ if queue.name != 'failed':
+ for job in queue.jobs:
+ add_job(job, queue.name)
+
+ # show failed jobs, if requested
if cint(show_failed):
- for q in queues:
- if q.name == 'failed':
- for j in q.get_jobs()[:10]:
- add_job(j, q.name)
+ for queue in queues:
+ fail_registry = queue.failed_job_registry
+ for job_id in fail_registry.get_job_ids():
+ job = queue.fetch_job(job_id)
+ add_job(job, queue.name)
return jobs
@frappe.whitelist()
-def remove_failed_jobs():
+def remove_failed_jobs() -> None:
conn = get_redis_conn()
queues = Queue.all(conn)
- for q in queues:
- if q.name == 'failed':
- q.empty()
+ for queue in queues:
+ fail_registry = queue.failed_job_registry
+ for job_id in fail_registry.get_job_ids():
+ job = queue.fetch_job(job_id)
+ fail_registry.remove(job, delete_job=True)
@frappe.whitelist()
-def get_scheduler_status():
+def get_scheduler_status() -> None:
if is_scheduler_inactive():
return [_("Inactive"), "red"]
return [_("Active"), "green"]
From 8cfd48449262caa19b0795d4b6c1c537372d0782 Mon Sep 17 00:00:00 2001
From: Rohan Bansal
Date: Fri, 6 Nov 2020 17:38:10 +0530
Subject: [PATCH 04/83] fix: add forward referencing
---
frappe/core/page/background_jobs/background_jobs.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py
index a8e6c91212..baad40009b 100644
--- a/frappe/core/page/background_jobs/background_jobs.py
+++ b/frappe/core/page/background_jobs/background_jobs.py
@@ -22,13 +22,13 @@ COLORS = {
@frappe.whitelist()
-def get_info(show_failed: bool = False) -> List[Dict]:
+def get_info(show_failed=False) -> List[Dict]:
conn = get_redis_conn()
queues = Queue.all(conn)
workers = Worker.all(conn)
jobs = []
- def add_job(job: Job, name: str) -> None:
+ def add_job(job: 'Job', name: str) -> None:
if job.kwargs.get('site') == frappe.local.site:
job_info = {
'job_name': job.kwargs.get('kwargs', {}).get('playbook_method')
@@ -69,7 +69,7 @@ def get_info(show_failed: bool = False) -> List[Dict]:
@frappe.whitelist()
-def remove_failed_jobs() -> None:
+def remove_failed_jobs():
conn = get_redis_conn()
queues = Queue.all(conn)
for queue in queues:
@@ -80,7 +80,7 @@ def remove_failed_jobs() -> None:
@frappe.whitelist()
-def get_scheduler_status() -> None:
+def get_scheduler_status():
if is_scheduler_inactive():
return [_("Inactive"), "red"]
return [_("Active"), "green"]
From 921fdffa72e0a5af6f3c0246c8a061be314ca0e2 Mon Sep 17 00:00:00 2001
From: Rohan Bansal
Date: Tue, 29 Dec 2020 16:11:03 +0530
Subject: [PATCH 05/83] fix: show remove button conditionally
---
frappe/core/page/background_jobs/background_jobs.js | 7 +++++++
.../core/page/background_jobs/background_jobs_outer.html | 6 ++----
2 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/frappe/core/page/background_jobs/background_jobs.js b/frappe/core/page/background_jobs/background_jobs.js
index 75d97b9411..397f097472 100644
--- a/frappe/core/page/background_jobs/background_jobs.js
+++ b/frappe/core/page/background_jobs/background_jobs.js
@@ -40,6 +40,13 @@ frappe.pages.background_jobs.refresh_jobs = function() {
page.body.find('.list-jobs').remove();
$(frappe.render_template('background_jobs', { jobs: r.message || [] })).appendTo(page.content);
+ let $remove_failed_btn = page.body.find('.remove-failed');
+ if (r.message && r.message.length > 0) {
+ $remove_failed_btn.show();
+ } else {
+ $remove_failed_btn.hide();
+ }
+
if (frappe.get_route()[0] === 'background_jobs') {
frappe.background_jobs_timeout = setTimeout(frappe.pages.background_jobs.refresh_jobs, 2000);
}
diff --git a/frappe/core/page/background_jobs/background_jobs_outer.html b/frappe/core/page/background_jobs/background_jobs_outer.html
index 8c79806005..f57339735a 100644
--- a/frappe/core/page/background_jobs/background_jobs_outer.html
+++ b/frappe/core/page/background_jobs/background_jobs_outer.html
@@ -6,10 +6,8 @@
- {{ __("Remove failed jobs") }}
+ {{ __("Remove failed jobs") }}
-
-
-
+
\ No newline at end of file
From e439c02561ab0fe073bd1587fb01fc88a6af22df Mon Sep 17 00:00:00 2001
From: Rohan Bansal
Date: Wed, 3 Feb 2021 15:22:47 +0530
Subject: [PATCH 06/83] fix: move button to top of the page
---
.../page/background_jobs/background_jobs.js | 18 ++++++++++--------
.../page/background_jobs/background_jobs.py | 6 ++++--
2 files changed, 14 insertions(+), 10 deletions(-)
diff --git a/frappe/core/page/background_jobs/background_jobs.js b/frappe/core/page/background_jobs/background_jobs.js
index bcd938459d..920ee2155e 100644
--- a/frappe/core/page/background_jobs/background_jobs.js
+++ b/frappe/core/page/background_jobs/background_jobs.js
@@ -28,6 +28,16 @@ class BackgroundJobs {
}
});
+ // add a "Remove Failed Jobs button"
+ this.remove_failed_button = this.page.add_inner_button(__("Remove Failed Jobs"), () => {
+ frappe.call({
+ method: 'frappe.core.page.background_jobs.background_jobs.remove_failed_jobs',
+ callback: res => {
+ this.refresh_jobs();
+ }
+ })
+ });
+
$(frappe.render_template('background_jobs_outer')).appendTo(this.page.body);
this.content = $(this.page.body).find('.table-area');
}
@@ -56,14 +66,6 @@ class BackgroundJobs {
this.page.body.find('.list-jobs').remove();
$(frappe.render_template('background_jobs', { jobs: res.message || [] })).appendTo(this.content);
- // add a "Remove Failed Jobs button"
- let $remove_failed_btn = this.body.find('.remove-failed');
- if (res.message && res.message.length > 0) {
- $remove_failed_btn.show();
- } else {
- $remove_failed_btn.hide();
- }
-
if (frappe.get_route()[0] === 'background_jobs') {
setTimeout(() => this.refresh_jobs(), 2000);
}
diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py
index baad40009b..92cc995eb4 100644
--- a/frappe/core/page/background_jobs/background_jobs.py
+++ b/frappe/core/page/background_jobs/background_jobs.py
@@ -1,12 +1,14 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
+import json
from typing import TYPE_CHECKING, Dict, List
+
from rq import Queue, Worker
import frappe
from frappe import _
-from frappe.utils import cint, convert_utc_to_user_timezone, format_datetime
+from frappe.utils import convert_utc_to_user_timezone, format_datetime
from frappe.utils.background_jobs import get_redis_conn
from frappe.utils.scheduler import is_scheduler_inactive
@@ -58,7 +60,7 @@ def get_info(show_failed=False) -> List[Dict]:
add_job(job, queue.name)
# show failed jobs, if requested
- if cint(show_failed):
+ if json.loads(show_failed):
for queue in queues:
fail_registry = queue.failed_job_registry
for job_id in fail_registry.get_job_ids():
From be1c68c07dd9b592fe2cce2abaf33dbc3cd3687d Mon Sep 17 00:00:00 2001
From: Rohan Bansal
Date: Wed, 3 Feb 2021 15:32:37 +0530
Subject: [PATCH 07/83] fix: sider
---
frappe/core/page/background_jobs/background_jobs.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/frappe/core/page/background_jobs/background_jobs.js b/frappe/core/page/background_jobs/background_jobs.js
index 920ee2155e..0b4d6792dc 100644
--- a/frappe/core/page/background_jobs/background_jobs.js
+++ b/frappe/core/page/background_jobs/background_jobs.js
@@ -32,10 +32,10 @@ class BackgroundJobs {
this.remove_failed_button = this.page.add_inner_button(__("Remove Failed Jobs"), () => {
frappe.call({
method: 'frappe.core.page.background_jobs.background_jobs.remove_failed_jobs',
- callback: res => {
+ callback: () => {
this.refresh_jobs();
}
- })
+ });
});
$(frappe.render_template('background_jobs_outer')).appendTo(this.page.body);
From c6d61123b5d50886b9614bf1b43a96eaa6c032b7 Mon Sep 17 00:00:00 2001
From: Rohan Bansal
Date: Thu, 4 Feb 2021 14:26:07 +0530
Subject: [PATCH 08/83] fix: type-check if failed jobs are to be shown
---
.../core/page/background_jobs/background_jobs.py | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py
index 92cc995eb4..847b23bd3e 100644
--- a/frappe/core/page/background_jobs/background_jobs.py
+++ b/frappe/core/page/background_jobs/background_jobs.py
@@ -15,7 +15,7 @@ from frappe.utils.scheduler import is_scheduler_inactive
if TYPE_CHECKING:
from rq.job import Job
-COLORS = {
+JOB_COLORS = {
'queued': 'orange',
'failed': 'red',
'started': 'blue',
@@ -25,6 +25,9 @@ COLORS = {
@frappe.whitelist()
def get_info(show_failed=False) -> List[Dict]:
+ if isinstance(show_failed, str):
+ show_failed = json.loads(show_failed)
+
conn = get_redis_conn()
queues = Queue.all(conn)
workers = Worker.all(conn)
@@ -39,7 +42,7 @@ def get_info(show_failed=False) -> List[Dict]:
'status': job.get_status(),
'queue': name,
'creation': format_datetime(convert_utc_to_user_timezone(job.created_at)),
- 'color': COLORS[job.get_status()]
+ 'color': JOB_COLORS[job.get_status()]
}
if job.exc_info:
@@ -53,15 +56,14 @@ def get_info(show_failed=False) -> List[Dict]:
if job:
add_job(job, worker.name)
- # show active queued jobs
for queue in queues:
+ # show active queued jobs
if queue.name != 'failed':
for job in queue.jobs:
add_job(job, queue.name)
- # show failed jobs, if requested
- if json.loads(show_failed):
- for queue in queues:
+ # show failed jobs, if requested
+ if show_failed:
fail_registry = queue.failed_job_registry
for job_id in fail_registry.get_job_ids():
job = queue.fetch_job(job_id)
From 446f5785a5db301de68adffe9e114b3ebafe0b6f Mon Sep 17 00:00:00 2001
From: Rohan Bansal
Date: Mon, 15 Feb 2021 15:06:14 +0530
Subject: [PATCH 09/83] test: add tests for removing failed jobs
---
frappe/tests/test_background_jobs.py | 31 ++++++++++++++++++++++++++++
1 file changed, 31 insertions(+)
create mode 100644 frappe/tests/test_background_jobs.py
diff --git a/frappe/tests/test_background_jobs.py b/frappe/tests/test_background_jobs.py
new file mode 100644
index 0000000000..5b66183333
--- /dev/null
+++ b/frappe/tests/test_background_jobs.py
@@ -0,0 +1,31 @@
+import unittest
+
+from rq import Queue
+
+import frappe
+from frappe.core.page.background_jobs.background_jobs import remove_failed_jobs
+from frappe.utils.background_jobs import get_redis_conn
+
+
+class TestBackgroundJobs(unittest.TestCase):
+ def test_remove_failed_jobs(self):
+ frappe.enqueue(method="frappe.tests.test_background_jobs.fail_function")
+
+ conn = get_redis_conn()
+ queues = Queue.all(conn)
+
+ for queue in queues:
+ if queue.name == "default":
+ fail_registry = queue.failed_job_registry
+ self.assertGreater(fail_registry.count, 0)
+
+ remove_failed_jobs()
+
+ for queue in queues:
+ if queue.name == "default":
+ fail_registry = queue.failed_job_registry
+ self.assertEqual(fail_registry.count, 0)
+
+
+def fail_function():
+ return 1 / 0
From 983a8c0d8f43156f636e379a7270b22c8f1804ff Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Tue, 16 Feb 2021 10:40:00 +0530
Subject: [PATCH 10/83] feat: Add option to make assignments via assignment
dialog
---
.../js/frappe/form/sidebar/assign_to.js | 47 ++++++++++++++++---
1 file changed, 40 insertions(+), 7 deletions(-)
diff --git a/frappe/public/js/frappe/form/sidebar/assign_to.js b/frappe/public/js/frappe/form/sidebar/assign_to.js
index 6257836bb4..eed92fe067 100644
--- a/frappe/public/js/frappe/form/sidebar/assign_to.js
+++ b/frappe/public/js/frappe/form/sidebar/assign_to.js
@@ -39,8 +39,7 @@ frappe.ui.form.AssignTo = Class.extend({
avatar_group.click(() => {
new frappe.ui.form.AssignmentDialog({
assignments: assigned_users,
- frm: this.frm,
- remove_action: this.remove.bind(this)
+ frm: this.frm
});
});
},
@@ -84,7 +83,7 @@ frappe.ui.form.AssignTo = Class.extend({
frappe.ui.form.AssignToDialog = Class.extend({
- init: function(opts){
+ init: function(opts) {
$.extend(this, opts);
this.make();
@@ -214,7 +213,6 @@ frappe.ui.form.AssignmentDialog = class {
constructor(opts) {
this.frm = opts.frm;
this.assignments = opts.assignments;
- this.remove_action = opts.remove_action;
this.make();
}
@@ -223,6 +221,19 @@ frappe.ui.form.AssignmentDialog = class {
title: __('Assigned To'),
size: 'small',
fields: [{
+ 'label': 'Assign Users',
+ 'fieldname': 'user',
+ 'fieldtype': 'Link',
+ 'options': 'User',
+ 'change': () => {
+ let value = this.dialog.get_value('user');
+ if (this.dialog.get_value('user')) {
+ this.add_assignment(value).then(() => {
+ this.dialog.set_value('user', null);
+ });
+ }
+ }
+ }, {
'fieldtype': 'HTML',
'fieldname': 'assignment_list'
}]
@@ -236,6 +247,26 @@ frappe.ui.form.AssignmentDialog = class {
});
this.dialog.show();
}
+ render(assignments) {
+ this.frm && this.frm.assign_to.render(assignments);
+ }
+ add_assignment(assignment) {
+ return frappe.xcall('frappe.desk.form.assign_to.add', {
+ doctype: this.frm.doctype,
+ name: this.frm.docname,
+ assign_to: [assignment],
+ }).then((assignments) => {
+ this.update_assignment(assignment);
+ this.render(assignments);
+ });
+ }
+ remove_assignment(assignment) {
+ return frappe.xcall('frappe.desk.form.assign_to.remove', {
+ doctype: this.frm.doctype,
+ name: this.frm.docname,
+ assign_to: assignment,
+ });
+ }
update_assignment(assignment) {
this.assignment_list.append(this.get_assignment_row(assignment));
}
@@ -256,10 +287,12 @@ frappe.ui.form.AssignmentDialog = class {
`);
row.find('.remove-btn').click(() => {
- this.remove_action && this.remove_action(assignment);
- row.remove();
+ this.remove_assignment(assignment).then((assignments) => {
+ row.remove();
+ this.render(assignments);
+ });
});
}
return row;
}
-};
\ No newline at end of file
+};
From cbc2dbf5d525d53562ec4330e217bef0537bdf9d Mon Sep 17 00:00:00 2001
From: shariquerik
Date: Wed, 24 Feb 2021 18:23:58 +0530
Subject: [PATCH 11/83] fix: Cannot change Entry Form Type
---
frappe/custom/doctype/customize_form/customize_form.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js
index 79978a49d7..d9d8ae196e 100644
--- a/frappe/custom/doctype/customize_form/customize_form.js
+++ b/frappe/custom/doctype/customize_form/customize_form.js
@@ -76,6 +76,7 @@ frappe.ui.form.on("Customize Form", {
frm.trigger("setup_sortable");
}
}
+ localStorage["customize_doctype"] = frm.doc.doc_type;
}
});
} else {
From a43806ce490f6bf176c02e5dd1dfb86b55e00445 Mon Sep 17 00:00:00 2001
From: Fisher Yu <12823863+szufisher@users.noreply.github.com>
Date: Wed, 24 Feb 2021 22:17:04 +0800
Subject: [PATCH 12/83] fix: same as previous row treated as one doc
treat repeated parent doc rows as same doc(parent), because legacy system downloaded data with inner join will normally have repeat same content for the parent docs
---
frappe/core/doctype/data_import/importer.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py
index dde3dfaee9..a3ae459629 100644
--- a/frappe/core/doctype/data_import/importer.py
+++ b/frappe/core/doctype/data_import/importer.py
@@ -449,8 +449,8 @@ class ImportFile:
data_without_first_row = data[1:]
for row in data_without_first_row:
row_values = row.get_values(parent_column_indexes)
- # if the row is blank, it's a child row doc
- if all([v in INVALID_VALUES for v in row_values]):
+ # if the row is blank or same content as the previous parent row, it's a child row doc
+ if all([v in INVALID_VALUES for v in row_values]) or row_values == parent_row_values:
rows.append(row)
continue
# if we encounter a row which has values in parent columns,
From e8d050a4f1729a42e2d3b0f8219a585c2b7a31af Mon Sep 17 00:00:00 2001
From: shariquerik
Date: Thu, 25 Feb 2021 18:17:35 +0530
Subject: [PATCH 13/83] fix: Email Password popup grammer fix
---
frappe/public/js/frappe/desk.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index e033ae4c5b..54f74e5eed 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -202,7 +202,7 @@ frappe.Application = Class.extend({
email_password_prompt: function(email_account,user,i) {
var me = this;
var d = new frappe.ui.Dialog({
- title: __('Email Account setup please enter your password for: {0}', [email_account[i]["email_id"]]),
+ title: __('Please enter your password for: {0}', [email_account[i]["email_id"]]),
fields: [
{ 'fieldname': 'password',
'fieldtype': 'Password',
From 1aca240a8bd98472aa32c986c5e16f0152ecd72f Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Fri, 26 Feb 2021 11:42:35 +0530
Subject: [PATCH 14/83] fix: UX of assigning a user
---
.../js/frappe/form/sidebar/assign_to.js | 19 +++++++++++++++----
1 file changed, 15 insertions(+), 4 deletions(-)
diff --git a/frappe/public/js/frappe/form/sidebar/assign_to.js b/frappe/public/js/frappe/form/sidebar/assign_to.js
index eed92fe067..4b4b39a65f 100644
--- a/frappe/public/js/frappe/form/sidebar/assign_to.js
+++ b/frappe/public/js/frappe/form/sidebar/assign_to.js
@@ -218,18 +218,26 @@ frappe.ui.form.AssignmentDialog = class {
make() {
this.dialog = new frappe.ui.Dialog({
- title: __('Assigned To'),
+ title: __('Assignments'),
size: 'small',
+ no_focus: true,
fields: [{
- 'label': 'Assign Users',
+ 'label': __('Assign a user'),
'fieldname': 'user',
'fieldtype': 'Link',
'options': 'User',
'change': () => {
let value = this.dialog.get_value('user');
- if (this.dialog.get_value('user')) {
+ if (value && !this.assigning) {
+ this.assigning = true;
+ this.dialog.set_df_property('user', 'read_only', 1);
+ this.dialog.set_df_property('user', 'description', __('Assigning...'));
this.add_assignment(value).then(() => {
this.dialog.set_value('user', null);
+ }).finally(() => {
+ this.dialog.set_df_property('user', 'description', null);
+ this.dialog.set_df_property('user', 'read_only', 0);
+ this.assigning = false;
});
}
}
@@ -268,7 +276,10 @@ frappe.ui.form.AssignmentDialog = class {
});
}
update_assignment(assignment) {
- this.assignment_list.append(this.get_assignment_row(assignment));
+ const in_the_list = this.assignment_list.find(`[data-user="${assignment}"]`).length;
+ if (!in_the_list) {
+ this.assignment_list.append(this.get_assignment_row(assignment));
+ }
}
get_assignment_row(assignment) {
let row = $(`
From 9e5ca78d788be3d54e3797e1668c5fb29ec5e56f Mon Sep 17 00:00:00 2001
From: "Parth J. Kharwar"
Date: Fri, 26 Feb 2021 13:03:42 +0530
Subject: [PATCH 15/83] feat: enables, disables and deletes notification
settings when a user is modified [CU-j8wutt] (#9)
---
frappe/core/doctype/user/user.py | 11 ++++++++++-
.../notification_settings/notification_settings.py | 3 +++
.../doctype/energy_point_log/energy_point_log.py | 5 ++++-
3 files changed, 17 insertions(+), 2 deletions(-)
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 3f19a6ef7b..1e3a25678f 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -8,7 +8,7 @@ from frappe.utils import cint, flt, has_gravatar, escape_html, format_datetime,
from frappe import throw, msgprint, _
from frappe.utils.password import update_password as _update_password, check_password
from frappe.desk.notifications import clear_notifications
-from frappe.desk.doctype.notification_settings.notification_settings import create_notification_settings
+from frappe.desk.doctype.notification_settings.notification_settings import create_notification_settings, enable_disable_notifications
from frappe.utils.user import get_system_managers
from bs4 import BeautifulSoup
import frappe.permissions
@@ -141,11 +141,17 @@ class User(Document):
if not cint(self.enabled):
self.a_system_manager_should_exist()
+ # disable notifications if the user has been disabled
+ enable_disable_notifications(self.name, enabled=False)
# clear sessions if disabled
if not cint(self.enabled) and getattr(frappe.local, "login_manager", None):
frappe.local.login_manager.logout(user=self.name)
+ # enable notifications if the user has been enabled
+ if cint(self.enabled):
+ enable_disable_notifications(self.name, enabled=True)
+
def add_system_manager_role(self):
# if adding system manager, do nothing
if not cint(self.enabled) or ("System Manager" in [user_role.role for user_role in
@@ -358,6 +364,9 @@ class User(Document):
set `user`=null
where `user`=%s""", (self.name))
+ # delete notification settings
+ frappe.delete_doc("Notification Settings", self.name, ignore_permissions=True)
+
def before_rename(self, old_name, new_name, merge=False):
self.check_demo()
diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py
index 34726bdf8a..4b32252ac8 100644
--- a/frappe/desk/doctype/notification_settings/notification_settings.py
+++ b/frappe/desk/doctype/notification_settings/notification_settings.py
@@ -43,6 +43,9 @@ def create_notification_settings(user):
_doc.name = user
_doc.insert(ignore_permissions=True)
+def enable_disable_notifications(user, enabled):
+ frappe.set_value("Notification Settings", user, 'enabled', enabled)
+
@frappe.whitelist()
def get_subscribed_documents():
diff --git a/frappe/social/doctype/energy_point_log/energy_point_log.py b/frappe/social/doctype/energy_point_log/energy_point_log.py
index 21a0e24598..e9425cec86 100644
--- a/frappe/social/doctype/energy_point_log/energy_point_log.py
+++ b/frappe/social/doctype/energy_point_log/energy_point_log.py
@@ -319,7 +319,10 @@ def send_summary(timespan):
from_date = getdate(from_date)
to_date = getdate()
- all_users = [user.email for user in get_enabled_system_users()]
+
+ # select only those users that have energy point email notifications enabled
+ all_users = [user.email for user in get_enabled_system_users() if
+ is_email_notifications_enabled_for_type(user.name, 'Energy Point')]
frappe.sendmail(
subject = '{} energy points summary'.format(timespan),
From a8d11f23d81ef406ebc20fa6d95de3206f137d24 Mon Sep 17 00:00:00 2001
From: Parth Kharwar
Date: Fri, 26 Feb 2021 16:40:09 +0530
Subject: [PATCH 16/83] fix: adds check before enabling notification settings
---
.../doctype/notification_settings/notification_settings.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py
index 4b32252ac8..3ee68c440c 100644
--- a/frappe/desk/doctype/notification_settings/notification_settings.py
+++ b/frappe/desk/doctype/notification_settings/notification_settings.py
@@ -44,7 +44,8 @@ def create_notification_settings(user):
_doc.insert(ignore_permissions=True)
def enable_disable_notifications(user, enabled):
- frappe.set_value("Notification Settings", user, 'enabled', enabled)
+ if frappe.db.exists("Notification Settings", user):
+ frappe.set_value("Notification Settings", user, 'enabled', enabled)
@frappe.whitelist()
@@ -78,4 +79,4 @@ def get_permission_query_conditions(user):
@frappe.whitelist()
def set_seen_value(value, user):
- frappe.db.set_value('Notification Settings', user, 'seen', value, update_modified=False)
\ No newline at end of file
+ frappe.db.set_value('Notification Settings', user, 'seen', value, update_modified=False)
From e630c53083f5c784e1b6abb4913ee6e66f5ed5ed Mon Sep 17 00:00:00 2001
From: prssanna
Date: Mon, 1 Mar 2021 10:44:25 +0530
Subject: [PATCH 17/83] fix: reduce datatable spacing
---
frappe/public/scss/desk/frappe_datatable.scss | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/public/scss/desk/frappe_datatable.scss b/frappe/public/scss/desk/frappe_datatable.scss
index 89e4f6c0f1..3519bab743 100644
--- a/frappe/public/scss/desk/frappe_datatable.scss
+++ b/frappe/public/scss/desk/frappe_datatable.scss
@@ -7,7 +7,7 @@
--dt-text-color: var(--text-light);
--dt-text-light: var(--bg-color);
--dt-spacer-1: 0.25rem;
- --dt-spacer-2: 0.5rem;
+ --dt-spacer-2: var(--padding-xs);
--dt-spacer-3: 1rem;
--dt-border-radius: var(--border-radius);
--dt-cell-bg: var(--fg-color);
From b1a04ed453cd40d7374d2f8b72d482bf4460ca12 Mon Sep 17 00:00:00 2001
From: prssanna
Date: Mon, 1 Mar 2021 10:57:30 +0530
Subject: [PATCH 18/83] fix: reference doctype for saved report
---
frappe/public/js/frappe/list/list_view_select.js | 7 ++++---
frappe/public/js/frappe/views/reports/report_view.js | 4 ++--
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/frappe/public/js/frappe/list/list_view_select.js b/frappe/public/js/frappe/list/list_view_select.js
index 58753b7550..826158ff3f 100644
--- a/frappe/public/js/frappe/list/list_view_select.js
+++ b/frappe/public/js/frappe/list/list_view_select.js
@@ -156,10 +156,11 @@ frappe.views.ListViewSelect = class ListViewSelect {
items.map(item => {
if (item.name.toLowerCase() == page_name.toLowerCase()) {
placeholder = item.name;
+ } else {
+ html += `${
+ item.name
+ } `;
}
- html += `${
- item.name
- } `;
});
}
diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js
index e2e0080e6f..b29b6b87e6 100644
--- a/frappe/public/js/frappe/views/reports/report_view.js
+++ b/frappe/public/js/frappe/views/reports/report_view.js
@@ -1203,11 +1203,11 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
// Rerender the reports dropdown,
// so that this report is included in the dropdown as well.
frappe.boot.user.all_reports[r.message] = {
- ref_doctype: "Item",
+ ref_doctype: this.doctype,
report_type: "Report Builder",
title: r.message,
};
- this.list_sidebar.setup_reports();
+
frappe.set_route('List', this.doctype, 'Report', r.message);
return;
}
From 7865f60cf3ff5fd21a0d0118b363a697f47ae594 Mon Sep 17 00:00:00 2001
From: prssanna
Date: Mon, 1 Mar 2021 15:06:21 +0530
Subject: [PATCH 19/83] fix: save selected print format
---
frappe/printing/page/print/print.js | 5 ++++-
.../page/print_format_builder/print_format_builder.js | 1 +
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js
index 0ae8786e95..1d361c8f81 100644
--- a/frappe/printing/page/print/print.js
+++ b/frappe/printing/page/print/print.js
@@ -641,10 +641,13 @@ frappe.ui.form.PrintView = class {
refresh_print_options() {
this.print_formats = frappe.meta.get_print_formats(this.frm.doctype);
- return this.print_sel.empty().add_options([
+ const print_format_select_val = this.print_sel.val();
+ this.print_sel.empty().add_options([
this.get_default_option_for_select(__('Select Print Format')),
...this.print_formats
]);
+ return this.print_formats.includes(print_format_select_val)
+ && this.print_sel.val(print_format_select_val);
}
selected_format() {
diff --git a/frappe/printing/page/print_format_builder/print_format_builder.js b/frappe/printing/page/print_format_builder/print_format_builder.js
index eb87190ab5..7e58e295b5 100644
--- a/frappe/printing/page/print_format_builder/print_format_builder.js
+++ b/frappe/printing/page/print_format_builder/print_format_builder.js
@@ -784,6 +784,7 @@ frappe.PrintFormatBuilder = Class.extend({
btn: this.page.btn_primary,
callback: function(r) {
me.print_format = r.message;
+ locals['Print Format'][me.print_format.name] = r.message;
frappe.show_alert({message: __("Saved"), indicator: 'green'});
}
});
From 724bd729040984b49bababcce06d630334051460 Mon Sep 17 00:00:00 2001
From: prssanna
Date: Mon, 1 Mar 2021 15:07:15 +0530
Subject: [PATCH 20/83] fix: render fieldname if no label on print format
builder sidebar
---
.../page/print_format_builder/print_format_builder_sidebar.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html b/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html
index 0cf8178f82..1ebb87ac31 100644
--- a/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html
+++ b/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html
@@ -13,7 +13,7 @@
- {%= __(f.label) %}
+ {%= __(f.label) || __(f.fieldname) %}
{% } %}
From 358a9fabea0388beeabb9267aad9f233d549aa66 Mon Sep 17 00:00:00 2001
From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
Date: Mon, 1 Mar 2021 16:43:18 +0530
Subject: [PATCH 21/83] fix: max_old_space_size limit for node processes
(#12494)
Co-authored-by: Gavin D'souza
---
frappe/build.py | 24 ++++++++++++++++++++++--
frappe/commands/__init__.py | 19 ++++++++++++-------
requirements.txt | 1 +
3 files changed, 35 insertions(+), 9 deletions(-)
diff --git a/frappe/build.py b/frappe/build.py
index c1c807c8db..baedb633b6 100644
--- a/frappe/build.py
+++ b/frappe/build.py
@@ -15,6 +15,7 @@ import frappe
from frappe.utils.minify import JavascriptMinify
import click
+import psutil
from six import iteritems, text_type
from six.moves.urllib.parse import urlparse
@@ -226,7 +227,7 @@ def bundle(no_compress, app=None, make_copy=False, restore=False, verbose=False,
frappe_app_path = os.path.abspath(os.path.join(app_paths[0], ".."))
check_yarn()
- frappe.commands.popen(command, cwd=frappe_app_path)
+ frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env())
def watch(no_compress):
@@ -238,13 +239,32 @@ def watch(no_compress):
frappe_app_path = os.path.abspath(os.path.join(app_paths[0], ".."))
check_yarn()
frappe_app_path = frappe.get_app_path("frappe", "..")
- frappe.commands.popen("{pacman} run watch".format(pacman=pacman), cwd=frappe_app_path)
+ frappe.commands.popen("{pacman} run watch".format(pacman=pacman),
+ cwd=frappe_app_path, env=get_node_env())
def check_yarn():
if not find_executable("yarn"):
print("Please install yarn using below command and try again.\nnpm install -g yarn")
+def get_node_env():
+ node_env = {
+ "NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}"
+ }
+ return node_env
+
+def get_safe_max_old_space_size():
+ safe_max_old_space_size = 0
+ try:
+ total_memory = psutil.virtual_memory().total / (1024 * 1024)
+ # reference for the safe limit assumption
+ # https://nodejs.org/api/cli.html#cli_max_old_space_size_size_in_megabytes
+ # set minimum value 1GB
+ safe_max_old_space_size = max(1024, int(total_memory * 0.75))
+ except Exception:
+ pass
+
+ return safe_max_old_space_size
def make_asset_dirs(make_copy=False, restore=False):
# don't even think of making assets_path absolute - rm -rf ahead.
diff --git a/frappe/commands/__init__.py b/frappe/commands/__init__.py
index b7294fff77..b9ae02e112 100644
--- a/frappe/commands/__init__.py
+++ b/frappe/commands/__init__.py
@@ -11,6 +11,7 @@ import frappe.utils
import subprocess # nosec
from functools import wraps
from six import StringIO
+from os import environ
click.disable_unicode_literals_warning = True
@@ -53,16 +54,20 @@ def get_site(context, raise_err=True):
return None
def popen(command, *args, **kwargs):
- output = kwargs.get('output', True)
- cwd = kwargs.get('cwd')
- shell = kwargs.get('shell', True)
+ output = kwargs.get('output', True)
+ cwd = kwargs.get('cwd')
+ shell = kwargs.get('shell', True)
raise_err = kwargs.get('raise_err')
+ env = kwargs.get('env')
+ if env:
+ env = dict(environ, **env)
proc = subprocess.Popen(command,
- stdout = None if output else subprocess.PIPE,
- stderr = None if output else subprocess.PIPE,
- shell = shell,
- cwd = cwd
+ stdout=None if output else subprocess.PIPE,
+ stderr=None if output else subprocess.PIPE,
+ shell=shell,
+ cwd=cwd,
+ env=env
)
return_ = proc.wait()
diff --git a/requirements.txt b/requirements.txt
index 1764834d8e..0f88a48f73 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -38,6 +38,7 @@ passlib==1.7.3
pdfkit==0.6.1
Pillow>=8.0.0
premailer==3.6.1
+psutil==5.7.2
psycopg2-binary==2.8.4
pyasn1==0.4.8
PyJWT==1.7.1
From 297d4c668e911c0b90a23fa86ba3f630856a7420 Mon Sep 17 00:00:00 2001
From: prssanna
Date: Tue, 2 Mar 2021 13:45:09 +0530
Subject: [PATCH 22/83] fix: print format custom css description
---
frappe/printing/doctype/print_format/print_format.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json
index 92d4a67d14..4032cef209 100644
--- a/frappe/printing/doctype/print_format/print_format.json
+++ b/frappe/printing/doctype/print_format/print_format.json
@@ -171,7 +171,7 @@
"fieldname": "custom_html_help",
"fieldtype": "HTML",
"label": "Custom HTML Help",
- "options": "Custom CSS Help \n\nNotes:
\n\n\nAll field groups (label + value) are set attributes data-fieldtype and data-fieldname \nAll values are given class value \nAll Section Breaks are given class section-break \nAll Column Breaks are given class column-break \n \n\nExamples \n\n1. Left align integers
\n\n[data-fieldtype=\"Int\"] .value { text-left: left; } \n\n1. Add border to sections except the last section
\n\n.section-break { padding: 30px 0px; border-bottom: 1px solid #eee; }\n.section-break:last-child { padding-bottom: 0px; border-bottom: 0px; } \n"
+ "options": "Custom CSS Help \n\nNotes:
\n\n\nAll field groups (label + value) are set attributes data-fieldtype and data-fieldname \nAll values are given class value \nAll Section Breaks are given class section-break \nAll Column Breaks are given class column-break \n \n\nExamples \n\n1. Left align integers
\n\n[data-fieldtype=\"Int\"] .value { text-align: left; } \n\n1. Add border to sections except the last section
\n\n.section-break { padding: 30px 0px; border-bottom: 1px solid #eee; }\n.section-break:last-child { padding-bottom: 0px; border-bottom: 0px; } \n"
},
{
"depends_on": "custom_format",
@@ -211,7 +211,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-12-14 11:38:49.132061",
+ "modified": "2021-03-01 15:25:46.578863",
"modified_by": "Administrator",
"module": "Printing",
"name": "Print Format",
From d5980b345f23a8a118ca16869a766d54fb5a8239 Mon Sep 17 00:00:00 2001
From: prssanna
Date: Tue, 2 Mar 2021 13:45:34 +0530
Subject: [PATCH 23/83] fix: set selected value in print format select
---
frappe/printing/page/print/print.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js
index 1d361c8f81..dfd93c4efa 100644
--- a/frappe/printing/page/print/print.js
+++ b/frappe/printing/page/print/print.js
@@ -269,6 +269,7 @@ frappe.ui.form.PrintView = class {
based_on: data.based_on,
};
frappe.set_route('print-format-builder');
+ this.print_sel.val(data.print_format_name);
},
__('New Custom Print Format'),
__('Start')
From 8488272743c82846f769172147b666f4813fe930 Mon Sep 17 00:00:00 2001
From: prssanna
Date: Tue, 2 Mar 2021 13:46:03 +0530
Subject: [PATCH 24/83] fix: typo
---
frappe/public/js/frappe/form/grid.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js
index 3bde525fff..6b125f3da1 100644
--- a/frappe/public/js/frappe/form/grid.js
+++ b/frappe/public/js/frappe/form/grid.js
@@ -649,7 +649,7 @@ export default class Grid {
duplicate_row(d, copy_doc) {
$.each(copy_doc, function (key, value) {
if (!["creation", "modified", "modified_by", "idx", "owner",
- "parent", "doctype", "name", "parentield"].includes(key)) {
+ "parent", "doctype", "name", "parentfield"].includes(key)) {
d[key] = value;
}
});
From afce76d4e144180a9e6baa973d702a304e95c860 Mon Sep 17 00:00:00 2001
From: prssanna
Date: Tue, 2 Mar 2021 13:46:45 +0530
Subject: [PATCH 25/83] fix: also support primary_action_label as a key
---
frappe/public/js/frappe/ui/messages.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/ui/messages.js b/frappe/public/js/frappe/ui/messages.js
index 36bd5dd42d..a4b1ad9f79 100644
--- a/frappe/public/js/frappe/ui/messages.js
+++ b/frappe/public/js/frappe/ui/messages.js
@@ -200,7 +200,7 @@ frappe.msgprint = function(msg, title, is_minimizable) {
}
frappe.msg_dialog.set_primary_action(
- __(data.primary_action.label || "Done"),
+ __(data.primary_action.label || data.primary_action_label || "Done"),
data.primary_action.action
);
} else {
From b340f955f9e60e599d95dd0875d1218c418d8387 Mon Sep 17 00:00:00 2001
From: prssanna
Date: Tue, 2 Mar 2021 13:47:10 +0530
Subject: [PATCH 26/83] fix: pre style
---
frappe/public/scss/desk/global.scss | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/frappe/public/scss/desk/global.scss b/frappe/public/scss/desk/global.scss
index b09d9146ae..f4d6384352 100644
--- a/frappe/public/scss/desk/global.scss
+++ b/frappe/public/scss/desk/global.scss
@@ -102,7 +102,8 @@ a.badge-hover {
}
pre {
- color: var(--text-light)
+ color: var(--text-light);
+ white-space: pre-wrap;
}
.col-xs-1 { @extend .col-1; }
From 50f0a57188c5920672a3e764054d1a690ccd9200 Mon Sep 17 00:00:00 2001
From: prssanna
Date: Tue, 2 Mar 2021 13:47:25 +0530
Subject: [PATCH 27/83] fix: select in datatable style
---
frappe/public/scss/desk/frappe_datatable.scss | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/frappe/public/scss/desk/frappe_datatable.scss b/frappe/public/scss/desk/frappe_datatable.scss
index 3519bab743..7e4e5b0ef8 100644
--- a/frappe/public/scss/desk/frappe_datatable.scss
+++ b/frappe/public/scss/desk/frappe_datatable.scss
@@ -26,6 +26,16 @@
border-radius: 0px;
border: none;
background-color: transparent;
+ height: 100%;
+
+ &:focus {
+ box-shadow: none;
+ }
+
+ &[data-fieldtype="Select"] .select-icon {
+ top: 5px;
+ right: 10px;
+ }
}
.dt-header {
From d498e0f245c48629dfea7fc1180dfe07ccba94e8 Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Tue, 2 Mar 2021 14:11:41 +0530
Subject: [PATCH 28/83] fix: Primary button text color
---
frappe/public/scss/common/buttons.scss | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/frappe/public/scss/common/buttons.scss b/frappe/public/scss/common/buttons.scss
index c8cd374937..591dc5bba6 100644
--- a/frappe/public/scss/common/buttons.scss
+++ b/frappe/public/scss/common/buttons.scss
@@ -78,8 +78,9 @@
.btn.btn-primary {
background-color: var(--primary-color);
+ color: var(--white);
white-space: nowrap;
- --icon-stroke: white;
+ --icon-stroke: currentColor;
--icon-fill-bg: var(--primary-color);
}
From ba2a85965c61a3b6b3b0228cfecc32018192f828 Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Tue, 2 Mar 2021 14:16:03 +0530
Subject: [PATCH 29/83] fix: Reduce btn-focus-outline width
---
frappe/public/scss/desk/variables.scss | 1 +
1 file changed, 1 insertion(+)
diff --git a/frappe/public/scss/desk/variables.scss b/frappe/public/scss/desk/variables.scss
index 4f43f22b9d..633d10b524 100644
--- a/frappe/public/scss/desk/variables.scss
+++ b/frappe/public/scss/desk/variables.scss
@@ -65,6 +65,7 @@ $input-color: var(--text-color);
$input-box-shadow: none;
$input-focus-border-color: var(--gray-500);
$input-border-radius: var(--border-radius);
+$input-btn-focus-width: 2px;
// dropdown
$dropdown-color: var(--text-color);
From 9609462f6d8633d966908811fde83c945e33b329 Mon Sep 17 00:00:00 2001
From: prssanna
Date: Tue, 2 Mar 2021 14:20:09 +0530
Subject: [PATCH 30/83] fix: strip html from kanban title
---
frappe/public/js/frappe/views/kanban/kanban_board.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/views/kanban/kanban_board.js b/frappe/public/js/frappe/views/kanban/kanban_board.js
index 8711deb4b4..e51a79ad7a 100644
--- a/frappe/public/js/frappe/views/kanban/kanban_board.js
+++ b/frappe/public/js/frappe/views/kanban/kanban_board.js
@@ -605,7 +605,7 @@ frappe.provide("frappe.views");
function make_dom() {
var opts = {
name: card.name,
- title: remove_img_tags(card.title),
+ title: frappe.utils.html2text(card.title),
disable_click: card._disable_click ? 'disable-click' : '',
creation: card.creation,
};
From f443c3100da17653bacc1e05e63c151ccc3e464c Mon Sep 17 00:00:00 2001
From: prssanna
Date: Tue, 2 Mar 2021 14:20:37 +0530
Subject: [PATCH 31/83] fix: strip html from email subject
---
frappe/public/js/frappe/views/communication.js | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js
index 82e3d2aa0e..a5f078fc7d 100755
--- a/frappe/public/js/frappe/views/communication.js
+++ b/frappe/public/js/frappe/views/communication.js
@@ -203,7 +203,9 @@ frappe.views.CommunicationComposer = Class.extend({
if(this.dialog.fields_dict.sender) {
this.dialog.fields_dict.sender.set_value(this.sender || '');
}
- this.dialog.fields_dict.subject.set_value(this.subject || '');
+ this.dialog.fields_dict.subject.set_value(
+ frappe.utils.html2text(this.subject) || ''
+ );
this.setup_earlier_reply();
},
From 0ef74e3681e1818aa2b83cab09b1104a088d61c3 Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Tue, 2 Mar 2021 15:38:27 +0530
Subject: [PATCH 32/83] fix: Breadcrumb arrow color in dark theme
---
frappe/public/icons/timeless/icon-right-arrow.svg | 3 ---
frappe/public/scss/desk/breadcrumb.scss | 2 +-
frappe/public/scss/desk/css_variables.scss | 3 +++
frappe/public/scss/desk/dark.scss | 2 ++
4 files changed, 6 insertions(+), 4 deletions(-)
delete mode 100644 frappe/public/icons/timeless/icon-right-arrow.svg
diff --git a/frappe/public/icons/timeless/icon-right-arrow.svg b/frappe/public/icons/timeless/icon-right-arrow.svg
deleted file mode 100644
index 1e044d0e4d..0000000000
--- a/frappe/public/icons/timeless/icon-right-arrow.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/frappe/public/scss/desk/breadcrumb.scss b/frappe/public/scss/desk/breadcrumb.scss
index 9bccb4b5a9..6324e6f012 100644
--- a/frappe/public/scss/desk/breadcrumb.scss
+++ b/frappe/public/scss/desk/breadcrumb.scss
@@ -12,7 +12,7 @@
font-size: var(--text-md);
margin-right: 10px;
&:before {
- content: url('/assets/frappe/icons/timeless/icon-right-arrow.svg');
+ content: var(--right-arrow-svg);
display: inline-block;
margin-right: 10px;
}
diff --git a/frappe/public/scss/desk/css_variables.scss b/frappe/public/scss/desk/css_variables.scss
index 735165ba6f..21b4ac6c1d 100644
--- a/frappe/public/scss/desk/css_variables.scss
+++ b/frappe/public/scss/desk/css_variables.scss
@@ -50,6 +50,7 @@ $input-height: 28px !default;
// input
--input-height: #{$input-height};
+ --input-disabled-bg: var(--gray-200);
// timeline
--timeline-item-icon-size: 34px;
@@ -60,4 +61,6 @@ $input-height: 28px !default;
// skeleton
--skeleton-bg: var(--gray-100);
+
+ --right-arrow-svg: url("data: image/svg+xml;utf8, ");
}
diff --git a/frappe/public/scss/desk/dark.scss b/frappe/public/scss/desk/dark.scss
index 3639a07664..e86505fcae 100644
--- a/frappe/public/scss/desk/dark.scss
+++ b/frappe/public/scss/desk/dark.scss
@@ -142,4 +142,6 @@
// skeleton
--skeleton-bg: var(--gray-800);
+
+ --right-arrow-svg: url("data: image/svg+xml;utf8, ");
}
From 8eddd49ea7bb9fcd691b0a6045e86e7f8c45e4b0 Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Tue, 2 Mar 2021 15:38:55 +0530
Subject: [PATCH 33/83] fix: Disabled input color in dark mode
---
frappe/public/scss/desk/dark.scss | 3 +++
frappe/public/scss/desk/variables.scss | 4 ++--
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/frappe/public/scss/desk/dark.scss b/frappe/public/scss/desk/dark.scss
index e86505fcae..7bbf582af0 100644
--- a/frappe/public/scss/desk/dark.scss
+++ b/frappe/public/scss/desk/dark.scss
@@ -72,6 +72,9 @@
--highlight-color: var(--gray-700);
--yellow-highlight-color: var(--yellow-700);
+ // input
+ --input-disabled-bg: none;
+
.frappe-card {
.btn-default {
background-color: var(--bg-color);
diff --git a/frappe/public/scss/desk/variables.scss b/frappe/public/scss/desk/variables.scss
index 633d10b524..57a117cd31 100644
--- a/frappe/public/scss/desk/variables.scss
+++ b/frappe/public/scss/desk/variables.scss
@@ -60,7 +60,7 @@ $link-color: var(--text-color);
// input
$input-bg: var(--control-bg);
$input-placeholder-color: var(--gray-500);
-$input-disabled-bg: var(--gray-200);
+$input-disabled-bg: var(--input-disabled-bg);
$input-color: var(--text-color);
$input-box-shadow: none;
$input-focus-border-color: var(--gray-500);
@@ -135,9 +135,9 @@ $grid-breakpoints: (
2xl: 1440px
) !default;
-@import 'dark';
@import 'typography';
@import '~bootstrap/scss/functions';
@import '~bootstrap/scss/variables';
@import "~bootstrap/scss/mixins";
@import 'css_variables';
+@import 'dark';
From d9912538495648aa1ade1865084aa736a8abf911 Mon Sep 17 00:00:00 2001
From: prssanna
Date: Tue, 2 Mar 2021 18:32:51 +0530
Subject: [PATCH 34/83] fix: datepicker dark theme style
---
frappe/public/scss/common/datepicker.scss | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/frappe/public/scss/common/datepicker.scss b/frappe/public/scss/common/datepicker.scss
index 8e709a130e..1205391a4b 100644
--- a/frappe/public/scss/common/datepicker.scss
+++ b/frappe/public/scss/common/datepicker.scss
@@ -4,6 +4,14 @@
font-family: inherit;
z-index: 9999 !important;
+ background: var(--bg-color);
+ color: var(--text-color);
+ border-radius: var(--border-radius);
+ border: 1px solid var(--border-color);
+
+ &--nav {
+ border-bottom: 1px solid var(--border-color);
+ }
&--time-current-hours, &--time-current-minutes, &--time-current-seconds {
font-family: inherit;
@@ -45,6 +53,10 @@
}
+ &--time, &--buttons {
+ border-top: 1px solid var(--border-color);;
+ }
+
&--time-row {
background-image: linear-gradient(to right, #0089FF, #0089FF);
background-repeat: no-repeat;
From 0da80778472f80b53d634352c70847f1547d210c Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 2 Mar 2021 18:33:13 +0530
Subject: [PATCH 35/83] fix: track setting changes
---
frappe/core/doctype/sms_settings/sms_settings.json | 6 +++---
.../doctype/contact_us_settings/contact_us_settings.json | 5 +++--
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/frappe/core/doctype/sms_settings/sms_settings.json b/frappe/core/doctype/sms_settings/sms_settings.json
index 3bb89604af..073fb88bc7 100755
--- a/frappe/core/doctype/sms_settings/sms_settings.json
+++ b/frappe/core/doctype/sms_settings/sms_settings.json
@@ -202,7 +202,7 @@
"issingle": 1,
"istable": 0,
"max_attachments": 0,
- "modified": "2017-11-01 12:57:20.943845",
+ "modified": "2021-03-02 18:06:00.868688",
"modified_by": "Administrator",
"module": "Core",
"name": "SMS Settings",
@@ -233,6 +233,6 @@
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
- "track_changes": 0,
+ "track_changes": 1,
"track_seen": 0
-}
\ No newline at end of file
+}
diff --git a/frappe/website/doctype/contact_us_settings/contact_us_settings.json b/frappe/website/doctype/contact_us_settings/contact_us_settings.json
index 4e7d1cff1b..5736e6d799 100644
--- a/frappe/website/doctype/contact_us_settings/contact_us_settings.json
+++ b/frappe/website/doctype/contact_us_settings/contact_us_settings.json
@@ -121,7 +121,7 @@
"idx": 1,
"issingle": 1,
"links": [],
- "modified": "2020-04-06 19:17:46.083764",
+ "modified": "2021-03-02 17:42:32.947404",
"modified_by": "Administrator",
"module": "Website",
"name": "Contact Us Settings",
@@ -138,5 +138,6 @@
}
],
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
From 6b903e2c6c1f67291b5d1745c58c81249be65acc Mon Sep 17 00:00:00 2001
From: prssanna
Date: Tue, 2 Mar 2021 18:34:08 +0530
Subject: [PATCH 36/83] fix: explicit call to .val to set first option in
select
---
frappe/public/js/frappe/form/controls/select.js | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/frappe/public/js/frappe/form/controls/select.js b/frappe/public/js/frappe/form/controls/select.js
index 026fbbf1e7..c1459d3a28 100644
--- a/frappe/public/js/frappe/form/controls/select.js
+++ b/frappe/public/js/frappe/form/controls/select.js
@@ -105,6 +105,7 @@ frappe.ui.form.ControlSelect = frappe.ui.form.ControlData.extend({
(function($) {
$.fn.add_options = function(options_list) {
// create options
+ let first_val;
for(var i=0, j=options_list.length; i').html(cstr(label))
.attr('value', value)
.prop('disabled', is_disabled)
@@ -129,6 +133,7 @@ frappe.ui.form.ControlSelect = frappe.ui.form.ControlData.extend({
}
// select the first option
this.selectedIndex = 0;
+ $(this).val(first_val);
return $(this);
};
$.fn.set_working = function() {
From 3cfb57cec1c7730188008066cd65823e68a06684 Mon Sep 17 00:00:00 2001
From: prssanna
Date: Tue, 2 Mar 2021 18:35:59 +0530
Subject: [PATCH 37/83] fix: don't show disabled users in leaderboard
---
frappe/desk/leaderboard.py | 18 ++++++++++--------
1 file changed, 10 insertions(+), 8 deletions(-)
diff --git a/frappe/desk/leaderboard.py b/frappe/desk/leaderboard.py
index 8d00ea9bc2..2a981f061b 100644
--- a/frappe/desk/leaderboard.py
+++ b/frappe/desk/leaderboard.py
@@ -16,8 +16,18 @@ def get_leaderboards():
@frappe.whitelist()
def get_energy_point_leaderboard(date_range, company = None, field = None, limit = None):
+ all_users = frappe.db.get_all('User',
+ filters = {
+ 'name': ['not in', ['Administrator', 'Guest']],
+ 'enabled': 1,
+ 'user_type': ['!=', 'Website User']
+ },
+ order_by = 'name ASC')
+ all_users_list = list(map(lambda x: x['name'], all_users))
+
filters = [
['type', '!=', 'Review'],
+ ['user', 'in', all_users_list]
]
if date_range:
date_range = frappe.parse_json(date_range)
@@ -28,15 +38,7 @@ def get_energy_point_leaderboard(date_range, company = None, field = None, limit
group_by = 'user',
order_by = 'value desc'
)
- all_users = frappe.db.get_all('User',
- filters = {
- 'name': ['not in', ['Administrator', 'Guest']],
- 'enabled': 1,
- 'user_type': ['!=', 'Website User']
- },
- order_by = 'name ASC')
- all_users_list = list(map(lambda x: x['name'], all_users))
energy_point_users_list = list(map(lambda x: x['name'], energy_point_users))
for user in all_users_list:
if user not in energy_point_users_list:
From 817af23daa93b98119ed4750584fcc81fcbf90b1 Mon Sep 17 00:00:00 2001
From: prssanna
Date: Tue, 2 Mar 2021 18:39:00 +0530
Subject: [PATCH 38/83] style: fix formatting
---
frappe/public/scss/common/datepicker.scss | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/public/scss/common/datepicker.scss b/frappe/public/scss/common/datepicker.scss
index 1205391a4b..870ef5783d 100644
--- a/frappe/public/scss/common/datepicker.scss
+++ b/frappe/public/scss/common/datepicker.scss
@@ -54,7 +54,7 @@
}
&--time, &--buttons {
- border-top: 1px solid var(--border-color);;
+ border-top: 1px solid var(--border-color);
}
&--time-row {
From 4f7512893bce118432ff62627f3cc3b1ca272133 Mon Sep 17 00:00:00 2001
From: prssanna
Date: Tue, 2 Mar 2021 19:11:53 +0530
Subject: [PATCH 39/83] fix: trigger select-change instead of setting first
value
---
frappe/public/js/frappe/form/controls/select.js | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/frappe/public/js/frappe/form/controls/select.js b/frappe/public/js/frappe/form/controls/select.js
index c1459d3a28..2ea32e032c 100644
--- a/frappe/public/js/frappe/form/controls/select.js
+++ b/frappe/public/js/frappe/form/controls/select.js
@@ -105,7 +105,6 @@ frappe.ui.form.ControlSelect = frappe.ui.form.ControlData.extend({
(function($) {
$.fn.add_options = function(options_list) {
// create options
- let first_val;
for(var i=0, j=options_list.length; i').html(cstr(label))
.attr('value', value)
.prop('disabled', is_disabled)
@@ -133,7 +130,7 @@ frappe.ui.form.ControlSelect = frappe.ui.form.ControlData.extend({
}
// select the first option
this.selectedIndex = 0;
- $(this).val(first_val);
+ $(this).trigger('select-change');
return $(this);
};
$.fn.set_working = function() {
From c098ed069ae2abce3e5b641786b7af51a0d449f4 Mon Sep 17 00:00:00 2001
From: Valmik
Date: Thu, 4 Mar 2021 11:43:32 +0000
Subject: [PATCH 40/83] fix: use db.set_value instead of set_value to avoid
permission issues
---
.../desk/doctype/notification_settings/notification_settings.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py
index 3ee68c440c..0831f4683d 100644
--- a/frappe/desk/doctype/notification_settings/notification_settings.py
+++ b/frappe/desk/doctype/notification_settings/notification_settings.py
@@ -45,7 +45,7 @@ def create_notification_settings(user):
def enable_disable_notifications(user, enabled):
if frappe.db.exists("Notification Settings", user):
- frappe.set_value("Notification Settings", user, 'enabled', enabled)
+ frappe.db.set_value("Notification Settings", user, 'enabled', enabled)
@frappe.whitelist()
From e8090889ce64c55a0bd293656ace0b057cc61812 Mon Sep 17 00:00:00 2001
From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
Date: Thu, 4 Mar 2021 19:44:53 +0530
Subject: [PATCH 41/83] fix: Fix naming
Co-authored-by: Sagar Vora
---
frappe/public/js/frappe/form/controls/attach.js | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/frappe/public/js/frappe/form/controls/attach.js b/frappe/public/js/frappe/form/controls/attach.js
index ba058ccf8e..b4665775db 100644
--- a/frappe/public/js/frappe/form/controls/attach.js
+++ b/frappe/public/js/frappe/form/controls/attach.js
@@ -81,10 +81,11 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({
this.$input.toggle(false);
// value can also be using this format: FILENAME,DATA_URL
// Important: We have to be careful because normal filenames may also contain ","
- let two_part_matches = this.value.match(/^([^:]+),(.+):(.+)$/);
- if (two_part_matches !== null) {
- let filename = two_part_matches[1];
- dataurl = two_part_matches[2] + ':' + two_part_matches[3];
+ let file_url_parts = this.value.match(/^([^:]+),(.+):(.+)$/);
+ let filename;
+ if (!file_url_parts) {
+ filename = file_url_parts[1];
+ dataurl = file_url_parts[2] + ':' + file_url_parts[3];
}
this.$value.toggle(true).find(".attached-file-link")
.html(filename || this.value)
From e586badf3bf51b47d821a8a8e623c2c8bb1c922b Mon Sep 17 00:00:00 2001
From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
Date: Thu, 4 Mar 2021 19:47:14 +0530
Subject: [PATCH 42/83] fix: Check logic
---
frappe/public/js/frappe/form/controls/attach.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/form/controls/attach.js b/frappe/public/js/frappe/form/controls/attach.js
index b4665775db..604510bb52 100644
--- a/frappe/public/js/frappe/form/controls/attach.js
+++ b/frappe/public/js/frappe/form/controls/attach.js
@@ -83,7 +83,7 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({
// Important: We have to be careful because normal filenames may also contain ","
let file_url_parts = this.value.match(/^([^:]+),(.+):(.+)$/);
let filename;
- if (!file_url_parts) {
+ if (file_url_parts) {
filename = file_url_parts[1];
dataurl = file_url_parts[2] + ':' + file_url_parts[3];
}
From 9501eddcf116a2677a12a5b3ee3dc218cd3a9891 Mon Sep 17 00:00:00 2001
From: walstanb
Date: Fri, 5 Mar 2021 12:16:58 +0530
Subject: [PATCH 43/83] fix: allows navigating back from dashboard
---
frappe/core/page/dashboard_view/dashboard_view.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/frappe/core/page/dashboard_view/dashboard_view.js b/frappe/core/page/dashboard_view/dashboard_view.js
index 686d11c6bf..e8e9cc9502 100644
--- a/frappe/core/page/dashboard_view/dashboard_view.js
+++ b/frappe/core/page/dashboard_view/dashboard_view.js
@@ -36,17 +36,17 @@ class Dashboard {
} else {
// last opened
if (frappe.last_dashboard) {
- frappe.set_route('dashboard-view', frappe.last_dashboard);
+ frappe.set_re_route('dashboard-view', frappe.last_dashboard);
} else {
// default dashboard
frappe.db.get_list('Dashboard', {filters: {is_default: 1}}).then(data => {
if (data && data.length) {
- frappe.set_route('dashboard-view', data[0].name);
+ frappe.set_re_route('dashboard-view', data[0].name);
} else {
// no default, get the latest one
frappe.db.get_list('Dashboard', {limit: 1}).then(data => {
if (data && data.length) {
- frappe.set_route('dashboard-view', data[0].name);
+ frappe.set_re_route('dashboard-view', data[0].name);
} else {
// create a new dashboard!
frappe.new_doc('Dashboard');
From 0f42096e87fde34497c7802910a709e255c2a5aa Mon Sep 17 00:00:00 2001
From: Parth Kharwar
Date: Fri, 5 Mar 2021 13:13:03 +0530
Subject: [PATCH 44/83] refactor: updates notification toggle function name and
if logic
---
frappe/core/doctype/user/user.py | 11 +++++------
.../notification_settings/notification_settings.py | 5 +++--
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 1e3a25678f..23ebe7d6b1 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -8,7 +8,7 @@ from frappe.utils import cint, flt, has_gravatar, escape_html, format_datetime,
from frappe import throw, msgprint, _
from frappe.utils.password import update_password as _update_password, check_password
from frappe.desk.notifications import clear_notifications
-from frappe.desk.doctype.notification_settings.notification_settings import create_notification_settings, enable_disable_notifications
+from frappe.desk.doctype.notification_settings.notification_settings import create_notification_settings, toggle_notifications
from frappe.utils.user import get_system_managers
from bs4 import BeautifulSoup
import frappe.permissions
@@ -142,16 +142,15 @@ class User(Document):
if not cint(self.enabled):
self.a_system_manager_should_exist()
# disable notifications if the user has been disabled
- enable_disable_notifications(self.name, enabled=False)
+ toggle_notifications(self.name, enable=False)
+ else:
+ # enable notifications if the user has been enabled
+ toggle_notifications(self.name, enable=True)
# clear sessions if disabled
if not cint(self.enabled) and getattr(frappe.local, "login_manager", None):
frappe.local.login_manager.logout(user=self.name)
- # enable notifications if the user has been enabled
- if cint(self.enabled):
- enable_disable_notifications(self.name, enabled=True)
-
def add_system_manager_role(self):
# if adding system manager, do nothing
if not cint(self.enabled) or ("System Manager" in [user_role.role for user_role in
diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py
index 0831f4683d..4ab40bffe9 100644
--- a/frappe/desk/doctype/notification_settings/notification_settings.py
+++ b/frappe/desk/doctype/notification_settings/notification_settings.py
@@ -43,9 +43,10 @@ def create_notification_settings(user):
_doc.name = user
_doc.insert(ignore_permissions=True)
-def enable_disable_notifications(user, enabled):
+
+def toggle_notifications(user, enable=False):
if frappe.db.exists("Notification Settings", user):
- frappe.db.set_value("Notification Settings", user, 'enabled', enabled)
+ frappe.db.set_value("Notification Settings", user, 'enabled', enable)
@frappe.whitelist()
From 7effd9db7a4bc808a7341d13ac2ad69e46d85034 Mon Sep 17 00:00:00 2001
From: Parth Kharwar
Date: Fri, 5 Mar 2021 14:35:34 +0530
Subject: [PATCH 45/83] refactor: moves notification toggle to individual code
block
---
frappe/core/doctype/user/user.py | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 23ebe7d6b1..0e7b9d43a7 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -141,16 +141,14 @@ class User(Document):
if not cint(self.enabled):
self.a_system_manager_should_exist()
- # disable notifications if the user has been disabled
- toggle_notifications(self.name, enable=False)
- else:
- # enable notifications if the user has been enabled
- toggle_notifications(self.name, enable=True)
# clear sessions if disabled
if not cint(self.enabled) and getattr(frappe.local, "login_manager", None):
frappe.local.login_manager.logout(user=self.name)
+ # toggle notifications based on the user's status
+ toggle_notifications(self.name, enable=cint(self.enabled))
+
def add_system_manager_role(self):
# if adding system manager, do nothing
if not cint(self.enabled) or ("System Manager" in [user_role.role for user_role in
From a1d77752989297020376f39d82a8a985498e543e Mon Sep 17 00:00:00 2001
From: Sagar Vora
Date: Fri, 5 Mar 2021 17:26:53 +0530
Subject: [PATCH 46/83] fix: 404 not loading on formview due to undefined
parameter
---
frappe/public/js/frappe/views/formview.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/views/formview.js b/frappe/public/js/frappe/views/formview.js
index 5d2d4ec303..6708a6ffb1 100644
--- a/frappe/public/js/frappe/views/formview.js
+++ b/frappe/public/js/frappe/views/formview.js
@@ -85,7 +85,7 @@ frappe.views.FormFactory = class FormFactory extends frappe.views.Factory {
if (name && name.substr(0, 3) === 'new') {
this.render_new_doc(doctype, name, doctype_layout);
} else {
- frappe.show_not_found(route);
+ frappe.show_not_found();
}
return;
}
From 268cbbf8d95ea7003267ad60ca2300dd92c93055 Mon Sep 17 00:00:00 2001
From: walstanb
Date: Fri, 5 Mar 2021 18:14:26 +0530
Subject: [PATCH 47/83] fix: doc not found after renaming doc
---
frappe/public/js/frappe/model/model.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js
index 9ec7b0e931..7167fc3982 100644
--- a/frappe/public/js/frappe/model/model.js
+++ b/frappe/public/js/frappe/model/model.js
@@ -621,6 +621,7 @@ $.extend(frappe.model, {
r.message || args.new_name]);
if(locals[doctype] && locals[doctype][docname])
delete locals[doctype][docname];
+ this.frm.reload_doc();
d.hide();
if(callback)
callback(r.message);
From 0e0413d0963897ef2ef1580c2ae945b4bcdf9125 Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Sun, 7 Mar 2021 10:43:40 +0530
Subject: [PATCH 48/83] fix: Remove redundant css_variables import
---
frappe/public/scss/desk/index.scss | 1 -
frappe/public/scss/website/index.scss | 1 -
frappe/public/scss/website/variables.scss | 1 +
3 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/frappe/public/scss/desk/index.scss b/frappe/public/scss/desk/index.scss
index 1643034322..5f5fef251b 100644
--- a/frappe/public/scss/desk/index.scss
+++ b/frappe/public/scss/desk/index.scss
@@ -1,5 +1,4 @@
@import "variables";
-@import "css_variables";
@import "../common/mixins.scss";
@import "../common/global.scss";
@import "../common/icons.scss";
diff --git a/frappe/public/scss/website/index.scss b/frappe/public/scss/website/index.scss
index 88f3b1db73..94b1249de1 100644
--- a/frappe/public/scss/website/index.scss
+++ b/frappe/public/scss/website/index.scss
@@ -1,6 +1,5 @@
@import '~quill/dist/quill.core';
@import 'variables';
-@import 'css_variables';
@import '~bootstrap/scss/bootstrap';
@import "../common/mixins";
@import "../common/global";
diff --git a/frappe/public/scss/website/variables.scss b/frappe/public/scss/website/variables.scss
index fa68b57ad6..293d02b06d 100644
--- a/frappe/public/scss/website/variables.scss
+++ b/frappe/public/scss/website/variables.scss
@@ -131,5 +131,6 @@ $spacers: (
@import "~bootstrap/scss/functions";
@import "~bootstrap/scss/variables";
@import "~bootstrap/scss/mixins";
+@import 'css_variables';
$code-color: $purple;
From caaed1a4e5ce0c0b079a31bd7b757076c7ad4358 Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Sun, 7 Mar 2021 10:46:41 +0530
Subject: [PATCH 49/83] fix: Move data-theme attribute to root (html)
- So that the HTML background matches with the theme background color
---
frappe/public/js/frappe/ui/theme_switcher.js | 33 ++----
frappe/www/app.html | 114 ++++++++++---------
2 files changed, 69 insertions(+), 78 deletions(-)
diff --git a/frappe/public/js/frappe/ui/theme_switcher.js b/frappe/public/js/frappe/ui/theme_switcher.js
index 31baf697f0..317198bca5 100644
--- a/frappe/public/js/frappe/ui/theme_switcher.js
+++ b/frappe/public/js/frappe/ui/theme_switcher.js
@@ -14,7 +14,7 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
}
refresh() {
- this.current_theme = document.body.dataset.theme;
+ this.current_theme = document.documentElement.getAttribute("data-theme") || "light";
this.fetch_themes().then(() => {
this.render();
});
@@ -45,7 +45,6 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
});
}
-
get_preview_html(theme) {
const preview = $(`
@@ -69,15 +68,9 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
`);
- // preview.on('mouseover', () => {
- // this.toggle_theme(theme.name, true)
- // })
-
- // preview.on('mouseleave', () => {
- // this.toggle_theme(this.current_theme, true)
- // })
-
preview.on('click', () => {
+ if (this.current_theme === theme.name) return;
+
this.themes.forEach((th) => {
th.$html.removeClass("selected");
});
@@ -89,19 +82,15 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
return preview;
}
- toggle_theme(theme, preview=false) {
- if (!preview) {
- document.body.dataset.theme = theme.toLowerCase();
- frappe.show_alert("Theme Changed", 3);
+ toggle_theme(theme) {
+ this.current_theme = theme.toLowerCase();
+ document.documentElement.setAttribute("data-theme", this.current_theme);
+ frappe.show_alert("Theme Changed", 3);
- frappe.call('frappe.core.doctype.user.user.switch_theme', {
- theme: toTitle(theme)
- });
- } else {
- document.body.dataset.theme = theme.toLowerCase();
- }
+ frappe.xcall("frappe.core.doctype.user.user.switch_theme", {
+ theme: toTitle(theme)
+ });
}
-
show() {
this.dialog.show();
}
@@ -109,4 +98,4 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
hide() {
this.dialog.hide();
}
-};
\ No newline at end of file
+};
diff --git a/frappe/www/app.html b/frappe/www/app.html
index 715ddaf409..8da4d11c00 100644
--- a/frappe/www/app.html
+++ b/frappe/www/app.html
@@ -1,65 +1,67 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Frappe
-
-
- {% for include in include_css -%}
-
- {%- endfor -%}
-
-
- {% include "public/icons/timeless/symbol-defs.svg" %}
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Frappe
+
+
+ {% for include in include_css -%}
+
+ {%- endfor -%}
+
+
+ {% include "public/icons/timeless/symbol-defs.svg" %}
+
+
+
+
-
+
-
+
- {% for include in include_js %}
-
- {% endfor %}
- {% include "templates/includes/app_analytics/google_analytics.html" %}
- {% include "templates/includes/app_analytics/mixpanel_analytics.html" %}
+ {% for include in include_js %}
+
+ {% endfor %}
+ {% include "templates/includes/app_analytics/google_analytics.html" %}
+ {% include "templates/includes/app_analytics/mixpanel_analytics.html" %}
- {% for sound in (sounds or []) %}
-
-
-
- {% endfor %}
-
+ {% for sound in (sounds or []) %}
+
+
+
+ {% endfor %}
+
+
From 738e6f50ab954bffaa83d805a466074e7a1cb0cb Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Sun, 7 Mar 2021 10:48:40 +0530
Subject: [PATCH 50/83] fix: Remove unnecessary background transitions
- to avoid inconsitent transition while switching the theme
---
frappe/public/scss/desk/global.scss | 13 -------------
frappe/public/scss/desk/variables.scss | 4 ++++
2 files changed, 4 insertions(+), 13 deletions(-)
diff --git a/frappe/public/scss/desk/global.scss b/frappe/public/scss/desk/global.scss
index f4d6384352..b145879e1f 100644
--- a/frappe/public/scss/desk/global.scss
+++ b/frappe/public/scss/desk/global.scss
@@ -3,19 +3,6 @@ html {
background-color: var(--bg-color);
}
-// transition
-* {
- transition: background-color 0.5s, background 0.5s;
-}
-
-a,
-.badge {
- transition: 0.2s;
-}
-
-.btn {
- transition: background-color 0.2s, background 0.2s;
-}
body {
diff --git a/frappe/public/scss/desk/variables.scss b/frappe/public/scss/desk/variables.scss
index 57a117cd31..2855277ccd 100644
--- a/frappe/public/scss/desk/variables.scss
+++ b/frappe/public/scss/desk/variables.scss
@@ -95,6 +95,10 @@ $btn-active-box-shadow: var(--shadow-inset);
$mark-bg: #FDF9AF;
$mark-padding: 0;
+// transitions
+$btn-transition: none;
+$input-transition: none;
+
// popover
$enable-shadows: true;
$popover-border-radius: var(--border-radius);
From 87826f024179d3989b48ecf15998526f00a3690b Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Sun, 7 Mar 2021 12:52:39 +0530
Subject: [PATCH 51/83] fix: Line hieght of checkbox label
---
frappe/public/scss/common/global.scss | 1 -
1 file changed, 1 deletion(-)
diff --git a/frappe/public/scss/common/global.scss b/frappe/public/scss/common/global.scss
index 91cc31c50d..20778176d4 100644
--- a/frappe/public/scss/common/global.scss
+++ b/frappe/public/scss/common/global.scss
@@ -8,7 +8,6 @@
--checkbox-right-margin: 8px;
.label-area {
- line-height: 1;
font-size: var(--text-sm);
}
From 8962ba064756a49c939d2ff0bf629d579cd98025 Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Sun, 7 Mar 2021 19:14:31 +0530
Subject: [PATCH 52/83] fix: Temporarily use firefox for testing
- https://travis-ci.com/github/frappe/frappe/jobs/488819006 testing is failing due to some incompatibility
---
.travis.yml | 2 +-
frappe/commands/utils.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index 53ad56a948..ffada0286f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,7 +7,7 @@ addons:
- test_site_producer
mariadb: 10.3
postgresql: 9.5
- chrome: stable
+ firefox: latest
services:
- xvfb
diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py
index e9fa7217a8..13c6ca812f 100644
--- a/frappe/commands/utils.py
+++ b/frappe/commands/utils.py
@@ -578,7 +578,7 @@ def run_ui_tests(context, app, headless=False):
frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 --no-lockfile")
# run for headless mode
- run_or_open = 'run --browser chrome --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open'
+ run_or_open = 'run --browser firefox --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open'
command = '{site_env} {password_env} {cypress} {run_or_open}'
formatted_command = command.format(site_env=site_env, password_env=password_env, cypress=cypress_path, run_or_open=run_or_open)
From d28f5a54a5dd203dfa6d163654dadd850cd05077 Mon Sep 17 00:00:00 2001
From: Sagar Vora
Date: Fri, 5 Mar 2021 12:37:03 +0530
Subject: [PATCH 53/83] fix: dashboard edit and show buttons are not working
---
frappe/public/js/frappe/views/pageview.js | 1 -
1 file changed, 1 deletion(-)
diff --git a/frappe/public/js/frappe/views/pageview.js b/frappe/public/js/frappe/views/pageview.js
index 6fd91d73c4..705d13b7f0 100644
--- a/frappe/public/js/frappe/views/pageview.js
+++ b/frappe/public/js/frappe/views/pageview.js
@@ -73,7 +73,6 @@ frappe.views.Page = class Page {
return;
}
this.wrapper = frappe.container.add_page(this.name);
- this.wrapper.label = this.pagedoc.title || this.pagedoc.name;
this.wrapper.page_name = this.pagedoc.name;
// set content, script and style
From f17cf17b57d8e825d0871d720f22222fd41c4678 Mon Sep 17 00:00:00 2001
From: Mohammad Hasnain Mohsin Rajan
Date: Mon, 8 Mar 2021 14:24:54 +0530
Subject: [PATCH 54/83] fix: remove data import mandatory check (#12363)
* fix: ignore data import mandatory fields error
* test: find the source of garbage
* test: fix extra messages
* test: fix: message log
* test: remove debugging lines
---
frappe/core/doctype/data_import/importer.py | 87 -------------------
.../core/doctype/data_import/test_importer.py | 22 ++---
2 files changed, 12 insertions(+), 97 deletions(-)
diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py
index dde3dfaee9..388d9389f2 100644
--- a/frappe/core/doctype/data_import/importer.py
+++ b/frappe/core/doctype/data_import/importer.py
@@ -472,32 +472,6 @@ class ImportFile:
doc = parent_doc
- if self.import_type == INSERT:
- # check if there is atleast one row for mandatory table fields
- meta = frappe.get_meta(self.doctype)
- mandatory_table_fields = [
- df
- for df in meta.fields
- if df.fieldtype in table_fieldtypes
- and df.reqd
- and len(doc.get(df.fieldname, [])) == 0
- ]
- if len(mandatory_table_fields) == 1:
- self.warnings.append(
- {
- "row": first_row.row_number,
- "message": _("There should be atleast one row for {0} table").format(
- frappe.bold(mandatory_table_fields[0].label)
- ),
- }
- )
- elif mandatory_table_fields:
- fields_string = ", ".join([df.label for df in mandatory_table_fields])
- message = _("There should be atleast one row for the following tables: {0}").format(
- fields_string
- )
- self.warnings.append({"row": first_row.row_number, "message": message})
-
return doc, rows, data[len(rows) :]
def get_warnings(self):
@@ -626,7 +600,6 @@ class Row:
new_doc.update(doc)
doc = new_doc
- self.check_mandatory_fields(doctype, doc, table_df)
return doc
def validate_value(self, value, col):
@@ -727,66 +700,6 @@ class Row:
pass
return value
- def check_mandatory_fields(self, doctype, doc, table_df=None):
- """If import type is Insert:
- Check for mandatory fields (except table fields) in doc
- if import type is Update:
- Check for name field or autoname field in doc
- """
- meta = frappe.get_meta(doctype)
- if self.import_type == UPDATE:
- if meta.istable:
- # when updating records with table rows,
- # there are two scenarios:
- # 1. if row 'name' is provided in the template
- # the table row will be updated
- # 2. if row 'name' is not provided
- # then a new row will be added
- # so we dont need to check for mandatory
- return
-
- # for update, only ID (name) field is mandatory
- id_field = get_id_field(doctype)
- if doc.get(id_field.fieldname) in INVALID_VALUES:
- self.warnings.append(
- {
- "row": self.row_number,
- "message": _("{0} is a mandatory field").format(id_field.label),
- }
- )
- return
-
- fields = [
- df
- for df in meta.fields
- if df.fieldtype not in table_fieldtypes
- and df.reqd
- and doc.get(df.fieldname) in INVALID_VALUES
- ]
-
- if not fields:
- return
-
- def get_field_label(df):
- return "{0}{1}".format(df.label, " ({})".format(table_df.label) if table_df else "")
-
- if len(fields) == 1:
- field_label = get_field_label(fields[0])
- self.warnings.append(
- {
- "row": self.row_number,
- "message": _("{0} is a mandatory field").format(frappe.bold(field_label)),
- }
- )
- else:
- fields_string = ", ".join([frappe.bold(get_field_label(df)) for df in fields])
- self.warnings.append(
- {
- "row": self.row_number,
- "message": _("{0} are mandatory fields").format(fields_string),
- }
- )
-
def get_values(self, indexes):
return [self.data[i] for i in indexes]
diff --git a/frappe/core/doctype/data_import/test_importer.py b/frappe/core/doctype/data_import/test_importer.py
index b083b9eaaa..f76d4504a4 100644
--- a/frappe/core/doctype/data_import/test_importer.py
+++ b/frappe/core/doctype/data_import/test_importer.py
@@ -13,7 +13,7 @@ doctype_name = 'DocType for Import'
class TestImporter(unittest.TestCase):
@classmethod
def setUpClass(cls):
- create_doctype_if_not_exists(doctype_name)
+ create_doctype_if_not_exists(doctype_name,)
def test_data_import_from_file(self):
import_file = get_import_file('sample_import_file')
@@ -59,18 +59,18 @@ class TestImporter(unittest.TestCase):
def test_data_import_without_mandatory_values(self):
import_file = get_import_file('sample_import_file_without_mandatory')
data_import = self.get_importer(doctype_name, import_file)
+ frappe.local.message_log = []
data_import.start_import()
data_import.reload()
- warnings = frappe.parse_json(data_import.template_warnings)
+ import_log = frappe.parse_json(data_import.import_log)
+ self.assertEqual(import_log[0]['row_indexes'], [2,3])
+ expected_error = "Error: Child 1 of DocType for Import Row #1: Value missing for: Child Title"
+ self.assertEqual(frappe.parse_json(import_log[0]['messages'][0])['message'], expected_error)
+ expected_error = "Error: Child 1 of DocType for Import Row #2: Value missing for: Child Title"
+ self.assertEqual(frappe.parse_json(import_log[0]['messages'][1])['message'], expected_error)
- self.assertEqual(warnings[0]['row'], 2)
- self.assertEqual(warnings[0]['message'], "Child Title (Table Field 1) is a mandatory field")
-
- self.assertEqual(warnings[1]['row'], 3)
- self.assertEqual(warnings[1]['message'], "Child Title (Table Field 1 Again) is a mandatory field")
-
- self.assertEqual(warnings[2]['row'], 4)
- self.assertEqual(warnings[2]['message'], "Title is a mandatory field")
+ self.assertEqual(import_log[1]['row_indexes'], [4])
+ self.assertEqual(frappe.parse_json(import_log[1]['messages'][0])['message'], "Title is required")
def test_data_import_update(self):
existing_doc = frappe.get_doc(
@@ -104,6 +104,8 @@ class TestImporter(unittest.TestCase):
data_import.reference_doctype = doctype
data_import.import_file = import_file.file_url
data_import.insert()
+ # Commit so that the first import failure does not rollback the Data Import insert.
+ frappe.db.commit()
return data_import
From bc219c1bbf088deb56b44c90a4a70ee63055dfe8 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 8 Mar 2021 17:34:16 +0530
Subject: [PATCH 55/83] fix: include quill theme in website css
Theme css are used by text editor in website view.
closes #12535
---
frappe/public/scss/website/index.scss | 2 ++
1 file changed, 2 insertions(+)
diff --git a/frappe/public/scss/website/index.scss b/frappe/public/scss/website/index.scss
index 94b1249de1..c6b3771562 100644
--- a/frappe/public/scss/website/index.scss
+++ b/frappe/public/scss/website/index.scss
@@ -1,4 +1,6 @@
@import '~quill/dist/quill.core';
+@import '~quill/dist/quill.snow.css';
+@import '~quill/dist/quill.bubble.css';
@import 'variables';
@import '~bootstrap/scss/bootstrap';
@import "../common/mixins";
From ffc18ac4bde1572ddf95919149439a9f2ac7bea1 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 8 Mar 2021 20:01:03 +0530
Subject: [PATCH 56/83] fix: show link based on READ ignoring select perm
(#12521)
Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
---
frappe/public/js/frappe/form/formatters.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js
index 5386f97528..4578cf2ded 100644
--- a/frappe/public/js/frappe/form/formatters.js
+++ b/frappe/public/js/frappe/form/formatters.js
@@ -131,7 +131,7 @@ frappe.form.formatters = {
return repl('%(value)s ',
{onclick: docfield.link_onclick.replace(/"/g, '"'), value:value});
} else if(docfield && doctype) {
- if (!frappe.model.can_select(doctype) && frappe.model.can_read(doctype)) {
+ if (frappe.model.can_read(doctype)) {
return `
Date: Tue, 9 Mar 2021 10:49:59 +0530
Subject: [PATCH 57/83] fix: datepicker background color
---
frappe/public/scss/common/datepicker.scss | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/public/scss/common/datepicker.scss b/frappe/public/scss/common/datepicker.scss
index 870ef5783d..d159f27fc1 100644
--- a/frappe/public/scss/common/datepicker.scss
+++ b/frappe/public/scss/common/datepicker.scss
@@ -4,7 +4,7 @@
font-family: inherit;
z-index: 9999 !important;
- background: var(--bg-color);
+ background: var(--fg-color);
color: var(--text-color);
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
From b44ee08a19ae1466a5a63c7c6adeb05b849c96a9 Mon Sep 17 00:00:00 2001
From: Leela vadlamudi
Date: Tue, 9 Mar 2021 11:40:30 +0530
Subject: [PATCH 58/83] fix: Patch - Remove Twilio doctypes from DB (#12514)
We missed to add migration while moving Twilio code from Frappe to
separate app. Added migration that removes Twilio doctypes from DB.
---
frappe/patches.txt | 1 +
.../patches/v13_0/remove_twilio_settings.py | 20 +++++++++++++++++++
2 files changed, 21 insertions(+)
create mode 100644 frappe/patches/v13_0/remove_twilio_settings.py
diff --git a/frappe/patches.txt b/frappe/patches.txt
index d43690eac2..a3f60ca210 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -331,3 +331,4 @@ execute:frappe.get_doc('Role', 'Guest').save() # remove desk access
frappe.patches.v13_0.rename_desk_page_to_workspace # 02.02.2021
frappe.patches.v13_0.delete_package_publish_tool
frappe.patches.v13_0.rename_list_view_setting_to_list_view_settings
+frappe.patches.v13_0.remove_twilio_settings
diff --git a/frappe/patches/v13_0/remove_twilio_settings.py b/frappe/patches/v13_0/remove_twilio_settings.py
new file mode 100644
index 0000000000..363cbdd4b6
--- /dev/null
+++ b/frappe/patches/v13_0/remove_twilio_settings.py
@@ -0,0 +1,20 @@
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+import frappe
+
+
+def execute():
+ """Add missing Twilio patch.
+
+ While making Twilio as a standaone app, we missed to delete Twilio records from DB through migration. Adding the missing patch.
+ """
+ frappe.delete_doc_if_exists('DocType', 'Twilio Number Group')
+ if twilio_settings_doctype_in_integrations():
+ frappe.delete_doc_if_exists('DocType', 'Twilio Settings')
+ frappe.db.sql("delete from `tabSingles` where `doctype`=%s", 'Twilio Settings')
+
+def twilio_settings_doctype_in_integrations() -> bool:
+ """Check Twilio Settings doctype exists in integrations module or not.
+ """
+ return frappe.db.exists("DocType", {'name': 'Twilio Settings', 'module': 'Integrations'})
From c1ab5b8479fc9d881c761c3a0987ab1ff1dbbf40 Mon Sep 17 00:00:00 2001
From: rohitwaghchaure
Date: Tue, 9 Mar 2021 11:48:24 +0530
Subject: [PATCH 59/83] fix: Section collapse in modal (#12536)
Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
---
frappe/public/js/frappe/form/layout.js | 2 +-
frappe/public/js/frappe/ui/dialog.js | 2 ++
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js
index aad3740ab0..d3480b1b75 100644
--- a/frappe/public/js/frappe/form/layout.js
+++ b/frappe/public/js/frappe/form/layout.js
@@ -289,7 +289,7 @@ frappe.ui.form.Layout = Class.extend({
},
refresh_section_collapse: function () {
- if (!this.doc) return;
+ if (!(this.sections && this.sections.length)) return;
for (var i = 0; i < this.sections.length; i++) {
var section = this.sections[i];
diff --git a/frappe/public/js/frappe/ui/dialog.js b/frappe/public/js/frappe/ui/dialog.js
index 59836fae23..6761d1d326 100644
--- a/frappe/public/js/frappe/ui/dialog.js
+++ b/frappe/public/js/frappe/ui/dialog.js
@@ -52,6 +52,8 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
// make fields (if any)
super.make();
+ this.refresh_section_collapse();
+
// show footer
this.action = this.action || { primary: { }, secondary: { } };
if (this.primary_action || (this.action.primary && this.action.primary.onsubmit)) {
From 08e2ad99043fa67ee7dc1a13bde3d915e0ac290f Mon Sep 17 00:00:00 2001
From: Leela vadlamudi
Date: Tue, 9 Mar 2021 11:59:17 +0530
Subject: [PATCH 60/83] feat: Lazy import functionality added (#12517)
---
frappe/__init__.py | 15 +++++++++------
frappe/utils/lazy_loader.py | 34 ++++++++++++++++++++++++++++++++++
2 files changed, 43 insertions(+), 6 deletions(-)
create mode 100644 frappe/utils/lazy_loader.py
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 160ed93c50..ab27bff9fe 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -18,9 +18,14 @@ import os, sys, importlib, inspect, json
from past.builtins import cmp
import click
-# public
+# Local application imports
from .exceptions import *
from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader)
+from .utils.lazy_loader import lazy_import
+
+# Lazy imports
+faker = lazy_import('faker')
+
# Harmless for Python 3
# For Python 2 set default encoding to utf-8
@@ -1749,15 +1754,13 @@ def parse_json(val):
return parse_json(val)
def mock(type, size=1, locale='en'):
- from faker import Faker
-
results = []
- faker = Faker(locale)
- if not type in dir(faker):
+ fake = faker.Faker(locale)
+ if type not in dir(fake):
raise ValueError('Not a valid mock type.')
else:
for i in range(size):
- data = getattr(faker, type)()
+ data = getattr(fake, type)()
results.append(data)
from frappe.chat.util import squashify
diff --git a/frappe/utils/lazy_loader.py b/frappe/utils/lazy_loader.py
new file mode 100644
index 0000000000..ae4cba66a1
--- /dev/null
+++ b/frappe/utils/lazy_loader.py
@@ -0,0 +1,34 @@
+import importlib.util
+import sys
+
+def lazy_import(name, package=None):
+ """Import a module lazily.
+
+ The module is loaded when modules's attribute is accessed for the first time.
+ This works with both absolute and relative imports.
+ $ cat mod.py
+ print("Loading mod.py")
+ $ python -i lazy_loader.py
+ >>> mod = lazy_import("mod") # Module is not loaded
+ >>> mod.__str__() # module is loaded on accessing attribute
+ Loading mod.py
+ ""
+ >>>
+
+ Code based on https://github.com/python/cpython/blob/master/Doc/library/importlib.rst#implementing-lazy-imports.
+ """
+ # Return if the module already loaded
+ if name in sys.modules:
+ return sys.modules[name]
+
+ # Find the spec if not loaded
+ spec = importlib.util.find_spec(name, package)
+ if not spec:
+ raise ImportError(f'Module {name} Not found.')
+
+ loader = importlib.util.LazyLoader(spec.loader)
+ spec.loader = loader
+ module = importlib.util.module_from_spec(spec)
+ sys.modules[name] = module
+ loader.exec_module(module)
+ return module
From a74b7b04ac2864a9d13d79aa6b7e7c335af0a381 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 9 Mar 2021 12:04:09 +0530
Subject: [PATCH 61/83] chore: add semgrep linting (#12524)
---
.github/workflows/semgrep.yml | 13 +++++++++++++
.semgrep.yml | 29 +++++++++++++++++++++++++++++
2 files changed, 42 insertions(+)
create mode 100644 .github/workflows/semgrep.yml
create mode 100644 .semgrep.yml
diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml
new file mode 100644
index 0000000000..26f1191a90
--- /dev/null
+++ b/.github/workflows/semgrep.yml
@@ -0,0 +1,13 @@
+name: Semgrep
+
+on:
+ pull_request: {}
+
+jobs:
+ semgrep:
+ name: Frappe Linter
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: returntocorp/semgrep-action@v1
+
diff --git a/.semgrep.yml b/.semgrep.yml
new file mode 100644
index 0000000000..99d237251e
--- /dev/null
+++ b/.semgrep.yml
@@ -0,0 +1,29 @@
+#Reference: https://semgrep.dev/docs/writing-rules/rule-syntax/
+
+rules:
+- id: eval
+ patterns:
+ - pattern-not: eval("...")
+ - pattern: eval(...)
+ message: |
+ Detected the use of eval(). eval() can be dangerous if used to evaluate
+ dynamic content. Avoid it or use safe_eval().
+ languages:
+ - python
+ severity: ERROR
+
+# translations
+- id: frappe-translation-syntax-python
+ pattern-either:
+ - pattern: _(f"...") # f-strings not allowed
+ - pattern: _("..." + "...") # concatenation not allowed
+ - pattern: _("") # empty string is meaningless
+ - pattern: _("..." % ...) # Only positional formatters are allowed.
+ - pattern: _("...".format(...)) # format should not be used before translating
+ - pattern: _("...") + ... + _("...") # don't split strings
+ message: |
+ Incorrect use of translation function detected.
+ Please refer: https://frappeframework.com/docs/user/en/translations
+ languages:
+ - python
+ severity: ERROR
From cfd4bd428c0064f42110f28b7017a68fd06716dc Mon Sep 17 00:00:00 2001
From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
Date: Tue, 9 Mar 2021 12:09:50 +0530
Subject: [PATCH 62/83] fix: Data control value
---
frappe/public/scss/common/controls.scss | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/public/scss/common/controls.scss b/frappe/public/scss/common/controls.scss
index 1d5bb265b2..83fc4461d6 100644
--- a/frappe/public/scss/common/controls.scss
+++ b/frappe/public/scss/common/controls.scss
@@ -254,7 +254,7 @@ textarea.form-control {
overflow-wrap: break-word;
}
-.frappe-control[data-fieldtype="Data"] .control-input {
+.frappe-control[data-fieldtype="Data"] .control-input, .control-value {
position: relative;
}
From a19c809a0456d9ca94033df7a5a4698a7a2c7ae5 Mon Sep 17 00:00:00 2001
From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
Date: Tue, 9 Mar 2021 13:11:47 +0530
Subject: [PATCH 63/83] chore: Update CODEOWNERS
---
CODEOWNERS | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CODEOWNERS b/CODEOWNERS
index 1afa3f72e3..92723ab035 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -8,10 +8,10 @@ website/ @prssanna
web_form/ @prssanna
templates/ @surajshetty3416
www/ @surajshetty3416
-integrations/ @nextchamp-saqib
+integrations/ @leela
patches/ @surajshetty3416
dashboard/ @prssanna
-email/ @saurabh6790
+email/ @leela
event_streaming/ @ruchamahabal
data_import* @netchampfaris
core/ @surajshetty3416
From ba16e4737b2447cd7e77bffd63b455949a5e2f5e Mon Sep 17 00:00:00 2001
From: leela
Date: Thu, 4 Mar 2021 12:19:10 +0530
Subject: [PATCH 64/83] refactor: rate limiter decorator added
We have rate limiter for reset passowrd alone and it is not re-usable
for other endpoints. Added a generic rate limiter decorator that can be
used for any endpoint.
---
frappe/rate_limiter.py | 43 +++++++++++++++++++++++++++++++++++++++++-
1 file changed, 42 insertions(+), 1 deletion(-)
diff --git a/frappe/rate_limiter.py b/frappe/rate_limiter.py
index e29b2b3061..e7f467f10d 100644
--- a/frappe/rate_limiter.py
+++ b/frappe/rate_limiter.py
@@ -5,10 +5,14 @@
from __future__ import unicode_literals
from datetime import datetime
+from functools import wraps
+from typing import Union
+
+from werkzeug.wrappers import Response
+
import frappe
from frappe import _
from frappe.utils import cint
-from werkzeug.wrappers import Response
def apply():
@@ -79,3 +83,40 @@ class RateLimiter:
def respond(self):
if self.rejected:
return Response(_("Too Many Requests"), status=429)
+
+def rate_limit(key: str, limit: int = 5, seconds: int= 24*60*60, methods: Union[str, list]='ALL'):
+ """Decorator to rate limit an endpoint.
+
+ This will limit Number of requests per endpoint to `limit` within `seconds`.
+ Uses redis cache to track request counts.
+
+ :param key: Key is used to identify the requests uniqueness
+ :param limit: Maximum number of requests to allow with in window time
+ :param seconds: window time to allow requests
+ :param methods: Limit the validation for these methods.
+ `ALL` is a wildcard that applies rate limit on all methods.
+ :type methods: string or list or tuple
+
+ :returns: a decorator function that limit the number of requests per endpoint
+ """
+ def ratelimit_decorator(fun):
+ @wraps(fun)
+ def wrapper(*args, **kwargs):
+ # Do not apply rate limits if method is not opted to check
+ if methods != 'ALL' and frappe.request.method.upper() not in methods:
+ return frappe.call(fun, **frappe.form_dict)
+
+ identity = frappe.form_dict[key]
+ cache_key = f"rl:{frappe.form_dict.cmd}:{identity}"
+
+ value = frappe.cache().get_value(cache_key, expires=True) or 0
+ if not value:
+ frappe.cache().set_value(cache_key, 0, expires_in_sec=seconds)
+
+ value = frappe.cache().incrby(cache_key, 1)
+ if value > limit:
+ frappe.throw(_("You hit the rate limit because of too many requests. Please try after sometime."))
+
+ return frappe.call(fun, **frappe.form_dict)
+ return wrapper
+ return ratelimit_decorator
From 2af0778857ea6c3dc9a661acd673eef957da6d54 Mon Sep 17 00:00:00 2001
From: leela
Date: Thu, 4 Mar 2021 12:33:02 +0530
Subject: [PATCH 65/83] refactor: Use rate_limit decorator for reset password
Using rate limit decorator for reset password endpoint.
---
frappe/core/doctype/user/user.py | 21 ++-------------------
1 file changed, 2 insertions(+), 19 deletions(-)
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 0e7b9d43a7..043836e7f4 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -16,6 +16,7 @@ import frappe.share
import frappe.defaults
from frappe.website.utils import is_signup_enabled
from frappe.utils.background_jobs import enqueue
+from frappe.rate_limiter import rate_limit
STANDARD_USERS = ("Guest", "Administrator")
@@ -241,11 +242,6 @@ class User(Document):
def reset_password(self, send_email=False, password_expired=False):
from frappe.utils import random_string, get_url
- rate_limit = frappe.db.get_single_value("System Settings", "password_reset_limit")
-
- if rate_limit:
- check_password_reset_limit(self.name, rate_limit)
-
key = random_string(32)
self.db_set("reset_password_key", key)
@@ -257,7 +253,6 @@ class User(Document):
if send_email:
self.password_reset_mail(link)
- update_password_reset_limit(self.name)
return link
def get_other_system_managers(self):
@@ -843,6 +838,7 @@ def sign_up(email, full_name, redirect_to):
return 2, _("Please ask your administrator to verify your sign-up")
@frappe.whitelist(allow_guest=True)
+@rate_limit(key='user', limit=3, seconds = 24*60*60, methods=['POST'])
def reset_password(user):
if user=="Administrator":
return 'not allowed'
@@ -1174,16 +1170,3 @@ def generate_keys(user):
def switch_theme(theme):
if theme in ["Dark", "Light"]:
frappe.db.set_value("User", frappe.session.user, "desk_theme", theme)
-
-def update_password_reset_limit(user):
- generated_link_count = get_generated_link_count(user)
- generated_link_count += 1
- frappe.cache().hset("password_reset_link_count", user, generated_link_count)
-
-def check_password_reset_limit(user, rate_limit):
- generated_link_count = get_generated_link_count(user)
- if generated_link_count >= rate_limit:
- frappe.throw(_("You have reached the hourly limit for generating password reset links. Please try again later."))
-
-def get_generated_link_count(user):
- return cint(frappe.cache().hget("password_reset_link_count", user)) or 0
From 14b3d344a5e88f6aa37a6b4d3306f05a0114beda Mon Sep 17 00:00:00 2001
From: leela
Date: Thu, 4 Mar 2021 13:53:08 +0530
Subject: [PATCH 66/83] test: Testcase for rate_limit decorator
---
frappe/core/doctype/user/user.py | 7 +++++++
frappe/tests/test_rate_limiter.py | 28 ++++++++++++++++++++++++++--
2 files changed, 33 insertions(+), 2 deletions(-)
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 043836e7f4..895da8c5c5 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -1170,3 +1170,10 @@ def generate_keys(user):
def switch_theme(theme):
if theme in ["Dark", "Light"]:
frappe.db.set_value("User", frappe.session.user, "desk_theme", theme)
+
+@frappe.whitelist(allow_guest=True)
+@rate_limit(key='user', limit=2, seconds = 60*60)
+def test_ratelimit(user):
+ """This endpoint is used by testcases to check the ratelimit is functioning as expected.
+ """
+ return
diff --git a/frappe/tests/test_rate_limiter.py b/frappe/tests/test_rate_limiter.py
index 292b521460..68d463ef74 100644
--- a/frappe/tests/test_rate_limiter.py
+++ b/frappe/tests/test_rate_limiter.py
@@ -4,13 +4,17 @@
# MIT License. See license.txt
from __future__ import unicode_literals
+
import unittest
-import frappe
+from werkzeug.wrappers import Response
import time
+
+import frappe
import frappe.rate_limiter
from frappe.rate_limiter import RateLimiter
from frappe.utils import cint
-from werkzeug.wrappers import Response
+from frappe.frappeclient import FrappeClient
+from frappe.utils.data import get_url
class TestRateLimiter(unittest.TestCase):
@@ -114,3 +118,23 @@ class TestRateLimiter(unittest.TestCase):
self.assertEqual(limiter.duration, cint(frappe.cache().get(limiter.key)))
frappe.cache().delete(limiter.key)
+
+ def test_rate_limit_decorator(self):
+ """Check that rate limit decorator raises 417 when limit is crossed.
+ """
+ url = get_url()
+ data={'cmd': 'frappe.core.doctype.user.user.test_ratelimit', 'user': 'test@test.com'}
+
+ # Clear rate limit tracker to start fresh
+ key = f"rl:{data['cmd']}:{data['user']}"
+ frappe.cache().delete(key)
+
+ c = FrappeClient(url)
+ res1 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
+ res2 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
+ res3 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
+
+ self.assertEqual(res1.status_code, 200)
+ self.assertEqual(res2.status_code, 200)
+ self.assertEqual(res3.status_code, 417)
+
From de210260a703409b0442891eb456297a79a649c8 Mon Sep 17 00:00:00 2001
From: leela
Date: Fri, 5 Mar 2021 13:21:21 +0530
Subject: [PATCH 67/83] refactor: allow callable limit arg for ratelimit deco
As we make all configurations editable through dashboard(ex: password_reset_limit), it makes sense
to provide limit as a callable so that it can be accessed dynamically.
---
frappe/core/doctype/user/user.py | 14 +++++++-------
frappe/hooks.py | 3 +--
frappe/rate_limiter.py | 9 ++++++---
frappe/utils/password.py | 11 +++--------
4 files changed, 17 insertions(+), 20 deletions(-)
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 895da8c5c5..573dcd7f17 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -2,21 +2,21 @@
# MIT License. See license.txt
from __future__ import unicode_literals, print_function
+
+from bs4 import BeautifulSoup
+
import frappe
from frappe.model.document import Document
from frappe.utils import cint, flt, has_gravatar, escape_html, format_datetime, now_datetime, get_formatted_email, today
from frappe import throw, msgprint, _
-from frappe.utils.password import update_password as _update_password, check_password
+from frappe.utils.password import update_password as _update_password, check_password, get_password_reset_limit
from frappe.desk.notifications import clear_notifications
from frappe.desk.doctype.notification_settings.notification_settings import create_notification_settings, toggle_notifications
from frappe.utils.user import get_system_managers
-from bs4 import BeautifulSoup
-import frappe.permissions
-import frappe.share
-import frappe.defaults
from frappe.website.utils import is_signup_enabled
-from frappe.utils.background_jobs import enqueue
from frappe.rate_limiter import rate_limit
+from frappe.utils.background_jobs import enqueue
+
STANDARD_USERS = ("Guest", "Administrator")
@@ -838,7 +838,7 @@ def sign_up(email, full_name, redirect_to):
return 2, _("Please ask your administrator to verify your sign-up")
@frappe.whitelist(allow_guest=True)
-@rate_limit(key='user', limit=3, seconds = 24*60*60, methods=['POST'])
+@rate_limit(key='user', limit=get_password_reset_limit, seconds = 24*60*60, methods=['POST'])
def reset_password(user):
if user=="Administrator":
return 'not allowed'
diff --git a/frappe/hooks.py b/frappe/hooks.py
index 3e206f0ad3..c9914237fe 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -207,8 +207,7 @@ scheduler_events = {
"frappe.deferred_insert.save_to_db",
"frappe.desk.form.document_follow.send_hourly_updates",
"frappe.integrations.doctype.google_calendar.google_calendar.sync",
- "frappe.email.doctype.newsletter.newsletter.send_scheduled_email",
- "frappe.utils.password.delete_password_reset_cache"
+ "frappe.email.doctype.newsletter.newsletter.send_scheduled_email"
],
"daily": [
"frappe.email.queue.set_expiry_for_email_queue",
diff --git a/frappe/rate_limiter.py b/frappe/rate_limiter.py
index e7f467f10d..40db8fe892 100644
--- a/frappe/rate_limiter.py
+++ b/frappe/rate_limiter.py
@@ -6,7 +6,7 @@ from __future__ import unicode_literals
from datetime import datetime
from functools import wraps
-from typing import Union
+from typing import Union, Callable
from werkzeug.wrappers import Response
@@ -84,7 +84,7 @@ class RateLimiter:
if self.rejected:
return Response(_("Too Many Requests"), status=429)
-def rate_limit(key: str, limit: int = 5, seconds: int= 24*60*60, methods: Union[str, list]='ALL'):
+def rate_limit(key: str, limit: Union[int, Callable] = 5, seconds: int= 24*60*60, methods: Union[str, list]='ALL'):
"""Decorator to rate limit an endpoint.
This will limit Number of requests per endpoint to `limit` within `seconds`.
@@ -92,6 +92,7 @@ def rate_limit(key: str, limit: int = 5, seconds: int= 24*60*60, methods: Union[
:param key: Key is used to identify the requests uniqueness
:param limit: Maximum number of requests to allow with in window time
+ :type limit: Callable or Integer
:param seconds: window time to allow requests
:param methods: Limit the validation for these methods.
`ALL` is a wildcard that applies rate limit on all methods.
@@ -106,6 +107,8 @@ def rate_limit(key: str, limit: int = 5, seconds: int= 24*60*60, methods: Union[
if methods != 'ALL' and frappe.request.method.upper() not in methods:
return frappe.call(fun, **frappe.form_dict)
+ _limit = limit() if callable(limit) else limit
+
identity = frappe.form_dict[key]
cache_key = f"rl:{frappe.form_dict.cmd}:{identity}"
@@ -114,7 +117,7 @@ def rate_limit(key: str, limit: int = 5, seconds: int= 24*60*60, methods: Union[
frappe.cache().set_value(cache_key, 0, expires_in_sec=seconds)
value = frappe.cache().incrby(cache_key, 1)
- if value > limit:
+ if value > _limit:
frappe.throw(_("You hit the rate limit because of too many requests. Please try after sometime."))
return frappe.call(fun, **frappe.form_dict)
diff --git a/frappe/utils/password.py b/frappe/utils/password.py
index 177a3118fb..19a538f703 100644
--- a/frappe/utils/password.py
+++ b/frappe/utils/password.py
@@ -90,14 +90,6 @@ def delete_login_failed_cache(user):
frappe.cache().hdel('login_failed_count', user)
frappe.cache().hdel('locked_account_time', user)
-
-def delete_password_reset_cache(user=None):
- if user:
- frappe.cache().hdel('password_reset_link_count', user)
- else:
- frappe.cache().delete_key('password_reset_link_count')
-
-
def update_password(user, pwd, doctype='User', fieldname='password', logout_all_sessions=False):
'''
Update the password for the User
@@ -179,3 +171,6 @@ def get_encryption_key():
frappe.local.conf.encryption_key = encryption_key
return frappe.local.conf.encryption_key
+
+def get_password_reset_limit():
+ return frappe.db.get_single_value("System Settings", "password_reset_limit") or 0
From 0fb2d330c9f8b29fff4116c9cfd9e074700ac1f1 Mon Sep 17 00:00:00 2001
From: leela
Date: Fri, 5 Mar 2021 14:16:41 +0530
Subject: [PATCH 68/83] test: Fix rate limiting reset password test
---
frappe/core/doctype/user/test_user.py | 21 ++++++++++++++-------
frappe/core/doctype/user/user.py | 10 +++-------
frappe/tests/test_rate_limiter.py | 22 ----------------------
3 files changed, 17 insertions(+), 36 deletions(-)
diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py
index d16db5fecd..8a8071423e 100644
--- a/frappe/core/doctype/user/test_user.py
+++ b/frappe/core/doctype/user/test_user.py
@@ -11,6 +11,7 @@ from frappe.utils import get_url
from frappe.core.doctype.user.user import get_total_users
from frappe.core.doctype.user.user import MaxUsersReachedError, test_password_strength
from frappe.core.doctype.user.user import extract_mentions
+from frappe.frappeclient import FrappeClient
test_records = frappe.get_test_records('User')
@@ -229,16 +230,22 @@ class TestUser(unittest.TestCase):
self.assertEqual(extract_mentions(comment)[1], "test.again@example1.com")
def test_rate_limiting_for_reset_password(self):
- from frappe.utils.password import delete_password_reset_cache
- delete_password_reset_cache()
-
+ # Allow only one reset request for a day
frappe.db.set_value("System Settings", "System Settings", "password_reset_limit", 1)
+ frappe.db.commit()
- user = frappe.get_doc("User", "testperm@example.com")
- link = user.reset_password()
- self.assertRegex(link, "\/update-password\?key=[A-Za-z0-9]*")
+ url = get_url()
+ data={'cmd': 'frappe.core.doctype.user.user.reset_password', 'user': 'test@test.com'}
- self.assertRaises(frappe.ValidationError, user.reset_password, False)
+ # Clear rate limit tracker to start fresh
+ key = f"rl:{data['cmd']}:{data['user']}"
+ frappe.cache().delete(key)
+
+ c = FrappeClient(url)
+ res1 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
+ res2 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
+ self.assertEqual(res1.status_code, 200)
+ self.assertEqual(res2.status_code, 417)
def test_user_rollback(self):
""" """
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 573dcd7f17..c103ad7e4a 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -6,6 +6,9 @@ from __future__ import unicode_literals, print_function
from bs4 import BeautifulSoup
import frappe
+import frappe.share
+import frappe.defaults
+import frappe.permissions
from frappe.model.document import Document
from frappe.utils import cint, flt, has_gravatar, escape_html, format_datetime, now_datetime, get_formatted_email, today
from frappe import throw, msgprint, _
@@ -1170,10 +1173,3 @@ def generate_keys(user):
def switch_theme(theme):
if theme in ["Dark", "Light"]:
frappe.db.set_value("User", frappe.session.user, "desk_theme", theme)
-
-@frappe.whitelist(allow_guest=True)
-@rate_limit(key='user', limit=2, seconds = 60*60)
-def test_ratelimit(user):
- """This endpoint is used by testcases to check the ratelimit is functioning as expected.
- """
- return
diff --git a/frappe/tests/test_rate_limiter.py b/frappe/tests/test_rate_limiter.py
index 68d463ef74..ae1857bb31 100644
--- a/frappe/tests/test_rate_limiter.py
+++ b/frappe/tests/test_rate_limiter.py
@@ -13,8 +13,6 @@ import frappe
import frappe.rate_limiter
from frappe.rate_limiter import RateLimiter
from frappe.utils import cint
-from frappe.frappeclient import FrappeClient
-from frappe.utils.data import get_url
class TestRateLimiter(unittest.TestCase):
@@ -118,23 +116,3 @@ class TestRateLimiter(unittest.TestCase):
self.assertEqual(limiter.duration, cint(frappe.cache().get(limiter.key)))
frappe.cache().delete(limiter.key)
-
- def test_rate_limit_decorator(self):
- """Check that rate limit decorator raises 417 when limit is crossed.
- """
- url = get_url()
- data={'cmd': 'frappe.core.doctype.user.user.test_ratelimit', 'user': 'test@test.com'}
-
- # Clear rate limit tracker to start fresh
- key = f"rl:{data['cmd']}:{data['user']}"
- frappe.cache().delete(key)
-
- c = FrappeClient(url)
- res1 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
- res2 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
- res3 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
-
- self.assertEqual(res1.status_code, 200)
- self.assertEqual(res2.status_code, 200)
- self.assertEqual(res3.status_code, 417)
-
From 3717c981544bb9be9d10d6a83c266dd7a8ee11a0 Mon Sep 17 00:00:00 2001
From: shariquerik
Date: Tue, 9 Mar 2021 15:18:44 +0530
Subject: [PATCH 69/83] fix: implemented suggestions
---
frappe/public/js/frappe/desk.js | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index 54f74e5eed..54d7c79797 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -201,12 +201,13 @@ frappe.Application = Class.extend({
email_password_prompt: function(email_account,user,i) {
var me = this;
- var d = new frappe.ui.Dialog({
- title: __('Please enter your password for: {0}', [email_account[i]["email_id"]]),
+ let d = new frappe.ui.Dialog({
+ title: __('Password missing in Email Account'),
fields: [
- { 'fieldname': 'password',
+ {
+ 'fieldname': 'password',
'fieldtype': 'Password',
- 'label': 'Email Account Password',
+ 'label': __('Please enter the password for: {0}', [`${email_account[i]["email_id"]} `]),
'reqd': 1
},
{
From 78a8cd74d032646d48ae07150fd6257cddef8333 Mon Sep 17 00:00:00 2001
From: shariquerik
Date: Tue, 9 Mar 2021 15:22:22 +0530
Subject: [PATCH 70/83] fix: sider fix
---
frappe/public/js/frappe/desk.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index 54d7c79797..4162b3254b 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -202,7 +202,7 @@ frappe.Application = Class.extend({
email_password_prompt: function(email_account,user,i) {
var me = this;
let d = new frappe.ui.Dialog({
- title: __('Password missing in Email Account'),
+ title: __('Password missing in Email Account'),
fields: [
{
'fieldname': 'password',
From a4b75e78445e30820eab321f2238a7ac35e2084e Mon Sep 17 00:00:00 2001
From: Prssanna Desai
Date: Tue, 9 Mar 2021 16:34:54 +0530
Subject: [PATCH 71/83] fix: translation syntax
---
frappe/public/js/frappe/desk.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index 4162b3254b..d59bd4cdb7 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -207,7 +207,7 @@ frappe.Application = Class.extend({
{
'fieldname': 'password',
'fieldtype': 'Password',
- 'label': __('Please enter the password for: {0}', [`${email_account[i]["email_id"]} `]),
+ 'label': __('Please enter the password for: {0} ', [email_account[i]["email_id"]]),
'reqd': 1
},
{
From 5cd3f8534b55658d24b7aff48cdeb5d208e10c2e Mon Sep 17 00:00:00 2001
From: Sagar Vora
Date: Tue, 9 Mar 2021 16:36:36 +0530
Subject: [PATCH 72/83] fix: create gender only if provided while creating
oauth user
---
frappe/utils/oauth.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/frappe/utils/oauth.py b/frappe/utils/oauth.py
index 40bde6c2cc..6596701ee3 100644
--- a/frappe/utils/oauth.py
+++ b/frappe/utils/oauth.py
@@ -231,14 +231,14 @@ def update_oauth_user(user, data, provider):
save = True
user = frappe.new_doc("User")
- gender = (data.get("gender") or "").title()
+ gender = data.get("gender", "").title()
- if not frappe.db.exists("Gender", gender):
+ if gender and not frappe.db.exists("Gender", gender):
doc = frappe.new_doc("Gender", {"gender": gender})
doc.insert(ignore_permissions=True)
user.update({
- "doctype":"User",
+ "doctype": "User",
"first_name": get_first_name(data),
"last_name": get_last_name(data),
"email": get_email(data),
From 45c07f14a574a7789ec379964d179bee8e98d704 Mon Sep 17 00:00:00 2001
From: Faris Ansari
Date: Tue, 9 Mar 2021 21:54:44 +0530
Subject: [PATCH 73/83] fix: Make feature card fully clickable
---
frappe/public/scss/website/page_builder.scss | 1 +
.../section_with_features/section_with_features.html | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/frappe/public/scss/website/page_builder.scss b/frappe/public/scss/website/page_builder.scss
index 1803e52cf7..6eb6dae5d2 100644
--- a/frappe/public/scss/website/page_builder.scss
+++ b/frappe/public/scss/website/page_builder.scss
@@ -657,6 +657,7 @@
display: flex;
flex-direction: column;
justify-content: space-between;
+ position: relative;
}
.feature-title, .feature-content {
diff --git a/frappe/website/web_template/section_with_features/section_with_features.html b/frappe/website/web_template/section_with_features/section_with_features.html
index e837bf52d7..d893f8dab3 100644
--- a/frappe/website/web_template/section_with_features/section_with_features.html
+++ b/frappe/website/web_template/section_with_features/section_with_features.html
@@ -22,7 +22,7 @@
From 51ee995c07ad98445d2a6b289c5faf020baf396a Mon Sep 17 00:00:00 2001
From: Faris Ansari
Date: Tue, 9 Mar 2021 21:55:22 +0530
Subject: [PATCH 74/83] fix: Bolder font-weight for h2, like h1 and h3
---
frappe/public/scss/website/doc.scss | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/frappe/public/scss/website/doc.scss b/frappe/public/scss/website/doc.scss
index f258e2ee47..1585f428f9 100644
--- a/frappe/public/scss/website/doc.scss
+++ b/frappe/public/scss/website/doc.scss
@@ -180,7 +180,7 @@ $navbar-height-lg: 4.5rem;
h2 {
font-size: $font-size-2xl;
- font-weight: 400;
+ font-weight: 500;
}
h3 {
@@ -250,4 +250,4 @@ $navbar-height-lg: 4.5rem;
.breadcrumb {
margin-bottom: 0;
}
-}
\ No newline at end of file
+}
From 5e6283f33d3cbb7e0b61c3c715adc794eeffbcce Mon Sep 17 00:00:00 2001
From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
Date: Tue, 9 Mar 2021 22:27:16 +0530
Subject: [PATCH 75/83] test: Reset recorder before each test to avoid flaky
tests (#12554)
---
cypress/integration/recorder.js | 26 ++++++++++++--------------
1 file changed, 12 insertions(+), 14 deletions(-)
diff --git a/cypress/integration/recorder.js b/cypress/integration/recorder.js
index 7236200741..d30cc3568c 100644
--- a/cypress/integration/recorder.js
+++ b/cypress/integration/recorder.js
@@ -3,6 +3,16 @@ context('Recorder', () => {
cy.login();
});
+ beforeEach(() => {
+ cy.visit('/app/recorder');
+ return cy.window().its('frappe').then(frappe => {
+ // reset recorder
+ return frappe.xcall("frappe.recorder.stop").then(() => {
+ return frappe.xcall("frappe.recorder.delete");
+ });
+ });
+ });
+
it('Navigate to Recorder', () => {
cy.visit('/app');
cy.awesomebar('recorder');
@@ -11,7 +21,6 @@ context('Recorder', () => {
});
it('Recorder Empty State', () => {
- cy.visit('/app/recorder');
cy.get('.title-text').should('contain', 'Recorder');
cy.get('.indicator-pill').should('contain', 'Inactive').should('have.class', 'red');
@@ -24,7 +33,6 @@ context('Recorder', () => {
});
it('Recorder Start', () => {
- cy.visit('/app/recorder');
cy.get('.primary-action').should('contain', 'Start').click();
cy.get('.indicator-pill').should('contain', 'Active').should('have.class', 'green');
@@ -40,15 +48,9 @@ context('Recorder', () => {
cy.visit('/app/recorder');
cy.get('.title-text').should('contain', 'Recorder');
cy.get('.result-list').should('contain', '/api/method/frappe.desk.reportview.get');
-
- cy.get('#page-recorder .primary-action').should('contain', 'Stop').click();
- cy.wait(500);
- cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click();
- cy.get('.msg-box').should('contain', 'Inactive');
});
- it('Recorder View Request', () => {
- cy.visit('/app/recorder');
+ it.only('Recorder View Request', () => {
cy.get('.primary-action').should('contain', 'Start').click();
cy.visit('/app/List/DocType/List');
@@ -64,9 +66,5 @@ context('Recorder', () => {
cy.url().should('include', '/recorder/request');
cy.get('form').should('contain', '/api/method/frappe');
-
- cy.get('#page-recorder .primary-action').should('contain', 'Stop').click();
- cy.wait(200);
- cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click();
});
-});
\ No newline at end of file
+});
From 7c05fd2a177eeb4e647a6ebc6eb6229fdc27bcd5 Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Wed, 10 Mar 2021 10:29:07 +0530
Subject: [PATCH 76/83] test: Wait for enqueued job to execute
---
frappe/tests/test_background_jobs.py | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/frappe/tests/test_background_jobs.py b/frappe/tests/test_background_jobs.py
index 5b66183333..88783f14f1 100644
--- a/frappe/tests/test_background_jobs.py
+++ b/frappe/tests/test_background_jobs.py
@@ -5,24 +5,26 @@ from rq import Queue
import frappe
from frappe.core.page.background_jobs.background_jobs import remove_failed_jobs
from frappe.utils.background_jobs import get_redis_conn
+import time
class TestBackgroundJobs(unittest.TestCase):
def test_remove_failed_jobs(self):
- frappe.enqueue(method="frappe.tests.test_background_jobs.fail_function")
-
+ frappe.enqueue(method="frappe.tests.test_background_jobs.fail_function", queue="short")
+ # wait for enqueued job to execute
+ time.sleep(2)
conn = get_redis_conn()
queues = Queue.all(conn)
for queue in queues:
- if queue.name == "default":
+ if queue.name == "short":
fail_registry = queue.failed_job_registry
self.assertGreater(fail_registry.count, 0)
remove_failed_jobs()
for queue in queues:
- if queue.name == "default":
+ if queue.name == "short":
fail_registry = queue.failed_job_registry
self.assertEqual(fail_registry.count, 0)
From 138c7841c2bdbf4cb1c674e7c2d4e238b72e919c Mon Sep 17 00:00:00 2001
From: Andy Zhu
Date: Wed, 10 Mar 2021 18:02:50 +1300
Subject: [PATCH 77/83] fix: set_primary and set_primary_email bug (#12299)
* fix: set_primary and set_primary_email bug
When user untick the `is_primary_phone` or `is_primary_mobile` from the contact numbers table, we should reset the phone, mobile number etc.
Same goes for the email address table.
* Update frappe/contacts/doctype/contact/contact.py
fix variable naming as suggested
Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
* Update frappe/contacts/doctype/contact/contact.py
fix variable naming as suggested
Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
* Update frappe/contacts/doctype/contact/contact.py
fix variable naming as suggested
Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
* Update frappe/contacts/doctype/contact/contact.py
fix variable naming as suggested
Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
* Update frappe/contacts/doctype/contact/contact.py
fix variable naming as suggested
Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
* Update frappe/contacts/doctype/contact/contact.py
fix variable naming as suggested
Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
* test: Set empty string instead of none for number
Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
Co-authored-by: Suraj Shetty
---
frappe/contacts/doctype/contact/contact.py | 10 ++++++++++
.../test_addresses_and_contacts.py | 2 +-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py
index 42fa039f74..b3d4c6fc5c 100644
--- a/frappe/contacts/doctype/contact/contact.py
+++ b/frappe/contacts/doctype/contact/contact.py
@@ -97,11 +97,16 @@ class Contact(Document):
if len([email.email_id for email in self.email_ids if email.is_primary]) > 1:
frappe.throw(_("Only one {0} can be set as primary.").format(frappe.bold("Email ID")))
+ primary_email_exists = False
for d in self.email_ids:
if d.is_primary == 1:
+ primary_email_exists = True
self.email_id = d.email_id.strip()
break
+ if not primary_email_exists:
+ self.email_id = ""
+
def set_primary(self, fieldname):
# Used to set primary mobile and phone no.
if len(self.phone_nos) == 0:
@@ -115,11 +120,16 @@ class Contact(Document):
if len(is_primary) > 1:
frappe.throw(_("Only one {0} can be set as primary.").format(frappe.bold(frappe.unscrub(fieldname))))
+ primary_number_exists = False
for d in self.phone_nos:
if d.get(field_name) == 1:
+ primary_number_exists = True
setattr(self, fieldname, d.phone)
break
+ if not primary_number_exists:
+ setattr(self, fieldname, "")
+
def get_default_contact(doctype, name):
'''Returns default contact for the given doctype, name'''
out = frappe.db.sql('''select parent,
diff --git a/frappe/contacts/report/addresses_and_contacts/test_addresses_and_contacts.py b/frappe/contacts/report/addresses_and_contacts/test_addresses_and_contacts.py
index 2db395102a..9e98dcf6f6 100644
--- a/frappe/contacts/report/addresses_and_contacts/test_addresses_and_contacts.py
+++ b/frappe/contacts/report/addresses_and_contacts/test_addresses_and_contacts.py
@@ -103,7 +103,7 @@ class TestAddressesAndContacts(unittest.TestCase):
create_linked_contact(links_list, d)
report_data = get_data({"reference_doctype": "Test Custom Doctype"})
for idx, link in enumerate(links_list):
- test_item = [link, 'test address line 1', 'test address line 2', 'Milan', None, None, 'Italy', 0, '_Test First Name', '_Test Last Name', '_Test Address-Billing', '+91 0000000000', None, 'test_contact@example.com', 1]
+ test_item = [link, 'test address line 1', 'test address line 2', 'Milan', None, None, 'Italy', 0, '_Test First Name', '_Test Last Name', '_Test Address-Billing', '+91 0000000000', '', 'test_contact@example.com', 1]
self.assertListEqual(test_item, report_data[idx])
def tearDown(self):
From 9aeb9ac1858b0812dc8cbe91db402262f0434f26 Mon Sep 17 00:00:00 2001
From: Sagar Vora
Date: Wed, 10 Mar 2021 16:08:18 +0530
Subject: [PATCH 78/83] fix(router): navigating forward not working for routes
in frappe.re_route
---
frappe/public/js/frappe/router.js | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js
index d56a7db823..d9bd24d1b4 100644
--- a/frappe/public/js/frappe/router.js
+++ b/frappe/public/js/frappe/router.js
@@ -101,6 +101,7 @@ frappe.router = {
let sub_path = this.get_sub_path();
if (this.re_route(sub_path)) return;
+ this.current_sub_path = sub_path;
this.current_route = this.parse();
this.set_history(sub_path);
this.render();
@@ -223,14 +224,14 @@ frappe.router = {
// it doesn't allow us to go back to the one prior to "new-doctype-1"
// Hence if this check is true, instead of changing location hash,
// we just do a back to go to the doc previous to the "new-doctype-1"
- var re_route_val = this.get_sub_path(frappe.re_route[sub_path]);
- if (decodeURIComponent(re_route_val) !== decodeURIComponent(sub_path)) {
+ const re_route_val = this.get_sub_path(frappe.re_route[sub_path]);
+ if (re_route_val === this.current_sub_path) {
window.history.back();
- return true;
} else {
frappe.set_route(re_route_val);
- return true;
}
+
+ return true;
}
},
From 0588e9c7ffbd9974b8bbe3571c97936e36cd32e8 Mon Sep 17 00:00:00 2001
From: Sagar Vora
Date: Wed, 10 Mar 2021 16:55:03 +0530
Subject: [PATCH 79/83] fix: routing issues in recorder
---
frappe/core/page/recorder/recorder.js | 3 ++-
frappe/public/js/frappe/recorder/RecorderRoot.vue | 9 ++++++---
frappe/public/js/frappe/recorder/RequestDetail.vue | 2 +-
frappe/public/js/frappe/recorder/recorder.js | 10 ++++++++--
4 files changed, 17 insertions(+), 7 deletions(-)
diff --git a/frappe/core/page/recorder/recorder.js b/frappe/core/page/recorder/recorder.js
index 4d6d6aa84c..b75ea6a41c 100644
--- a/frappe/core/page/recorder/recorder.js
+++ b/frappe/core/page/recorder/recorder.js
@@ -22,6 +22,7 @@ class Recorder {
}
show() {
-
+ if (!this.view || this.view.$route.name == "recorder-detail") return;
+ this.view.$router.replace({name: "recorder-detail"});
}
}
diff --git a/frappe/public/js/frappe/recorder/RecorderRoot.vue b/frappe/public/js/frappe/recorder/RecorderRoot.vue
index 0877acc463..479ab1b2ca 100644
--- a/frappe/public/js/frappe/recorder/RecorderRoot.vue
+++ b/frappe/public/js/frappe/recorder/RecorderRoot.vue
@@ -7,8 +7,11 @@
diff --git a/frappe/public/js/frappe/recorder/RequestDetail.vue b/frappe/public/js/frappe/recorder/RequestDetail.vue
index ac349d7937..cc056686d5 100644
--- a/frappe/public/js/frappe/recorder/RequestDetail.vue
+++ b/frappe/public/js/frappe/recorder/RequestDetail.vue
@@ -284,7 +284,7 @@ export default {
frappe.breadcrumbs.add({
type: 'Custom',
label: __('Recorder'),
- route: '#recorder'
+ route: '/app/recorder'
});
frappe.call({
method: "frappe.recorder.get",
diff --git a/frappe/public/js/frappe/recorder/recorder.js b/frappe/public/js/frappe/recorder/recorder.js
index f3a33e6a8f..c80fad62f6 100644
--- a/frappe/public/js/frappe/recorder/recorder.js
+++ b/frappe/public/js/frappe/recorder/recorder.js
@@ -18,6 +18,12 @@ const routes = [
path: '/request/:id',
component: RequestDetail,
},
+ {
+ path: '/',
+ redirect: {
+ name: "recorder-detail"
+ }
+ }
];
const router = new VueRouter({
@@ -26,11 +32,11 @@ const router = new VueRouter({
routes: routes,
});
-new Vue({
+frappe.recorder.view = new Vue({
el: ".recorder-container",
router: router,
data: {
- page: cur_page.page.page
+ page: frappe.pages["recorder"].page
},
template: " ",
components: {
From e2b91b5454fc44151b97787e0034431b83ae811b Mon Sep 17 00:00:00 2001
From: this-gavagai
Date: Wed, 10 Mar 2021 17:34:53 +0545
Subject: [PATCH 80/83] chore(calendar): Updated fullcalendar.js to v3.10.2
(#12519)
Co-authored-by: theopen-institute
---
.../js/lib/fullcalendar/fullcalendar.min.css | 6 +++---
.../js/lib/fullcalendar/fullcalendar.min.js | 16 +++++++++-------
frappe/public/js/lib/fullcalendar/locale-all.js | 11 ++++++-----
3 files changed, 18 insertions(+), 15 deletions(-)
diff --git a/frappe/public/js/lib/fullcalendar/fullcalendar.min.css b/frappe/public/js/lib/fullcalendar/fullcalendar.min.css
index 4eab5405e8..ab2403ecb3 100644
--- a/frappe/public/js/lib/fullcalendar/fullcalendar.min.css
+++ b/frappe/public/js/lib/fullcalendar/fullcalendar.min.css
@@ -1,5 +1,5 @@
/*!
- * FullCalendar v3.4.0 Stylesheet
+ * FullCalendar v3.10.2
* Docs & License: https://fullcalendar.io/
- * (c) 2017 Adam Shaw
- */.fc-icon,body .fc{font-size:1em}.fc-button-group,.fc-icon{display:inline-block}.fc-bg,.fc-row .fc-bgevent-skeleton,.fc-row .fc-highlight-skeleton{bottom:0}.fc-icon,.fc-unselectable{-khtml-user-select:none;-webkit-touch-callout:none}.fc{direction:ltr;text-align:left}.fc-rtl{text-align:right}.fc th,.fc-basic-view td.fc-week-number,.fc-icon,.fc-toolbar{text-align:center}.fc-unthemed .fc-content,.fc-unthemed .fc-divider,.fc-unthemed .fc-list-heading td,.fc-unthemed .fc-list-view,.fc-unthemed .fc-popover,.fc-unthemed .fc-row,.fc-unthemed tbody,.fc-unthemed td,.fc-unthemed th,.fc-unthemed thead{border-color:#ddd}.fc-unthemed .fc-popover{background-color:#fff}.fc-unthemed .fc-divider,.fc-unthemed .fc-list-heading td,.fc-unthemed .fc-popover .fc-header{background:#eee}.fc-unthemed .fc-popover .fc-header .fc-close{color:#666}.fc-unthemed td.fc-today{background:#fcf8e3}.fc-highlight{background:#bce8f1;opacity:.3}.fc-bgevent{background:#8fdf82;opacity:.3}.fc-nonbusiness{background:#d7d7d7}.fc-unthemed .fc-disabled-day{background:#d7d7d7;opacity:.3}.ui-widget .fc-disabled-day{background-image:none}.fc-icon{height:1em;line-height:1em;overflow:hidden;font-family:"Courier New",Courier,monospace;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.fc-icon:after{position:relative}.fc-icon-left-single-arrow:after{content:"\02039";font-weight:700;font-size:200%;top:-7%}.fc-icon-right-single-arrow:after{content:"\0203A";font-weight:700;font-size:200%;top:-7%}.fc-icon-left-double-arrow:after{content:"\000AB";font-size:160%;top:-7%}.fc-icon-right-double-arrow:after{content:"\000BB";font-size:160%;top:-7%}.fc-icon-left-triangle:after{content:"\25C4";font-size:125%;top:3%}.fc-icon-right-triangle:after{content:"\25BA";font-size:125%;top:3%}.fc-icon-down-triangle:after{content:"\25BC";font-size:125%;top:2%}.fc-icon-x:after{content:"\000D7";font-size:200%;top:6%}.fc button{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;height:2.1em;padding:0 .6em;font-size:1em;white-space:nowrap;cursor:pointer}.fc button::-moz-focus-inner{margin:0;padding:0}.fc-state-default{border:1px solid;background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);color:#333;text-shadow:0 1px 1px rgba(255,255,255,.75);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05)}.fc-state-default.fc-corner-left{border-top-left-radius:4px;border-bottom-left-radius:4px}.fc-state-default.fc-corner-right{border-top-right-radius:4px;border-bottom-right-radius:4px}.fc button .fc-icon{position:relative;top:-.05em;margin:0 .2em;vertical-align:middle}.fc-state-active,.fc-state-disabled,.fc-state-down,.fc-state-hover{color:#333;background-color:#e6e6e6}.fc-state-hover{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.fc-state-active,.fc-state-down{background-color:#ccc;background-image:none;box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05)}.fc-state-disabled{cursor:default;background-image:none;opacity:.65;box-shadow:none}.fc-event.fc-draggable,.fc-event[href],.fc-popover .fc-header .fc-close,a[data-goto]{cursor:pointer}.fc .fc-button-group>*{float:left;margin:0 0 0 -1px}.fc .fc-button-group>:first-child{margin-left:0}.fc-popover{position:absolute;box-shadow:0 2px 6px rgba(0,0,0,.15)}.fc-popover .fc-header{padding:2px 4px}.fc-popover .fc-header .fc-title{margin:0 2px}.fc-ltr .fc-popover .fc-header .fc-title,.fc-rtl .fc-popover .fc-header .fc-close{float:left}.fc-ltr .fc-popover .fc-header .fc-close,.fc-rtl .fc-popover .fc-header .fc-title{float:right}.fc-unthemed .fc-popover{border-width:1px;border-style:solid}.fc-unthemed .fc-popover .fc-header .fc-close{font-size:.9em;margin-top:2px}.fc-popover>.ui-widget-header+.ui-widget-content{border-top:0}.fc-divider{border-style:solid;border-width:1px}hr.fc-divider{height:0;margin:0;padding:0 0 2px;border-width:1px 0}.fc-bg table,.fc-row .fc-bgevent-skeleton table,.fc-row .fc-highlight-skeleton table{height:100%}.fc-clear{clear:both}.fc-bg,.fc-bgevent-skeleton,.fc-helper-skeleton,.fc-highlight-skeleton{position:absolute;top:0;left:0;right:0}.fc table{width:100%;box-sizing:border-box;table-layout:fixed;border-collapse:collapse;border-spacing:0;font-size:1em}.fc td,.fc th{border-style:solid;border-width:1px;padding:0;vertical-align:top}.fc td.fc-today{border-style:double}a[data-goto]:hover{text-decoration:underline}.fc .fc-row{border-style:solid;border-width:0}.fc-row table{border-left:0 hidden transparent;border-right:0 hidden transparent;border-bottom:0 hidden transparent}.fc-row:first-child table{border-top:0 hidden transparent}.fc-row{position:relative}.fc-row .fc-bg{z-index:1}.fc-row .fc-bgevent-skeleton td,.fc-row .fc-highlight-skeleton td{border-color:transparent}.fc-row .fc-bgevent-skeleton{z-index:2}.fc-row .fc-highlight-skeleton{z-index:3}.fc-row .fc-content-skeleton{position:relative;z-index:4;padding-bottom:2px}.fc-row .fc-helper-skeleton{z-index:5}.fc-row .fc-content-skeleton td,.fc-row .fc-helper-skeleton td{background:0 0;border-color:transparent;border-bottom:0}.fc-row .fc-content-skeleton tbody td,.fc-row .fc-helper-skeleton tbody td{border-top:0}.fc-scroller{-webkit-overflow-scrolling:touch}.fc-row.fc-rigid,.fc-time-grid-event{overflow:hidden}.fc-scroller>.fc-day-grid,.fc-scroller>.fc-time-grid{position:relative;width:100%}.fc-event{position:relative;display:block;font-size:.85em;line-height:1.3;border-radius:3px;border:1px solid #3a87ad;font-weight:400}.fc-event,.fc-event-dot{background-color:#3a87ad}.fc-event,.fc-event:hover,.ui-widget .fc-event{color:#fff;text-decoration:none}.fc-not-allowed,.fc-not-allowed .fc-event{cursor:not-allowed}.fc-event .fc-bg{z-index:1;background:#fff;opacity:.25}.fc-event .fc-content{position:relative;z-index:2}.fc-event .fc-resizer{position:absolute;z-index:4;display:none}.fc-event.fc-allow-mouse-resize .fc-resizer,.fc-event.fc-selected .fc-resizer{display:block}.fc-event.fc-selected .fc-resizer:before{content:"";position:absolute;z-index:9999;top:50%;left:50%;width:40px;height:40px;margin-left:-20px;margin-top:-20px}.fc-event.fc-selected{z-index:9999!important;box-shadow:0 2px 5px rgba(0,0,0,.2)}.fc-event.fc-selected.fc-dragging{box-shadow:0 2px 7px rgba(0,0,0,.3)}.fc-h-event.fc-selected:before{content:"";position:absolute;z-index:3;top:-10px;bottom:-10px;left:0;right:0}.fc-ltr .fc-h-event.fc-not-start,.fc-rtl .fc-h-event.fc-not-end{margin-left:0;border-left-width:0;padding-left:1px;border-top-left-radius:0;border-bottom-left-radius:0}.fc-ltr .fc-h-event.fc-not-end,.fc-rtl .fc-h-event.fc-not-start{margin-right:0;border-right-width:0;padding-right:1px;border-top-right-radius:0;border-bottom-right-radius:0}.fc-ltr .fc-h-event .fc-start-resizer,.fc-rtl .fc-h-event .fc-end-resizer{cursor:w-resize;left:-1px}.fc-ltr .fc-h-event .fc-end-resizer,.fc-rtl .fc-h-event .fc-start-resizer{cursor:e-resize;right:-1px}.fc-h-event.fc-allow-mouse-resize .fc-resizer{width:7px;top:-1px;bottom:-1px}.fc-h-event.fc-selected .fc-resizer{border-radius:4px;border-width:1px;width:6px;height:6px;border-style:solid;border-color:inherit;background:#fff;top:50%;margin-top:-4px}.fc-ltr .fc-h-event.fc-selected .fc-start-resizer,.fc-rtl .fc-h-event.fc-selected .fc-end-resizer{margin-left:-4px}.fc-ltr .fc-h-event.fc-selected .fc-end-resizer,.fc-rtl .fc-h-event.fc-selected .fc-start-resizer{margin-right:-4px}.fc-day-grid-event{margin:1px 2px 0;padding:0 1px}tr:first-child>td>.fc-day-grid-event{margin-top:2px}.fc-day-grid-event.fc-selected:after{content:"";position:absolute;z-index:1;top:-1px;right:-1px;bottom:-1px;left:-1px;background:#000;opacity:.25}.fc-day-grid-event .fc-content{white-space:nowrap;overflow:hidden}.fc-day-grid-event .fc-time{font-weight:700}.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer,.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer{margin-left:-2px}.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer,.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer{margin-right:-2px}a.fc-more{margin:1px 3px;font-size:.85em;cursor:pointer;text-decoration:none}a.fc-more:hover{text-decoration:underline}.fc-limited{display:none}.fc-day-grid .fc-row{z-index:1}.fc-more-popover{z-index:2;width:220px}.fc-more-popover .fc-event-container{padding:10px}.fc-now-indicator{position:absolute;border:0 solid red}.fc-unselectable{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent}.fc-toolbar.fc-header-toolbar{margin-bottom:1em}.fc-toolbar.fc-footer-toolbar{margin-top:1em}.fc-toolbar .fc-left{float:left}.fc-toolbar .fc-right{float:right}.fc-toolbar .fc-center{display:inline-block}.fc .fc-toolbar>*>*{float:left;margin-left:.75em}.fc .fc-toolbar>*>:first-child{margin-left:0}.fc-toolbar h2{margin:0}.fc-toolbar button{position:relative}.fc-toolbar .fc-state-hover,.fc-toolbar .ui-state-hover{z-index:2}.fc-toolbar .fc-state-down{z-index:3}.fc-toolbar .fc-state-active,.fc-toolbar .ui-state-active{z-index:4}.fc-toolbar button:focus{z-index:5}.fc-view-container *,.fc-view-container :after,.fc-view-container :before{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}.fc-view,.fc-view>table{position:relative;z-index:1}.fc-basicDay-view .fc-content-skeleton,.fc-basicWeek-view .fc-content-skeleton{padding-bottom:1em}.fc-basic-view .fc-body .fc-row{min-height:4em}.fc-row.fc-rigid .fc-content-skeleton{position:absolute;top:0;left:0;right:0}.fc-day-top.fc-other-month{opacity:.3}.fc-basic-view .fc-day-number,.fc-basic-view .fc-week-number{padding:2px}.fc-basic-view th.fc-day-number,.fc-basic-view th.fc-week-number{padding:0 2px}.fc-ltr .fc-basic-view .fc-day-top .fc-day-number{float:right}.fc-rtl .fc-basic-view .fc-day-top .fc-day-number{float:left}.fc-ltr .fc-basic-view .fc-day-top .fc-week-number{float:left;border-radius:0 0 3px}.fc-rtl .fc-basic-view .fc-day-top .fc-week-number{float:right;border-radius:0 0 0 3px}.fc-basic-view .fc-day-top .fc-week-number{min-width:1.5em;text-align:center;background-color:#f2f2f2;color:grey}.fc-basic-view td.fc-week-number>*{display:inline-block;min-width:1.25em}.fc-agenda-view .fc-day-grid{position:relative;z-index:2}.fc-agenda-view .fc-day-grid .fc-row{min-height:3em}.fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton{padding-bottom:1em}.fc .fc-axis{vertical-align:middle;padding:0 4px;white-space:nowrap}.fc-ltr .fc-axis{text-align:right}.fc-rtl .fc-axis{text-align:left}.ui-widget td.fc-axis{font-weight:400}.fc-time-grid,.fc-time-grid-container{position:relative;z-index:1}.fc-time-grid{min-height:100%}.fc-time-grid table{border:0 hidden transparent}.fc-time-grid>.fc-bg{z-index:1}.fc-time-grid .fc-slats,.fc-time-grid>hr{position:relative;z-index:2}.fc-time-grid .fc-content-col{position:relative}.fc-time-grid .fc-content-skeleton{position:absolute;z-index:3;top:0;left:0;right:0}.fc-time-grid .fc-business-container{position:relative;z-index:1}.fc-time-grid .fc-bgevent-container{position:relative;z-index:2}.fc-time-grid .fc-highlight-container{z-index:3;position:relative}.fc-time-grid .fc-event-container{position:relative;z-index:4}.fc-time-grid .fc-now-indicator-line{z-index:5}.fc-time-grid .fc-helper-container{position:relative;z-index:6}.fc-time-grid .fc-slats td{height:1.5em;border-bottom:0}.fc-time-grid .fc-slats .fc-minor td{border-top-style:dotted}.fc-time-grid .fc-slats .ui-widget-content{background:0 0}.fc-time-grid .fc-highlight{position:absolute;left:0;right:0}.fc-ltr .fc-time-grid .fc-event-container{margin:0 2.5% 0 2px}.fc-rtl .fc-time-grid .fc-event-container{margin:0 2px 0 2.5%}.fc-time-grid .fc-bgevent,.fc-time-grid .fc-event{position:absolute;z-index:1}.fc-time-grid .fc-bgevent{left:0;right:0}.fc-v-event.fc-not-start{border-top-width:0;padding-top:1px;border-top-left-radius:0;border-top-right-radius:0}.fc-v-event.fc-not-end{border-bottom-width:0;padding-bottom:1px;border-bottom-left-radius:0;border-bottom-right-radius:0}.fc-time-grid-event.fc-selected{overflow:visible}.fc-time-grid-event.fc-selected .fc-bg{display:none}.fc-time-grid-event .fc-content{overflow:hidden}.fc-time-grid-event .fc-time,.fc-time-grid-event .fc-title{padding:0 1px}.fc-time-grid-event .fc-time{font-size:.85em;white-space:nowrap}.fc-time-grid-event.fc-short .fc-content{white-space:nowrap}.fc-time-grid-event.fc-short .fc-time,.fc-time-grid-event.fc-short .fc-title{display:inline-block;vertical-align:top}.fc-time-grid-event.fc-short .fc-time span{display:none}.fc-time-grid-event.fc-short .fc-time:before{content:attr(data-start)}.fc-time-grid-event.fc-short .fc-time:after{content:"\000A0-\000A0"}.fc-time-grid-event.fc-short .fc-title{font-size:.85em;padding:0}.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer{left:0;right:0;bottom:0;height:8px;overflow:hidden;line-height:8px;font-size:11px;font-family:monospace;text-align:center;cursor:s-resize}.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer:after{content:"="}.fc-time-grid-event.fc-selected .fc-resizer{border-radius:5px;border-width:1px;width:8px;height:8px;border-style:solid;border-color:inherit;background:#fff;left:50%;margin-left:-5px;bottom:-5px}.fc-time-grid .fc-now-indicator-line{border-top-width:1px;left:0;right:0}.fc-time-grid .fc-now-indicator-arrow{margin-top:-5px}.fc-ltr .fc-time-grid .fc-now-indicator-arrow{left:0;border-width:5px 0 5px 6px;border-top-color:transparent;border-bottom-color:transparent}.fc-rtl .fc-time-grid .fc-now-indicator-arrow{right:0;border-width:5px 6px 5px 0;border-top-color:transparent;border-bottom-color:transparent}.fc-event-dot{display:inline-block;width:10px;height:10px;border-radius:5px}.fc-rtl .fc-list-view{direction:rtl}.fc-list-view{border-width:1px;border-style:solid}.fc .fc-list-table{table-layout:auto}.fc-list-table td{border-width:1px 0 0;padding:8px 14px}.fc-list-table tr:first-child td{border-top-width:0}.fc-list-heading{border-bottom-width:1px}.fc-list-heading td{font-weight:700}.fc-ltr .fc-list-heading-main{float:left}.fc-ltr .fc-list-heading-alt,.fc-rtl .fc-list-heading-main{float:right}.fc-rtl .fc-list-heading-alt{float:left}.fc-list-item.fc-has-url{cursor:pointer}.fc-list-item:hover td{background-color:#f5f5f5}.fc-list-item-marker,.fc-list-item-time{white-space:nowrap;width:1px}.fc-ltr .fc-list-item-marker{padding-right:0}.fc-rtl .fc-list-item-marker{padding-left:0}.fc-list-item-title a{text-decoration:none;color:inherit}.fc-list-item-title a[href]:hover{text-decoration:underline}.fc-list-empty-wrap2{position:absolute;top:0;left:0;right:0;bottom:0}.fc-list-empty-wrap1{width:100%;height:100%;display:table}.fc-list-empty{display:table-cell;vertical-align:middle;text-align:center}.fc-unthemed .fc-list-empty{background-color:#eee}
\ No newline at end of file
+ * (c) 2019 Adam Shaw
+ */.fc button,.fc table,body .fc{font-size:1em}.fc .fc-axis,.fc button,.fc-day-grid-event .fc-content,.fc-list-item-marker,.fc-list-item-time,.fc-time-grid-event .fc-time,.fc-time-grid-event.fc-short .fc-content{white-space:nowrap}.fc-event,.fc-event:hover,.fc-state-hover,.fc.fc-bootstrap3 a,.ui-widget .fc-event,a.fc-more{text-decoration:none}.fc{direction:ltr;text-align:left}.fc-rtl{text-align:right}.fc th,.fc-basic-view .fc-day-top .fc-week-number,.fc-basic-view td.fc-week-number,.fc-icon,.fc-toolbar{text-align:center}.fc-highlight{background:#bce8f1;opacity:.3}.fc-bgevent{background:#8fdf82;opacity:.3}.fc-nonbusiness{background:#d7d7d7}.fc button{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;height:2.1em;padding:0 .6em;cursor:pointer}.fc button::-moz-focus-inner{margin:0;padding:0}.fc-state-default{border:1px solid;background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);color:#333;text-shadow:0 1px 1px rgba(255,255,255,.75);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05)}.fc-state-default.fc-corner-left{border-top-left-radius:4px;border-bottom-left-radius:4px}.fc-state-default.fc-corner-right{border-top-right-radius:4px;border-bottom-right-radius:4px}.fc button .fc-icon{position:relative;top:-.05em;margin:0 .2em;vertical-align:middle}.fc-state-active,.fc-state-disabled,.fc-state-down,.fc-state-hover{color:#333;background-color:#e6e6e6}.fc-state-hover{color:#333;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.fc-state-active,.fc-state-down{background-color:#ccc;background-image:none;box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05)}.fc-state-disabled{cursor:default;background-image:none;opacity:.65;box-shadow:none}.fc-event.fc-draggable,.fc-event[href],.fc-popover .fc-header .fc-close,a[data-goto]{cursor:pointer}.fc-button-group{display:inline-block}.fc .fc-button-group>*{float:left;margin:0 0 0 -1px}.fc .fc-button-group>:first-child{margin-left:0}.fc-popover{position:absolute;box-shadow:0 2px 6px rgba(0,0,0,.15)}.fc-popover .fc-header{padding:2px 4px}.fc-popover .fc-header .fc-title{margin:0 2px}.fc-ltr .fc-popover .fc-header .fc-title,.fc-rtl .fc-popover .fc-header .fc-close{float:left}.fc-ltr .fc-popover .fc-header .fc-close,.fc-rtl .fc-popover .fc-header .fc-title{float:right}.fc-divider{border-style:solid;border-width:1px}hr.fc-divider{height:0;margin:0;padding:0 0 2px;border-width:1px 0}.fc-bg table,.fc-row .fc-bgevent-skeleton table,.fc-row .fc-highlight-skeleton table{height:100%}.fc-clear{clear:both}.fc-bg,.fc-bgevent-skeleton,.fc-helper-skeleton,.fc-highlight-skeleton{position:absolute;top:0;left:0;right:0}.fc-bg{bottom:0}.fc table{width:100%;box-sizing:border-box;table-layout:fixed;border-collapse:collapse;border-spacing:0}.fc td,.fc th{border-style:solid;border-width:1px;padding:0;vertical-align:top}.fc td.fc-today{border-style:double}a[data-goto]:hover{text-decoration:underline}.fc .fc-row{border-style:solid;border-width:0}.fc-row table{border-left:0 hidden transparent;border-right:0 hidden transparent;border-bottom:0 hidden transparent}.fc-row:first-child table{border-top:0 hidden transparent}.fc-row{position:relative}.fc-row .fc-bg{z-index:1}.fc-row .fc-bgevent-skeleton,.fc-row .fc-highlight-skeleton{bottom:0}.fc-row .fc-bgevent-skeleton td,.fc-row .fc-highlight-skeleton td{border-color:transparent}.fc-row .fc-bgevent-skeleton{z-index:2}.fc-row .fc-highlight-skeleton{z-index:3}.fc-row .fc-content-skeleton{position:relative;z-index:4;padding-bottom:2px}.fc-row .fc-helper-skeleton{z-index:5}.fc .fc-row .fc-content-skeleton table,.fc .fc-row .fc-content-skeleton td,.fc .fc-row .fc-helper-skeleton td{background:0 0;border-color:transparent}.fc-row .fc-content-skeleton td,.fc-row .fc-helper-skeleton td{border-bottom:0}.fc-row .fc-content-skeleton tbody td,.fc-row .fc-helper-skeleton tbody td{border-top:0}.fc-scroller{-webkit-overflow-scrolling:touch}.fc-day-grid-event .fc-content,.fc-icon,.fc-row.fc-rigid,.fc-time-grid-event{overflow:hidden}.fc-scroller>.fc-day-grid,.fc-scroller>.fc-time-grid{position:relative;width:100%}.fc-event{position:relative;display:block;font-size:.85em;line-height:1.3;border-radius:3px;border:1px solid #3a87ad}.fc-event,.fc-event-dot{background-color:#3a87ad}.fc-event,.fc-event:hover{color:#fff}.fc-not-allowed,.fc-not-allowed .fc-event{cursor:not-allowed}.fc-event .fc-bg{z-index:1;background:#fff;opacity:.25}.fc-event .fc-content{position:relative;z-index:2}.fc-event .fc-resizer{position:absolute;z-index:4;display:none}.fc-event.fc-allow-mouse-resize .fc-resizer,.fc-event.fc-selected .fc-resizer{display:block}.fc-event.fc-selected .fc-resizer:before{content:"";position:absolute;z-index:9999;top:50%;left:50%;width:40px;height:40px;margin-left:-20px;margin-top:-20px}.fc-event.fc-selected{z-index:9999!important;box-shadow:0 2px 5px rgba(0,0,0,.2)}.fc-event.fc-selected.fc-dragging{box-shadow:0 2px 7px rgba(0,0,0,.3)}.fc-h-event.fc-selected:before{content:"";position:absolute;z-index:3;top:-10px;bottom:-10px;left:0;right:0}.fc-ltr .fc-h-event.fc-not-start,.fc-rtl .fc-h-event.fc-not-end{margin-left:0;border-left-width:0;padding-left:1px;border-top-left-radius:0;border-bottom-left-radius:0}.fc-ltr .fc-h-event.fc-not-end,.fc-rtl .fc-h-event.fc-not-start{margin-right:0;border-right-width:0;padding-right:1px;border-top-right-radius:0;border-bottom-right-radius:0}.fc-ltr .fc-h-event .fc-start-resizer,.fc-rtl .fc-h-event .fc-end-resizer{cursor:w-resize;left:-1px}.fc-ltr .fc-h-event .fc-end-resizer,.fc-rtl .fc-h-event .fc-start-resizer{cursor:e-resize;right:-1px}.fc-h-event.fc-allow-mouse-resize .fc-resizer{width:7px;top:-1px;bottom:-1px}.fc-h-event.fc-selected .fc-resizer{border-radius:4px;border-width:1px;width:6px;height:6px;border-style:solid;border-color:inherit;background:#fff;top:50%;margin-top:-4px}.fc-ltr .fc-h-event.fc-selected .fc-start-resizer,.fc-rtl .fc-h-event.fc-selected .fc-end-resizer{margin-left:-4px}.fc-ltr .fc-h-event.fc-selected .fc-end-resizer,.fc-rtl .fc-h-event.fc-selected .fc-start-resizer{margin-right:-4px}.fc-day-grid-event{margin:1px 2px 0;padding:0 1px}tr:first-child>td>.fc-day-grid-event{margin-top:2px}.fc-day-grid-event.fc-selected:after{content:"";position:absolute;z-index:1;top:-1px;right:-1px;bottom:-1px;left:-1px;background:#000;opacity:.25}.fc-day-grid-event .fc-time{font-weight:700}.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer,.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer{margin-left:-2px}.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer,.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer{margin-right:-2px}a.fc-more{margin:1px 3px;font-size:.85em;cursor:pointer}a.fc-more:hover{text-decoration:underline}.fc-limited{display:none}.fc-day-grid .fc-row{z-index:1}.fc-more-popover{z-index:2;width:220px}.fc-more-popover .fc-event-container{padding:10px}.fc-bootstrap3 .fc-popover .panel-body,.fc-bootstrap4 .fc-popover .card-body{padding:0}.fc-now-indicator{position:absolute;border:0 solid red}.fc-bootstrap3 .fc-today.alert,.fc-bootstrap4 .fc-today.alert{border-radius:0}.fc-unselectable{-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent}.fc-unthemed .fc-content,.fc-unthemed .fc-divider,.fc-unthemed .fc-list-heading td,.fc-unthemed .fc-list-view,.fc-unthemed .fc-popover,.fc-unthemed .fc-row,.fc-unthemed tbody,.fc-unthemed td,.fc-unthemed th,.fc-unthemed thead{border-color:#ddd}.fc-unthemed .fc-popover{background-color:#fff;border-width:1px;border-style:solid}.fc-unthemed .fc-divider,.fc-unthemed .fc-list-heading td,.fc-unthemed .fc-popover .fc-header{background:#eee}.fc-unthemed td.fc-today{background:#fcf8e3}.fc-unthemed .fc-disabled-day{background:#d7d7d7;opacity:.3}.fc-icon{display:inline-block;height:1em;line-height:1em;font-size:1em;font-family:"Courier New",Courier,monospace;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.fc-icon:after{position:relative}.fc-icon-left-single-arrow:after{content:"\2039";font-weight:700;font-size:200%;top:-7%}.fc-icon-right-single-arrow:after{content:"\203A";font-weight:700;font-size:200%;top:-7%}.fc-icon-left-double-arrow:after{content:"\AB";font-size:160%;top:-7%}.fc-icon-right-double-arrow:after{content:"\BB";font-size:160%;top:-7%}.fc-icon-left-triangle:after{content:"\25C4";font-size:125%;top:3%}.fc-icon-right-triangle:after{content:"\25BA";font-size:125%;top:3%}.fc-icon-down-triangle:after{content:"\25BC";font-size:125%;top:2%}.fc-icon-x:after{content:"\D7";font-size:200%;top:6%}.fc-unthemed .fc-popover .fc-header .fc-close{color:#666;font-size:.9em;margin-top:2px}.fc-unthemed .fc-list-item:hover td{background-color:#f5f5f5}.ui-widget .fc-disabled-day{background-image:none}.fc-bootstrap3 .fc-time-grid .fc-slats table,.fc-bootstrap4 .fc-time-grid .fc-slats table,.fc-time-grid .fc-slats .ui-widget-content{background:0 0}.fc-popover>.ui-widget-header+.ui-widget-content{border-top:0}.fc-bootstrap3 hr.fc-divider,.fc-bootstrap4 hr.fc-divider{border-color:inherit}.ui-widget .fc-event{color:#fff;font-weight:400}.ui-widget td.fc-axis{font-weight:400}.fc.fc-bootstrap3 a[data-goto]:hover{text-decoration:underline}.fc.fc-bootstrap4 a{text-decoration:none}.fc.fc-bootstrap4 a[data-goto]:hover{text-decoration:underline}.fc-bootstrap4 a.fc-event:not([href]):not([tabindex]){color:#fff}.fc-bootstrap4 .fc-popover.card{position:absolute}.fc-toolbar.fc-header-toolbar{margin-bottom:1em}.fc-toolbar.fc-footer-toolbar{margin-top:1em}.fc-toolbar .fc-left{float:left}.fc-toolbar .fc-right{float:right}.fc-toolbar .fc-center{display:inline-block}.fc .fc-toolbar>*>*{float:left;margin-left:.75em}.fc .fc-toolbar>*>:first-child{margin-left:0}.fc-toolbar h2{margin:0}.fc-toolbar button{position:relative}.fc-toolbar .fc-state-hover,.fc-toolbar .ui-state-hover{z-index:2}.fc-toolbar .fc-state-down{z-index:3}.fc-toolbar .fc-state-active,.fc-toolbar .ui-state-active{z-index:4}.fc-toolbar button:focus{z-index:5}.fc-view-container *,.fc-view-container :after,.fc-view-container :before{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}.fc-view,.fc-view>table{position:relative;z-index:1}.fc-basicDay-view .fc-content-skeleton,.fc-basicWeek-view .fc-content-skeleton{padding-bottom:1em}.fc-basic-view .fc-body .fc-row{min-height:4em}.fc-row.fc-rigid .fc-content-skeleton{position:absolute;top:0;left:0;right:0}.fc-day-top.fc-other-month{opacity:.3}.fc-basic-view .fc-day-number,.fc-basic-view .fc-week-number{padding:2px}.fc-basic-view th.fc-day-number,.fc-basic-view th.fc-week-number{padding:0 2px}.fc-ltr .fc-basic-view .fc-day-top .fc-day-number{float:right}.fc-rtl .fc-basic-view .fc-day-top .fc-day-number{float:left}.fc-ltr .fc-basic-view .fc-day-top .fc-week-number{float:left;border-radius:0 0 3px}.fc-rtl .fc-basic-view .fc-day-top .fc-week-number{float:right;border-radius:0 0 0 3px}.fc-basic-view .fc-day-top .fc-week-number{min-width:1.5em;background-color:#f2f2f2;color:grey}.fc-basic-view td.fc-week-number>*{display:inline-block;min-width:1.25em}.fc-agenda-view .fc-day-grid{position:relative;z-index:2}.fc-agenda-view .fc-day-grid .fc-row{min-height:3em}.fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton{padding-bottom:1em}.fc .fc-axis{vertical-align:middle;padding:0 4px}.fc-ltr .fc-axis{text-align:right}.fc-rtl .fc-axis{text-align:left}.fc-time-grid,.fc-time-grid-container{position:relative;z-index:1}.fc-time-grid{min-height:100%}.fc-time-grid table{border:0 hidden transparent}.fc-time-grid>.fc-bg{z-index:1}.fc-time-grid .fc-slats,.fc-time-grid>hr{position:relative;z-index:2}.fc-time-grid .fc-content-col{position:relative}.fc-time-grid .fc-content-skeleton{position:absolute;z-index:3;top:0;left:0;right:0}.fc-time-grid .fc-business-container{position:relative;z-index:1}.fc-time-grid .fc-bgevent-container{position:relative;z-index:2}.fc-time-grid .fc-highlight-container{z-index:3;position:relative}.fc-time-grid .fc-event-container{position:relative;z-index:4}.fc-time-grid .fc-now-indicator-line{z-index:5}.fc-time-grid .fc-helper-container{position:relative;z-index:6}.fc-time-grid .fc-slats td{height:1.5em;border-bottom:0}.fc-time-grid .fc-slats .fc-minor td{border-top-style:dotted}.fc-time-grid .fc-highlight{position:absolute;left:0;right:0}.fc-ltr .fc-time-grid .fc-event-container{margin:0 2.5% 0 2px}.fc-rtl .fc-time-grid .fc-event-container{margin:0 2px 0 2.5%}.fc-time-grid .fc-bgevent,.fc-time-grid .fc-event{position:absolute;z-index:1}.fc-time-grid .fc-bgevent{left:0;right:0}.fc-v-event.fc-not-start{border-top-width:0;padding-top:1px;border-top-left-radius:0;border-top-right-radius:0}.fc-v-event.fc-not-end{border-bottom-width:0;padding-bottom:1px;border-bottom-left-radius:0;border-bottom-right-radius:0}.fc-time-grid-event.fc-selected{overflow:visible}.fc-time-grid-event.fc-selected .fc-bg{display:none}.fc-time-grid-event .fc-content{overflow:hidden}.fc-time-grid-event .fc-time,.fc-time-grid-event .fc-title{padding:0 1px}.fc-time-grid-event .fc-time{font-size:.85em}.fc-time-grid-event.fc-short .fc-time,.fc-time-grid-event.fc-short .fc-title{display:inline-block;vertical-align:top}.fc-time-grid-event.fc-short .fc-time span{display:none}.fc-time-grid-event.fc-short .fc-time:before{content:attr(data-start)}.fc-time-grid-event.fc-short .fc-time:after{content:"\A0-\A0"}.fc-time-grid-event.fc-short .fc-title{font-size:.85em;padding:0}.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer{left:0;right:0;bottom:0;height:8px;overflow:hidden;line-height:8px;font-size:11px;font-family:monospace;text-align:center;cursor:s-resize}.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer:after{content:"="}.fc-time-grid-event.fc-selected .fc-resizer{border-radius:5px;border-width:1px;width:8px;height:8px;border-style:solid;border-color:inherit;background:#fff;left:50%;margin-left:-5px;bottom:-5px}.fc-time-grid .fc-now-indicator-line{border-top-width:1px;left:0;right:0}.fc-time-grid .fc-now-indicator-arrow{margin-top:-5px}.fc-ltr .fc-time-grid .fc-now-indicator-arrow{left:0;border-width:5px 0 5px 6px;border-top-color:transparent;border-bottom-color:transparent}.fc-rtl .fc-time-grid .fc-now-indicator-arrow{right:0;border-width:5px 6px 5px 0;border-top-color:transparent;border-bottom-color:transparent}.fc-event-dot{display:inline-block;width:10px;height:10px;border-radius:5px}.fc-rtl .fc-list-view{direction:rtl}.fc-list-view{border-width:1px;border-style:solid}.fc .fc-list-table{table-layout:auto}.fc-list-table td{border-width:1px 0 0;padding:8px 14px}.fc-list-table tr:first-child td{border-top-width:0}.fc-list-heading{border-bottom-width:1px}.fc-list-heading td{font-weight:700}.fc-ltr .fc-list-heading-main{float:left}.fc-ltr .fc-list-heading-alt,.fc-rtl .fc-list-heading-main{float:right}.fc-rtl .fc-list-heading-alt{float:left}.fc-list-item.fc-has-url{cursor:pointer}.fc-list-item-marker,.fc-list-item-time{width:1px}.fc-ltr .fc-list-item-marker{padding-right:0}.fc-rtl .fc-list-item-marker{padding-left:0}.fc-list-item-title a{text-decoration:none;color:inherit}.fc-list-item-title a[href]:hover{text-decoration:underline}.fc-list-empty-wrap2{position:absolute;top:0;left:0;right:0;bottom:0}.fc-list-empty-wrap1{width:100%;height:100%;display:table}.fc-list-empty{display:table-cell;vertical-align:middle;text-align:center}.fc-unthemed .fc-list-empty{background-color:#eee}
\ No newline at end of file
diff --git a/frappe/public/js/lib/fullcalendar/fullcalendar.min.js b/frappe/public/js/lib/fullcalendar/fullcalendar.min.js
index 76e1d43834..b5c219bc23 100644
--- a/frappe/public/js/lib/fullcalendar/fullcalendar.min.js
+++ b/frappe/public/js/lib/fullcalendar/fullcalendar.min.js
@@ -1,10 +1,12 @@
/*!
- * FullCalendar v3.4.0
+ * FullCalendar v3.10.2
* Docs & License: https://fullcalendar.io/
- * (c) 2017 Adam Shaw
+ * (c) 2019 Adam Shaw
*/
-!function(t){"function"==typeof define&&define.amd?define(["jquery","moment"],t):"object"==typeof exports?module.exports=t(require("jquery"),require("moment")):t(jQuery,moment)}(function(t,e){function n(t){return it(t,Qt)}function i(t,e){e.left&&t.css({"border-left-width":1,"margin-left":e.left-1}),e.right&&t.css({"border-right-width":1,"margin-right":e.right-1})}function r(t){t.css({"margin-left":"","margin-right":"","border-left-width":"","border-right-width":""})}function s(){t("body").addClass("fc-not-allowed")}function o(){t("body").removeClass("fc-not-allowed")}function a(e,n,i){var r=Math.floor(n/e.length),s=Math.floor(n-r*(e.length-1)),o=[],a=[],u=[],h=0;l(e),e.each(function(n,i){var l=n===e.length-1?s:r,c=t(i).outerHeight(!0);c *").each(function(e,i){var r=t(i).outerWidth();r>n&&(n=r)}),n++,e.width(n),n}function h(t,e){var n,i=t.add(e);return i.css({position:"relative",left:-1}),n=t.outerHeight()-e.outerHeight(),i.css({position:"",left:""}),n}function c(e){var n=e.css("position"),i=e.parents().filter(function(){var e=t(this);return/(auto|scroll)/.test(e.css("overflow")+e.css("overflow-y")+e.css("overflow-x"))}).eq(0);return"fixed"!==n&&i.length?i:t(e[0].ownerDocument||document)}function d(t,e){var n=t.offset(),i=n.left-(e?e.left:0),r=n.top-(e?e.top:0);return{left:i,right:i+t.outerWidth(),top:r,bottom:r+t.outerHeight()}}function f(t,e){var n=t.offset(),i=p(t),r=n.left+w(t,"border-left-width")+i.left-(e?e.left:0),s=n.top+w(t,"border-top-width")+i.top-(e?e.top:0);return{left:r,right:r+t[0].clientWidth,top:s,bottom:s+t[0].clientHeight}}function g(t,e){var n=t.offset(),i=n.left+w(t,"border-left-width")+w(t,"padding-left")-(e?e.left:0),r=n.top+w(t,"border-top-width")+w(t,"padding-top")-(e?e.top:0);return{left:i,right:i+t.width(),top:r,bottom:r+t.height()}}function p(t){var e,n=t[0].offsetWidth-t[0].clientWidth,i=t[0].offsetHeight-t[0].clientHeight;return n=v(n),i=v(i),e={left:0,right:0,top:0,bottom:i},m()&&"rtl"==t.css("direction")?e.left=n:e.right=n,e}function v(t){return t=Math.max(0,t),t=Math.round(t)}function m(){return null===Xt&&(Xt=y()),Xt}function y(){var e=t("").css({position:"absolute",top:-1e3,left:0,border:0,padding:0,overflow:"scroll",direction:"rtl"}).appendTo("body"),n=e.children(),i=n.offset().left>e.offset().left;return e.remove(),i}function w(t,e){return parseFloat(t.css(e))||0}function S(t){return 1==t.which&&!t.ctrlKey}function b(t){var e=t.originalEvent.touches;return e&&e.length?e[0].pageX:t.pageX}function E(t){var e=t.originalEvent.touches;return e&&e.length?e[0].pageY:t.pageY}function D(t){return/^touch/.test(t.type)}function T(t){t.addClass("fc-unselectable").on("selectstart",H)}function C(t){t.removeClass("fc-unselectable").off("selectstart",H)}function H(t){t.preventDefault()}function R(t,e){var n={left:Math.max(t.left,e.left),right:Math.min(t.right,e.right),top:Math.max(t.top,e.top),bottom:Math.min(t.bottom,e.bottom)};return n.leftl&&o=l?(n=o.clone(),r=!0):(n=l.clone(),r=!1),a<=u?(i=a.clone(),s=!0):(i=u.clone(),s=!1),{start:n,end:i,isStart:r,isEnd:s}}function F(t,n){return e.duration({days:t.clone().stripTime().diff(n.clone().stripTime(),"days"),ms:t.time()-n.time()})}function A(t,n){return e.duration({days:t.clone().stripTime().diff(n.clone().stripTime(),"days")})}function G(t,n,i){return e.duration(Math.round(t.diff(n,i,!0)),i)}function V(t,e){var n,i,r;for(n=0;n=1&&vt(r)));n++);return i}function O(t,e){var n=V(t);return"week"===n&&"object"==typeof e&&e.days&&(n="day"),n}function P(t,n,i){return null!=i?i.diff(n,t,!0):e.isDuration(n)?n.as(t):n.end.diff(n.start,t,!0)}function _(t,e,n){var i;return tt(n)?(e-t)/n:(i=n.asMonths(),Math.abs(i)>=1&&vt(i)?e.diff(t,"months",!0)/i:e.diff(t,"days",!0)/n.asDays())}function W(t,e){var n,i;return tt(t)||tt(e)?t/e:(n=t.asMonths(),i=e.asMonths(),Math.abs(n)>=1&&vt(n)&&Math.abs(i)>=1&&vt(i)?n/i:t.asDays()/e.asDays())}function Y(t,n){var i;return tt(t)?e.duration(t*n):(i=t.asMonths(),Math.abs(i)>=1&&vt(i)?e.duration({months:i*n}):e.duration({days:t.asDays()*n}))}function q(t){return{start:t.start.clone(),end:t.end.clone()}}function U(t,e){return t=q(t),e.start&&(t.start=j(t.start,e)),e.end&&(t.end=K(t.end,e.end)),t}function j(t,e){return t=t.clone(),e.start&&(t=J(t,e.start)),e.end&&t>=e.end&&(t=e.end.clone().subtract(1)),t}function Z(t,e){return(!e.start||t>=e.start)&&(!e.end||t=e.start)&&(!e.end||t.start=e.start)&&(!e.end||t.end<=e.end)}function X(t,e){return(t.start&&e.start&&t.start.isSame(e.start)||!t.start&&!e.start)&&(t.end&&e.end&&t.end.isSame(e.end)||!t.end&&!e.end)}function K(t,e){return(t.isBefore(e)?t:e).clone()}function J(t,e){return(t.isAfter(e)?t:e).clone()}function tt(t){return Boolean(t.hours()||t.minutes()||t.seconds()||t.milliseconds())}function et(t){return"[object Date]"===Object.prototype.toString.call(t)||t instanceof Date}function nt(t){return/^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(t)}function it(t,e){var n,i,r,s,o,a,l={};if(e)for(n=0;n=0;s--)if("object"==typeof(o=t[s][i]))r.unshift(o);else if(void 0!==o){l[i]=o;break}r.length&&(l[i]=it(r))}for(n=t.length-1;n>=0;n--){a=t[n];for(i in a)i in l||(l[i]=a[i])}return l}function rt(t){var e=function(){};return e.prototype=t,new e}function st(t,e){for(var n in t)ot(t,n)&&(e[n]=t[n])}function ot(t,e){return te.call(t,e)}function at(e){return/undefined|null|boolean|number|string/.test(t.type(e))}function lt(e,n,i){if(t.isFunction(e)&&(e=[e]),e){var r,s;for(r=0;r /g,">").replace(/'/g,"'").replace(/"/g,""").replace(/\n/g," ")}function ct(t){return t.replace(/&.*?;/g,"")}function dt(e){var n=[];return t.each(e,function(t,e){null!=e&&n.push(t+":"+e)}),n.join(";")}function ft(e){var n=[];return t.each(e,function(t,e){null!=e&&n.push(t+'="'+ht(e)+'"')}),n.join(" ")}function gt(t){return t.charAt(0).toUpperCase()+t.slice(1)}function pt(t,e){return t-e}function vt(t){return t%1==0}function mt(t,e){var n=t[e];return function(){return n.apply(t,arguments)}}function yt(t,e,n){var i,r,s,o,a,l=function(){var u=+new Date-o;u=t.leftCol)return!0;return!1}function Ft(t,e){return t.leftCol-e.leftCol}function At(t){var e,n,i,r=[];for(e=0;ee.top&&t.top"),g.append(o("left")).append(o("right")).append(o("center")).append('
')):s()}function s(){g&&(g.remove(),g=f.el=null)}function o(i){var r=t('
'),s=n.layout[i],o=e.opt("customButtons")||{},a=e.opt("buttonText")||{};return s&&t.each(s.split(" "),function(n){var i,s=t(),l=!0;t.each(this.split(","),function(n,i){var r,u,h,c,d,f,g,m,y,w;"title"==i?(s=s.add(t(" ")),l=!1):((r=o[i])?(h=function(t){r.click&&r.click.call(w[0],t)},c="",d=r.text):(u=e.getViewSpec(i))?(h=function(){e.changeView(i)},v.push(i),c=u.buttonTextOverride,d=u.buttonTextDefault):e[i]&&(h=function(){e[i]()},c=(e.overrides.buttonText||{})[i],d=a[i]),h&&(f=r?r.themeIcon:e.opt("themeButtonIcons")[i],g=r?r.icon:e.opt("buttonIcons")[i],m=c?ht(c):f&&e.opt("theme")?" ":g&&!e.opt("theme")?" ":ht(d),y=["fc-"+i+"-button",p+"-button",p+"-state-default"],w=t(''+m+" ").click(function(t){w.hasClass(p+"-state-disabled")||(h(t),(w.hasClass(p+"-state-active")||w.hasClass(p+"-state-disabled"))&&w.removeClass(p+"-state-hover"))}).mousedown(function(){w.not("."+p+"-state-active").not("."+p+"-state-disabled").addClass(p+"-state-down")}).mouseup(function(){w.removeClass(p+"-state-down")}).hover(function(){w.not("."+p+"-state-active").not("."+p+"-state-disabled").addClass(p+"-state-hover")},function(){w.removeClass(p+"-state-hover").removeClass(p+"-state-down")}),s=s.add(w)))}),l&&s.first().addClass(p+"-corner-left").end().last().addClass(p+"-corner-right").end(),s.length>1?(i=t("
"),l&&i.addClass("fc-button-group"),i.append(s),r.append(i)):r.append(s)}),r}function a(t){g&&g.find("h2").text(t)}function l(t){g&&g.find(".fc-"+t+"-button").addClass(p+"-state-active")}function u(t){g&&g.find(".fc-"+t+"-button").removeClass(p+"-state-active")}function h(t){g&&g.find(".fc-"+t+"-button").prop("disabled",!0).addClass(p+"-state-disabled")}function c(t){g&&g.find(".fc-"+t+"-button").prop("disabled",!1).removeClass(p+"-state-disabled")}function d(){return v}var f=this;f.setToolbarOptions=i,f.render=r,f.removeElement=s,f.updateTitle=a,f.activateButton=l,f.deactivateButton=u,f.disableButton=h,f.enableButton=c,f.getViewsWithButtons=d,f.el=null;var g,p,v=[]}function Yt(e){t.each(Me,function(t,n){null==e[t]&&(e[t]=n(e))})}function qt(t){return e.localeData(t)||e.localeData("en")}function Ut(){function n(t,e){return!q.opt("lazyFetching")||s(t,e)?o(t,e):he.resolve(Z)}function i(){Z=r(K),q.trigger("eventsReset",Z)}function r(t){var e,n,i=[];for(e=0;eU&&i.push(n);return i}function s(t,e){return!U||tj}function o(t,e){return U=t,j=e,a()}function a(){return u(Q,"reset")}function l(t){return u(b(t))}function u(t,e){var n,i;for("reset"===e?K=[]:"add"!==e&&(K=C(K,t)),n=0;ns&&(!l[o]||u.isSame(h,l[o]))&&(o-1!==s||"."!==f[o]);o--)v=f[o]+v;for(a=s;a<=o;a++)m+=f[a],y+=g[a];return(m||y)&&(w=r?y+i+m:m+i+y),d(p+w+v)}function r(t){return S[t]||(S[t]=s(t))}function s(t){var e=o(t);return{fakeFormatString:l(e),sameUnits:u(e)}}function o(t){for(var e,n=[],i=/\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g;e=i.exec(t);)e[1]?n.push.apply(n,a(e[1])):e[2]?n.push({maybe:o(e[2])}):e[3]?n.push({token:e[3]}):e[5]&&n.push.apply(n,a(e[5]));return n}function a(t){return". "===t?["."," "]:[t]}function l(t){var e,n,i=[];for(e=0;er.value)&&(r=i);return r?r.unit:null}Zt.formatDate=t,Zt.formatRange=n,Zt.oldMomentFormat=e,Zt.queryMostGranularFormatUnit=f;var g="\v",p="",v="",m=new RegExp(v+"([^"+v+"]*)"+v,"g"),y={t:function(t){return e(t,"a").charAt(0)},T:function(t){return e(t,"A").charAt(0)}},w={Y:{value:1,unit:"year"},M:{value:2,unit:"month"},W:{value:3,unit:"week"},w:{value:3,unit:"week"},D:{value:4,unit:"day"},d:{value:4,unit:"day"}},S={}}();var oe=Zt.formatDate,ae=Zt.formatRange,le=Zt.oldMomentFormat;Zt.Class=bt,bt.extend=function(){var t,e,n=arguments.length;for(t=0;t=0;e--)n=i[e],n.namespace!==t.namespace||"add"!==n.type&&"remove"!==n.type||i.splice(e,1);"destroy"===t.type?i.length&&(n=i[i.length-1],n.namespace===t.namespace&&("init"===n.type?(r=!1,i.pop()):"destroy"===n.type&&(r=!1))):"init"===t.type&&i.length&&(n=i[i.length-1],n.namespace===t.namespace&&"init"===n.type&&i.pop())}return r&&i.push(t),r}});Zt.RenderQueue=de;var fe=Zt.EmitterMixin={on:function(e,n){return t(this).on(e,this._prepareIntercept(n)),this},one:function(e,n){return t(this).one(e,this._prepareIntercept(n)),this},_prepareIntercept:function(e){var n=function(t,n){return e.apply(n.context||this,n.args||[])};return e.guid||(e.guid=t.guid++),n.guid=e.guid,n},
-off:function(e,n){return t(this).off(e,n),this},trigger:function(e){var n=Array.prototype.slice.call(arguments,1);return t(this).triggerHandler(e,{args:n}),this},triggerWith:function(e,n,i){return t(this).triggerHandler(e,{context:n,args:i}),this}},ge=Zt.ListenerMixin=function(){var e=0;return{listenerId:null,listenTo:function(e,n,i){if("object"==typeof n)for(var r in n)n.hasOwnProperty(r)&&this.listenTo(e,r,n[r]);else"string"==typeof n&&e.on(n+"."+this.getListenerNamespace(),t.proxy(i,this))},stopListeningTo:function(t,e){t.off((e||"")+"."+this.getListenerNamespace())},getListenerNamespace:function(){return null==this.listenerId&&(this.listenerId=e++),"_listener"+this.listenerId}}}(),pe=bt.extend(ge,{isHidden:!0,options:null,el:null,margin:10,constructor:function(t){this.options=t||{}},show:function(){this.isHidden&&(this.el||this.render(),this.el.show(),this.position(),this.isHidden=!1,this.trigger("show"))},hide:function(){this.isHidden||(this.el.hide(),this.isHidden=!0,this.trigger("hide"))},render:function(){var e=this,n=this.options;this.el=t('
').addClass(n.className||"").css({top:0,left:0}).append(n.content).appendTo(n.parentEl),this.el.on("click",".fc-close",function(){e.hide()}),n.autoHide&&this.listenTo(t(document),"mousedown",this.documentMousedown)},documentMousedown:function(e){this.el&&!t(e.target).closest(this.el).length&&this.hide()},removeElement:function(){this.hide(),this.el&&(this.el.remove(),this.el=null),this.stopListeningTo(t(document),"mousedown")},position:function(){var e,n,i,r,s,o=this.options,a=this.el.offsetParent().offset(),l=this.el.outerWidth(),u=this.el.outerHeight(),h=t(window),d=c(this.el);r=o.top||0,s=void 0!==o.left?o.left:void 0!==o.right?o.right-l:0,d.is(window)||d.is(document)?(d=h,e=0,n=0):(i=d.offset(),e=i.top,n=i.left),e+=h.scrollTop(),n+=h.scrollLeft(),!1!==o.viewportConstrain&&(r=Math.min(r,e+d.outerHeight()-u-this.margin),r=Math.max(r,e+this.margin),s=Math.min(s,n+d.outerWidth()-l-this.margin),s=Math.max(s,n+this.margin)),this.el.css({top:r-a.top,left:s-a.left})},trigger:function(t){this.options[t]&&this.options[t].apply(this,Array.prototype.slice.call(arguments,1))}}),ve=Zt.CoordCache=bt.extend({els:null,forcedOffsetParentEl:null,origin:null,boundingRect:null,isHorizontal:!1,isVertical:!1,lefts:null,rights:null,tops:null,bottoms:null,constructor:function(e){this.els=t(e.els),this.isHorizontal=e.isHorizontal,this.isVertical=e.isVertical,this.forcedOffsetParentEl=e.offsetParent?t(e.offsetParent):null},build:function(){var t=this.forcedOffsetParentEl;!t&&this.els.length>0&&(t=this.els.eq(0).offsetParent()),this.origin=t?t.offset():null,this.boundingRect=this.queryBoundingRect(),this.isHorizontal&&this.buildElHorizontals(),this.isVertical&&this.buildElVerticals()},clear:function(){this.origin=null,this.boundingRect=null,this.lefts=null,this.rights=null,this.tops=null,this.bottoms=null},ensureBuilt:function(){this.origin||this.build()},buildElHorizontals:function(){var e=[],n=[];this.els.each(function(i,r){var s=t(r),o=s.offset().left,a=s.outerWidth();e.push(o),n.push(o+a)}),this.lefts=e,this.rights=n},buildElVerticals:function(){var e=[],n=[];this.els.each(function(i,r){var s=t(r),o=s.offset().top,a=s.outerHeight();e.push(o),n.push(o+a)}),this.tops=e,this.bottoms=n},getHorizontalIndex:function(t){this.ensureBuilt();var e,n=this.lefts,i=this.rights,r=n.length;for(e=0;e=n[e]&&t=n[e]&&t0&&(t=c(this.els.eq(0)),!t.is(document))?f(t):null},isPointInBounds:function(t,e){return this.isLeftInBounds(t)&&this.isTopInBounds(e)},isLeftInBounds:function(t){return!this.boundingRect||t>=this.boundingRect.left&&t=this.boundingRect.top&&t=i*i&&this.handleDistanceSurpassed(t),this.isDragging&&this.handleDrag(e,n,t)},handleDrag:function(t,e,n){this.trigger("drag",t,e,n),this.updateAutoScroll(n)},endDrag:function(t){this.isDragging&&(this.isDragging=!1,this.handleDragEnd(t))},handleDragEnd:function(t){this.trigger("dragEnd",t)},startDelay:function(t){var e=this;this.delay?this.delayTimeoutId=setTimeout(function(){e.handleDelayEnd(t)},this.delay):this.handleDelayEnd(t)},handleDelayEnd:function(t){this.isDelayEnded=!0,this.isDistanceSurpassed&&this.startDrag(t)},handleDistanceSurpassed:function(t){this.isDistanceSurpassed=!0,this.isDelayEnded&&this.startDrag(t)},handleTouchMove:function(t){this.isDragging&&this.shouldCancelTouchScroll&&t.preventDefault(),this.handleMove(t)},handleMouseMove:function(t){this.handleMove(t)},handleTouchScroll:function(t){this.isDragging&&!this.scrollAlwaysKills||this.endInteraction(t,!0)},trigger:function(t){this.options[t]&&this.options[t].apply(this,Array.prototype.slice.call(arguments,1)),this["_"+t]&&this["_"+t].apply(this,Array.prototype.slice.call(arguments,1))}});me.mixin({isAutoScroll:!1,scrollBounds:null,scrollTopVel:null,scrollLeftVel:null,scrollIntervalId:null,scrollSensitivity:30,scrollSpeed:200,scrollIntervalMs:50,initAutoScroll:function(){var t=this.scrollEl;this.isAutoScroll=this.options.scroll&&t&&!t.is(window)&&!t.is(document),this.isAutoScroll&&this.listenTo(t,"scroll",yt(this.handleDebouncedScroll,100))},destroyAutoScroll:function(){this.endAutoScroll(),this.isAutoScroll&&this.stopListeningTo(this.scrollEl,"scroll")},computeScrollBounds:function(){this.isAutoScroll&&(this.scrollBounds=d(this.scrollEl))},updateAutoScroll:function(t){var e,n,i,r,s=this.scrollSensitivity,o=this.scrollBounds,a=0,l=0;o&&(e=(s-(E(t)-o.top))/s,n=(s-(o.bottom-E(t)))/s,i=(s-(b(t)-o.left))/s,r=(s-(o.right-b(t)))/s,e>=0&&e<=1?a=e*this.scrollSpeed*-1:n>=0&&n<=1&&(a=n*this.scrollSpeed),i>=0&&i<=1?l=i*this.scrollSpeed*-1:r>=0&&r<=1&&(l=r*this.scrollSpeed)),this.setScrollVel(a,l)},setScrollVel:function(t,e){this.scrollTopVel=t,this.scrollLeftVel=e,this.constrainScrollVel(),!this.scrollTopVel&&!this.scrollLeftVel||this.scrollIntervalId||(this.scrollIntervalId=setInterval(mt(this,"scrollIntervalFunc"),this.scrollIntervalMs))},constrainScrollVel:function(){var t=this.scrollEl;this.scrollTopVel<0?t.scrollTop()<=0&&(this.scrollTopVel=0):this.scrollTopVel>0&&t.scrollTop()+t[0].clientHeight>=t[0].scrollHeight&&(this.scrollTopVel=0),this.scrollLeftVel<0?t.scrollLeft()<=0&&(this.scrollLeftVel=0):this.scrollLeftVel>0&&t.scrollLeft()+t[0].clientWidth>=t[0].scrollWidth&&(this.scrollLeftVel=0)},scrollIntervalFunc:function(){var t=this.scrollEl,e=this.scrollIntervalMs/1e3;this.scrollTopVel&&t.scrollTop(t.scrollTop()+this.scrollTopVel*e),this.scrollLeftVel&&t.scrollLeft(t.scrollLeft()+this.scrollLeftVel*e),this.constrainScrollVel(),this.scrollTopVel||this.scrollLeftVel||this.endAutoScroll()},endAutoScroll:function(){this.scrollIntervalId&&(clearInterval(this.scrollIntervalId),this.scrollIntervalId=null,this.handleScrollEnd())},handleDebouncedScroll:function(){this.scrollIntervalId||this.handleScrollEnd()},handleScrollEnd:function(){}});var ye=me.extend({component:null,origHit:null,hit:null,coordAdjust:null,constructor:function(t,e){me.call(this,e),this.component=t},handleInteractionStart:function(t){var e,n,i,r=this.subjectEl;this.component.hitsNeeded(),this.computeScrollBounds(),t?(n={left:b(t),top:E(t)},i=n,r&&(e=d(r),i=x(i,e)),this.origHit=this.queryHit(i.left,i.top),r&&this.options.subjectCenter&&(this.origHit&&(e=R(this.origHit,e)||e),i=I(e)),this.coordAdjust=k(i,n)):(this.origHit=null,this.coordAdjust=null),me.prototype.handleInteractionStart.apply(this,arguments)},handleDragStart:function(t){var e;me.prototype.handleDragStart.apply(this,arguments),(e=this.queryHit(b(t),E(t)))&&this.handleHitOver(e)},handleDrag:function(t,e,n){var i;me.prototype.handleDrag.apply(this,arguments),i=this.queryHit(b(n),E(n)),Ht(i,this.hit)||(this.hit&&this.handleHitOut(),i&&this.handleHitOver(i))},handleDragEnd:function(){this.handleHitDone(),me.prototype.handleDragEnd.apply(this,arguments)},handleHitOver:function(t){var e=Ht(t,this.origHit);this.hit=t,this.trigger("hitOver",this.hit,e,this.origHit)},handleHitOut:function(){this.hit&&(this.trigger("hitOut",this.hit),this.handleHitDone(),this.hit=null)},handleHitDone:function(){this.hit&&this.trigger("hitDone",this.hit)},handleInteractionEnd:function(){me.prototype.handleInteractionEnd.apply(this,arguments),this.origHit=null,this.hit=null,this.component.hitsNotNeeded()},handleScrollEnd:function(){me.prototype.handleScrollEnd.apply(this,arguments),this.isDragging&&(this.component.releaseHits(),this.component.prepareHits())},queryHit:function(t,e){return this.coordAdjust&&(t+=this.coordAdjust.left,e+=this.coordAdjust.top),this.component.queryHit(t,e)}});Zt.touchMouseIgnoreWait=500;var we=bt.extend(ge,fe,{isTouching:!1,mouseIgnoreDepth:0,handleScrollProxy:null,bind:function(){var e=this;this.listenTo(t(document),{touchstart:this.handleTouchStart,touchcancel:this.handleTouchCancel,touchend:this.handleTouchEnd,mousedown:this.handleMouseDown,mousemove:this.handleMouseMove,mouseup:this.handleMouseUp,click:this.handleClick,selectstart:this.handleSelectStart,contextmenu:this.handleContextMenu}),window.addEventListener("touchmove",this.handleTouchMoveProxy=function(n){e.handleTouchMove(t.Event(n))},{passive:!1}),window.addEventListener("scroll",this.handleScrollProxy=function(n){e.handleScroll(t.Event(n))},!0)},unbind:function(){this.stopListeningTo(t(document)),window.removeEventListener("touchmove",this.handleTouchMoveProxy),window.removeEventListener("scroll",this.handleScrollProxy,!0)},handleTouchStart:function(t){this.stopTouch(t,!0),this.isTouching=!0,this.trigger("touchstart",t)},handleTouchMove:function(t){this.isTouching&&this.trigger("touchmove",t)},handleTouchCancel:function(t){this.isTouching&&(this.trigger("touchcancel",t),this.stopTouch(t))},handleTouchEnd:function(t){this.stopTouch(t)},handleMouseDown:function(t){this.shouldIgnoreMouse()||this.trigger("mousedown",t)},handleMouseMove:function(t){this.shouldIgnoreMouse()||this.trigger("mousemove",t)},handleMouseUp:function(t){this.shouldIgnoreMouse()||this.trigger("mouseup",t)},handleClick:function(t){this.shouldIgnoreMouse()||this.trigger("click",t)},handleSelectStart:function(t){this.trigger("selectstart",t)},handleContextMenu:function(t){this.trigger("contextmenu",t)},handleScroll:function(t){this.trigger("scroll",t)},stopTouch:function(t,e){this.isTouching&&(this.isTouching=!1,this.trigger("touchend",t),e||this.startTouchMouseIgnore())},startTouchMouseIgnore:function(){var t=this,e=Zt.touchMouseIgnoreWait;e&&(this.mouseIgnoreDepth++,setTimeout(function(){t.mouseIgnoreDepth--},e))},shouldIgnoreMouse:function(){return this.isTouching||Boolean(this.mouseIgnoreDepth)}});!function(){var t=null,e=0;we.get=function(){return t||(t=new we,t.bind()),t},we.needed=function(){we.get(),e++},we.unneeded=function(){--e||(t.unbind(),t=null)}}();var Se=bt.extend(ge,{options:null,sourceEl:null,el:null,parentEl:null,top0:null,left0:null,y0:null,x0:null,topDelta:null,leftDelta:null,isFollowing:!1,isHidden:!1,isAnimating:!1,constructor:function(e,n){this.options=n=n||{},this.sourceEl=e,this.parentEl=n.parentEl?t(n.parentEl):e.parent()},start:function(e){this.isFollowing||(this.isFollowing=!0,this.y0=E(e),this.x0=b(e),this.topDelta=0,this.leftDelta=0,this.isHidden||this.updatePosition(),D(e)?this.listenTo(t(document),"touchmove",this.handleMove):this.listenTo(t(document),"mousemove",this.handleMove))},stop:function(e,n){function i(){r.isAnimating=!1,r.removeElement(),r.top0=r.left0=null,n&&n()}var r=this,s=this.options.revertDuration;this.isFollowing&&!this.isAnimating&&(this.isFollowing=!1,this.stopListeningTo(t(document)),e&&s&&!this.isHidden?(this.isAnimating=!0,this.el.animate({top:this.top0,left:this.left0},{duration:s,complete:i})):i())},getEl:function(){var t=this.el;return t||(t=this.el=this.sourceEl.clone().addClass(this.options.additionalClass||"").css({position:"absolute",visibility:"",display:this.isHidden?"none":"",margin:0,right:"auto",bottom:"auto",width:this.sourceEl.width(),height:this.sourceEl.height(),opacity:this.options.opacity||"",zIndex:this.options.zIndex}),t.addClass("fc-unselectable"),t.appendTo(this.parentEl)),t},removeElement:function(){this.el&&(this.el.remove(),this.el=null)},updatePosition:function(){var t,e;this.getEl(),null===this.top0&&(t=this.sourceEl.offset(),e=this.el.offsetParent().offset(),this.top0=t.top-e.top,this.left0=t.left-e.left),this.el.css({top:this.top0+this.topDelta,left:this.left0+this.leftDelta})},handleMove:function(t){this.topDelta=E(t)-this.y0,this.leftDelta=b(t)-this.x0,this.isHidden||this.updatePosition()},hide:function(){this.isHidden||(this.isHidden=!0,this.el&&this.el.hide())},show:function(){this.isHidden&&(this.isHidden=!1,this.updatePosition(),this.getEl().show())}}),be=Zt.Grid=bt.extend(ge,{hasDayInteractions:!0,view:null,isRTL:null,start:null,end:null,el:null,elsByFill:null,eventTimeFormat:null,displayEventTime:null,displayEventEnd:null,minResizeDuration:null,largeUnit:null,dayClickListener:null,daySelectListener:null,segDragListener:null,segResizeListener:null,externalDragListener:null,constructor:function(t){this.view=t,this.isRTL=t.opt("isRTL"),this.elsByFill={},this.dayClickListener=this.buildDayClickListener(),this.daySelectListener=this.buildDaySelectListener()},computeEventTimeFormat:function(){return this.view.opt("smallTimeFormat")},computeDisplayEventTime:function(){return!0},computeDisplayEventEnd:function(){return!0},setRange:function(t){this.start=t.start.clone(),this.end=t.end.clone(),this.rangeUpdated(),this.processRangeOptions()},rangeUpdated:function(){},processRangeOptions:function(){var t,e,n=this.view;this.eventTimeFormat=n.opt("eventTimeFormat")||n.opt("timeFormat")||this.computeEventTimeFormat(),t=n.opt("displayEventTime"),null==t&&(t=this.computeDisplayEventTime()),e=n.opt("displayEventEnd"),null==e&&(e=this.computeDisplayEventEnd()),this.displayEventTime=t,this.displayEventEnd=e},spanToSegs:function(t){},diffDates:function(t,e){return this.largeUnit?G(t,e,this.largeUnit):F(t,e)},hitsNeededDepth:0,hitsNeeded:function(){this.hitsNeededDepth++||this.prepareHits()},hitsNotNeeded:function(){this.hitsNeededDepth&&!--this.hitsNeededDepth&&this.releaseHits()},prepareHits:function(){},releaseHits:function(){},queryHit:function(t,e){},getSafeHitSpan:function(t){var e=this.getHitSpan(t);return Q(e,this.view.activeRange)?e:null},getHitSpan:function(t){},getHitEl:function(t){},setElement:function(t){this.el=t,this.hasDayInteractions&&(T(t),this.bindDayHandler("touchstart",this.dayTouchStart),this.bindDayHandler("mousedown",this.dayMousedown)),this.bindSegHandlers(),this.bindGlobalHandlers()},bindDayHandler:function(e,n){var i=this;this.el.on(e,function(e){if(!t(e.target).is(i.segSelector+","+i.segSelector+" *,.fc-more,a[data-goto]"))return n.call(i,e)})},removeElement:function(){this.unbindGlobalHandlers(),this.clearDragListeners(),this.el.remove()},renderSkeleton:function(){},renderDates:function(){},unrenderDates:function(){},bindGlobalHandlers:function(){this.listenTo(t(document),{dragstart:this.externalDragStart,sortstart:this.externalDragStart})},unbindGlobalHandlers:function(){this.stopListeningTo(t(document))},dayMousedown:function(t){var e=this.view;we.get().shouldIgnoreMouse()||(this.dayClickListener.startInteraction(t),e.opt("selectable")&&this.daySelectListener.startInteraction(t,{distance:e.opt("selectMinDistance")}))},dayTouchStart:function(t){var e,n=this.view;n.isSelected||n.selectedEvent||(e=n.opt("selectLongPressDelay"),null==e&&(e=n.opt("longPressDelay")),this.dayClickListener.startInteraction(t),n.opt("selectable")&&this.daySelectListener.startInteraction(t,{delay:e}))},buildDayClickListener:function(){var t,e=this,n=this.view,i=new ye(this,{scroll:n.opt("dragScroll"),interactionStart:function(){t=i.origHit},hitOver:function(e,n,i){n||(t=null)},hitOut:function(){t=null},interactionEnd:function(i,r){var s;!r&&t&&(s=e.getSafeHitSpan(t))&&n.triggerDayClick(s,e.getHitEl(t),i)}});return i.shouldCancelTouchScroll=!1,i.scrollAlwaysKills=!0,i},buildDaySelectListener:function(){var t,e=this,n=this.view;return new ye(this,{scroll:n.opt("dragScroll"),interactionStart:function(){t=null},dragStart:function(){n.unselect()},hitOver:function(n,i,r){var o,a;r&&(o=e.getSafeHitSpan(r),a=e.getSafeHitSpan(n),t=o&&a?e.computeSelection(o,a):null,t?e.renderSelection(t):!1===t&&s())},hitOut:function(){t=null,e.unrenderSelection()},hitDone:function(){o()},interactionEnd:function(e,i){!i&&t&&n.reportSelection(t,e)}})},clearDragListeners:function(){this.dayClickListener.endInteraction(),this.daySelectListener.endInteraction(),this.segDragListener&&this.segDragListener.endInteraction(),this.segResizeListener&&this.segResizeListener.endInteraction(),this.externalDragListener&&this.externalDragListener.endInteraction()},renderEventLocationHelper:function(t,e){var n=this.fabricateHelperEvent(t,e);return this.renderHelper(n,e)},fabricateHelperEvent:function(t,e){var n=e?rt(e.event):{};return n.start=t.start.clone(),n.end=t.end?t.end.clone():null,n.allDay=null,this.view.calendar.normalizeEventDates(n),n.className=(n.className||[]).concat("fc-helper"),e||(n.editable=!1),n},renderHelper:function(t,e){},unrenderHelper:function(){},renderSelection:function(t){this.renderHighlight(t)},unrenderSelection:function(){this.unrenderHighlight()},computeSelection:function(t,e){var n=this.computeSelectionSpan(t,e);return!(n&&!this.view.calendar.isSelectionSpanAllowed(n))&&n},computeSelectionSpan:function(t,e){var n=[t.start,t.end,e.start,e.end];return n.sort(pt),{start:n[0].clone(),end:n[3].clone()}},renderHighlight:function(t){this.renderFill("highlight",this.spanToSegs(t))},unrenderHighlight:function(){this.unrenderFill("highlight")},highlightSegClasses:function(){return["fc-highlight"]},renderBusinessHours:function(){},unrenderBusinessHours:function(){},getNowIndicatorUnit:function(){},renderNowIndicator:function(t){},unrenderNowIndicator:function(){},renderFill:function(t,e){},unrenderFill:function(t){var e=this.elsByFill[t];e&&(e.remove(),delete this.elsByFill[t])},renderFillSegEls:function(e,n){var i,r=this,s=this[e+"SegEl"],o="",a=[];if(n.length){for(i=0;i "},getDayClasses:function(t,e){var n,i=this.view,r=[];return Z(t,i.activeRange)?(r.push("fc-"+Kt[t.day()]),1==i.currentRangeAs("months")&&t.month()!=i.currentRange.start.month()&&r.push("fc-other-month"),n=i.calendar.getNow(),t.isSame(n,"day")?(r.push("fc-today"),!0!==e&&r.push(i.highlightStateClass)):t *",mousedOverSeg:null,isDraggingSeg:!1,isResizingSeg:!1,isDraggingExternal:!1,segs:null,renderEvents:function(t){var e,n=[],i=[];for(e=0;ea&&o.push({start:a,end:n.start}),n.end>a&&(a=n.end);return a=e.length?e[e.length-1]+1:e[n]},computeColHeadFormat:function(){return this.rowCnt>1||this.colCnt>10?"ddd":this.colCnt>1?this.view.opt("dayOfMonthFormat"):"dddd"},sliceRangeByRow:function(t){var e,n,i,r,s,o=this.daysPerRow,a=this.view.computeDayRange(t),l=this.getDateDayIndex(a.start),u=this.getDateDayIndex(a.end.clone().subtract(1,"days")),h=[];for(e=0;e "},updateSegVerticals:function(t){this.computeSegVerticals(t),this.assignSegVerticals(t)},computeSegVerticals:function(t){var e,n,i;for(e=0;e1?"ll":"LL"},formatRange:function(t,e,n){var i=t.end;return i.hasTime()||(i=i.clone().subtract(1)),ae(t.start,i,e,n,this.opt("isRTL"))},getAllDayHtml:function(){return this.opt("allDayHtml")||ht(this.opt("allDayText"))},buildGotoAnchorHtml:function(e,n,i){var r,s,o,a;return t.isPlainObject(e)?(r=e.date,s=e.type,o=e.forceOff):r=e,r=Zt.moment(r),a={date:r.format("YYYY-MM-DD"),type:s||"day"},"string"==typeof n&&(i=n,n=null),n=n?" "+ft(n):"",i=i||"",!o&&this.opt("navLinks")?"'+i+" ":""+i+" "},setElement:function(t){this.el=t,this.bindGlobalHandlers(),this.bindBaseRenderHandlers(),this.renderSkeleton()},
-removeElement:function(){this.unsetDate(),this.unrenderSkeleton(),this.unbindGlobalHandlers(),this.unbindBaseRenderHandlers(),this.el.remove()},renderSkeleton:function(){},unrenderSkeleton:function(){},setDate:function(t){var e=this.get("dateProfile"),n=this.buildDateProfile(t,null,!0);return e&&X(e.activeRange,n.activeRange)||this.set("dateProfile",n),n.date},unsetDate:function(){this.unset("dateProfile")},requestDateRender:function(t){var e=this;this.renderQueue.queue(function(){e.executeDateRender(t)},"date","init")},requestDateUnrender:function(){var t=this;this.renderQueue.queue(function(){t.executeDateUnrender()},"date","destroy")},fetchInitialEvents:function(t){return this.calendar.requestEvents(t.activeRange.start,t.activeRange.end)},bindEventChanges:function(){this.listenTo(this.calendar,"eventsReset",this.resetEvents)},unbindEventChanges:function(){this.stopListeningTo(this.calendar,"eventsReset")},setEvents:function(t){this.set("currentEvents",t),this.set("hasEvents",!0)},unsetEvents:function(){this.unset("currentEvents"),this.unset("hasEvents")},resetEvents:function(t){this.startBatchRender(),this.unsetEvents(),this.setEvents(t),this.stopBatchRender()},requestEventsRender:function(t){var e=this;this.renderQueue.queue(function(){e.executeEventsRender(t)},"event","init")},requestEventsUnrender:function(){var t=this;this.renderQueue.queue(function(){t.executeEventsUnrender()},"event","destroy")},executeDateRender:function(t,e){this.setDateProfileForRendering(t),this.updateTitle(),this.calendar.updateToolbarButtons(),this.render&&this.render(),this.renderDates(),this.updateSize(),this.renderBusinessHours(),this.startNowIndicator(),e||this.addScroll(this.computeInitialDateScroll()),this.isDatesRendered=!0,this.trigger("datesRendered")},executeDateUnrender:function(){this.unselect(),this.stopNowIndicator(),this.trigger("before:datesUnrendered"),this.unrenderBusinessHours(),this.unrenderDates(),this.destroy&&this.destroy(),this.isDatesRendered=!1},renderDates:function(){},unrenderDates:function(){},bindBaseRenderHandlers:function(){var t=this;this.on("datesRendered.baseHandler",function(){t.onBaseRender()}),this.on("before:datesUnrendered.baseHandler",function(){t.onBeforeBaseUnrender()})},unbindBaseRenderHandlers:function(){this.off(".baseHandler")},onBaseRender:function(){this.applyScreenState(),this.publiclyTrigger("viewRender",this,this,this.el)},onBeforeBaseUnrender:function(){this.applyScreenState(),this.publiclyTrigger("viewDestroy",this,this,this.el)},bindGlobalHandlers:function(){this.listenTo(we.get(),{touchstart:this.processUnselect,mousedown:this.handleDocumentMousedown})},unbindGlobalHandlers:function(){this.stopListeningTo(we.get())},initThemingProps:function(){var t=this.opt("theme")?"ui":"fc";this.widgetHeaderClass=t+"-widget-header",this.widgetContentClass=t+"-widget-content",this.highlightStateClass=t+"-state-highlight"},renderBusinessHours:function(){},unrenderBusinessHours:function(){},startNowIndicator:function(){var t,n,i,r=this;this.opt("nowIndicator")&&(t=this.getNowIndicatorUnit())&&(n=mt(this,"updateNowIndicator"),this.initialNowDate=this.calendar.getNow(),this.initialNowQueriedMs=+new Date,this.renderNowIndicator(this.initialNowDate),this.isNowIndicatorRendered=!0,i=this.initialNowDate.clone().startOf(t).add(1,t)-this.initialNowDate,this.nowIndicatorTimeoutID=setTimeout(function(){r.nowIndicatorTimeoutID=null,n(),i=+e.duration(1,t),i=Math.max(100,i),r.nowIndicatorIntervalID=setInterval(n,i)},i))},updateNowIndicator:function(){this.isNowIndicatorRendered&&(this.unrenderNowIndicator(),this.renderNowIndicator(this.initialNowDate.clone().add(new Date-this.initialNowQueriedMs)))},stopNowIndicator:function(){this.isNowIndicatorRendered&&(this.nowIndicatorTimeoutID&&(clearTimeout(this.nowIndicatorTimeoutID),this.nowIndicatorTimeoutID=null),this.nowIndicatorIntervalID&&(clearTimeout(this.nowIndicatorIntervalID),this.nowIndicatorIntervalID=null),this.unrenderNowIndicator(),this.isNowIndicatorRendered=!1)},getNowIndicatorUnit:function(){},renderNowIndicator:function(t){},unrenderNowIndicator:function(){},updateSize:function(t){var e;t&&(e=this.queryScroll()),this.updateHeight(t),this.updateWidth(t),this.updateNowIndicator(),t&&this.applyScroll(e)},updateWidth:function(t){},updateHeight:function(t){var e=this.calendar;this.setHeight(e.getSuggestedViewHeight(),e.isHeightAuto())},setHeight:function(t,e){},addForcedScroll:function(e){this.addScroll(t.extend(e,{isForced:!0}))},addScroll:function(e){var n=this.queuedScroll||(this.queuedScroll={});n.isForced||t.extend(n,e)},popScroll:function(){this.applyQueuedScroll(),this.queuedScroll=null},applyQueuedScroll:function(){this.queuedScroll&&this.applyScroll(this.queuedScroll)},queryScroll:function(){var e={};return this.isDatesRendered&&t.extend(e,this.queryDateScroll()),e},applyScroll:function(t){this.isDatesRendered&&this.applyDateScroll(t)},computeInitialDateScroll:function(){return{}},queryDateScroll:function(){return{}},applyDateScroll:function(t){},freezeHeight:function(){this.calendar.freezeContentHeight()},thawHeight:function(){this.calendar.thawContentHeight()},executeEventsRender:function(t){this.renderEvents(t),this.isEventsRendered=!0,this.onEventsRender()},executeEventsUnrender:function(){this.onBeforeEventsUnrender(),this.destroyEvents&&this.destroyEvents(),this.unrenderEvents(),this.isEventsRendered=!1},onEventsRender:function(){this.applyScreenState(),this.renderedEventSegEach(function(t){this.publiclyTrigger("eventAfterRender",t.event,t.event,t.el)}),this.publiclyTrigger("eventAfterAllRender")},onBeforeEventsUnrender:function(){this.applyScreenState(),this.renderedEventSegEach(function(t){this.publiclyTrigger("eventDestroy",t.event,t.event,t.el)})},applyScreenState:function(){this.thawHeight(),this.freezeHeight(),this.applyQueuedScroll()},renderEvents:function(t){},unrenderEvents:function(){},resolveEventEl:function(e,n){var i=this.publiclyTrigger("eventRender",e,e,n);return!1===i?n=null:i&&!0!==i&&(n=t(i)),n},showEvent:function(t){this.renderedEventSegEach(function(t){t.el.css("visibility","")},t)},hideEvent:function(t){this.renderedEventSegEach(function(t){t.el.css("visibility","hidden")},t)},renderedEventSegEach:function(t,e){var n,i=this.getEventSegs();for(n=0;n=this.nextDayThreshold&&r.add(1,"days")),(!i||r<=n)&&(r=n.clone().add(1,"days")),{start:n,end:r}},isMultiDayEvent:function(t){var e=this.computeDayRange(t);return e.end.diff(e.start,"days")>1}});Ce.watch("displayingDates",["dateProfile"],function(t){this.requestDateRender(t.dateProfile)},function(){this.requestDateUnrender()}),Ce.watch("initialEvents",["dateProfile"],function(t){return this.fetchInitialEvents(t.dateProfile)}),Ce.watch("bindingEvents",["initialEvents"],function(t){this.setEvents(t.initialEvents),this.bindEventChanges()},function(){this.unbindEventChanges(),this.unsetEvents()}),Ce.watch("displayingEvents",["displayingDates","hasEvents"],function(){this.requestEventsRender(this.get("currentEvents"))},function(){this.requestEventsUnrender()}),Ce.mixin({currentRange:null,currentRangeUnit:null,renderRange:null,activeRange:null,validRange:null,dateIncrement:null,minTime:null,maxTime:null,usesMinMaxTime:!1,start:null,end:null,intervalStart:null,intervalEnd:null,setDateProfileForRendering:function(t){this.currentRange=t.currentRange,this.currentRangeUnit=t.currentRangeUnit,this.renderRange=t.renderRange,this.activeRange=t.activeRange,this.validRange=t.validRange,this.dateIncrement=t.dateIncrement,this.minTime=t.minTime,this.maxTime=t.maxTime,this.start=t.activeRange.start,this.end=t.activeRange.end,this.intervalStart=t.currentRange.start,this.intervalEnd=t.currentRange.end},buildPrevDateProfile:function(t){var e=t.clone().startOf(this.currentRangeUnit).subtract(this.dateIncrement);return this.buildDateProfile(e,-1)},buildNextDateProfile:function(t){var e=t.clone().startOf(this.currentRangeUnit).add(this.dateIncrement);return this.buildDateProfile(e,1)},buildDateProfile:function(t,n,i){var r,s,o,a,l=this.buildValidRange(),u=null,h=null;return i&&(t=j(t,l)),r=this.buildCurrentRangeInfo(t,n),s=this.buildRenderRange(r.range,r.unit),o=q(s),this.opt("showNonCurrentDates")||(o=U(o,r.range)),u=e.duration(this.opt("minTime")),h=e.duration(this.opt("maxTime")),this.adjustActiveRange(o,u,h),o=U(o,l),t=j(t,o),a=$(r.range,l),{validRange:l,currentRange:r.range,currentRangeUnit:r.unit,activeRange:o,renderRange:s,minTime:u,maxTime:h,isValid:a,date:t,dateIncrement:this.buildDateIncrement(r.duration)}},buildValidRange:function(){return this.getRangeOption("validRange",this.calendar.getNow())||{}},buildCurrentRangeInfo:function(t,e){var n,i=null,r=null,s=null;return this.viewSpec.duration?(i=this.viewSpec.duration,r=this.viewSpec.durationUnit,s=this.buildRangeFromDuration(t,e,i,r)):(n=this.opt("dayCount"))?(r="day",s=this.buildRangeFromDayCount(t,e,n)):(s=this.buildCustomVisibleRange(t))?r=V(s.start,s.end):(i=this.getFallbackDuration(),r=V(i),s=this.buildRangeFromDuration(t,e,i,r)),this.normalizeCurrentRange(s,r),{duration:i,unit:r,range:s}},getFallbackDuration:function(){return e.duration({days:1})},normalizeCurrentRange:function(t,e){/^(year|month|week|day)$/.test(e)?(t.start.stripTime(),t.end.stripTime()):(t.start.hasTime()||t.start.time(0),t.end.hasTime()||t.end.time(0))},adjustActiveRange:function(t,e,n){var i=!1;this.usesMinMaxTime&&(e<0&&(t.start.time(0).add(e),i=!0),n>864e5&&(t.end.time(n-864e5),i=!0),i&&(t.start.hasTime()||t.start.time(0),t.end.hasTime()||t.end.time(0)))},buildRangeFromDuration:function(t,n,i,r){var s,o,a,l=this.opt("dateAlignment"),u=t.clone();return i.as("days")<=1&&this.isHiddenDay(u)&&(u=this.skipHiddenDays(u,n),u.startOf("day")),l||(o=this.opt("dateIncrement"),o?(a=e.duration(o),l=a')},clear:function(){this.setHeight("auto"),this.applyOverflow()},destroy:function(){this.el.remove()},applyOverflow:function(){this.scrollEl.css({"overflow-x":this.overflowX,"overflow-y":this.overflowY})},lockOverflow:function(t){var e=this.overflowX,n=this.overflowY;t=t||this.getScrollbarWidths(),"auto"===e&&(e=t.top||t.bottom||this.scrollEl[0].scrollWidth-1>this.scrollEl[0].clientWidth?"scroll":"hidden"),"auto"===n&&(n=t.left||t.right||this.scrollEl[0].scrollHeight-1>this.scrollEl[0].clientHeight?"scroll":"hidden"),this.scrollEl.css({"overflow-x":e,"overflow-y":n})},setHeight:function(t){this.scrollEl.height(t)},getScrollTop:function(){return this.scrollEl.scrollTop()},setScrollTop:function(t){this.scrollEl.scrollTop(t)},getClientWidth:function(){return this.scrollEl[0].clientWidth},getClientHeight:function(){return this.scrollEl[0].clientHeight},getScrollbarWidths:function(){return p(this.scrollEl)}});_t.prototype.proxyCall=function(t){var e=Array.prototype.slice.call(arguments,1),n=[];return this.items.forEach(function(i){n.push(i[t].apply(i,e))}),n};var Re=Zt.Calendar=bt.extend(fe,{view:null,viewsByType:null,currentDate:null,loadingLevel:0,constructor:function(t,e){we.needed(),this.el=t,this.viewsByType={},this.viewSpecCache={},this.initOptionsInternals(e),this.initMomentInternals(),this.initCurrentDate(),Ut.call(this),this.initialize()},initialize:function(){},getCalendar:function(){return this},getView:function(){return this.view},publiclyTrigger:function(t,e){var n=Array.prototype.slice.call(arguments,2),i=this.opt(t);if(e=e||this.el[0],this.triggerWith(t,e,n),i)return i.apply(e,n)},instantiateView:function(t){var e=this.getViewSpec(t);return new e.class(this,e)},isValidViewType:function(t){return Boolean(this.getViewSpec(t))},changeView:function(t,e){e&&(e.start&&e.end?this.recordOptionOverrides({visibleRange:e}):this.currentDate=this.moment(e).stripZone()),this.renderView(t)},zoomTo:function(t,e){var n;e=e||"day",n=this.getViewSpec(e)||this.getUnitViewSpec(e),this.currentDate=t.clone(),this.renderView(n?n.type:null)},initCurrentDate:function(){var t=this.opt("defaultDate");this.currentDate=null!=t?this.moment(t).stripZone():this.getNow()},prev:function(){var t=this.view.buildPrevDateProfile(this.currentDate);t.isValid&&(this.currentDate=t.date,this.renderView())},next:function(){var t=this.view.buildNextDateProfile(this.currentDate);t.isValid&&(this.currentDate=t.date,this.renderView())},prevYear:function(){this.currentDate.add(-1,"years"),this.renderView()},nextYear:function(){this.currentDate.add(1,"years"),this.renderView()},today:function(){this.currentDate=this.getNow(),this.renderView()},gotoDate:function(t){this.currentDate=this.moment(t).stripZone(),this.renderView()},incrementDate:function(t){this.currentDate.add(e.duration(t)),this.renderView()},getDate:function(){return this.applyTimezone(this.currentDate)},pushLoading:function(){this.loadingLevel++||this.publiclyTrigger("loading",null,!0,this.view)},popLoading:function(){--this.loadingLevel||this.publiclyTrigger("loading",null,!1,this.view)},select:function(t,e){this.view.select(this.buildSelectSpan.apply(this,arguments))},unselect:function(){this.view&&this.view.unselect()},buildSelectSpan:function(t,e){var n,i=this.moment(t).stripZone();return n=e?this.moment(e).stripZone():i.hasTime()?i.clone().add(this.defaultTimedEventDuration):i.clone().add(this.defaultAllDayEventDuration),{start:i,end:n}},parseRange:function(t){var e=null,n=null;return t.start&&(e=this.moment(t.start).stripZone()),t.end&&(n=this.moment(t.end).stripZone()),e||n?e&&n&&n.isBefore(e)?null:{start:e,end:n}:null},rerenderEvents:function(){this.elementVisible()&&this.reportEventChange()}});Re.mixin({dirDefaults:null,localeDefaults:null,overrides:null,dynamicOverrides:null,optionsModel:null,initOptionsInternals:function(e){this.overrides=t.extend({},e),this.dynamicOverrides={},this.optionsModel=new ue,this.populateOptionsHash()},option:function(t,e){var n;if("string"==typeof t){if(void 0===e)return this.optionsModel.get(t);n={},n[t]=e,this.setOptions(n)}else"object"==typeof t&&this.setOptions(t)},opt:function(t){return this.optionsModel.get(t)},setOptions:function(t){var e,n=0;this.recordOptionOverrides(t);for(e in t)n++;if(1===n){if("height"===e||"contentHeight"===e||"aspectRatio"===e)return void this.updateSize(!0);if("defaultDate"===e)return;if("businessHours"===e)return void(this.view&&(this.view.unrenderBusinessHours(),this.view.renderBusinessHours()));if("timezone"===e)return this.rezoneArrayEventSources(),void this.refetchEvents()}this.renderHeader(),this.renderFooter(),this.viewsByType={},this.reinitView()},populateOptionsHash:function(){var t,e,i,r,s;t=ut(this.dynamicOverrides.locale,this.overrides.locale),e=xe[t],e||(t=Re.defaults.locale,e=xe[t]||{}),i=ut(this.dynamicOverrides.isRTL,this.overrides.isRTL,e.isRTL,Re.defaults.isRTL),r=i?Re.rtlDefaults:{},this.dirDefaults=r,this.localeDefaults=e,s=n([Re.defaults,r,e,this.overrides,this.dynamicOverrides]),Yt(s),this.optionsModel.reset(s)},recordOptionOverrides:function(t){var e;for(e in t)this.dynamicOverrides[e]=t[e];this.viewSpecCache={},this.populateOptionsHash()}}),Re.mixin({defaultAllDayEventDuration:null,defaultTimedEventDuration:null,localeData:null,initMomentInternals:function(){var t=this;this.defaultAllDayEventDuration=e.duration(this.opt("defaultAllDayEventDuration")),this.defaultTimedEventDuration=e.duration(this.opt("defaultTimedEventDuration")),this.optionsModel.watch("buildingMomentLocale",["?locale","?monthNames","?monthNamesShort","?dayNames","?dayNamesShort","?firstDay","?weekNumberCalculation"],function(e){var n,i=e.weekNumberCalculation,r=e.firstDay;"iso"===i&&(i="ISO");var s=rt(qt(e.locale));e.monthNames&&(s._months=e.monthNames),e.monthNamesShort&&(s._monthsShort=e.monthNamesShort),e.dayNames&&(s._weekdays=e.dayNames),e.dayNamesShort&&(s._weekdaysShort=e.dayNamesShort),null==r&&"ISO"===i&&(r=1),null!=r&&(n=rt(s._week),n.dow=r,s._week=n),"ISO"!==i&&"local"!==i&&"function"!=typeof i||(s._fullCalendar_weekCalc=i),t.localeData=s,t.currentDate&&t.localizeMoment(t.currentDate)})},moment:function(){var t;return"local"===this.opt("timezone")?(t=Zt.moment.apply(null,arguments),t.hasTime()&&t.local()):t="UTC"===this.opt("timezone")?Zt.moment.utc.apply(null,arguments):Zt.moment.parseZone.apply(null,arguments),this.localizeMoment(t),t},localizeMoment:function(t){t._locale=this.localeData},getIsAmbigTimezone:function(){return"local"!==this.opt("timezone")&&"UTC"!==this.opt("timezone")},applyTimezone:function(t){if(!t.hasTime())return t.clone();var e,n=this.moment(t.toArray()),i=t.time()-n.time();return i&&(e=n.clone().add(i),t.time()-e.time()==0&&(n=e)),n},getNow:function(){var t=this.opt("now");return"function"==typeof t&&(t=t()),this.moment(t).stripZone()},humanizeDuration:function(t){return t.locale(this.opt("locale")).humanize()},getEventEnd:function(t){return t.end?t.end.clone():this.getDefaultEventEnd(t.allDay,t.start)},getDefaultEventEnd:function(t,e){var n=e.clone();return t?n.stripTime().add(this.defaultAllDayEventDuration):n.add(this.defaultTimedEventDuration),this.getIsAmbigTimezone()&&n.stripZone(),n}}),Re.mixin({viewSpecCache:null,getViewSpec:function(t){var e=this.viewSpecCache;return e[t]||(e[t]=this.buildViewSpec(t))},getUnitViewSpec:function(e){var n,i,r;if(-1!=t.inArray(e,Jt))for(n=this.header.getViewsWithButtons(),t.each(Zt.views,function(t){n.push(t)}),i=0;i ").prependTo(n),this.initToolbars(),this.renderHeader(),this.renderFooter(),this.renderView(this.opt("defaultView")),this.opt("handleWindowResize")&&t(window).resize(this.windowResizeProxy=yt(this.windowResize.bind(this),this.opt("windowResizeDelay")))},destroy:function(){this.view&&this.view.removeElement(),this.toolbarsManager.proxyCall("removeElement"),this.contentEl.remove(),this.el.removeClass("fc fc-ltr fc-rtl fc-unthemed ui-widget"),this.el.off(".fc"),this.windowResizeProxy&&(t(window).unbind("resize",this.windowResizeProxy),this.windowResizeProxy=null),we.unneeded()},elementVisible:function(){return this.el.is(":visible")},renderView:function(e,n){this.ignoreWindowResize++;var i=this.view&&e&&this.view.type!==e;i&&(this.freezeContentHeight(),this.clearView()),!this.view&&e&&(this.view=this.viewsByType[e]||(this.viewsByType[e]=this.instantiateView(e)),this.view.setElement(t("
").appendTo(this.contentEl)),this.toolbarsManager.proxyCall("activateButton",e)),this.view&&(n&&this.view.addForcedScroll(n),this.elementVisible()&&(this.currentDate=this.view.setDate(this.currentDate))),i&&this.thawContentHeight(),this.ignoreWindowResize--},clearView:function(){this.toolbarsManager.proxyCall("deactivateButton",this.view.type),this.view.removeElement(),this.view=null},reinitView:function(){this.ignoreWindowResize++,this.freezeContentHeight();var t=this.view.type,e=this.view.queryScroll();this.clearView(),this.calcSize(),this.renderView(t,e),this.thawContentHeight(),this.ignoreWindowResize--},getSuggestedViewHeight:function(){return null===this.suggestedViewHeight&&this.calcSize(),this.suggestedViewHeight},isHeightAuto:function(){return"auto"===this.opt("contentHeight")||"auto"===this.opt("height")},updateSize:function(t){if(this.elementVisible())return t&&this._calcSize(),this.ignoreWindowResize++,this.view.updateSize(!0),this.ignoreWindowResize--,!0},calcSize:function(){this.elementVisible()&&this._calcSize()},_calcSize:function(){var t=this.opt("contentHeight"),e=this.opt("height");this.suggestedViewHeight="number"==typeof t?t:"function"==typeof t?t():"number"==typeof e?e-this.queryToolbarsHeight():"function"==typeof e?e()-this.queryToolbarsHeight():"parent"===e?this.el.parent().height()-this.queryToolbarsHeight():Math.round(this.contentEl.width()/Math.max(this.opt("aspectRatio"),.5))},windowResize:function(t){!this.ignoreWindowResize&&t.target===window&&this.view.renderRange&&this.updateSize(!0)&&this.view.publiclyTrigger("windowResize",this.el[0])},freezeContentHeight:function(){this.contentEl.css({width:"100%",height:this.contentEl.height(),overflow:"hidden"})},thawContentHeight:function(){this.contentEl.css({width:"",height:"",overflow:""})}}),Re.mixin({header:null,footer:null,toolbarsManager:null,initToolbars:function(){this.header=new Wt(this,this.computeHeaderOptions()),this.footer=new Wt(this,this.computeFooterOptions()),this.toolbarsManager=new _t([this.header,this.footer])},computeHeaderOptions:function(){return{extraClasses:"fc-header-toolbar",layout:this.opt("header")}},computeFooterOptions:function(){return{extraClasses:"fc-footer-toolbar",layout:this.opt("footer")}},renderHeader:function(){var t=this.header;t.setToolbarOptions(this.computeHeaderOptions()),t.render(),t.el&&this.el.prepend(t.el)},renderFooter:function(){var t=this.footer;t.setToolbarOptions(this.computeFooterOptions()),t.render(),t.el&&this.el.append(t.el)},setToolbarsTitle:function(t){this.toolbarsManager.proxyCall("updateTitle",t)},updateToolbarButtons:function(){var t=this.getNow(),e=this.view,n=e.buildDateProfile(t),i=e.buildPrevDateProfile(this.currentDate),r=e.buildNextDateProfile(this.currentDate);this.toolbarsManager.proxyCall(n.isValid&&!Z(t,e.currentRange)?"enableButton":"disableButton","today"),this.toolbarsManager.proxyCall(i.isValid?"enableButton":"disableButton","prev"),this.toolbarsManager.proxyCall(r.isValid?"enableButton":"disableButton","next")},queryToolbarsHeight:function(){return this.toolbarsManager.items.reduce(function(t,e){return t+(e.el?e.el.outerHeight(!0):0)},0)}}),Re.defaults={titleRangeSeparator:" – ",monthYearFormat:"MMMM YYYY",defaultTimedEventDuration:"02:00:00",defaultAllDayEventDuration:{days:1},forceEventDuration:!1,nextDayThreshold:"09:00:00",defaultView:"month",aspectRatio:1.35,header:{left:"title",center:"",right:"today prev,next"},weekends:!0,weekNumbers:!1,weekNumberTitle:"W",weekNumberCalculation:"local",scrollTime:"06:00:00",minTime:"00:00:00",maxTime:"24:00:00",showNonCurrentDates:!0,lazyFetching:!0,startParam:"start",endParam:"end",timezoneParam:"timezone",timezone:!1,isRTL:!1,buttonText:{prev:"prev",next:"next",prevYear:"prev year",nextYear:"next year",year:"year",today:"today",month:"month",week:"week",day:"day"},buttonIcons:{prev:"left-single-arrow",next:"right-single-arrow",prevYear:"left-double-arrow",nextYear:"right-double-arrow"},allDayText:"all-day",theme:!1,themeButtonIcons:{prev:"circle-triangle-w",next:"circle-triangle-e",prevYear:"seek-prev",nextYear:"seek-next"},dragOpacity:.75,dragRevertDuration:500,dragScroll:!0,unselectAuto:!0,dropAccept:"*",eventOrder:"title",eventLimit:!1,eventLimitText:"more",eventLimitClick:"popover",dayPopoverFormat:"LL",handleWindowResize:!0,windowResizeDelay:100,longPressDelay:1e3},Re.englishDefaults={dayPopoverFormat:"dddd, MMMM D"},Re.rtlDefaults={header:{left:"next,prev today",center:"",right:"title"},buttonIcons:{prev:"right-single-arrow",next:"left-single-arrow",prevYear:"right-double-arrow",nextYear:"left-double-arrow"},themeButtonIcons:{prev:"circle-triangle-e",next:"circle-triangle-w",nextYear:"seek-prev",prevYear:"seek-next"}};var xe=Zt.locales={};Zt.datepickerLocale=function(e,n,i){var r=xe[e]||(xe[e]={});r.isRTL=i.isRTL,r.weekNumberTitle=i.weekHeader,t.each(Ie,function(t,e){r[t]=e(i)}),t.datepicker&&(t.datepicker.regional[n]=t.datepicker.regional[e]=i,t.datepicker.regional.en=t.datepicker.regional[""],t.datepicker.setDefaults(i))},Zt.locale=function(e,i){var r,s;r=xe[e]||(xe[e]={}),i&&(r=xe[e]=n([r,i])),s=qt(e),t.each(ke,function(t,e){null==r[t]&&(r[t]=e(s,r))}),Re.defaults.locale=e};var Ie={buttonText:function(t){return{prev:ct(t.prevText),next:ct(t.nextText),today:ct(t.currentText)}},monthYearFormat:function(t){return t.showMonthAfterYear?"YYYY["+t.yearSuffix+"] MMMM":"MMMM YYYY["+t.yearSuffix+"]"}},ke={dayOfMonthFormat:function(t,e){var n=t.longDateFormat("l");return n=n.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g,""),e.isRTL?n+=" ddd":n="ddd "+n,n},mediumTimeFormat:function(t){return t.longDateFormat("LT").replace(/\s*a$/i,"a")},smallTimeFormat:function(t){return t.longDateFormat("LT").replace(":mm","(:mm)").replace(/(\Wmm)$/,"($1)").replace(/\s*a$/i,"a")},extraSmallTimeFormat:function(t){return t.longDateFormat("LT").replace(":mm","(:mm)").replace(/(\Wmm)$/,"($1)").replace(/\s*a$/i,"t")},hourFormat:function(t){return t.longDateFormat("LT").replace(":mm","").replace(/(\Wmm)$/,"").replace(/\s*a$/i,"a")},noMeridiemTimeFormat:function(t){return t.longDateFormat("LT").replace(/\s*a$/i,"")}},Me={smallDayDateFormat:function(t){return t.isRTL?"D dd":"dd D"},weekFormat:function(t){return t.isRTL?"w[ "+t.weekNumberTitle+"]":"["+t.weekNumberTitle+" ]w"},smallWeekFormat:function(t){return t.isRTL?"w["+t.weekNumberTitle+"]":"["+t.weekNumberTitle+"]w"}};Zt.locale("en",Re.englishDefaults),Zt.sourceNormalizers=[],Zt.sourceFetchers=[];var Be={dataType:"json",cache:!1},Le=1;Re.prototype.mutateSeg=function(t,e){return this.mutateEvent(t.event,e)},Re.prototype.normalizeEvent=function(t){},Re.prototype.spanContainsSpan=function(t,e){var n=t.start.clone().stripZone(),i=this.getEventEnd(t).stripZone()
-;return e.start>=n&&e.end<=i},Re.prototype.getPeerEvents=function(t,e){var n,i,r=this.getEventCache(),s=[];for(n=0;nn};var Ne={id:"_fcBusinessHours",start:"09:00",end:"17:00",dow:[1,2,3,4,5],rendering:"inverse-background"};Re.prototype.getCurrentBusinessHourEvents=function(t){return this.computeBusinessHourEvents(t,this.opt("businessHours"))},Re.prototype.computeBusinessHourEvents=function(e,n){return!0===n?this.expandBusinessHourEvents(e,[{}]):t.isPlainObject(n)?this.expandBusinessHourEvents(e,[n]):t.isArray(n)?this.expandBusinessHourEvents(e,n,!0):[]},Re.prototype.expandBusinessHourEvents=function(e,n,i){var r,s,o=this.getView(),a=[];for(r=0;r1,this.opt("weekNumbers")&&(this.opt("weekNumbersWithinDays")?(this.cellWeekNumbersVisible=!0,this.colWeekNumbersVisible=!1):(this.cellWeekNumbersVisible=!1,this.colWeekNumbersVisible=!0)),this.dayGrid.numbersVisible=this.dayNumbersVisible||this.cellWeekNumbersVisible||this.colWeekNumbersVisible,this.el.addClass("fc-basic-view").html(this.renderSkeletonHtml()),this.renderHead(),this.scroller.render();var e=this.scroller.el.addClass("fc-day-grid-container"),n=t('
').appendTo(e);this.el.find(".fc-body > tr > td").append(e),this.dayGrid.setElement(n),this.dayGrid.renderDates(this.hasRigidRows())},renderHead:function(){this.headContainerEl=this.el.find(".fc-head-container").html(this.dayGrid.renderHeadHtml()),this.headRowEl=this.headContainerEl.find(".fc-row")},unrenderDates:function(){this.dayGrid.unrenderDates(),this.dayGrid.removeElement(),this.scroller.destroy()},renderBusinessHours:function(){this.dayGrid.renderBusinessHours()},unrenderBusinessHours:function(){this.dayGrid.unrenderBusinessHours()},renderSkeletonHtml:function(){return''},weekNumberStyleAttr:function(){return null!==this.weekNumberWidth?'style="width:'+this.weekNumberWidth+'px"':""},hasRigidRows:function(){var t=this.opt("eventLimit");return t&&"number"!=typeof t},updateWidth:function(){this.colWeekNumbersVisible&&(this.weekNumberWidth=u(this.el.find(".fc-week-number")))},setHeight:function(t,e){var n,s,o=this.opt("eventLimit");this.scroller.clear(),r(this.headRowEl),this.dayGrid.removeSegPopover(),o&&"number"==typeof o&&this.dayGrid.limitRows(o),n=this.computeScrollerHeight(t),this.setGridHeight(n,e),o&&"number"!=typeof o&&this.dayGrid.limitRows(o),e||(this.scroller.setHeight(n),s=this.scroller.getScrollbarWidths(),(s.left||s.right)&&(i(this.headRowEl,s),n=this.computeScrollerHeight(t),this.scroller.setHeight(n)),this.scroller.lockOverflow(s))},computeScrollerHeight:function(t){return t-h(this.el,this.scroller.el)},setGridHeight:function(t,e){e?l(this.dayGrid.rowEls):a(this.dayGrid.rowEls,t,!0)},computeInitialDateScroll:function(){return{top:0}},queryDateScroll:function(){return{top:this.scroller.getScrollTop()}},applyDateScroll:function(t){void 0!==t.top&&this.scroller.setScrollTop(t.top)},hitsNeeded:function(){this.dayGrid.hitsNeeded()},hitsNotNeeded:function(){this.dayGrid.hitsNotNeeded()},prepareHits:function(){this.dayGrid.prepareHits()},releaseHits:function(){this.dayGrid.releaseHits()},queryHit:function(t,e){return this.dayGrid.queryHit(t,e)},getHitSpan:function(t){return this.dayGrid.getHitSpan(t)},getHitEl:function(t){return this.dayGrid.getHitEl(t)},renderEvents:function(t){this.dayGrid.renderEvents(t),this.updateHeight()},getEventSegs:function(){return this.dayGrid.getEventSegs()},unrenderEvents:function(){this.dayGrid.unrenderEvents()},renderDrag:function(t,e){return this.dayGrid.renderDrag(t,e)},unrenderDrag:function(){this.dayGrid.unrenderDrag()},renderSelection:function(t){this.dayGrid.renderSelection(t)},unrenderSelection:function(){this.dayGrid.unrenderSelection()}}),Fe={renderHeadIntroHtml:function(){var t=this.view;return t.colWeekNumbersVisible?'":""},renderNumberIntroHtml:function(t){var e=this.view,n=this.getCellDate(t,0);return e.colWeekNumbersVisible?'"+e.buildGotoAnchorHtml({date:n,type:"week",forceOff:1===this.colCnt},n.format("w"))+" ":""},renderBgIntroHtml:function(){var t=this.view;return t.colWeekNumbersVisible?' ":""},renderIntroHtml:function(){var t=this.view;return t.colWeekNumbersVisible?' ":""}},Ae=Zt.MonthView=ze.extend({buildRenderRange:function(){var t,e=ze.prototype.buildRenderRange.apply(this,arguments);return this.isFixedWeeks()&&(t=Math.ceil(e.end.diff(e.start,"weeks",!0)),e.end.add(6-t,"weeks")),e},setGridHeight:function(t,e){e&&(t*=this.rowCnt/6),a(this.dayGrid.rowEls,t,!e)},isFixedWeeks:function(){return this.opt("fixedWeekCount")}});$t.basic={class:ze},$t.basicDay={type:"basic",duration:{days:1}},$t.basicWeek={type:"basic",duration:{weeks:1}},$t.month={class:Ae,duration:{months:1},defaults:{fixedWeekCount:!0}};var Ge=Zt.AgendaView=Ce.extend({scroller:null,timeGridClass:Te,timeGrid:null,dayGridClass:De,dayGrid:null,axisWidth:null,headContainerEl:null,noScrollRowEls:null,bottomRuleEl:null,usesMinMaxTime:!0,initialize:function(){this.timeGrid=this.instantiateTimeGrid(),this.opt("allDaySlot")&&(this.dayGrid=this.instantiateDayGrid()),this.scroller=new He({overflowX:"hidden",overflowY:"auto"})},instantiateTimeGrid:function(){return new(this.timeGridClass.extend(Ve))(this)},instantiateDayGrid:function(){return new(this.dayGridClass.extend(Oe))(this)},renderDates:function(){this.timeGrid.setRange(this.renderRange),this.dayGrid&&this.dayGrid.setRange(this.renderRange),this.el.addClass("fc-agenda-view").html(this.renderSkeletonHtml()),this.renderHead(),this.scroller.render();var e=this.scroller.el.addClass("fc-time-grid-container"),n=t('
').appendTo(e);this.el.find(".fc-body > tr > td").append(e),this.timeGrid.setElement(n),this.timeGrid.renderDates(),this.bottomRuleEl=t('').appendTo(this.timeGrid.el),this.dayGrid&&(this.dayGrid.setElement(this.el.find(".fc-day-grid")),this.dayGrid.renderDates(),this.dayGrid.bottomCoordPadding=this.dayGrid.el.next("hr").outerHeight()),this.noScrollRowEls=this.el.find(".fc-row:not(.fc-scroller *)")},renderHead:function(){this.headContainerEl=this.el.find(".fc-head-container").html(this.timeGrid.renderHeadHtml())},unrenderDates:function(){this.timeGrid.unrenderDates(),this.timeGrid.removeElement(),this.dayGrid&&(this.dayGrid.unrenderDates(),this.dayGrid.removeElement()),this.scroller.destroy()},renderSkeletonHtml:function(){return'"},axisStyleAttr:function(){return null!==this.axisWidth?'style="width:'+this.axisWidth+'px"':""},renderBusinessHours:function(){this.timeGrid.renderBusinessHours(),this.dayGrid&&this.dayGrid.renderBusinessHours()},unrenderBusinessHours:function(){this.timeGrid.unrenderBusinessHours(),this.dayGrid&&this.dayGrid.unrenderBusinessHours()},getNowIndicatorUnit:function(){return this.timeGrid.getNowIndicatorUnit()},renderNowIndicator:function(t){this.timeGrid.renderNowIndicator(t)},unrenderNowIndicator:function(){this.timeGrid.unrenderNowIndicator()},updateSize:function(t){this.timeGrid.updateSize(t),Ce.prototype.updateSize.call(this,t)},updateWidth:function(){this.axisWidth=u(this.el.find(".fc-axis"))},setHeight:function(t,e){var n,s,o;this.bottomRuleEl.hide(),this.scroller.clear(),r(this.noScrollRowEls),this.dayGrid&&(this.dayGrid.removeSegPopover(),n=this.opt("eventLimit"),n&&"number"!=typeof n&&(n=Pe),n&&this.dayGrid.limitRows(n)),e||(s=this.computeScrollerHeight(t),this.scroller.setHeight(s),o=this.scroller.getScrollbarWidths(),(o.left||o.right)&&(i(this.noScrollRowEls,o),s=this.computeScrollerHeight(t),this.scroller.setHeight(s)),this.scroller.lockOverflow(o),this.timeGrid.getTotalSlatHeight()