@@ -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/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/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/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..c26b63a9f6 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;
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/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/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/includes/comments/comments.py b/frappe/templates/includes/comments/comments.py
index e352c7368b..99afb580d8 100644
--- a/frappe/templates/includes/comments/comments.py
+++ b/frappe/templates/includes/comments/comments.py
@@ -28,16 +28,6 @@ def add_comment(comment, comment_email, comment_by, reference_doctype, reference
frappe.msgprint(_('Comments cannot have links or email addresses'))
return False
- comments_count = frappe.db.count("Comment", {
- "comment_type": "Comment",
- "comment_email": comment_email,
- "creation": (">", add_to_date(now(), hours=-1))
- })
-
- if comments_count > 20:
- frappe.msgprint(_('Hourly comment limit reached for: {0}').format(frappe.bold(comment_email)))
- return False
-
comment = doc.add_comment(
text=comment,
comment_email=comment_email,
@@ -54,14 +44,17 @@ def add_comment(comment, comment_email, comment_by, reference_doctype, reference
comment.name,
_("View Comment")))
- # notify creator
- frappe.sendmail(
- recipients=frappe.db.get_value('User', doc.owner, 'email') or doc.owner,
- subject=_('New Comment on {0}: {1}').format(doc.doctype, doc.name),
- message=content,
- reference_doctype=doc.doctype,
- reference_name=doc.name
- )
+ if doc.doctype == "Blog Post" and not doc.enable_email_notification:
+ pass
+ else:
+ # notify creator
+ frappe.sendmail(
+ recipients=frappe.db.get_value('User', doc.owner, 'email') or doc.owner,
+ subject=_('New Comment on {0}: {1}').format(doc.doctype, doc.name),
+ message=content,
+ reference_doctype=doc.doctype,
+ reference_name=doc.name
+ )
# revert with template if all clear (no backlinks)
template = frappe.get_template("templates/includes/comments/comment.html")
diff --git a/frappe/templates/includes/feedback/feedback.py b/frappe/templates/includes/feedback/feedback.py
index 62fdc3f746..279ff05e6d 100644
--- a/frappe/templates/includes/feedback/feedback.py
+++ b/frappe/templates/includes/feedback/feedback.py
@@ -12,8 +12,8 @@ from frappe.website.doctype.blog_settings.blog_settings import get_feedback_limi
@rate_limit(key='reference_name', limit=get_feedback_limit, seconds=60*60)
def give_feedback(reference_doctype, reference_name, like):
like = frappe.parse_json(like)
- doc = frappe.get_doc(reference_doctype, reference_name)
- if doc.disable_feedback == 1:
+ ref_doc = frappe.get_doc(reference_doctype, reference_name)
+ if ref_doc.disable_feedback == 1:
return
filters = {
@@ -33,7 +33,7 @@ def give_feedback(reference_doctype, reference_name, like):
doc.save(ignore_permissions=True)
subject = _('Feedback on {0}: {1}').format(reference_doctype, reference_name)
- send_mail(doc, subject)
+ ref_doc.enable_email_notification and send_mail(doc, subject)
return doc
def send_mail(feedback, subject):
diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html
index 55c76a00c2..9986e45999 100644
--- a/frappe/templates/print_formats/standard_macros.html
+++ b/frappe/templates/print_formats/standard_macros.html
@@ -195,9 +195,9 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}"
{% else %}
- {{ doc.select_print_heading or (doc.print_heading if doc.print_heading != None
+
{{ _(doc.select_print_heading) or (_(doc.print_heading) if doc.print_heading != None
else _(doc.doctype)) }}
-
{{ doc.sub_heading if doc.sub_heading != None
+ {{ _(doc.sub_heading) if doc.sub_heading != None
else doc.name }}
diff --git a/frappe/tests/test_boilerplate.py b/frappe/tests/test_boilerplate.py
index 259d5a9194..6a9544b2e9 100644
--- a/frappe/tests/test_boilerplate.py
+++ b/frappe/tests/test_boilerplate.py
@@ -11,14 +11,7 @@ from frappe.utils.boilerplate import make_boilerplate
class TestBoilerPlate(unittest.TestCase):
@classmethod
- def tearDownClass(cls):
-
- bench_path = frappe.utils.get_bench_path()
- test_app_dir = os.path.join(bench_path, "apps", "test_app")
- if os.path.exists(test_app_dir):
- shutil.rmtree(test_app_dir)
-
- def test_create_app(self):
+ def setUpClass(cls):
title = "Test App"
description = "This app's description contains 'single quotes' and \"double quotes\"."
publisher = "Test Publisher"
@@ -27,7 +20,7 @@ class TestBoilerPlate(unittest.TestCase):
color = ""
app_license = "MIT"
- user_input = [
+ cls.user_input = [
title,
description,
publisher,
@@ -37,22 +30,21 @@ class TestBoilerPlate(unittest.TestCase):
app_license,
]
- bench_path = frappe.utils.get_bench_path()
- apps_dir = os.path.join(bench_path, "apps")
- app_name = "test_app"
+ cls.bench_path = frappe.utils.get_bench_path()
+ cls.apps_dir = os.path.join(cls.bench_path, "apps")
+ cls.app_names = ("test_app", "test_app_no_git")
+ cls.gitignore_file = ".gitignore"
+ cls.git_folder = ".git"
- with patch("builtins.input", side_effect=user_input):
- make_boilerplate(apps_dir, app_name)
-
- root_paths = [
- app_name,
+ cls.root_paths = [
"requirements.txt",
"README.md",
"setup.py",
"license.txt",
- ".git",
+ cls.git_folder,
+ cls.gitignore_file
]
- paths_inside_app = [
+ cls.paths_inside_app = [
"__init__.py",
"hooks.py",
"patches.txt",
@@ -60,25 +52,68 @@ class TestBoilerPlate(unittest.TestCase):
"www",
"config",
"modules.txt",
- "public",
- app_name,
+ "public"
]
- new_app_dir = os.path.join(bench_path, apps_dir, app_name)
+ @classmethod
+ def tearDownClass(cls):
+ test_app_dirs = (os.path.join(cls.bench_path, "apps", app_name) for app_name in cls.app_names)
+ for test_app_dir in test_app_dirs:
+ if os.path.exists(test_app_dir):
+ shutil.rmtree(test_app_dir)
+ def test_create_app(self):
+ with patch("builtins.input", side_effect=self.user_input):
+ make_boilerplate(self.apps_dir, self.app_names[0])
+
+ new_app_dir = os.path.join(self.bench_path, self.apps_dir, self.app_names[0])
+
+ paths = self.get_paths(new_app_dir, self.app_names[0])
+ for path in paths:
+ self.assertTrue(
+ os.path.exists(path),
+ msg=f"{path} should exist in {self.app_names[0]} app"
+ )
+
+ self.check_parsable_python_files(new_app_dir)
+
+ def test_create_app_without_git_init(self):
+ with patch("builtins.input", side_effect=self.user_input):
+ make_boilerplate(self.apps_dir, self.app_names[1], no_git=True)
+
+ new_app_dir = os.path.join(self.apps_dir, self.app_names[1])
+
+ paths = self.get_paths(new_app_dir, self.app_names[1])
+ for path in paths:
+ if os.path.basename(path) in (self.git_folder, self.gitignore_file):
+ self.assertFalse(
+ os.path.exists(path),
+ msg=f"{path} shouldn't exist in {self.app_names[1]} app"
+ )
+ else:
+ self.assertTrue(
+ os.path.exists(path),
+ msg=f"{path} should exist in {self.app_names[1]} app"
+ )
+
+ self.check_parsable_python_files(new_app_dir)
+
+ def get_paths(self, app_dir, app_name):
all_paths = list()
- for path in root_paths:
- all_paths.append(os.path.join(new_app_dir, path))
+ for path in self.root_paths:
+ all_paths.append(os.path.join(app_dir, path))
- for path in paths_inside_app:
- all_paths.append(os.path.join(new_app_dir, app_name, path))
+ all_paths.append(os.path.join(app_dir, app_name))
- for path in all_paths:
- self.assertTrue(os.path.exists(path), msg=f"{path} should exist in new app")
+ for path in self.paths_inside_app:
+ all_paths.append(os.path.join(app_dir, app_name, path))
+ return all_paths
+
+ def check_parsable_python_files(self, app_dir):
# check if python files are parsable
- python_files = glob.glob(new_app_dir + "**/*.py", recursive=True)
+ python_files = glob.glob(app_dir + "**/*.py", recursive=True)
for python_file in python_files:
with open(python_file) as p:
diff --git a/frappe/tests/test_commands.py b/frappe/tests/test_commands.py
index c048e23949..94389cd7a3 100644
--- a/frappe/tests/test_commands.py
+++ b/frappe/tests/test_commands.py
@@ -5,6 +5,7 @@ import gzip
import json
import os
import shlex
+import shutil
import subprocess
import sys
import unittest
@@ -102,14 +103,24 @@ def exists_in_backup(doctypes, file):
class BaseTestCommands(unittest.TestCase):
def execute(self, command, kwargs=None):
site = {"site": frappe.local.site}
+ cmd_input = None
if kwargs:
+ cmd_input = kwargs.get("cmd_input", None)
+ if cmd_input:
+ if not isinstance(cmd_input, bytes):
+ raise Exception(
+ f"The input should be of type bytes, not {type(cmd_input).__name__}"
+ )
+
+ del kwargs["cmd_input"]
kwargs.update(site)
else:
kwargs = site
+
self.command = " ".join(command.split()).format(**kwargs)
print("{0}$ {1}{2}".format(color.silver, self.command, color.nc))
command = shlex.split(self.command)
- self._proc = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ self._proc = subprocess.run(command, input=cmd_input, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self.stdout = clean(self._proc.stdout)
self.stderr = clean(self._proc.stderr)
self.returncode = clean(self._proc.returncode)
@@ -466,6 +477,28 @@ class TestCommands(BaseTestCommands):
self.assertEqual(self.returncode, 0)
self.assertEqual(check_password('Administrator', 'test2'), 'Administrator')
+ def test_make_app(self):
+ user_input = [
+ b"Test App", # title
+ b"This app's description contains 'single quotes' and \"double quotes\".", # description
+ b"Test Publisher", # publisher
+ b"example@example.org", # email
+ b"", # icon
+ b"", # color
+ b"MIT" # app_license
+ ]
+ app_name = "testapp0"
+ apps_path = os.path.join(frappe.utils.get_bench_path(), "apps")
+ test_app_path = os.path.join(apps_path, app_name)
+ self.execute(f"bench make-app {apps_path} {app_name}", {"cmd_input": b'\n'.join(user_input)})
+ self.assertEqual(self.returncode, 0)
+ self.assertTrue(
+ os.path.exists(test_app_path)
+ )
+
+ # cleanup
+ shutil.rmtree(test_app_path)
+
class RemoveAppUnitTests(unittest.TestCase):
def test_delete_modules(self):
diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py
index 9077655dc9..6e49ef3c54 100644
--- a/frappe/tests/test_db.py
+++ b/frappe/tests/test_db.py
@@ -34,7 +34,21 @@ class TestDB(unittest.TestCase):
self.assertEqual(frappe.db.sql("""SELECT name FROM `tabUser` WHERE name >= 't' ORDER BY MODIFIED DESC""")[0][0],
frappe.db.get_value("User", {"name": [">=", "t"]}))
- self.assertIn("concat_ws", frappe.db.get_value("User", filters={"name": "Administrator"}, fieldname=Concat_ws(" ", "LastName"), run=False).lower())
+ self.assertIn(
+ "concat_ws",
+ frappe.db.get_value(
+ "User",
+ filters={"name": "Administrator"},
+ fieldname=Concat_ws(" ", "LastName"),
+ run=False,
+ ).lower(),
+ )
+ self.assertEqual(
+ frappe.db.sql("select email from tabUser where name='Administrator' order by modified DESC"),
+ frappe.db.get_values(
+ "User", filters=[["name", "=", "Administrator"]], fieldname="email"
+ ),
+ )
def test_set_value(self):
todo1 = frappe.get_doc(dict(doctype='ToDo', description = 'test_set_value 1')).insert()
diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py
index 47ad029274..29cec8b230 100644
--- a/frappe/tests/test_document.py
+++ b/frappe/tests/test_document.py
@@ -157,7 +157,7 @@ class TestDocument(unittest.TestCase):
def test_varchar_length(self):
d = self.test_insert()
- d.subject = "abcde"*100
+ d.sender = "abcde"*100 + "@user.com"
self.assertRaises(frappe.CharacterLengthExceededError, d.save)
def test_xss_filter(self):
@@ -251,4 +251,4 @@ class TestDocument(unittest.TestCase):
'doctype': 'Test Formatted',
'currency': 100000
})
- self.assertEquals(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00')
\ No newline at end of file
+ self.assertEquals(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00')
diff --git a/frappe/tests/test_naming.py b/frappe/tests/test_naming.py
index 4435a8bb20..3031d3e344 100644
--- a/frappe/tests/test_naming.py
+++ b/frappe/tests/test_naming.py
@@ -7,6 +7,7 @@ from frappe.utils import now_datetime
from frappe.model.naming import getseries
from frappe.model.naming import append_number_if_name_exists, revert_series_if_last
+from frappe.model.naming import determine_consecutive_week_number, parse_naming_series
class TestNaming(unittest.TestCase):
def tearDown(self):
@@ -60,6 +61,34 @@ class TestNaming(unittest.TestCase):
self.assertEqual(todo.name, 'TODO-{month}-{status}-{series}'.format(
month=now_datetime().strftime('%m'), status=todo.status, series=series))
+ def test_format_autoname_for_consecutive_week_number(self):
+ '''
+ Test if braced params are replaced for consecutive week number in format autoname
+ '''
+ doctype = 'ToDo'
+
+ todo_doctype = frappe.get_doc('DocType', doctype)
+ todo_doctype.autoname = 'format:TODO-{WW}-{##}'
+ todo_doctype.save()
+
+ description = 'Format'
+
+ todo = frappe.new_doc(doctype)
+ todo.description = description
+ todo.insert()
+
+ series = getseries('', 2)
+
+ series = str(int(series)-1)
+
+ if len(series) < 2:
+ series = '0' + series
+
+ week = determine_consecutive_week_number(now_datetime())
+
+ self.assertEqual(todo.name, 'TODO-{week}-{series}'.format(
+ week=week, series=series))
+
def test_revert_series(self):
from datetime import datetime
year = datetime.now().year
@@ -150,3 +179,32 @@ class TestNaming(unittest.TestCase):
self.assertEqual(amended_doc.name, "{}-CANC-1".format(original_name))
submittable_doctype.delete()
+
+ def test_parse_naming_series_for_consecutive_week_number(self):
+ week = determine_consecutive_week_number(now_datetime())
+ name = parse_naming_series('PREFIX-.WW.-SUFFIX')
+ expected_name = 'PREFIX-{}-SUFFIX'.format(week)
+ self.assertEqual(name, expected_name)
+
+ def test_determine_consecutive_week_number(self):
+ from datetime import datetime
+
+ dt = datetime.fromisoformat("2019-12-31")
+ w = determine_consecutive_week_number(dt)
+ self.assertEqual(w, "53")
+
+ dt = datetime.fromisoformat("2020-01-01")
+ w = determine_consecutive_week_number(dt)
+ self.assertEqual(w, "01")
+
+ dt = datetime.fromisoformat("2020-01-15")
+ w = determine_consecutive_week_number(dt)
+ self.assertEqual(w, "03")
+
+ dt = datetime.fromisoformat("2021-01-01")
+ w = determine_consecutive_week_number(dt)
+ self.assertEqual(w, "00")
+
+ dt = datetime.fromisoformat("2021-12-31")
+ w = determine_consecutive_week_number(dt)
+ self.assertEqual(w, "52")
diff --git a/frappe/tests/test_printview.py b/frappe/tests/test_printview.py
new file mode 100644
index 0000000000..0fc4c4869b
--- /dev/null
+++ b/frappe/tests/test_printview.py
@@ -0,0 +1,22 @@
+import unittest
+
+import frappe
+from frappe.www.printview import get_html_and_style
+
+
+class PrintViewTest(unittest.TestCase):
+ def test_print_view_without_errors(self):
+
+ user = frappe.get_last_doc("User")
+
+ messages_before = frappe.get_message_log()
+ ret = get_html_and_style(doc=user.as_json(), print_format="Standard", no_letterhead=1)
+ messages_after = frappe.get_message_log()
+
+ if len(messages_after) > len(messages_before):
+ new_messages = messages_after[len(messages_before):]
+ self.fail("Print view showing error/warnings: \n"
+ + "\n".join(str(msg) for msg in new_messages))
+
+ # html should exist
+ self.assertTrue(bool(ret["html"]))
diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py
index d0dd1669b4..91f7dbb2f8 100755
--- a/frappe/utils/boilerplate.py
+++ b/frappe/utils/boilerplate.py
@@ -3,7 +3,7 @@
import frappe, os, re, git
from frappe.utils import touch_file, cstr
-def make_boilerplate(dest, app_name):
+def make_boilerplate(dest, app_name, no_git=False):
if not os.path.exists(dest):
print("Destination directory does not exist")
return
@@ -63,9 +63,6 @@ def make_boilerplate(dest, app_name):
with open(os.path.join(dest, hooks.app_name, "MANIFEST.in"), "w") as f:
f.write(frappe.as_unicode(manifest_template.format(**hooks)))
- with open(os.path.join(dest, hooks.app_name, ".gitignore"), "w") as f:
- f.write(frappe.as_unicode(gitignore_template.format(app_name = hooks.app_name)))
-
with open(os.path.join(dest, hooks.app_name, "requirements.txt"), "w") as f:
f.write("# frappe -- https://github.com/frappe/frappe is installed via 'bench init'")
@@ -98,11 +95,16 @@ def make_boilerplate(dest, app_name):
with open(os.path.join(dest, hooks.app_name, hooks.app_name, "config", "docs.py"), "w") as f:
f.write(frappe.as_unicode(docs_template.format(**hooks)))
- # initialize git repository
app_directory = os.path.join(dest, hooks.app_name)
- app_repo = git.Repo.init(app_directory)
- app_repo.git.add(A=True)
- app_repo.index.commit("feat: Initialize App")
+
+ if not no_git:
+ with open(os.path.join(dest, hooks.app_name, ".gitignore"), "w") as f:
+ f.write(frappe.as_unicode(gitignore_template.format(app_name = hooks.app_name)))
+
+ # initialize git repository
+ app_repo = git.Repo.init(app_directory)
+ app_repo.git.add(A=True)
+ app_repo.index.commit("feat: Initialize App")
print("'{app}' created at {path}".format(app=app_name, path=app_directory))
diff --git a/frappe/utils/data.py b/frappe/utils/data.py
index f5c46dc184..d39d32d8df 100644
--- a/frappe/utils/data.py
+++ b/frappe/utils/data.py
@@ -868,7 +868,7 @@ def fmt_money(amount, precision=None, currency=None, format=None):
if currency and frappe.defaults.get_global_default("hide_currency_symbol") != "Yes":
symbol = frappe.db.get_value("Currency", currency, "symbol", cache=True) or currency
- amount = symbol + " " + amount
+ amount = frappe._(symbol) + " " + amount
return amount
diff --git a/frappe/utils/pdf.py b/frappe/utils/pdf.py
index 90bb4f63de..9a7c0889b5 100644
--- a/frappe/utils/pdf.py
+++ b/frappe/utils/pdf.py
@@ -95,7 +95,7 @@ def prepare_options(html, options):
'quiet': None,
# 'no-outline': None,
'encoding': "UTF-8",
- #'load-error-handling': 'ignore'
+ # 'load-error-handling': 'ignore'
})
if not options.get("margin-right"):
@@ -111,8 +111,21 @@ def prepare_options(html, options):
options.update(get_cookie_options())
# page size
- if not options.get("page-size"):
- options['page-size'] = frappe.db.get_single_value("Print Settings", "pdf_page_size") or "A4"
+ pdf_page_size = (
+ options.get("page-size")
+ or frappe.db.get_single_value("Print Settings", "pdf_page_size")
+ or "A4"
+ )
+
+ if pdf_page_size == "Custom":
+ options["page-height"] = options.get("page-height") or frappe.db.get_single_value(
+ "Print Settings", "pdf_page_height"
+ )
+ options["page-width"] = options.get("page-width") or frappe.db.get_single_value(
+ "Print Settings", "pdf_page_width"
+ )
+ else:
+ options["page-size"] = pdf_page_size
return html, options
diff --git a/frappe/utils/print_format.py b/frappe/utils/print_format.py
index 6dfa3a350b..06f15ced27 100644
--- a/frappe/utils/print_format.py
+++ b/frappe/utils/print_format.py
@@ -11,7 +11,7 @@ base_template_path = "www/printview.html"
standard_format = "templates/print_formats/standard.html"
@frappe.whitelist()
-def download_multi_pdf(doctype, name, format=None, no_letterhead=0):
+def download_multi_pdf(doctype, name, format=None, no_letterhead=False, options=None):
"""
Concatenate multiple docs as PDF .
@@ -54,18 +54,21 @@ def download_multi_pdf(doctype, name, format=None, no_letterhead=0):
import json
output = PdfFileWriter()
+ if isinstance(options, str):
+ options = json.loads(options)
+
if not isinstance(doctype, dict):
result = json.loads(name)
# Concatenating pdf files
for i, ss in enumerate(result):
- output = frappe.get_print(doctype, ss, format, as_pdf = True, output = output, no_letterhead=no_letterhead)
+ output = frappe.get_print(doctype, ss, format, as_pdf=True, output=output, no_letterhead=no_letterhead, pdf_options=options)
frappe.local.response.filename = "{doctype}.pdf".format(doctype=doctype.replace(" ", "-").replace("/", "-"))
else:
for doctype_name in doctype:
for doc_name in doctype[doctype_name]:
try:
- output = frappe.get_print(doctype_name, doc_name, format, as_pdf = True, output = output, no_letterhead=no_letterhead)
+ output = frappe.get_print(doctype_name, doc_name, format, as_pdf=True, output=output, no_letterhead=no_letterhead, pdf_options=options)
except Exception:
frappe.log_error("Permission Error on doc {} of doctype {}".format(doc_name, doctype_name))
frappe.local.response.filename = "{}.pdf".format(name)
diff --git a/frappe/website/doctype/blog_post/blog_post.json b/frappe/website/doctype/blog_post/blog_post.json
index 9400016c48..b05293f28b 100644
--- a/frappe/website/doctype/blog_post/blog_post.json
+++ b/frappe/website/doctype/blog_post/blog_post.json
@@ -17,6 +17,7 @@
"published",
"featured",
"hide_cta",
+ "enable_email_notification",
"disable_comments",
"disable_feedback",
"section_break_5",
@@ -197,6 +198,13 @@
"fieldname": "disable_feedback",
"fieldtype": "Check",
"label": "Disable Feedback"
+ },
+ {
+ "default": "1",
+ "description": "Enable email notification for any comment or feedback on your Blog Post.",
+ "fieldname": "enable_email_notification",
+ "fieldtype": "Check",
+ "label": "Enable Email Notification"
}
],
"has_web_view": 1,
@@ -206,7 +214,7 @@
"is_published_field": "published",
"links": [],
"max_attachments": 5,
- "modified": "2021-09-13 17:19:35.436045",
+ "modified": "2021-11-23 10:42:01.759723",
"modified_by": "Administrator",
"module": "Website",
"name": "Blog Post",
@@ -240,4 +248,4 @@
"sort_order": "ASC",
"title_field": "title",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/website/doctype/blog_post/blog_post.py b/frappe/website/doctype/blog_post/blog_post.py
index 2b723c107a..3536896a5f 100644
--- a/frappe/website/doctype/blog_post/blog_post.py
+++ b/frappe/website/doctype/blog_post/blog_post.py
@@ -104,7 +104,7 @@ class BlogPost(WebsiteGenerator):
context.parents = [{"name": _("Home"), "route":"/"},
{"name": "Blog", "route": "/blog"},
{"label": context.category.title, "route":context.category.route}]
- context.guest_allowed = frappe.db.get_single_value("Blog Settings", "allow_guest_to_comment", cache=True)
+ context.guest_allowed = frappe.db.get_single_value("Blog Settings", "allow_guest_to_comment")
def fetch_cta(self):
if frappe.db.get_single_value("Blog Settings", "show_cta_in_blog", cache=True):
diff --git a/frappe/workflow/doctype/workflow_state/workflow_state.json b/frappe/workflow/doctype/workflow_state/workflow_state.json
index a08f713bb1..be5804f390 100644
--- a/frappe/workflow/doctype/workflow_state/workflow_state.json
+++ b/frappe/workflow/doctype/workflow_state/workflow_state.json
@@ -112,7 +112,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "modified": "2017-02-20 13:33:44.011509",
+ "modified": "2021-11-22 17:56:40.495232",
"modified_by": "Administrator",
"module": "Workflow",
"name": "Workflow State",
@@ -137,6 +137,10 @@
"share": 1,
"submit": 0,
"write": 1
+ },
+ {
+ "role": "All",
+ "select": 1
}
],
"quick_entry": 1,
diff --git a/frappe/www/404.html b/frappe/www/404.html
index a796924f1a..c03b5d3e96 100644
--- a/frappe/www/404.html
+++ b/frappe/www/404.html
@@ -3,32 +3,22 @@
{%- block title -%}{{_("Not Found")}}{%- endblock -%}
{% block page_content %}
-
+
-
-
-
{{_("Page Missing or Moved")}}
+
+
+

+
+
+ {{ _("There's nothing here") }}
+
+
+ {{ _("The page you are looking for have gone missing.") }}
+
+
-
{{_("The page you are looking for is missing. This could be because it is moved or there is a typo in the link.")}}
-
-
{{ _("Error Code: {0}").format('404') }}
-
+
{% endblock %}
\ No newline at end of file
diff --git a/frappe/www/app.html b/frappe/www/app.html
index 68a6dc8e86..37579066e0 100644
--- a/frappe/www/app.html
+++ b/frappe/www/app.html
@@ -1,5 +1,5 @@
-
+