Merge branch 'frappe:develop' into blog-email-notification

This commit is contained in:
Shariq Ansari 2021-11-17 17:28:14 +05:30 committed by GitHub
commit 34ccabe90e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 192 additions and 219 deletions

View file

@ -10,6 +10,7 @@ concurrency:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 60
name: Patch Test

View file

@ -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
flags: server

View file

@ -13,6 +13,7 @@ concurrency:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false

View file

@ -13,6 +13,7 @@ concurrency:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false

View file

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

View file

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

View file

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

View file

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

View file

@ -15,7 +15,9 @@
"enable_email_energy_point",
"enable_email_share",
"user",
"seen"
"seen",
"system_notifications_section",
"energy_points_system_notifications"
],
"fields": [
{
@ -84,15 +86,27 @@
"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"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-11-04 12:54:57.989317",
"modified": "2021-11-16 12:18:46.955501",
"modified_by": "Administrator",
"module": "Desk",
"name": "Notification Settings",
"naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
@ -111,4 +125,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View file

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

View file

@ -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: `
<div id="heatmap-${frappe.model.scrub(this.frm.doctype)}" class="heatmap"></div>
@ -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');
}
});

View file

@ -156,8 +156,11 @@ frappe.ui.form.Form = class FrappeForm {
let dashboard_parent = $('<div class="form-dashboard">');
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({

View file

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

View file

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

View file

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

View file

@ -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 = $(`<div class="${this.current_theme == theme.name ? "selected" : "" }">
<div data-theme=${theme.name}>
<div data-theme=${is_auto_theme ? "light" : theme.name}
data-is-auto-theme="${is_auto_theme}" title="${theme.info}">
<div class="background">
<div>
<div class="preview-check">${frappe.utils.icon('tick', 'xs')}</div>
<div class="preview-check" data-theme=${is_auto_theme ? "dark" : theme.name}>
${frappe.utils.icon('tick', 'xs')}
</div>
</div>
<div class="navbar"></div>
<div class="p-2">
@ -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);
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies and Contributors
# See license.txt
# import frappe
import unittest
class TestEnergyPointSettings(unittest.TestCase):
pass

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html data-theme="{{ desk_theme.lower() }}" dir={{ layout_direction }} lang="{{ lang }}">
<html data-theme-mode="{{ desk_theme.lower() }}" data-theme="{{ desk_theme.lower() }}" dir={{ layout_direction }} lang="{{ lang }}">
<head>
<!-- Chrome, Firefox OS and Opera -->
<meta name="theme-color" content="#0089FF">

View file

@ -45,7 +45,7 @@ def get_context(context):
"lang": frappe.local.lang,
"sounds": hooks["sounds"],
"boot": boot if context.get("for_mobile") else boot_json,
"desk_theme": desk_theme or "Light",
"desk_theme": desk_theme,
"csrf_token": csrf_token,
"google_analytics_id": frappe.conf.get("google_analytics_id"),
"google_analytics_anonymize_ip": frappe.conf.get("google_analytics_anonymize_ip"),