diff --git a/.github/workflows/patch-mariadb-tests.yml b/.github/workflows/patch-mariadb-tests.yml
index d9a6ca6f59..52fa987994 100644
--- a/.github/workflows/patch-mariadb-tests.yml
+++ b/.github/workflows/patch-mariadb-tests.yml
@@ -10,6 +10,7 @@ concurrency:
jobs:
test:
runs-on: ubuntu-latest
+ timeout-minutes: 60
name: Patch Test
diff --git a/.github/workflows/server-mariadb-tests.yml b/.github/workflows/server-mariadb-tests.yml
index 588f357f26..4edf74ba71 100644
--- a/.github/workflows/server-mariadb-tests.yml
+++ b/.github/workflows/server-mariadb-tests.yml
@@ -14,6 +14,7 @@ concurrency:
jobs:
test:
runs-on: ubuntu-latest
+ timeout-minutes: 60
strategy:
fail-fast: false
@@ -128,4 +129,4 @@ jobs:
fail_ci_if_error: true
files: /home/runner/frappe-bench/sites/coverage.xml
verbose: true
- flags: server
\ No newline at end of file
+ flags: server
diff --git a/.github/workflows/server-postgres-tests.yml b/.github/workflows/server-postgres-tests.yml
index 78f379837b..895af5184e 100644
--- a/.github/workflows/server-postgres-tests.yml
+++ b/.github/workflows/server-postgres-tests.yml
@@ -13,6 +13,7 @@ concurrency:
jobs:
test:
runs-on: ubuntu-latest
+ timeout-minutes: 60
strategy:
fail-fast: false
diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
index fcc53ba59c..cb502f68a7 100644
--- a/.github/workflows/ui-tests.yml
+++ b/.github/workflows/ui-tests.yml
@@ -13,6 +13,7 @@ concurrency:
jobs:
test:
runs-on: ubuntu-latest
+ timeout-minutes: 60
strategy:
fail-fast: false
diff --git a/cypress/integration/grid_pagination.js b/cypress/integration/grid_pagination.js
index c07230d2b8..84b3320282 100644
--- a/cypress/integration/grid_pagination.js
+++ b/cypress/integration/grid_pagination.js
@@ -13,7 +13,7 @@ context('Grid Pagination', () => {
it('creates pages for child table', () => {
cy.visit('/app/contact/Test Contact');
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
- cy.get('@table').find('.current-page-number').should('contain', '1');
+ cy.get('@table').find('.current-page-number').should('have.value', '1');
cy.get('@table').find('.total-page-number').should('contain', '20');
cy.get('@table').find('.grid-body .grid-row').should('have.length', 50);
});
@@ -21,10 +21,10 @@ context('Grid Pagination', () => {
cy.visit('/app/contact/Test Contact');
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
cy.get('@table').find('.next-page').click();
- cy.get('@table').find('.current-page-number').should('contain', '2');
+ cy.get('@table').find('.current-page-number').should('have.value', '2');
cy.get('@table').find('.grid-body .grid-row').first().should('have.attr', 'data-idx', '51');
cy.get('@table').find('.prev-page').click();
- cy.get('@table').find('.current-page-number').should('contain', '1');
+ cy.get('@table').find('.current-page-number').should('have.value', '1');
cy.get('@table').find('.grid-body .grid-row').first().should('have.attr', 'data-idx', '1');
});
it('adds and deletes rows and changes page', () => {
@@ -32,14 +32,35 @@ context('Grid Pagination', () => {
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
cy.get('@table').findByRole('button', {name: 'Add Row'}).click();
cy.get('@table').find('.grid-body .row-index').should('contain', 1001);
- cy.get('@table').find('.current-page-number').should('contain', '21');
+ cy.get('@table').find('.current-page-number').should('have.value', '21');
cy.get('@table').find('.total-page-number').should('contain', '21');
cy.get('@table').find('.grid-body .grid-row .grid-row-check').click({ force: true });
cy.get('@table').findByRole('button', {name: 'Delete'}).click();
cy.get('@table').find('.grid-body .row-index').last().should('contain', 1000);
- cy.get('@table').find('.current-page-number').should('contain', '20');
+ cy.get('@table').find('.current-page-number').should('have.value', '20');
cy.get('@table').find('.total-page-number').should('contain', '20');
});
+ it('go to specific page, use up and down arrow, type characters, 0 page and more than existing page', () => {
+ cy.visit('/app/contact/Test Contact');
+ cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
+ cy.get('@table').find('.current-page-number').focus().clear().type('17').blur();
+ cy.get('@table').find('.grid-body .row-index').should('contain', 801);
+
+ cy.get('@table').find('.current-page-number').focus().type('{uparrow}{uparrow}');
+ cy.get('@table').find('.current-page-number').should('have.value', '19');
+
+ cy.get('@table').find('.current-page-number').focus().type('{downarrow}{downarrow}');
+ cy.get('@table').find('.current-page-number').should('have.value', '17');
+
+ cy.get('@table').find('.current-page-number').focus().clear().type('700').blur();
+ cy.get('@table').find('.current-page-number').should('have.value', '20');
+
+ cy.get('@table').find('.current-page-number').focus().clear().type('0').blur();
+ cy.get('@table').find('.current-page-number').should('have.value', '1');
+
+ cy.get('@table').find('.current-page-number').focus().clear().type('abc').blur();
+ cy.get('@table').find('.current-page-number').should('have.value', '1');
+ });
// it('deletes all rows', ()=> {
// cy.visit('/app/contact/Test Contact');
// cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
diff --git a/cypress/integration/report_view.js b/cypress/integration/report_view.js
index 0253e8fd43..629ae72eb8 100644
--- a/cypress/integration/report_view.js
+++ b/cypress/integration/report_view.js
@@ -7,6 +7,8 @@ context('Report View', () => {
cy.visit('/app/website');
cy.insert_doc('DocType', custom_submittable_doctype, true);
cy.clear_cache();
+ });
+ it('Field with enabled allow_on_submit should be editable.', () => {
cy.insert_doc(doctype_name, {
'title': 'Doc 1',
'description': 'Random Text',
@@ -14,8 +16,6 @@ context('Report View', () => {
// submit document
'docstatus': 1
}, true).as('doc');
- });
- it('Field with enabled allow_on_submit should be editable.', () => {
cy.intercept('POST', 'api/method/frappe.client.set_value').as('value-update');
cy.visit(`/app/List/${doctype_name}/Report`);
// check status column added from docstatus
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 4218aa113b..895bdcaddc 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -1523,8 +1523,8 @@ def format(*args, **kwargs):
import frappe.utils.formatters
return frappe.utils.formatters.format_value(*args, **kwargs)
-def get_print(doctype=None, name=None, print_format=None, style=None,
- html=None, as_pdf=False, doc=None, output=None, no_letterhead=0, password=None):
+def get_print(doctype=None, name=None, print_format=None, style=None, html=None,
+ as_pdf=False, doc=None, output=None, no_letterhead=0, password=None, pdf_options=None):
"""Get Print Format for given document.
:param doctype: DocType of document.
@@ -1543,15 +1543,15 @@ def get_print(doctype=None, name=None, print_format=None, style=None,
local.form_dict.doc = doc
local.form_dict.no_letterhead = no_letterhead
- options = None
+ pdf_options = pdf_options or {}
if password:
- options = {'password': password}
+ pdf_options['password'] = password
if not html:
html = get_response_content("printview")
if as_pdf:
- return get_pdf(html, output = output, options = options)
+ return get_pdf(html, options=pdf_options, output=output)
else:
return html
diff --git a/frappe/app.py b/frappe/app.py
index 8e1534e7ef..70575fe2f1 100644
--- a/frappe/app.py
+++ b/frappe/app.py
@@ -120,6 +120,8 @@ def init_request(request):
else:
frappe.connect(set_admin_as_user=False)
+ request.max_content_length = frappe.local.conf.get('max_file_size') or 10 * 1024 * 1024
+
make_form_dict(request)
if request.method != "OPTIONS":
diff --git a/frappe/commands/site.py b/frappe/commands/site.py
index c5f78e2680..3c7f2f5525 100755
--- a/frappe/commands/site.py
+++ b/frappe/commands/site.py
@@ -461,6 +461,7 @@ def migrate(context, skip_failing=False, skip_search_index=False):
skip_search_index=skip_search_index
)
finally:
+ print()
frappe.destroy()
if not context.sites:
raise SiteNotSpecifiedError
diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py
index e311b8db6a..41b607b192 100644
--- a/frappe/commands/utils.py
+++ b/frappe/commands/utils.py
@@ -791,10 +791,11 @@ def request(context, args=None, path=None):
@click.command('make-app')
@click.argument('destination')
@click.argument('app_name')
-def make_app(destination, app_name):
+@click.option('--no-git', is_flag=True, default=False, help='Do not initialize git repository for the app')
+def make_app(destination, app_name, no_git=False):
"Creates a boilerplate app"
from frappe.utils.boilerplate import make_boilerplate
- make_boilerplate(destination, app_name)
+ make_boilerplate(destination, app_name, no_git=no_git)
@click.command('set-config')
diff --git a/frappe/core/doctype/access_log/access_log.py b/frappe/core/doctype/access_log/access_log.py
index 48c12fd93f..db2e64e868 100644
--- a/frappe/core/doctype/access_log/access_log.py
+++ b/frappe/core/doctype/access_log/access_log.py
@@ -11,11 +11,26 @@ class AccessLog(Document):
@frappe.whitelist()
+def make_access_log(
+ doctype=None,
+ document=None,
+ method=None,
+ file_type=None,
+ report_name=None,
+ filters=None,
+ page=None,
+ columns=None,
+):
+ _make_access_log(
+ doctype, document, method, file_type, report_name, filters, page, columns,
+ )
+
+
@frappe.write_only()
@retry(
stop=stop_after_attempt(3), retry=retry_if_exception_type(frappe.DuplicateEntryError)
)
-def make_access_log(
+def _make_access_log(
doctype=None,
document=None,
method=None,
@@ -42,6 +57,7 @@ def make_access_log(
}).db_insert()
# `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview`
- # dont commit in test mode
+ # dont commit in test mode. It must be tempting to put this block along with the in_request in the
+ # whitelisted method...yeah, don't do it. That part would be executed possibly on a read only DB conn
if not frappe.flags.in_test or in_request:
frappe.db.commit()
diff --git a/frappe/core/doctype/comment/test_comment.py b/frappe/core/doctype/comment/test_comment.py
index 99bd19c106..cd9af498aa 100644
--- a/frappe/core/doctype/comment/test_comment.py
+++ b/frappe/core/doctype/comment/test_comment.py
@@ -5,6 +5,15 @@ import frappe, json
import unittest
class TestComment(unittest.TestCase):
+ def tearDown(self):
+ frappe.form_dict.comment = None
+ frappe.form_dict.comment_email = None
+ frappe.form_dict.comment_by = None
+ frappe.form_dict.reference_doctype = None
+ frappe.form_dict.reference_name = None
+ frappe.form_dict.route = None
+ frappe.local.request_ip = None
+
def test_comment_creation(self):
test_doc = frappe.get_doc(dict(doctype = 'ToDo', description = 'test'))
test_doc.insert()
@@ -33,8 +42,16 @@ class TestComment(unittest.TestCase):
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})
from frappe.templates.includes.comments.comments import add_comment
- add_comment('Good comment with 10 chars', 'test@test.com', 'Good Tester',
- 'Blog Post', test_blog.name, test_blog.route)
+
+ frappe.form_dict.comment = 'Good comment with 10 chars'
+ frappe.form_dict.comment_email = 'test@test.com'
+ frappe.form_dict.comment_by = 'Good Tester'
+ frappe.form_dict.reference_doctype = 'Blog Post'
+ frappe.form_dict.reference_name = test_blog.name
+ frappe.form_dict.route = test_blog.route
+ frappe.local.request_ip = '127.0.0.1'
+
+ add_comment()
self.assertEqual(frappe.get_all('Comment', fields = ['*'], filters = dict(
reference_doctype = test_blog.doctype,
@@ -43,8 +60,10 @@ class TestComment(unittest.TestCase):
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})
- add_comment('pleez vizits my site http://mysite.com', 'test@test.com', 'bad commentor',
- 'Blog Post', test_blog.name, test_blog.route)
+ frappe.form_dict.comment = 'pleez vizits my site http://mysite.com'
+ frappe.form_dict.comment_by = 'bad commentor'
+
+ add_comment()
self.assertEqual(len(frappe.get_all('Comment', fields = ['*'], filters = dict(
reference_doctype = test_blog.doctype,
diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py
index 4d22075b78..54ddbce2c4 100755
--- a/frappe/core/doctype/communication/email.py
+++ b/frappe/core/doctype/communication/email.py
@@ -146,25 +146,43 @@ def add_attachments(name, attachments):
})
_file.save(ignore_permissions=True)
-@frappe.whitelist(allow_guest=True)
-def mark_email_as_seen(name=None):
+@frappe.whitelist(allow_guest=True, methods=("GET",))
+def mark_email_as_seen(name: str = None):
try:
- if name and frappe.db.exists("Communication", name) and not frappe.db.get_value("Communication", name, "read_by_recipient"):
- frappe.db.set_value("Communication", name, "read_by_recipient", 1)
- frappe.db.set_value("Communication", name, "delivery_status", "Read")
- frappe.db.set_value("Communication", name, "read_by_recipient_on", get_datetime())
- frappe.db.commit()
+ update_communication_as_read(name)
+ frappe.db.commit() # nosemgrep: this will be called in a GET request
+
except Exception:
frappe.log_error(frappe.get_traceback())
- finally:
- # Return image as response under all circumstances
- from PIL import Image
- import io
- im = Image.new('RGBA', (1, 1))
- im.putdata([(255,255,255,0)])
- buffered_obj = io.BytesIO()
- im.save(buffered_obj, format="PNG")
- frappe.response["type"] = 'binary'
- frappe.response["filename"] = "imaginary_pixel.png"
- frappe.response["filecontent"] = buffered_obj.getvalue()
+ finally:
+ frappe.response.update({
+ "type": "binary",
+ "filename": "imaginary_pixel.png",
+ "filecontent": (
+ b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00"
+ b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\r"
+ b"IDATx\x9cc\xf8\xff\xff?\x03\x00\x08\xfc\x02\xfe\xa7\x9a\xa0"
+ b"\xa0\x00\x00\x00\x00IEND\xaeB`\x82"
+ )
+ })
+
+def update_communication_as_read(name):
+ if not name or not isinstance(name, str):
+ return
+
+ communication = frappe.db.get_value(
+ "Communication",
+ name,
+ "read_by_recipient",
+ as_dict=True
+ )
+
+ if not communication or communication.read_by_recipient:
+ return
+
+ frappe.db.set_value("Communication", name, {
+ "read_by_recipient": 1,
+ "delivery_status": "Read",
+ "read_by_recipient_on": get_datetime()
+ })
diff --git a/frappe/core/doctype/data_import/exporter.py b/frappe/core/doctype/data_import/exporter.py
index 684328a4c7..21faf98e49 100644
--- a/frappe/core/doctype/data_import/exporter.py
+++ b/frappe/core/doctype/data_import/exporter.py
@@ -191,7 +191,7 @@ class Exporter:
[format_column_name(df) for df in self.fields if df.parent == child_table_doctype]
)
)
- data = frappe.db.get_list(
+ data = frappe.db.get_all(
child_table_doctype,
filters={
"parent": ("in", parent_names),
diff --git a/frappe/core/doctype/feedback/feedback.json b/frappe/core/doctype/feedback/feedback.json
index b77e7a6677..f8380cfda6 100644
--- a/frappe/core/doctype/feedback/feedback.json
+++ b/frappe/core/doctype/feedback/feedback.json
@@ -8,34 +8,14 @@
"reference_doctype",
"reference_name",
"column_break_3",
- "rating",
- "ip_address",
- "section_break_6",
- "feedback"
+ "like",
+ "ip_address"
],
"fields": [
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
- {
- "fieldname": "rating",
- "fieldtype": "Float",
- "in_list_view": 1,
- "label": "Rating",
- "precision": "1",
- "reqd": 1
- },
- {
- "fieldname": "section_break_6",
- "fieldtype": "Section Break"
- },
- {
- "fieldname": "feedback",
- "fieldtype": "Small Text",
- "label": "Feedback",
- "reqd": 1
- },
{
"fieldname": "reference_doctype",
"fieldtype": "Select",
@@ -57,11 +37,17 @@
"hidden": 1,
"label": "IP Address",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "like",
+ "fieldtype": "Check",
+ "label": "Like"
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-06-23 12:45:42.045696",
+ "modified": "2021-11-10 20:53:21.255593",
"modified_by": "Administrator",
"module": "Core",
"name": "Feedback",
diff --git a/frappe/core/doctype/feedback/test_feedback.py b/frappe/core/doctype/feedback/test_feedback.py
index f3cf8dfe6b..66f644ccd3 100644
--- a/frappe/core/doctype/feedback/test_feedback.py
+++ b/frappe/core/doctype/feedback/test_feedback.py
@@ -8,8 +8,7 @@ class TestFeedback(unittest.TestCase):
def tearDown(self):
frappe.form_dict.reference_doctype = None
frappe.form_dict.reference_name = None
- frappe.form_dict.rating = None
- frappe.form_dict.feedback = None
+ frappe.form_dict.like = None
frappe.local.request_ip = None
def test_feedback_creation_updation(self):
@@ -18,23 +17,22 @@ class TestFeedback(unittest.TestCase):
frappe.db.delete("Feedback", {"reference_doctype": "Blog Post"})
- from frappe.templates.includes.feedback.feedback import add_feedback, update_feedback
+ from frappe.templates.includes.feedback.feedback import give_feedback
frappe.form_dict.reference_doctype = 'Blog Post'
frappe.form_dict.reference_name = test_blog.name
- frappe.form_dict.rating = 5
- frappe.form_dict.feedback = 'New feedback'
+ frappe.form_dict.like = True
frappe.local.request_ip = '127.0.0.1'
- feedback = add_feedback()
+ feedback = give_feedback()
- self.assertEqual(feedback.feedback, 'New feedback')
- self.assertEqual(feedback.rating, 5)
+ self.assertEqual(feedback.like, True)
- updated_feedback = update_feedback('Blog Post', test_blog.name, 6, 'Updated feedback')
+ frappe.form_dict.like = False
- self.assertEqual(updated_feedback.feedback, 'Updated feedback')
- self.assertEqual(updated_feedback.rating, 6)
+ updated_feedback = give_feedback()
+
+ self.assertEqual(updated_feedback.like, False)
frappe.db.delete("Feedback", {"reference_doctype": "Blog Post"})
diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py
index 4df9ef3132..0021240106 100755
--- a/frappe/core/doctype/file/file.py
+++ b/frappe/core/doctype/file/file.py
@@ -716,13 +716,11 @@ def delete_file(path):
os.remove(path)
-
-
+@frappe.whitelist()
def get_max_file_size():
return cint(conf.get('max_file_size')) or 10485760
-
def has_permission(doc, ptype=None, user=None):
has_access = False
user = user or frappe.session.user
diff --git a/frappe/core/doctype/navbar_settings/navbar_settings.py b/frappe/core/doctype/navbar_settings/navbar_settings.py
index 46eb5c3e7a..c46d0081b6 100644
--- a/frappe/core/doctype/navbar_settings/navbar_settings.py
+++ b/frappe/core/doctype/navbar_settings/navbar_settings.py
@@ -13,6 +13,9 @@ class NavbarSettings(Document):
def validate_standard_navbar_items(self):
doc_before_save = self.get_doc_before_save()
+ if not doc_before_save:
+ return
+
before_save_items = [item for item in \
doc_before_save.help_dropdown + doc_before_save.settings_dropdown if item.is_standard]
@@ -32,7 +35,3 @@ def get_app_logo():
def get_navbar_settings():
navbar_settings = frappe.get_single('Navbar Settings')
return navbar_settings
-
-
-
-
diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py
index be0346d869..266017dd71 100644
--- a/frappe/core/doctype/report/report.py
+++ b/frappe/core/doctype/report/report.py
@@ -51,6 +51,14 @@ class Report(Document):
and not frappe.flags.in_patch):
frappe.throw(_("You are not allowed to delete Standard Report"))
delete_custom_role('report', self.name)
+ self.delete_prepared_reports()
+
+ def delete_prepared_reports(self):
+ prepared_reports = frappe.get_all("Prepared Report", filters={'ref_report_doctype': self.name}, pluck='name')
+
+ for report in prepared_reports:
+ frappe.delete_doc("Prepared Report", report, ignore_missing=True, force=True,
+ delete_permanently=True)
def get_columns(self):
return [d.as_dict(no_default_fields = True) for d in self.columns]
diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py
index 98d2d72fc2..389e18dd4c 100644
--- a/frappe/core/doctype/role/role.py
+++ b/frappe/core/doctype/role/role.py
@@ -2,15 +2,22 @@
# License: MIT. See LICENSE
import frappe
-
from frappe.model.document import Document
desk_properties = ("search_bar", "notifications", "list_sidebar",
"bulk_actions", "view_switcher", "form_sidebar", "timeline", "dashboard")
+STANDARD_ROLES = (
+ "Administrator",
+ "System Manager",
+ "Script Manager",
+ "All",
+ "Guest"
+)
+
class Role(Document):
def before_rename(self, old, new, merge=False):
- if old in ("Guest", "Administrator", "System Manager", "All"):
+ if old in STANDARD_ROLES:
frappe.throw(frappe._("Standard roles cannot be renamed"))
def after_insert(self):
@@ -23,7 +30,7 @@ class Role(Document):
self.set_desk_properties()
def disable_role(self):
- if self.name in ("Guest", "Administrator", "System Manager", "All"):
+ if self.name in STANDARD_ROLES:
frappe.throw(frappe._("Standard roles cannot be disabled"))
else:
self.remove_roles()
diff --git a/frappe/core/doctype/server_script/server_script.js b/frappe/core/doctype/server_script/server_script.js
index dda39115bf..ca34af11ab 100644
--- a/frappe/core/doctype/server_script/server_script.js
+++ b/frappe/core/doctype/server_script/server_script.js
@@ -10,6 +10,13 @@ frappe.ui.form.on('Server Script', {
frm.dashboard.hide();
}
+ if (!frm.is_new()) {
+ frm.add_custom_button(__('Compare Versions'), () => {
+ new frappe.ui.DiffView("Server Script", "script", frm.doc.name);
+ });
+ }
+
+
frm.call('get_autocompletion_items')
.then(r => r.message)
.then(items => {
diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json
index ea31e76a57..cf05ce0c15 100644
--- a/frappe/core/doctype/user/user.json
+++ b/frappe/core/doctype/user/user.json
@@ -599,7 +599,7 @@
"fieldname": "desk_theme",
"fieldtype": "Select",
"label": "Desk Theme",
- "options": "Light\nDark"
+ "options": "Light\nDark\nAutomatic"
},
{
"fieldname": "module_profile",
@@ -669,7 +669,7 @@
}
],
"max_attachments": 5,
- "modified": "2021-10-27 17:17:16.098457",
+ "modified": "2021-11-17 17:17:16.098457",
"modified_by": "Administrator",
"module": "Core",
"name": "User",
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 86fd1cb4a6..b127cf5f0c 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -1046,7 +1046,7 @@ def generate_keys(user):
@frappe.whitelist()
def switch_theme(theme):
- if theme in ["Dark", "Light"]:
+ if theme in ["Dark", "Light", "Automatic"]:
frappe.db.set_value("User", frappe.session.user, "desk_theme", theme)
def get_enabled_users():
diff --git a/frappe/custom/doctype/client_script/client_script.js b/frappe/custom/doctype/client_script/client_script.js
index 27d11af4d1..ad9c9e4e42 100644
--- a/frappe/custom/doctype/client_script/client_script.js
+++ b/frappe/custom/doctype/client_script/client_script.js
@@ -43,6 +43,12 @@ frappe.ui.form.on('Client Script', {
d.show();
});
});
+
+ if (!frm.is_new()) {
+ frm.add_custom_button(__('Compare Versions'), () => {
+ new frappe.ui.DiffView("Client Script", "script", frm.doc.name);
+ });
+ }
}
frm.set_query('dt', {
diff --git a/frappe/database/database.py b/frappe/database/database.py
index a7dd9b6b66..49187f9eaa 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -568,11 +568,10 @@ class Database(object):
def _get_value_for_many_names(self, doctype, names, field, debug=False, run=True):
names = list(filter(None, names))
-
if names:
return self.get_all(doctype,
- fields=['name', field],
- filters=[['name', 'in', names]],
+ fields=field,
+ filters=names,
debug=debug, as_list=1, run=run)
else:
return {}
diff --git a/frappe/desk/doctype/event/event.json b/frappe/desk/doctype/event/event.json
index 5768f00f32..2f67c36fc0 100644
--- a/frappe/desk/doctype/event/event.json
+++ b/frappe/desk/doctype/event/event.json
@@ -53,7 +53,7 @@
},
{
"fieldname": "subject",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"in_global_search": 1,
"in_list_view": 1,
"label": "Subject",
@@ -277,10 +277,11 @@
"icon": "fa fa-calendar",
"idx": 1,
"links": [],
- "modified": "2020-01-14 21:47:15.825287",
+ "modified": "2021-11-18 05:06:24.881742",
"modified_by": "Administrator",
"module": "Desk",
"name": "Event",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py
index d4c185e56f..86f0656bc6 100644
--- a/frappe/desk/doctype/event/event.py
+++ b/frappe/desk/doctype/event/event.py
@@ -11,6 +11,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils.user import get_enabled_system_users
from frappe.desk.reportview import get_filters_cond
+from frappe.desk.doctype.notification_settings.notification_settings import is_email_notifications_enabled_for_type
weekdays = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"]
communication_mapping = {"": "Event", "Event": "Event", "Meeting": "Meeting", "Call": "Phone", "Sent/Received Email": "Email", "Other": "Other"}
@@ -141,7 +142,12 @@ def has_permission(doc, user):
def send_event_digest():
today = nowdate()
- for user in get_enabled_system_users():
+
+ # select only those users that have event reminder email notifications enabled
+ users = [user for user in get_enabled_system_users() if
+ is_email_notifications_enabled_for_type(user.name, 'Event Reminders')]
+
+ for user in users:
events = get_events(today, today, user.name, for_reminder=True)
if events:
frappe.set_user_lang(user.name, user.language)
diff --git a/frappe/desk/doctype/notification_settings/notification_settings.json b/frappe/desk/doctype/notification_settings/notification_settings.json
index fc12022e89..1a6efd5a0d 100644
--- a/frappe/desk/doctype/notification_settings/notification_settings.json
+++ b/frappe/desk/doctype/notification_settings/notification_settings.json
@@ -14,8 +14,11 @@
"enable_email_assignment",
"enable_email_energy_point",
"enable_email_share",
+ "enable_email_event_reminders",
"user",
- "seen"
+ "seen",
+ "system_notifications_section",
+ "energy_points_system_notifications"
],
"fields": [
{
@@ -84,15 +87,34 @@
"fieldtype": "Check",
"hidden": 1,
"label": "Seen"
+ },
+ {
+ "fieldname": "system_notifications_section",
+ "fieldtype": "Section Break",
+ "label": "System Notifications"
+ },
+ {
+ "default": "1",
+ "fieldname": "energy_points_system_notifications",
+ "fieldtype": "Check",
+ "label": "Energy Points"
+ },
+ {
+ "default": "1",
+ "depends_on": "enable_email_notifications",
+ "fieldname": "enable_email_event_reminders",
+ "fieldtype": "Check",
+ "label": "Event Reminders"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-11-04 12:54:57.989317",
+ "modified": "2021-11-24 14:45:31.931154",
"modified_by": "Administrator",
"module": "Desk",
"name": "Notification Settings",
+ "naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/desk/doctype/notification_settings/test_notification_settings.py b/frappe/desk/doctype/notification_settings/test_notification_settings.py
new file mode 100644
index 0000000000..e3dac0af5f
--- /dev/null
+++ b/frappe/desk/doctype/notification_settings/test_notification_settings.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+class TestNotificationSettings(unittest.TestCase):
+ pass
diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py
old mode 100755
new mode 100644
index a118240488..7c0e2dfe87
--- a/frappe/email/doctype/newsletter/newsletter.py
+++ b/frappe/email/doctype/newsletter/newsletter.py
@@ -159,11 +159,10 @@ class Newsletter(WebsiteGenerator):
def send_newsletter(self, emails: List[str]):
"""Trigger email generation for `emails` and add it in Email Queue.
"""
- # TODO: get rid of this maybe?
- message = self.get_message()
attachments = self.get_newsletter_attachments()
sender = self.send_from or frappe.utils.get_formatted_email(self.owner)
- args = {"message": message, "name": self.name}
+ args = self.as_dict()
+ args["message"] = self.get_message()
is_auto_commit_set = bool(frappe.db.auto_commit_on_many_writes)
frappe.db.auto_commit_on_many_writes = not frappe.flags.in_test
@@ -172,7 +171,6 @@ class Newsletter(WebsiteGenerator):
subject=self.subject,
sender=sender,
recipients=emails,
- message=message,
attachments=attachments,
template="newsletter",
add_unsubscribe_link=self.send_unsubscribe_link,
diff --git a/frappe/installer.py b/frappe/installer.py
index d1a13fdaab..9eed44ea15 100755
--- a/frappe/installer.py
+++ b/frappe/installer.py
@@ -240,6 +240,7 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
if not dry_run:
remove_from_installed_apps(app_name)
+ frappe.get_single('Installed Applications').update_versions()
frappe.db.commit()
click.secho(f"Uninstalled App {app_name} from Site {site}", fg="green")
diff --git a/frappe/model/naming.py b/frappe/model/naming.py
index deea6698b3..f3d68f3715 100644
--- a/frappe/model/naming.py
+++ b/frappe/model/naming.py
@@ -175,6 +175,8 @@ def parse_naming_series(parts, doctype='', doc=''):
part = today.strftime("%d")
elif e == 'YYYY':
part = today.strftime('%Y')
+ elif e == 'WW':
+ part = determine_consecutive_week_number(today)
elif e == 'timestamp':
part = str(today)
elif e == 'FY':
@@ -193,6 +195,19 @@ def parse_naming_series(parts, doctype='', doc=''):
return n
+def determine_consecutive_week_number(datetime):
+ """Determines the consecutive calendar week"""
+ m = datetime.month
+ # ISO 8601 calandar week
+ w = datetime.strftime('%V')
+ # Ensure consecutiveness for the first and last days of a year
+ if m == 1 and int(w) >= 52:
+ w = '00'
+ elif m == 12 and int(w) <= 1:
+ w = '53'
+ return w
+
+
def getseries(key, digits):
# series created ?
# Using frappe.qb as frappe.get_values does not allow order_by=None
diff --git a/frappe/patches/v13_0/rename_custom_client_script.py b/frappe/patches/v13_0/rename_custom_client_script.py
index 718f1f6a46..b74c518aeb 100644
--- a/frappe/patches/v13_0/rename_custom_client_script.py
+++ b/frappe/patches/v13_0/rename_custom_client_script.py
@@ -1,9 +1,13 @@
import frappe
+from frappe.model.rename_doc import rename_doc
def execute():
if frappe.db.exists("DocType", "Client Script"):
return
- frappe.rename_doc("DocType", "Custom Script", "Client Script")
+ frappe.flags.ignore_route_conflict_validation = True
+ rename_doc("DocType", "Custom Script", "Client Script")
+ frappe.flags.ignore_route_conflict_validation = False
+
frappe.reload_doctype("Client Script", force=True)
diff --git a/frappe/printing/doctype/print_settings/print_settings.json b/frappe/printing/doctype/print_settings/print_settings.json
index babbae248d..f45de7637d 100644
--- a/frappe/printing/doctype/print_settings/print_settings.json
+++ b/frappe/printing/doctype/print_settings/print_settings.json
@@ -10,6 +10,8 @@
"repeat_header_footer",
"column_break_4",
"pdf_page_size",
+ "pdf_page_height",
+ "pdf_page_width",
"view_link_in_email",
"with_letterhead",
"allow_print_for_draft",
@@ -56,7 +58,7 @@
"fieldname": "pdf_page_size",
"fieldtype": "Select",
"label": "PDF Page Size",
- "options": "A4\nLetter"
+ "options": "A0\nA1\nA2\nA3\nA4\nA5\nA6\nA7\nA8\nA9\nB0\nB1\nB2\nB3\nB4\nB5\nB6\nB7\nB8\nB9\nB10\nC5E\nComm10E\nDLE\nExecutive\nFolio\nLedger\nLegal\nLetter\nTabloid\nCustom"
},
{
"fieldname": "view_link_in_email",
@@ -156,6 +158,18 @@
"fieldname": "font_size",
"fieldtype": "Float",
"label": "Font Size"
+ },
+ {
+ "depends_on": "eval:doc.pdf_page_size == \"Custom\"",
+ "fieldname": "pdf_page_height",
+ "fieldtype": "Float",
+ "label": "PDF Page Height (in mm)"
+ },
+ {
+ "depends_on": "eval:doc.pdf_page_size == \"Custom\"",
+ "fieldname": "pdf_page_width",
+ "fieldtype": "Float",
+ "label": "PDF Page Width (in mm)"
}
],
"icon": "fa fa-cog",
diff --git a/frappe/printing/doctype/print_settings/print_settings.py b/frappe/printing/doctype/print_settings/print_settings.py
index ff00317cf8..3253cea2dc 100644
--- a/frappe/printing/doctype/print_settings/print_settings.py
+++ b/frappe/printing/doctype/print_settings/print_settings.py
@@ -8,14 +8,23 @@ from frappe.utils import cint
from frappe.model.document import Document
+
class PrintSettings(Document):
+ def validate(self):
+ if self.pdf_page_size == "Custom" and not (
+ self.pdf_page_height and self.pdf_page_width
+ ):
+ frappe.throw(_("Page height and width cannot be zero"))
+
def on_update(self):
frappe.clear_cache()
+
@frappe.whitelist()
def is_print_server_enabled():
- if not hasattr(frappe.local, 'enable_print_server'):
- frappe.local.enable_print_server = cint(frappe.db.get_single_value('Print Settings',
- 'enable_print_server'))
+ if not hasattr(frappe.local, "enable_print_server"):
+ frappe.local.enable_print_server = cint(
+ frappe.db.get_single_value("Print Settings", "enable_print_server")
+ )
return frappe.local.enable_print_server
diff --git a/frappe/public/images/ui-states/404.png b/frappe/public/images/ui-states/404.png
new file mode 100644
index 0000000000..1cbf7eeee0
Binary files /dev/null and b/frappe/public/images/ui-states/404.png differ
diff --git a/frappe/public/js/desk.bundle.js b/frappe/public/js/desk.bundle.js
index 50947bd9bc..677b012efe 100644
--- a/frappe/public/js/desk.bundle.js
+++ b/frappe/public/js/desk.bundle.js
@@ -62,6 +62,7 @@ import "./frappe/utils/help_links.js";
import "./frappe/utils/address_and_contact.js";
import "./frappe/utils/preview_email.js";
import "./frappe/utils/file_manager.js";
+import "./frappe/utils/diffview";
import "./frappe/upload.js";
import "./frappe/ui/tree.js";
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index a53368d67a..2855c6ae7c 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -64,6 +64,19 @@ frappe.Application = class Application {
}
});
+ frappe.ui.add_system_theme_switch_listener();
+ const root = document.documentElement;
+
+ const observer = new MutationObserver(() => {
+ frappe.ui.set_theme();
+ });
+ observer.observe(root, {
+ attributes: true,
+ attributeFilter: ['data-theme-mode']
+ });
+
+ frappe.ui.set_theme();
+
// page container
this.make_page_container();
this.set_route();
diff --git a/frappe/public/js/frappe/file_uploader/FilePreview.vue b/frappe/public/js/frappe/file_uploader/FilePreview.vue
index 43dbacb17d..5972a975f2 100644
--- a/frappe/public/js/frappe/file_uploader/FilePreview.vue
+++ b/frappe/public/js/frappe/file_uploader/FilePreview.vue
@@ -29,21 +29,26 @@
Optimize
+
+
+ {{ file.error_message }}
+
+
@@ -89,18 +94,18 @@ export default {
return this.file.doc ? this.file.doc.is_private : this.file.private;
},
uploaded() {
- return this.file.total && this.file.total === this.file.progress && !this.file.failed;
+ return this.file.request_succeeded;
},
is_image() {
return this.file.file_obj.type.startsWith('image');
},
is_optimizable() {
let is_svg = this.file.file_obj.type == 'image/svg+xml';
- return this.is_image && !is_svg;
+ return this.is_image && !is_svg && !this.uploaded && !this.file.failed;
},
is_cropable() {
let croppable_types = ['image/jpeg', 'image/png'];
- return !this.uploaded && !this.file.uploading && croppable_types.includes(this.file.file_obj.type);
+ return !this.uploaded && !this.file.uploading && !this.file.failed && croppable_types.includes(this.file.file_obj.type);
},
progress() {
let value = Math.round((this.file.progress * 100) / this.file.total);
@@ -208,4 +213,9 @@ export default {
align-items: center;
padding-top: 0.25rem;
}
+
+.file-error {
+ font-size: var(--text-sm);
+ font-weight: var(--text-bold);
+}
diff --git a/frappe/public/js/frappe/file_uploader/FileUploader.vue b/frappe/public/js/frappe/file_uploader/FileUploader.vue
index 90aa545941..167b4955fa 100644
--- a/frappe/public/js/frappe/file_uploader/FileUploader.vue
+++ b/frappe/public/js/frappe/file_uploader/FileUploader.vue
@@ -197,6 +197,7 @@ export default {
show_image_cropper: false,
crop_image_with_index: -1,
trigger_upload: false,
+ close_dialog: false,
hide_dialog_footer: false,
allow_take_photo: false,
allow_web_link: true,
@@ -218,6 +219,12 @@ export default {
}
});
}
+ if (this.restrictions.max_file_size == null) {
+ frappe.call('frappe.core.doctype.file.file.get_max_file_size')
+ .then(res => {
+ this.restrictions.max_file_size = Number(res.message);
+ });
+ }
},
watch: {
files(newvalue, oldvalue) {
@@ -289,6 +296,8 @@ export default {
progress: 0,
total: 0,
failed: false,
+ request_succeeded: false,
+ error_message: null,
uploading: false,
private: !is_image
}
@@ -329,9 +338,17 @@ export default {
if (!is_correct_type) {
console.warn('File skipped because of invalid file type', file);
+ frappe.show_alert({
+ message: __('File "{0}" was skipped because of invalid file type', [file.name]),
+ indicator: 'orange'
+ });
}
if (!valid_file_size) {
console.warn('File skipped because of invalid file size', file.size, file);
+ frappe.show_alert({
+ message: __('File "{0}" was skipped because size exceeds {1} MB', [file.name, max_file_size / (1024 * 1024)]),
+ indicator: 'orange'
+ });
}
return is_correct_type && valid_file_size;
@@ -357,9 +374,10 @@ export default {
let selected_file = this.$refs.file_browser.selected_node;
if (!selected_file.value) {
frappe.msgprint(__('Click on a file to select it.'));
+ this.close_dialog = true;
return Promise.reject();
}
-
+ this.close_dialog = true;
return this.upload_file({
file_url: selected_file.file_url
});
@@ -368,9 +386,11 @@ export default {
let file_url = this.$refs.web_link.url;
if (!file_url) {
frappe.msgprint(__('Invalid URL'));
+ this.close_dialog = true;
return Promise.reject();
}
file_url = decodeURI(file_url)
+ this.close_dialog = true;
return this.upload_file({
file_url
});
@@ -383,6 +403,7 @@ export default {
this.on_success && this.on_success(file);
})
);
+ this.close_dialog = true;
return Promise.all(promises);
},
upload_file(file, i) {
@@ -410,6 +431,7 @@ export default {
xhr.onreadystatechange = () => {
if (xhr.readyState == XMLHttpRequest.DONE) {
if (xhr.status === 200) {
+ file.request_succeeded = true;
let r = null;
let file_doc = null;
try {
@@ -426,15 +448,24 @@ export default {
if (this.on_success) {
this.on_success(file_doc, r);
}
+
+ if (i == this.files.length - 1 && this.files.every(file => file.request_succeeded)) {
+ this.close_dialog = true;
+ }
+
} else if (xhr.status === 403) {
+ file.failed = true;
let response = JSON.parse(xhr.responseText);
- frappe.msgprint({
- title: __('Not permitted'),
- indicator: 'red',
- message: response._error_message
- });
+ file.error_message = `Not permitted. ${response._error_message || ''}`;
+
+ } else if (xhr.status === 413) {
+ file.failed = true;
+ file.error_message = 'Size exceeds the maximum allowed file size.';
+
} else {
file.failed = true;
+ file.error_message = xhr.status === 0 ? 'XMLHttpRequest Error' : `${xhr.status} : ${xhr.statusText}`;
+
let error = null;
try {
error = JSON.parse(xhr.responseText);
diff --git a/frappe/public/js/frappe/file_uploader/index.js b/frappe/public/js/frappe/file_uploader/index.js
index 87bc1c8ec8..ec90b19a1a 100644
--- a/frappe/public/js/frappe/file_uploader/index.js
+++ b/frappe/public/js/frappe/file_uploader/index.js
@@ -67,6 +67,12 @@ export default class FileUploader {
}
});
+ this.uploader.$watch('close_dialog', (close_dialog) => {
+ if (close_dialog) {
+ this.dialog && this.dialog.hide();
+ }
+ });
+
this.uploader.$watch('hide_dialog_footer', (hide_dialog_footer) => {
if (hide_dialog_footer) {
this.dialog && this.dialog.footer.addClass('hide');
@@ -84,10 +90,8 @@ export default class FileUploader {
upload_files() {
this.dialog && this.dialog.get_primary_btn().prop('disabled', true);
- return this.uploader.upload_files()
- .then(() => {
- this.dialog && this.dialog.hide();
- });
+ this.dialog && this.dialog.get_secondary_btn().prop('disabled', true);
+ return this.uploader.upload_files();
}
make_dialog() {
diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js
index 080a1cbb48..280eac3941 100644
--- a/frappe/public/js/frappe/form/controls/geolocation.js
+++ b/frappe/public/js/frappe/form/controls/geolocation.js
@@ -3,6 +3,11 @@ frappe.provide('frappe.utils.utils');
frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.form.ControlData {
static horizontal = false
+ async make() {
+ await frappe.require(this.required_libs);
+ super.make();
+ }
+
make_wrapper() {
// Create the elements for map area
super.make_wrapper();
@@ -196,4 +201,17 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
this.editableLayers.removeLayer(l);
});
}
+
+ get required_libs() {
+ return [
+ "assets/frappe/js/lib/leaflet/easy-button.css",
+ "assets/frappe/js/lib/leaflet/L.Control.Locate.css",
+ "assets/frappe/js/lib/leaflet/leaflet.draw.css",
+ "assets/frappe/js/lib/leaflet/leaflet.css",
+ "assets/frappe/js/lib/leaflet/leaflet.js",
+ "assets/frappe/js/lib/leaflet/easy-button.js",
+ "assets/frappe/js/lib/leaflet/leaflet.draw.js",
+ "assets/frappe/js/lib/leaflet/L.Control.Locate.js",
+ ];
+ }
};
diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js
index 9d5e7cbe09..df4dbf09e7 100644
--- a/frappe/public/js/frappe/form/dashboard.js
+++ b/frappe/public/js/frappe/form/dashboard.js
@@ -14,6 +14,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
this.progress_area = this.make_section({
css_class: 'progress-area',
hidden: 1,
+ collapsible: 1,
is_dashboard_section: 1,
});
@@ -21,6 +22,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
label: __("Overview"),
css_class: 'form-heatmap',
hidden: 1,
+ collapsible: 1,
is_dashboard_section: 1,
body_html: `
@@ -32,6 +34,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
label: __("Graph"),
css_class: 'form-graph',
hidden: 1,
+ collapsible: 1,
is_dashboard_section: 1
});
@@ -40,6 +43,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
label: __("Stats"),
css_class: 'form-stats',
hidden: 1,
+ collapsible: 1,
is_dashboard_section: 1,
body_html: this.stats_area_row
});
@@ -50,6 +54,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
label: __("Connections"),
css_class: 'form-links',
hidden: 1,
+ collapsible: 1,
is_dashboard_section: 1,
body_html: this.transactions_area
});
@@ -84,9 +89,10 @@ frappe.ui.form.Dashboard = class FormDashboard {
hidden,
body_html,
make_card: true,
+ collapsible: 1,
is_dashboard_section: 1
};
- return new Section(this.frm.layout.wrapper, options).body;
+ return new Section(this.parent, options).body;
}
add_progress(title, percent, message) {
@@ -203,7 +209,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
after_refresh() {
// show / hide new buttons (if allowed)
this.links_area.body.find('.btn-new').each((i, el) => {
- if (this.frm.can_create($(this).attr('data-doctype'))) {
+ if (this.frm.can_create($(el).attr('data-doctype'))) {
$(el).removeClass('hidden');
}
});
diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js
index 75d68b12db..27281d8927 100644
--- a/frappe/public/js/frappe/form/form.js
+++ b/frappe/public/js/frappe/form/form.js
@@ -156,8 +156,11 @@ frappe.ui.form.Form = class FrappeForm {
let dashboard_parent = $('');
- let main_page = this.layout.tabs.length ? this.layout.tabs[0].wrapper : this.layout.wrapper;
- main_page.prepend(dashboard_parent);
+ if (this.layout.tabs.length) {
+ this.layout.tabs[0].wrapper.prepend(dashboard_parent);
+ } else {
+ dashboard_parent.insertAfter(this.layout.wrapper.find('.form-message'));
+ }
this.dashboard = new frappe.ui.form.Dashboard(dashboard_parent, this);
this.tour = new frappe.ui.form.FormTour({
diff --git a/frappe/public/js/frappe/form/grid_pagination.js b/frappe/public/js/frappe/form/grid_pagination.js
index 35daafe89d..76a5f7b50b 100644
--- a/frappe/public/js/frappe/form/grid_pagination.js
+++ b/frappe/public/js/frappe/form/grid_pagination.js
@@ -46,8 +46,55 @@ export default class GridPagination {
this.last_page_button.on('click', () => {
this.go_to_page(this.total_pages);
});
+
+ this.$page_number.on('keyup', (e) => {
+ e.currentTarget.style.width = ((e.currentTarget.value.length + 1) * 8) + 'px';
+ });
+
+ this.$page_number.on('keydown', (e) => {
+ e = (e) ? e : window.event;
+ var charCode = (e.which) ? e.which : e.keyCode;
+ let arrow = { up: 38, down: 40 };
+
+ switch (charCode) {
+ case arrow.up:
+ this.inc_dec_number(true);
+ break;
+ case arrow.down:
+ this.inc_dec_number(false);
+ break;
+ }
+
+ // only allow numbers from 0-9 and up, down, left, right arrow keys
+ if (charCode > 31 && (charCode < 48 || charCode > 57) &&
+ ![37, 38, 39, 40].includes(charCode)) {
+ return false;
+ }
+ return true;
+ });
+
+ this.$page_number.on('focusout', (e) => {
+ if (this.page_index == e.currentTarget.value) return;
+ this.page_index = e.currentTarget.value;
+
+ if (this.page_index < 1) {
+ this.page_index = 1;
+ } else if (this.page_index > this.total_pages) {
+ this.page_index = this.total_pages;
+ }
+
+ this.go_to_page();
+ });
}
+ inc_dec_number(increment) {
+ let new_value = parseInt(this.$page_number.val());
+ increment ? new_value++ : new_value--;
+
+ if (new_value < 1 || new_value > this.total_pages) return;
+
+ this.$page_number.val(new_value);
+ }
update_page_numbers() {
let total_pages = Math.ceil(this.grid.data.length/this.page_length);
@@ -65,7 +112,7 @@ export default class GridPagination {
get_pagination_html() {
let page_text_html = `
- ${__(this.page_index)}
+
${__('of')}
${__(this.total_pages)}
`;
@@ -104,7 +151,8 @@ export default class GridPagination {
let $rows = $(this.grid.parent).find(".rows").empty();
this.grid.render_result_rows($rows, true);
if (this.$page_number) {
- this.$page_number.text(index);
+ this.$page_number.val(index);
+ this.$page_number.css('width', ((index.toString().length + 1) * 8) + 'px');
}
this.update_page_numbers();
diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js
index 7710c82ee7..0de6b1db0d 100644
--- a/frappe/public/js/frappe/form/layout.js
+++ b/frappe/public/js/frappe/form/layout.js
@@ -245,7 +245,7 @@ frappe.ui.form.Layout = class Layout {
}
make_section(df) {
- this.section = new Section(this.current_tab ? this.current_tab.wrapper : this.page, df, this.card_layout);
+ this.section = new Section(this.current_tab ? this.current_tab.wrapper : this.page, df, this.card_layout, this);
// append to layout fields
if (df) {
diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js
index 2cf2ac38a9..e412b1dec8 100644
--- a/frappe/public/js/frappe/form/quick_entry.js
+++ b/frappe/public/js/frappe/form/quick_entry.js
@@ -267,7 +267,7 @@ frappe.ui.form.QuickEntryForm = class QuickEntryForm {
render_edit_in_full_page_link() {
var me = this;
this.dialog.add_custom_action(
- `${frappe.utils.icon('edit', 'xs')} ${__("Edit in full page")}`,
+ `${__("Edit in full page")}`,
() => me.open_doc(true)
);
}
diff --git a/frappe/public/js/frappe/form/section.js b/frappe/public/js/frappe/form/section.js
index e0120f6afc..b0ec491ce6 100644
--- a/frappe/public/js/frappe/form/section.js
+++ b/frappe/public/js/frappe/form/section.js
@@ -1,5 +1,6 @@
export default class Section {
- constructor(parent, df, card_layout) {
+ constructor(parent, df, card_layout, layout) {
+ this.layout = layout;
this.card_layout = card_layout;
this.parent = parent;
this.df = df || {};
@@ -25,6 +26,7 @@ export default class Section {
${this.df.is_dashboard_section ? "form-dashboard-section" : "form-section"}
${ make_card ? "card-section" : "" }">
`).appendTo(this.parent);
+ this.layout && this.layout.sections.push(this);
if (this.df) {
if (this.df.label) {
diff --git a/frappe/public/js/frappe/list/bulk_operations.js b/frappe/public/js/frappe/list/bulk_operations.js
index ee6e6d753c..94ec9d4e67 100644
--- a/frappe/public/js/frappe/list/bulk_operations.js
+++ b/frappe/public/js/frappe/list/bulk_operations.js
@@ -24,51 +24,84 @@ export default class BulkOperations {
return;
}
- if (valid_docs.length > 0) {
- const dialog = new frappe.ui.Dialog({
- title: __('Print Documents'),
- fields: [
- {
- 'fieldtype': 'Select',
- 'label': __('Letter Head'),
- 'fieldname': 'letter_sel',
- 'default': __('No Letterhead'),
- options: this.get_letterhead_options()
- },
- {
- 'fieldtype': 'Select',
- 'label': __('Print Format'),
- 'fieldname': 'print_sel',
- options: frappe.meta.get_print_formats(this.doctype)
- }
- ]
- });
-
- dialog.set_primary_action(__('Print'), args => {
- if (!args) return;
- const default_print_format = frappe.get_meta(this.doctype).default_print_format;
- const with_letterhead = args.letter_sel == __("No Letterhead") ? 0 : 1;
- const print_format = args.print_sel ? args.print_sel : default_print_format;
- const json_string = JSON.stringify(valid_docs);
- const letterhead = args.letter_sel;
- const w = window.open('/api/method/frappe.utils.print_format.download_multi_pdf?' +
- 'doctype=' + encodeURIComponent(this.doctype) +
- '&name=' + encodeURIComponent(json_string) +
- '&format=' + encodeURIComponent(print_format) +
- '&no_letterhead=' + (with_letterhead ? '0' : '1') +
- '&letterhead=' + encodeURIComponent(letterhead)
- );
-
- if (!w) {
- frappe.msgprint(__('Please enable pop-ups'));
- return;
- }
- });
-
- dialog.show();
- } else {
+ if (valid_docs.length === 0) {
frappe.msgprint(__('Select atleast 1 record for printing'));
+ return;
}
+
+ const dialog = new frappe.ui.Dialog({
+ title: __('Print Documents'),
+ fields: [{
+ fieldtype: 'Select',
+ label: __('Letter Head'),
+ fieldname: 'letter_sel',
+ default: __('No Letterhead'),
+ options: this.get_letterhead_options()
+ },
+ {
+ fieldtype: 'Select',
+ label: __('Print Format'),
+ fieldname: 'print_sel',
+ options: frappe.meta.get_print_formats(this.doctype)
+ },
+ {
+ fieldtype: 'Select',
+ label: __('Page Size'),
+ fieldname: 'page_size',
+ options: frappe.meta.get_print_sizes(),
+ default: print_settings.pdf_page_size
+ },
+ {
+ fieldtype: 'Float',
+ label: __('Page Height (in mm)'),
+ fieldname: 'page_height',
+ depends_on: 'eval:doc.page_size == "Custom"',
+ default: print_settings.pdf_page_height
+ },
+ {
+ fieldtype: 'Float',
+ label: __('Page Width (in mm)'),
+ fieldname: 'page_width',
+ depends_on: 'eval:doc.page_size == "Custom"',
+ default: print_settings.pdf_page_width
+ }]
+ });
+
+ dialog.set_primary_action(__('Print'), args => {
+ if (!args) return;
+ const default_print_format = frappe.get_meta(this.doctype).default_print_format;
+ const with_letterhead = args.letter_sel == __("No Letterhead") ? 0 : 1;
+ const print_format = args.print_sel ? args.print_sel : default_print_format;
+ const json_string = JSON.stringify(valid_docs);
+ const letterhead = args.letter_sel;
+
+ let pdf_options;
+ if (args.page_size === "Custom") {
+ if (args.page_height === 0 || args.page_width === 0) {
+ frappe.throw(__('Page height and width cannot be zero'));
+ }
+ pdf_options = JSON.stringify({ "page-height": args.page_height, "page-width": args.page_width });
+ } else {
+ pdf_options = JSON.stringify({ "page-size": args.page_size });
+ }
+
+ const w = window.open(
+ '/api/method/frappe.utils.print_format.download_multi_pdf?' +
+ 'doctype=' + encodeURIComponent(this.doctype) +
+ '&name=' + encodeURIComponent(json_string) +
+ '&format=' + encodeURIComponent(print_format) +
+ '&no_letterhead=' + (with_letterhead ? '0' : '1') +
+ '&letterhead=' + encodeURIComponent(letterhead) +
+ '&options=' + encodeURIComponent(pdf_options)
+ );
+
+ if (!w) {
+ frappe.msgprint(__('Please enable pop-ups'));
+ return;
+ }
+ });
+
+ dialog.show();
}
get_letterhead_options () {
diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js
index 07c8acef27..64530e15ef 100644
--- a/frappe/public/js/frappe/list/list_view.js
+++ b/frappe/public/js/frappe/list/list_view.js
@@ -307,6 +307,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}
update_checkbox(target) {
+ if (!this.$checkbox_actions) return;
+
let $check_all_checkbox = this.$checkbox_actions.find(".list-check-all");
if ($check_all_checkbox.prop("checked") && target && !target.prop("checked")) {
diff --git a/frappe/public/js/frappe/model/meta.js b/frappe/public/js/frappe/model/meta.js
index 6ee9084adc..3c9ddc4d96 100644
--- a/frappe/public/js/frappe/model/meta.js
+++ b/frappe/public/js/frappe/model/meta.js
@@ -192,6 +192,15 @@ $.extend(frappe.meta, {
}
},
+ get_print_sizes: function() {
+ return [
+ "A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9",
+ "B0", "B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", "B9", "B10",
+ "C5E", "Comm10E", "DLE", "Executive", "Folio", "Ledger", "Legal",
+ "Letter", "Tabloid", "Custom"
+ ];
+ },
+
get_print_formats: function(doctype) {
var print_format_list = ["Standard"];
var default_print_format = locals.DocType[doctype].default_print_format;
diff --git a/frappe/public/js/frappe/ui/theme_switcher.js b/frappe/public/js/frappe/ui/theme_switcher.js
index 4524472415..2c1d93a2ec 100644
--- a/frappe/public/js/frappe/ui/theme_switcher.js
+++ b/frappe/public/js/frappe/ui/theme_switcher.js
@@ -42,7 +42,7 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
}
refresh() {
- this.current_theme = document.documentElement.getAttribute("data-theme") || "light";
+ this.current_theme = document.documentElement.getAttribute("data-theme-mode") || "light";
this.fetch_themes().then(() => {
this.render();
});
@@ -54,10 +54,17 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
{
name: "light",
label: __("Frappe Light"),
+ info: __("Light Theme")
},
{
name: "dark",
label: __("Timeless Night"),
+ info: __("Dark Theme")
+ },
+ {
+ name: "automatic",
+ label: __("Automatic"),
+ info: __("Uses system's theme to switch between light and dark mode")
}
];
@@ -74,11 +81,15 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
}
get_preview_html(theme) {
+ const is_auto_theme = theme.name === "automatic";
const preview = $(`
-
+
-
${frappe.utils.icon('tick', 'xs')}
+
+ ${frappe.utils.icon('tick', 'xs')}
+
@@ -112,13 +123,14 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
toggle_theme(theme) {
this.current_theme = theme.toLowerCase();
- document.documentElement.setAttribute("data-theme", this.current_theme);
+ document.documentElement.setAttribute("data-theme-mode", this.current_theme);
frappe.show_alert("Theme Changed", 3);
frappe.xcall("frappe.core.doctype.user.user.switch_theme", {
theme: toTitle(theme)
});
}
+
show() {
this.dialog.show();
}
@@ -127,3 +139,22 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
this.dialog.hide();
}
};
+
+frappe.ui.add_system_theme_switch_listener = () => {
+ frappe.ui.dark_theme_media_query.addEventListener('change', () => {
+ frappe.ui.set_theme();
+ });
+};
+
+frappe.ui.dark_theme_media_query = window.matchMedia("(prefers-color-scheme: dark)");
+
+frappe.ui.set_theme = (theme) => {
+ const root = document.documentElement;
+ let theme_mode = root.getAttribute("data-theme-mode");
+ if (!theme) {
+ if (theme_mode === "automatic") {
+ theme = frappe.ui.dark_theme_media_query.matches ? 'dark' : 'light';
+ }
+ }
+ root.setAttribute("data-theme", theme || theme_mode);
+};
\ No newline at end of file
diff --git a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js
index 6d1d7228e3..502837bcd7 100644
--- a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js
+++ b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js
@@ -305,7 +305,7 @@ frappe.search.AwesomeBar = class AwesomeBar {
index: 80,
default: "Calculator",
onclick: function() {
- frappe.msgprint(formatted_value, "Result");
+ frappe.msgprint(formatted_value, __("Result"));
}
});
} catch(e) {
@@ -317,10 +317,10 @@ frappe.search.AwesomeBar = class AwesomeBar {
make_random(txt) {
if(txt.toLowerCase().includes('random')) {
this.options.push({
- label: "Generate Random Password",
+ label: __("Generate Random Password"),
value: frappe.utils.get_random(16),
onclick: function() {
- frappe.msgprint(frappe.utils.get_random(16), "Result");
+ frappe.msgprint(frappe.utils.get_random(16), __("Result"));
}
})
}
diff --git a/frappe/public/js/frappe/utils/diffview.js b/frappe/public/js/frappe/utils/diffview.js
new file mode 100644
index 0000000000..ebface7f05
--- /dev/null
+++ b/frappe/public/js/frappe/utils/diffview.js
@@ -0,0 +1,100 @@
+frappe.provide("frappe.ui");
+
+frappe.ui.DiffView = class DiffView {
+ constructor(doctype, fieldname, docname) {
+ this.dialog = null;
+ this.handler = null;
+ this.doctype = doctype;
+ this.fieldname = fieldname;
+ this.docname = docname;
+
+ this.dialog = this.make_dialog();
+ this.set_empty_state();
+ this.dialog.show();
+ }
+
+ make_dialog() {
+ const get_query = () => ({
+ query: "frappe.utils.diff.version_query",
+ filters: { docname: this.docname, ref_doctype: this.doctype },
+ });
+ const onchange = () => this.compute_diff();
+ let dialog = new frappe.ui.Dialog({
+ title: __("Compare Versions"),
+ fields: [
+ {
+ label: __("From version"),
+ fieldtype: "Link",
+ fieldname: "from_version",
+ options: "Version",
+ reqd: 1,
+ get_query,
+ onchange,
+ },
+ {
+ fieldtype: "Column Break",
+ fieldname: "cb",
+ },
+ {
+ label: __("To version"),
+ fieldtype: "Link",
+ fieldname: "to_version",
+ options: "Version",
+ reqd: 1,
+ get_query,
+ onchange,
+ },
+ {
+ fieldtype: "Section Break",
+ fieldname: "sb",
+ },
+ {
+ label: __("Diff"),
+ fieldtype: "HTML",
+ fieldname: "diff",
+ },
+ ],
+ size: "large",
+ });
+ return dialog;
+ }
+
+ compute_diff() {
+ const from_version = this.dialog.get_value("from_version");
+ const to_version = this.dialog.get_value("to_version");
+ const fieldname = this.fieldname;
+
+ if (from_version && to_version) {
+ frappe
+ .xcall("frappe.utils.diff.get_version_diff", {
+ from_version,
+ to_version,
+ fieldname,
+ })
+ .then((data) => {
+ this.dialog.set_value("diff", this.prettify_diff(data));
+ });
+ } else {
+ this.set_empty_state();
+ }
+ }
+
+ prettify_diff(diff) {
+ let html = ``;
+
+ diff.forEach((line) => {
+ let line_class = "";
+ if (line.startsWith("+")) {
+ line_class = "insert";
+ } else if (line.startsWith("-")) {
+ line_class = "delete";
+ }
+ html += `
${line}
`;
+ });
+ return `
${html}
`;
+ }
+
+ set_empty_state() {
+ this.dialog.set_value("diff", __("Select two versions to view the diff."));
+ }
+};
diff --git a/frappe/public/js/frappe/utils/number_format.js b/frappe/public/js/frappe/utils/number_format.js
index 32e3669caf..b0d66ccec5 100644
--- a/frappe/public/js/frappe/utils/number_format.js
+++ b/frappe/public/js/frappe/utils/number_format.js
@@ -129,7 +129,7 @@ function format_currency(v, currency, decimals) {
}
if (symbol)
- return symbol + " " + format_number(v, format, decimals);
+ return __(symbol) + " " + format_number(v, format, decimals);
else
return format_number(v, format, decimals);
}
diff --git a/frappe/public/js/frappe/utils/user.js b/frappe/public/js/frappe/utils/user.js
index aeeb83f630..3eb73b21e5 100644
--- a/frappe/public/js/frappe/utils/user.js
+++ b/frappe/public/js/frappe/utils/user.js
@@ -12,7 +12,7 @@ frappe.user_info = function(uid) {
if(!(frappe.boot.user_info && frappe.boot.user_info[uid])) {
var user_info = {
- fullname: frappe.utils.capitalize(uid.split("@")[0]) || "Unknown"
+ fullname: frappe.utils.to_title_case(uid.split("@")[0]) || "Unknown"
};
} else {
var user_info = frappe.boot.user_info[uid];
diff --git a/frappe/public/js/frappe/views/container.js b/frappe/public/js/frappe/views/container.js
index 126feea16e..cf1d6c9466 100644
--- a/frappe/public/js/frappe/views/container.js
+++ b/frappe/public/js/frappe/views/container.js
@@ -42,7 +42,6 @@ frappe.views.Container = class Container {
cur_page = this;
if(this.page && this.page.label === label) {
$(this.page).trigger('show');
- return;
}
var me = this;
diff --git a/frappe/public/js/frappe/views/interaction.js b/frappe/public/js/frappe/views/interaction.js
index 119eba13fb..a1f5947e11 100644
--- a/frappe/public/js/frappe/views/interaction.js
+++ b/frappe/public/js/frappe/views/interaction.js
@@ -224,6 +224,9 @@ frappe.views.InteractionComposer = class InteractionComposer {
if (!("owner" in interaction_values)){
interaction_values["owner"] = frappe.session.user;
}
+ if (!("assigned_by" in interaction_values) && interaction_values["doctype"] == "ToDo") {
+ interaction_values["assigned_by"] = frappe.session.user;
+ }
return frappe.call({
method:"frappe.client.insert",
args: { doc: interaction_values},
diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js
index 04cc1b9880..448b3f6fd2 100644
--- a/frappe/public/js/frappe/views/reports/query_report.js
+++ b/frappe/public/js/frappe/views/reports/query_report.js
@@ -634,6 +634,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
this.render_datatable();
this.add_chart_buttons_to_toolbar(true);
this.add_card_button_to_toolbar();
+ this.$report.show();
} else {
this.data = [];
this.toggle_nothing_to_show(true);
@@ -882,7 +883,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
hide_loading_screen() {
this.$loading.hide();
- this.$report.show();
}
get_chart_options(data) {
@@ -1789,6 +1789,19 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
this.$chart.toggle(flag);
this.$summary.toggle(flag);
}
+
+ get_checked_items(only_docnames) {
+ const indexes = this.datatable.rowmanager.getCheckedRows();
+
+ return indexes.reduce((items, i) => {
+ if (i === undefined) return items;
+
+ const item = this.data[i];
+ items.push(only_docnames ? item.name : item);
+ return items;
+ }, []);
+ }
+
// backward compatibility
get get_values() {
return this.get_filter_values;
diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js
index 8866a4b2af..c70c64be0e 100644
--- a/frappe/public/js/frappe/views/reports/report_view.js
+++ b/frappe/public/js/frappe/views/reports/report_view.js
@@ -106,6 +106,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
get_args() {
const args = super.get_args();
+ delete args.group_by;
this.group_by_control.set_args(args);
return args;
@@ -387,6 +388,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
this.$charts_wrapper.addClass('hidden');
this.save_view_user_settings(
{ chart_args: null });
+ this.chart_args = null;
}
}
diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js
index 82d056bb31..11e567af42 100644
--- a/frappe/public/js/frappe/widgets/number_card_widget.js
+++ b/frappe/public/js/frappe/widgets/number_card_widget.js
@@ -211,7 +211,7 @@ export default class NumberCardWidget extends Widget {
const symbol = number_parts[1] || '';
const formatted_number = $(frappe.format(number_parts[0], df)).text();
- this.formatted_number = formatted_number + ' ' + symbol;
+ this.formatted_number = formatted_number + ' ' + __(symbol);
}
render_number() {
diff --git a/frappe/public/scss/common/grid.scss b/frappe/public/scss/common/grid.scss
index 57d0583b35..3014211222 100644
--- a/frappe/public/scss/common/grid.scss
+++ b/frappe/public/scss/common/grid.scss
@@ -373,6 +373,18 @@
.page-text {
display: inline-block;
+ cursor: default;
+}
+
+.current-page-number {
+ width: 16px;
+ text-align: center;
+ border: none;
+ cursor: text;
+
+ &:focus {
+ outline: none;
+ }
}
.prev-page,
diff --git a/frappe/public/scss/desk/global.scss b/frappe/public/scss/desk/global.scss
index d157a43bc3..7c2ae3c8b1 100644
--- a/frappe/public/scss/desk/global.scss
+++ b/frappe/public/scss/desk/global.scss
@@ -574,6 +574,31 @@ details > summary:focus {
}
}
+.diffview {
+ font-family: monospace;
+ white-space: pre;
+ word-wrap: break-word;
+ color: var(--text-color);
+}
+
+
+.diffview .insert {
+ background-color: var(--green-100);
+}
+
+.diffview .delete {
+ background-color: var(--red-100);
+}
+
+[data-theme="dark"] {
+ .diffview .insert {
+ background-color: var(--green-800);
+ }
+ .diffview .delete {
+ background-color: var(--red-800);
+ }
+}
+
// REDESIGN TODO: Handling of broken images?
// img.no-image:before {
// .img-background();
@@ -603,4 +628,4 @@ details > summary:focus {
.chart-container {
direction: ltr;
}
-*/
\ No newline at end of file
+*/
diff --git a/frappe/public/scss/desk/theme_switcher.scss b/frappe/public/scss/desk/theme_switcher.scss
index 00e3f35be8..924c2edd9d 100644
--- a/frappe/public/scss/desk/theme_switcher.scss
+++ b/frappe/public/scss/desk/theme_switcher.scss
@@ -1,6 +1,6 @@
.modal-body .theme-grid {
display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
+ grid-template-columns: repeat(3, minmax(0, 1fr));
grid-gap: 18px;
.background {
@@ -9,7 +9,7 @@
border-radius: var(--border-radius-lg);
overflow: hidden;
cursor: pointer;
- height: 160px;
+ height: 120px;
position: relative;
&:hover {
@@ -28,6 +28,7 @@
margin-right: var(--margin-sm);
border-radius: var(--border-radius-full);
+ z-index: 1;
}
}
@@ -72,6 +73,7 @@
border-radius: var(--border-radius-sm);
height: 10px;
width: 20px;
+ z-index: 1;
}
.text {
@@ -80,4 +82,17 @@
height: 10px;
width: 40px;
}
+}
+
+// TODO: Replace with better alternative
+[data-is-auto-theme="true"] {
+ .background::after {
+ content: "";
+ top: 0;
+ right: 0;
+ height: 100%;
+ width: 50%;
+ background: var(--gray-900);
+ position: absolute;
+ }
}
\ No newline at end of file
diff --git a/frappe/public/scss/website/blog.scss b/frappe/public/scss/website/blog.scss
index ea82efed21..e599210435 100644
--- a/frappe/public/scss/website/blog.scss
+++ b/frappe/public/scss/website/blog.scss
@@ -1,3 +1,8 @@
+:root {
+ --comment-timeline-bottom: 60px;
+ --comment-timeline-top: 8px;
+}
+
.blog-list {
display: flex;
flex-wrap: wrap;
@@ -96,4 +101,124 @@
margin-top: 3rem;
}
}
+
+
+ .feedback-item svg {
+ vertical-align: sub;
+ }
+
+ .blog-feedback {
+ display: flex;
+
+ .like-icon {
+ cursor: pointer;
+
+ &.gray use {
+ fill: var(--gray-600);
+ stroke: none;
+ }
+ }
+ }
+
+ .add-comment-button {
+ margin-left: 35px;
+ }
+
+ .timeline-dot {
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ position: absolute;
+ top: 8px;
+ left: 22px;
+ background-color: var(--fg-color);
+ border: 1px solid var(--dark-border-color);
+
+ &:before {
+ content: ' ';
+ background: var(--gray-600);
+ position: absolute;
+ top: 5px;
+ left: 5px;
+ border-radius: 50%;
+ height: 4px;
+ width: 4px;
+ }
+ }
+
+ .blog-comments {
+ .comment-form-wrapper {
+ display: none;
+ }
+
+ .add-comment-section {
+ .login-required {
+ padding: var(--padding-sm);
+ border-radius: var(--border-radius-sm);
+ box-shadow: var(--card-shadow);
+ }
+
+ .new-comment {
+ display: flex;
+ padding: var(--padding-lg);
+ box-shadow: var(--card-shadow);
+ border-radius: var(--border-radius-md);
+
+ .new-comment-fields {
+ flex: 1;
+
+ .form-label {
+ font-weight: var(--text-bold);
+ }
+
+ .comment-text-area textarea {
+ resize: none;
+ }
+
+ @media (min-width: 576px) {
+ .comment-by {
+ padding-right: 0px !important;
+ padding-bottom: 0px !important;
+ }
+ }
+ }
+ }
+ }
+
+
+ #comment-list {
+ position: relative;
+ padding-left: var(--padding-xl);
+
+ &:before {
+ content: " ";
+ position: absolute;
+ top: var(--comment-timeline-top);
+ bottom: var(--comment-timeline-bottom);
+ border-left: 1px solid var(--dark-border-color);
+ }
+
+ .comment-row {
+ position: relative;
+
+ .comment-avatar {
+ position: absolute;
+ top: 10px;
+ left: -17px;
+ }
+
+ .comment-content {
+ box-shadow: var(--card-shadow);
+ border-radius: var(--border-radius-md);
+ padding: var(--padding-md);
+ margin-left: 35px;
+ flex: 1;
+
+ .content p{
+ margin-bottom: 0px;
+ }
+ }
+ }
+ }
+ }
}
diff --git a/frappe/public/scss/website/error-state.scss b/frappe/public/scss/website/error-state.scss
new file mode 100644
index 0000000000..6f88009ecb
--- /dev/null
+++ b/frappe/public/scss/website/error-state.scss
@@ -0,0 +1,18 @@
+.error-page {
+ text-align: center;
+
+ .img-404 {
+ width: 40%;
+ margin: var(--margin-2xl) auto;
+
+ @include media-breakpoint-down(sm) {
+ width: 80%
+ }
+ }
+
+ .back-to-home {
+ font-size: var(--text-base);
+ }
+}
+
+
diff --git a/frappe/public/scss/website/index.scss b/frappe/public/scss/website/index.scss
index 2957a0b499..9c84e99a5a 100644
--- a/frappe/public/scss/website/index.scss
+++ b/frappe/public/scss/website/index.scss
@@ -26,6 +26,7 @@
@import 'doc';
@import 'navbar';
@import 'footer';
+@import 'error-state';
.ql-editor.read-mode {
padding: 0;
diff --git a/frappe/sessions.py b/frappe/sessions.py
index 9a0f19df80..91c8bbdecb 100644
--- a/frappe/sessions.py
+++ b/frappe/sessions.py
@@ -158,6 +158,8 @@ def get():
bootinfo["setup_complete"] = cint(frappe.db.get_single_value('System Settings', 'setup_complete'))
bootinfo["is_first_startup"] = cint(frappe.db.get_single_value('System Settings', 'is_first_startup'))
+ bootinfo['desk_theme'] = frappe.db.get_value("User", frappe.session.user, "desk_theme") or 'Light'
+
return bootinfo
@frappe.whitelist()
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 3ffabcd241..86843302e9 100644
--- a/frappe/social/doctype/energy_point_log/energy_point_log.py
+++ b/frappe/social/doctype/energy_point_log/energy_point_log.py
@@ -32,7 +32,9 @@ class EnergyPointLog(Document):
frappe.cache().hdel('energy_points', self.user)
frappe.publish_realtime('update_points', after_commit=True)
- if self.type != 'Review':
+ if self.type != 'Review' and \
+ frappe.get_cached_value('Notification Settings', self.user, 'energy_points_system_notifications'):
+
reference_user = self.user if self.type == 'Auto' else self.owner
notification_doc = {
'type': 'Energy Point',
diff --git a/frappe/social/doctype/energy_point_log/test_energy_point_log.py b/frappe/social/doctype/energy_point_log/test_energy_point_log.py
index c2bcbde825..a1f4503c34 100644
--- a/frappe/social/doctype/energy_point_log/test_energy_point_log.py
+++ b/frappe/social/doctype/energy_point_log/test_energy_point_log.py
@@ -8,6 +8,18 @@ from frappe.utils.testutils import add_custom_field, clear_custom_fields
from frappe.desk.form.assign_to import add as assign_to
class TestEnergyPointLog(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ settings = frappe.get_single('Energy Point Settings')
+ settings.enabled = 1
+ settings.save()
+
+ @classmethod
+ def tearDownClass(cls):
+ settings = frappe.get_single('Energy Point Settings')
+ settings.enabled = 0
+ settings.save()
+
def setUp(self):
frappe.cache().delete_value('energy_point_rule_map')
@@ -336,4 +348,4 @@ def assign_users_to_todo(todo_name, users):
'assign_to': [user],
'doctype': 'ToDo',
'name': todo_name
- })
\ No newline at end of file
+ })
diff --git a/frappe/social/doctype/energy_point_settings/energy_point_settings.json b/frappe/social/doctype/energy_point_settings/energy_point_settings.json
index 0001b26529..d1f9aea3d0 100644
--- a/frappe/social/doctype/energy_point_settings/energy_point_settings.json
+++ b/frappe/social/doctype/energy_point_settings/energy_point_settings.json
@@ -1,229 +1,70 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
+ "actions": [],
"creation": "2019-03-19 13:17:51.710241",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
- "document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
+ "field_order": [
+ "enabled",
+ "section_break_2",
+ "review_levels",
+ "point_allocation_periodicity",
+ "last_point_allocation_date"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "1",
- "fetch_if_empty": 0,
+ "default": "0",
"fieldname": "enabled",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Enabled",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Enabled"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "enabled",
- "fetch_if_empty": 0,
"fieldname": "section_break_2",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Section Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "review_levels",
"fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Review Levels",
- "length": 0,
- "no_copy": 0,
- "options": "Review Level",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Review Level"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "Weekly",
- "fetch_if_empty": 0,
"fieldname": "point_allocation_periodicity",
"fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Point Allocation Periodicity",
- "length": 0,
- "no_copy": 0,
- "options": "Daily\nWeekly\nMonthly",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Daily\nWeekly\nMonthly"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "last_point_allocation_date",
"fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Last Point Allocation Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
}
],
- "has_web_view": 0,
"hide_toolbar": 1,
- "idx": 0,
- "in_create": 0,
- "is_submittable": 0,
"issingle": 1,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-03-26 19:10:14.087840",
+ "links": [],
+ "modified": "2021-11-16 23:24:01.366928",
"modified_by": "Administrator",
"module": "Social",
"name": "Energy Point Settings",
- "name_case": "",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
- "report": 0,
"role": "System Manager",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
}
],
"quick_entry": 1,
- "read_only": 0,
- "show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "ASC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/social/doctype/energy_point_settings/test_energy_point_settings.py b/frappe/social/doctype/energy_point_settings/test_energy_point_settings.py
new file mode 100644
index 0000000000..3b0a756878
--- /dev/null
+++ b/frappe/social/doctype/energy_point_settings/test_energy_point_settings.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+class TestEnergyPointSettings(unittest.TestCase):
+ pass
diff --git a/frappe/templates/emails/newsletter.html b/frappe/templates/emails/newsletter.html
index a3afb906cf..051840ef69 100644
--- a/frappe/templates/emails/newsletter.html
+++ b/frappe/templates/emails/newsletter.html
@@ -3,8 +3,11 @@
{{ message }}
+
+{% if published and send_webview_link %}
\ No newline at end of file
+
+{% endif %}
\ No newline at end of file
diff --git a/frappe/templates/includes/avatar_macro.html b/frappe/templates/includes/avatar_macro.html
index 6983477f9c..b652b573b3 100644
--- a/frappe/templates/includes/avatar_macro.html
+++ b/frappe/templates/includes/avatar_macro.html
@@ -1,6 +1,6 @@
-{% macro avatar(user_id=None, css_style=None) %}
+{% macro avatar(user_id=None, css_style=None, size="avatar-small") %}
{% set user_info = frappe.utils.get_user_info_for_avatar(user_id) %}
-
+
{% if user_info.image %}
- {{ frappe.utils.get_abbr(user_info.name) }}
+ {{ frappe.utils.get_abbr(user_info.name).upper() }}
{% endif %}
diff --git a/frappe/templates/includes/comments/comment.html b/frappe/templates/includes/comments/comment.html
index 08a2b79ee6..e0fc1c3c54 100644
--- a/frappe/templates/includes/comments/comment.html
+++ b/frappe/templates/includes/comments/comment.html
@@ -1,18 +1,14 @@
-{% from "frappe/templates/includes/macros.html" import square_image_with_fallback %}
+{% from "frappe/templates/includes/avatar_macro.html" import avatar %}
-
+
\ No newline at end of file
diff --git a/frappe/templates/includes/comments/comments.html b/frappe/templates/includes/comments/comments.html
index 935fa5367e..8a9058bb4d 100644
--- a/frappe/templates/includes/comments/comments.html
+++ b/frappe/templates/includes/comments/comments.html
@@ -1,86 +1,146 @@