瀏覽代碼

fix: fixed merged conflicts

version-14
Aradhya 3 年之前
父節點
當前提交
d37aa55607
共有 53 個檔案被更改,包括 787 行新增569 行删除
  1. +1
    -0
      .github/workflows/patch-mariadb-tests.yml
  2. +2
    -1
      .github/workflows/server-mariadb-tests.yml
  3. +1
    -0
      .github/workflows/server-postgres-tests.yml
  4. +1
    -0
      .github/workflows/ui-tests.yml
  5. +26
    -5
      cypress/integration/grid_pagination.js
  6. +2
    -2
      cypress/integration/report_view.js
  7. +23
    -4
      frappe/core/doctype/comment/test_comment.py
  8. +36
    -18
      frappe/core/doctype/communication/email.py
  9. +9
    -23
      frappe/core/doctype/feedback/feedback.json
  10. +9
    -11
      frappe/core/doctype/feedback/test_feedback.py
  11. +2
    -2
      frappe/core/doctype/user/user.json
  12. +1
    -1
      frappe/core/doctype/user/user.py
  13. +3
    -4
      frappe/database/database.py
  14. +17
    -3
      frappe/desk/doctype/notification_settings/notification_settings.json
  15. +5
    -1
      frappe/patches/v13_0/rename_custom_client_script.py
  16. +13
    -0
      frappe/public/js/frappe/desk.js
  17. +8
    -2
      frappe/public/js/frappe/form/dashboard.js
  18. +5
    -2
      frappe/public/js/frappe/form/form.js
  19. +50
    -2
      frappe/public/js/frappe/form/grid_pagination.js
  20. +1
    -1
      frappe/public/js/frappe/form/layout.js
  21. +3
    -1
      frappe/public/js/frappe/form/section.js
  22. +2
    -0
      frappe/public/js/frappe/list/list_view.js
  23. +35
    -4
      frappe/public/js/frappe/ui/theme_switcher.js
  24. +1
    -1
      frappe/public/js/frappe/utils/user.js
  25. +3
    -0
      frappe/public/js/frappe/views/interaction.js
  26. +1
    -1
      frappe/public/js/frappe/views/reports/query_report.js
  27. +1
    -0
      frappe/public/js/frappe/views/reports/report_view.js
  28. +12
    -0
      frappe/public/scss/common/grid.scss
  29. +17
    -2
      frappe/public/scss/desk/theme_switcher.scss
  30. +125
    -0
      frappe/public/scss/website/blog.scss
  31. +6
    -0
      frappe/public/scss/website/index.scss
  32. +2
    -0
      frappe/sessions.py
  33. +3
    -1
      frappe/social/doctype/energy_point_log/energy_point_log.py
  34. +13
    -1
      frappe/social/doctype/energy_point_log/test_energy_point_log.py
  35. +17
    -176
      frappe/social/doctype/energy_point_settings/energy_point_settings.json
  36. +8
    -0
      frappe/social/doctype/energy_point_settings/test_energy_point_settings.py
  37. +3
    -3
      frappe/templates/includes/avatar_macro.html
  38. +13
    -17
      frappe/templates/includes/comments/comment.html
  39. +129
    -68
      frappe/templates/includes/comments/comments.html
  40. +3
    -0
      frappe/templates/includes/comments/comments.py
  41. +28
    -145
      frappe/templates/includes/feedback/feedback.html
  42. +15
    -28
      frappe/templates/includes/feedback/feedback.py
  43. +1
    -1
      frappe/templates/web.html
  44. +7
    -0
      frappe/tests/test_db.py
  45. +22
    -0
      frappe/tests/test_printview.py
  46. +15
    -0
      frappe/utils/nestedset.py
  47. +21
    -12
      frappe/utils/weasyprint.py
  48. +19
    -9
      frappe/website/doctype/blog_post/blog_post.py
  49. +12
    -12
      frappe/website/doctype/blog_post/templates/blog_post.html
  50. +23
    -3
      frappe/website/doctype/blog_settings/blog_settings.json
  51. +4
    -1
      frappe/website/doctype/blog_settings/blog_settings.py
  52. +7
    -0
      frappe/website/doctype/website_theme/website_theme_template.scss
  53. +1
    -1
      frappe/www/app.html

+ 1
- 0
.github/workflows/patch-mariadb-tests.yml 查看文件

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

name: Patch Test



+ 2
- 1
.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
flags: server

+ 1
- 0
.github/workflows/server-postgres-tests.yml 查看文件

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

strategy:
fail-fast: false


+ 1
- 0
.github/workflows/ui-tests.yml 查看文件

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

strategy:
fail-fast: false


+ 26
- 5
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');


+ 2
- 2
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


+ 23
- 4
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,


+ 36
- 18
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()
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()
})

+ 9
- 23
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",


+ 9
- 11
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"})


+ 2
- 2
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",


+ 1
- 1
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():


+ 3
- 4
frappe/database/database.py 查看文件

@@ -570,12 +570,11 @@ class Database(object):

def _get_value_for_many_names(self, doctype, names, field, debug=False, run=True, **kwargs):
names = list(filter(None, names))

if names:
return self.get_all(doctype,
fields=['name', field],
filters=[['name', 'in', names]],
debug=debug, as_list=1, run=run, **kwargs),
fields=field,
filters=names,
debug=debug, as_list=1, run=run)
else:
return {}



+ 17
- 3
frappe/desk/doctype/notification_settings/notification_settings.json 查看文件

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

+ 5
- 1
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)

+ 13
- 0
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();


+ 8
- 2
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: `
<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');
}
});


+ 5
- 2
frappe/public/js/frappe/form/form.js 查看文件

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


+ 50
- 2
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 = `<div class="page-text">
<span class="current-page-number page-number">${__(this.page_index)}</span>
<input class="current-page-number page-number" type="text" value="${__(this.page_index)}"/>
<span>${__('of')}</span>
<span class="total-page-number page-number"> ${__(this.total_pages)} </span>
</div>`;
@@ -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();


+ 1
- 1
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) {


+ 3
- 1
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) {


+ 2
- 0
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")) {


+ 35
- 4
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 = $(`<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);
};

+ 1
- 1
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];


+ 3
- 0
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},


+ 1
- 1
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) {


+ 1
- 0
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();
args.group_by = null;
this.group_by_control.set_args(args);

return args;


+ 12
- 0
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,


+ 17
- 2
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;
}
}

+ 125
- 0
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;
}
}
}
}
}
}

+ 6
- 0
frappe/public/scss/website/index.scss 查看文件

@@ -99,6 +99,12 @@
}
}

.page-header-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
}

.breadcrumb-container {
margin-top: 1rem;
padding-top: 0.25rem;


+ 2
- 0
frappe/sessions.py 查看文件

@@ -171,6 +171,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()


+ 3
- 1
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',


+ 13
- 1
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
})
})

+ 17
- 176
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
}

+ 8
- 0
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

+ 3
- 3
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) %}
<span class="avatar avatar-small" title="{{ user_info.name }}" style="{{ css_style or '' }}">
<span class="avatar {{ size }}" title="{{ user_info.name }}" style="{{ css_style or '' }}">
{% if user_info.image %}
<img
class="avatar-frame standard-image"
@@ -11,7 +11,7 @@
<span
class="avatar-frame standard-image"
title="{{ user_info.name }}">
{{ frappe.utils.get_abbr(user_info.name) }}
{{ frappe.utils.get_abbr(user_info.name).upper() }}
</span>
{% endif %}
</span>

+ 13
- 17
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 %}

<div class="comment-row media">
{{ square_image_with_fallback(src=frappe.get_gravatar(comment.comment_email or comment.sender), size='extra-small', alt=comment.sender_full_name, class='align-self-start mr-4') }}
<div class="media-body">
<div class="d-flex justify-content-between align-items-start">
<span class="font-weight-bold text-muted">
{{ comment.sender_full_name or comment.comment_by }}
</span>
<span class="text-muted small">
{{ comment.creation | global_date_format }}
</span>
</div>
<div class="text-muted">
{{ comment.content | markdown }}
</div>
</div>
</div>
<div class="comment-row media my-5">
<div class="comment-avatar">
{{ avatar(user_id=(comment.comment_email or comment.sender), size='avatar-medium') }}
</div>
<div class="comment-content">
<div class="head mb-2">
<span class="title font-weight-bold mr-2">{{ comment.sender_full_name or comment.comment_by }}</span>
<span class="time small text-muted">{{ frappe.utils.pretty_date(comment.creation) }}</span>
</div>
<div class="content">{{ comment.content | markdown }}</div>
</div>
</div>

+ 129
- 68
frappe/templates/includes/comments/comments.html 查看文件

@@ -1,86 +1,146 @@
<div class="comment-view mb-6">
{% if comment_text %}
<div class="comment-header mb-6">{{ comment_text }}</div>
{% endif %}
{% if not comment_list %}
<div class="no-comment">
<p class="text-muted small">{{ _("No comments yet. Start a new discussion.") }}</p>
</div>
<div class="no-comment">
<p class="text-muted small">{{ _("No comments yet. ") }}
<span class="hidden login-required">
<a href="/login?redirect-to={{ pathname }}">{{ _("Login to start a new discussion") }}</a>
</span>
<span class="hidden start-discussion">{{ _("Start a new discussion") }}</span>
</p>
</div>
{% endif %}

<div itemscope itemtype="http://schema.org/UserComments" id="comment-list">
{% for comment in comment_list %}
<div class="my-3">
{% include "templates/includes/comments/comment.html" %}
{% if not is_communication %}
<div class="add-comment-section mb-5">
<div class="comment-form-wrapper">
<div id="comment-form">
<form class="new-comment">
<fieldset class="new-comment-fields">
<div class="user-details row" style="margin-bottom: 15px; display:none;">
<div class="comment-by col-sm-6 pb-4">
<div class="form-label mb-1">{{ _("Your Name") }}</div>
<input class="form-control comment_by" name="comment_by" type="text">
</div>
<div class="col-sm-6">
<div class="form-label mb-1">{{ _("Email") }}</div>
<input class="form-control comment_email" name="comment_email" type="email">
</div>
</div>
<div class="comment-text-area">
<div class="form-label mb-1">{{ _("Add a comment") }}</div>
<textarea class="form-control" name="comment" rows=5 ></textarea>
<div class="text-muted small mt-1 mb-4">{{ _("Ctrl+Enter to add comment") }}</div>
</div>
<button class="btn btn-sm small" id="submit-comment">{{ _("Comment") }}</button>
</fieldset>
</form>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}

{% if not is_communication %}
<div class="add-comment-section">
<div class="text-muted hidden login-required">
<a href="/login?redirect-to={{ pathname }}">{{ _("Login to comment") }}</a>
</div>
<hr class="add-comment-hr my-5">

<div class="comment-form-wrapper">
<a class="add-comment btn btn-light btn-sm">{{ _("Add Comment") }}</a>
<div style="display: none;" id="comment-form">
<p>{{ _("Leave a Comment") }}</p>
<div class="alert" style="display:none;"></div>
<form>
<fieldset>
<div class="row" style="margin-bottom: 15px;">
<div class="col-sm-6">
<input class="form-control comment_by" name="comment_by" placeholder="{{ _("Your Name") }}" type="text">
</div>
<div class="col-sm-6">
<input class="form-control comment_email" name="comment_email" placeholder="{{ _("Your Email Address") }}" type="email">
</div>
</div>
<p><textarea class="form-control" name="comment" rows=10
placeholder="{{ _("Comment") }}"></textarea></p>
<button class="btn btn-primary btn-sm" id="submit-comment" style="margin-top:10px">{{ _("Submit") }}</button>
</fieldset>
</form>
<div itemscope itemtype="http://schema.org/UserComments" id="comment-list">
<div class="add-comment mb-5">
<div class="timeline-dot"></div>
<button class="btn btn-sm small add-comment-button">{{ _("Add a comment") }}</button>
</div>
<div class="comment-list">
{% for comment in comment_list %}
{% include "templates/includes/comments/comment.html" %}
{% endfor %}
</div>
</div>
</div>
{% endif %}

<script>
frappe.ready(function() {
let guest_allowed = "{{ guest_allowed or ''}}";
let guest_allowed = parseInt("{{ guest_allowed or 0}}");
let comment_count = "{{ comment_text }}";
let full_name = ""
let user_id = "";

let update_timeline_line_length = function(direction, size) {
if (direction == 'top') {
$('.blog-container')[0].style.setProperty('--comment-timeline-top', size);
} else {
let comment_timeline_bottom = $('.comment-list .comment-row:last-child').height() - 10;
$('.blog-container')[0].style.setProperty('--comment-timeline-bottom', comment_timeline_bottom +'px');
}
}

let show_comment_box = function() {
$('.comment-form-wrapper').show();
update_timeline_line_length('top', '-20px');
$('.add-comment-hr').hide();
$('.add-comment').hide();
}

let hide_comment_box = function() {
$('.comment-form-wrapper').hide();
update_timeline_line_length('top', '8px');
update_timeline_line_length('bottom');
$('.add-comment-hr').show();
$('.add-comment').show();
}

let $comment_count = $(`
<div class="feedback-item">
<span class="comment-icon">${frappe.utils.icon('small-message', 'md')}</span>
<span class="comment-count"></span>
</div>
`);

$('form').keydown(function(event) {
if (event.ctrlKey && event.keyCode === 13) {
$(this).find('#submit-comment').trigger('click');
}
})

if (!frappe.is_user_logged_in()) {
!guest_allowed && $(".login-required, .comment-form-wrapper").toggleClass("hidden");
$(".user-details").toggle('hide');
if (guest_allowed) {
$('.start-discussion').removeClass('hidden');
} else {
$(".login-required, .comment-form-wrapper").toggleClass("hidden");

$('.add-comment-button').text('{{ _("Login to comment") }}');
$('.add-comment-button').click(() => {
window.location.href = '/login?redirect-to={{ pathname }}';
});
}
} else {
$('input.comment_by').prop("disabled", true);
$('input.comment_email').prop("disabled", true);
}

var n_comments = $(".comment-row").length;
full_name = frappe.get_cookie("full_name");
user_id = frappe.get_cookie("user_id");
if(user_id != "Guest") {
$("[name='comment_email']").val(user_id);
$("[name='comment_by']").val(full_name);
}

if(n_comments) {
$(".no_comment").toggle(false);
$('.start-discussion').removeClass('hidden');
}

$('.blog-feedback').append($comment_count);
$('.comment-count').text(comment_count);
$("#comment-form textarea").val("");

update_timeline_line_length('bottom');

let n_comments = $(".comment-row").length;
n_comments ? $(".no_comment").toggle(false) : show_comment_box();

if(n_comments > 50) {
$(".add-comment").toggle(false)
.parent().append("<div class='text-muted'>Comments are closed.</div>")
}
$(".add-comment").click(function() {
$(this).toggle(false);
$("#comment-form").toggle();
var full_name = "", user_id = "";
if(frappe.is_user_logged_in()) {
full_name = frappe.get_cookie("full_name");
user_id = frappe.get_cookie("user_id");
if(user_id != "Guest") {
$("[name='comment_email']").val(user_id);
$("[name='comment_by']").val(full_name);
}
}
$("#comment-form textarea").val("");
})

$('.add-comment-button').click(() => {
show_comment_box();
});

$("#submit-comment").click(function() {
var args = {
@@ -94,17 +154,17 @@
}

if(!args.comment_by || !args.comment_email || !args.comment) {
frappe.msgprint("{{ _("All fields are necessary to submit the comment.") }}");
frappe.msgprint('{{ _("All fields are necessary to submit the comment.") }}');
return false;
}

if (args.comment_email!=='Administrator' && !validate_email(args.comment_email)) {
frappe.msgprint("{{ _("Please enter a valid email address.") }}");
frappe.msgprint('{{ _("Please enter a valid email address.") }}');
return false;
}

if(!args.comment || !args.comment.trim()) {
frappe.msgprint("{{ _("Please add a valid comment.") }}");
frappe.msgprint('{{ _("Please add a valid comment.") }}');
return false;
}

@@ -119,17 +179,18 @@
frappe.msgprint(r._server_messages);
} else {
if (r.message) {
$(r.message).appendTo("#comment-list");
$(".add-comment").text(__("Add Another Comment"));
$(r.message).prependTo(".comment-list");
comment_count = cint(comment_count) + 1;
$('.comment-count').text(comment_count);
}
$(".no-comment, .add-comment").toggle(false);
$("#comment-form").toggle();
$(".add-comment").toggle();
$(".no-comment").toggle(false);
$("#comment-form textarea").val("");
hide_comment_box();
}
}
})

return false;
})
});
});
</script>

+ 3
- 0
frappe/templates/includes/comments/comments.py 查看文件

@@ -3,11 +3,14 @@
import frappe
import re
from frappe.website.utils import clear_cache
from frappe.rate_limiter import rate_limit
from frappe.utils import add_to_date, now
from frappe.website.doctype.blog_settings.blog_settings import get_comment_limit

from frappe import _

@frappe.whitelist(allow_guest=True)
@rate_limit(key='reference_name', limit=get_comment_limit, seconds=60*60)
def add_comment(comment, comment_email, comment_by, reference_doctype, reference_name, route):
doc = frappe.get_doc(reference_doctype, reference_name)



+ 28
- 145
frappe/templates/includes/feedback/feedback.html 查看文件

@@ -1,160 +1,43 @@
<div class="add-feedback-section">
<div class="feedback-form-wrapper">
<a class="give-feedback btn btn-light btn-sm">{{ _("How would you rate the blog?") }}</a>
<div style="display: none;" id="feedback-form">
<p>{{ _("How would you rate the blog?") }}</p>
<div class="alert" style="display:none;"></div>
<form>
<fieldset>
<div class="row" style="margin-bottom: 15px;">
<div class="col-sm-6">
<div class="rating">
{% for rating in [1, 2, 3, 4, 5 ,6, 7, 8, 9, 10] %}
<div class="icon rating-box" data-rating="{{ rating }}">{{ rating }}</div>
{% endfor %}
</div>
</div>
</div>
<p>
<textarea class="form-control" name="feedback" rows=10 placeholder="{{ _("Feedback") }}"></textarea>
</p>
<button class="btn btn-sm" id="toggle-feedback" style="margin-top:10px; margin-right:2px;">
{{ _("Back") }}
</button>
<button class="btn btn-primary btn-sm" id="submit-feedback" style="margin-top:10px">
{{ _("Submit") }}
</button>
</fieldset>
</form>
</div>
</div>
<div class="feedback-item mr-3">
<span class="like-icon"></span>
<span class="like-count"></span>
</div>

<script type="text/javascript">
frappe.ready(() => {
let feedback = "{{ user_feedback.feedback or ''}}"
let user_rating = parseInt("{{ user_feedback.rating or 0 }}")
let rating = user_rating;
feedback && $("#submit-feedback").html(__("Update"));

if (frappe.is_user_logged_in()) {
if (feedback) {
$("[name='feedback']").val(feedback);
toggle_feedback();
set_rating(rating);
}
let like = parseInt("{{ user_feedback.like or 0 }}");
let like_count = parseInt("{{ like_count or 0 }}");

let update_like = function() {
like = !like;
like ? like_count++ : like_count--;
toggle_like_icon(like);
$('.like-count').text(like_count);
}

$('.give-feedback').click(() => toggle_feedback());


$('.rating').find('.rating-box').hover((ev) => {
const el = $(ev.currentTarget);
rating = el.data('rating');
el.parent().children('.rating-box').each( function(e) {
if (e < rating) {
$(this).addClass('rating-hover');
} else {
$(this).removeClass('rating-hover');
}
});
}, (ev) => {
const el = $(ev.currentTarget);
el.parent().children('.rating-box').each( function() {
$(this).removeClass('rating-hover');
});
});

$('.rating').find('.rating-box').click((ev) => {
const el = $(ev.currentTarget);
rating = el.data('rating');
el.parent().children('.rating-box').each( function(e) {
if (e < rating) {
$(this).addClass('rating-click');
} else {
$(this).removeClass('rating-click');
}
});
});

$('#submit-feedback').click((ev) => {
let update = ev.target.innerText !== __("Submit");
let rating = $('.rating').find('.rating-click').length;
let args = {
reference_doctype: "{{ reference_doctype or doctype }}",
reference_name: "{{ reference_name or name }}",
rating: rating,
feedback: $("[name='feedback']").val()
}

if (args.rating == 0) {
frappe.msgprint("{{ _("Rating is required!") }}");
return false;
}
let toggle_like_icon = function(active) {
active ? $('.like-icon').addClass('gray') : $('.like-icon').removeClass('gray');
}

if (!args.feedback || !args.feedback.trim()) {
frappe.msgprint("{{ _("Please add a valid feedback.") }}");
return false;
}
$('.like-icon').append(frappe.utils.icon('heart', 'md'))
toggle_like_icon(like);

if (!update) {
frappe.call({
method: "frappe.templates.includes.feedback.feedback.add_feedback",
args: args,
callback: function(r) {
if (!r.message) {
return
}
toggle_feedback();
if (!frappe.is_user_logged_in()) {
$("[name='feedback']").val('');
set_rating(0);
} else {
feedback = $("[name='feedback']").val();
user_rating = rating;
$("#submit-feedback").html(__("Update"));
}
frappe.msgprint({message:__("Thank you for your valuable feedback!"), indicator:'green'});
}
})
} else {
if (feedback == $("[name='feedback']").val() && rating == user_rating) {
frappe.msgprint({message:__("Please update rating or feedback before saving."), indicator:'red'});
return false;
}
frappe.call({
method: "frappe.templates.includes.feedback.feedback.update_feedback",
args: args,
callback: function(r) {
toggle_feedback();
feedback = $("[name='feedback']").val();
user_rating = rating;
frappe.msgprint({message:__("Feedback updated successfully!"), indicator:'green'});
}
})
}
return false;
})
$('.like-count').text(like_count);

$('#toggle-feedback').click(() => {
toggle_feedback();
return false;
$('.like-icon').click(() => {
update_like();
update_feedback();
})

function set_rating(rating) {
let el = $('.rating').find('.rating-box');
el.children('.rating-box').prevObject.each( function(e) {
if (e < rating) {
$(this).addClass('rating-click');
} else {
$(this).removeClass('rating-click');
let update_feedback = function() {
return frappe.call({
method: "frappe.templates.includes.feedback.feedback.give_feedback",
args: {
reference_doctype: "{{ reference_doctype or doctype }}",
reference_name: "{{ reference_name or name }}",
like
}
});
}

function toggle_feedback() {
$(".give-feedback").toggle();
$("#feedback-form").toggle();
}
}
});
</script>

+ 15
- 28
frappe/templates/includes/feedback/feedback.py 查看文件

@@ -10,25 +10,8 @@ from frappe.website.doctype.blog_settings.blog_settings import get_feedback_limi

@frappe.whitelist(allow_guest=True)
@rate_limit(key='reference_name', limit=get_feedback_limit, seconds=60*60)
def add_feedback(reference_doctype, reference_name, rating, feedback):
doc = frappe.get_doc(reference_doctype, reference_name)
if doc.disable_feedback == 1:
return

doc = frappe.new_doc('Feedback')
doc.reference_doctype = reference_doctype
doc.reference_name = reference_name
doc.rating = rating
doc.feedback = feedback
doc.ip_address = frappe.local.request_ip
doc.save(ignore_permissions=True)

subject = _('New Feedback on {0}: {1}').format(reference_doctype, reference_name)
send_mail(doc, subject)
return doc

@frappe.whitelist()
def update_feedback(reference_doctype, reference_name, rating, feedback):
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:
return
@@ -39,22 +22,26 @@ def update_feedback(reference_doctype, reference_name, rating, feedback):
"reference_name": reference_name
}
d = frappe.get_all('Feedback', filters=filters, limit=1)
doc = frappe.get_doc('Feedback', d[0].name)
doc.rating = rating
doc.feedback = feedback
if d:
doc = frappe.get_doc('Feedback', d[0].name)
else:
doc = doc = frappe.new_doc('Feedback')
doc.reference_doctype = reference_doctype
doc.reference_name = reference_name
doc.ip_address = frappe.local.request_ip
doc.like = like
doc.save(ignore_permissions=True)

subject = _('Feedback updated on {0}: {1}').format(reference_doctype, reference_name)
subject = _('Feedback on {0}: {1}').format(reference_doctype, reference_name)
send_mail(doc, subject)
return doc

def send_mail(feedback, subject):
doc = frappe.get_doc(feedback.reference_doctype, feedback.reference_name)

message = ("<p>{0} ({1})</p>".format(feedback.feedback, feedback.rating)
+ "<p><a href='{0}/app/feedback/{1}' style='font-size: 80%'>{2}</a></p>".format(frappe.utils.get_request_site_address(),
feedback.name,
_("View Feedback")))
if feedback.like:
message = "<p>Hey, </p><p>You have received a ❤️ heart on your blog post <b>{0}</b></p>".format(feedback.reference_name)
else:
return

# notify creator
frappe.sendmail(


+ 1
- 1
frappe/templates/web.html 查看文件

@@ -14,7 +14,7 @@

{% block page_container %}
<main class="{% if not full_width %}container my-4{% endif %}">
<div class="d-flex justify-content-between align-items-center">
<div class="page-header-wrapper">
<div class="page-header">
{% block header %}{% endblock %}
</div>


+ 7
- 0
frappe/tests/test_db.py 查看文件

@@ -43,6 +43,13 @@ class TestDB(unittest.TestCase):
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()
todo2 = frappe.get_doc(dict(doctype='ToDo', description = 'test_set_value 2')).insert()


+ 22
- 0
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"]))

+ 15
- 0
frappe/utils/nestedset.py 查看文件

@@ -10,6 +10,8 @@
# 3. call update_nsm(doc_obj) in the on_upate method

# ------------------------------------------
from typing import Iterator

import frappe
from frappe import _
from frappe.model.document import Document
@@ -271,6 +273,19 @@ class NestedSet(Document):
def get_ancestors(self):
return get_ancestors_of(self.doctype, self.name)

def get_parent(self) -> "NestedSet":
"""Return the parent Document."""
parent_name = self.get(self.nsm_parent_field)
if parent_name:
return frappe.get_doc(self.doctype, parent_name)

def get_children(self) -> Iterator["NestedSet"]:
"""Return a generator that yields child Documents."""
child_names = frappe.get_list(self.doctype, filters={self.nsm_parent_field: self.name}, pluck="name")
for name in child_names:
yield frappe.get_doc(self.doctype, name)


def get_root_of(doctype):
"""Get root element of a DocType with a tree structure"""
result = frappe.db.sql("""select t1.name from `tab{0}` t1 where


+ 21
- 12
frappe/utils/weasyprint.py 查看文件

@@ -5,18 +5,6 @@ import click

import frappe

try:
from weasyprint import HTML, CSS
except OSError:
click.secho(
"\n".join(["WeasyPrint depdends on additional system dependencies.",
"Follow instructions specific to your operating system:",
"https://doc.courtbouillon.org/weasyprint/stable/first_steps.html"]),
fg="yellow"
)
raise


@frappe.whitelist()
def download_pdf(doctype, name, print_format, letterhead=None):
doc = frappe.get_doc(doctype, name)
@@ -121,6 +109,8 @@ class PrintFormatGenerator:
pdf: a bytes sequence
The rendered PDF.
"""
HTML, CSS = import_weasyprint()

self._make_header_footer()

self.context.update(
@@ -151,6 +141,8 @@ class PrintFormatGenerator:
element_height: float
The height of this element, which will be then translated in a html height
"""
HTML, CSS = import_weasyprint()

html = HTML(string=getattr(self, f"{element}_html"), base_url=self.base_url,)
element_doc = html.render(
stylesheets=[CSS(string="@page {size: A4 portrait; margin: 0;}")]
@@ -254,3 +246,20 @@ class PrintFormatGenerator:
if box.element_tag == element:
return box
return PrintFormatGenerator.get_element(box.all_children(), element)


def import_weasyprint():
try:
from weasyprint import HTML, CSS
return HTML, CSS
except OSError:
message = "\n".join([
"WeasyPrint depdends on additional system dependencies.",
"Follow instructions specific to your operating system:",
"https://doc.courtbouillon.org/weasyprint/stable/first_steps.html"
])
click.secho(
message,
fg="yellow"
)
frappe.throw(message)

+ 19
- 9
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 = True
context.guest_allowed = frappe.db.get_single_value("Blog Settings", "allow_guest_to_comment", cache=True)

def fetch_cta(self):
if frappe.db.get_single_value("Blog Settings", "show_cta_in_blog", cache=True):
@@ -139,26 +139,36 @@ class BlogPost(WebsiteGenerator):
context.comment_list = get_comment_list(self.doctype, self.name)

if not context.comment_list:
context.comment_text = _('No comments yet')
context.comment_text = 0
else:
if(len(context.comment_list)) == 1:
context.comment_text = _('1 comment')
else:
context.comment_text = _('{0} comments').format(len(context.comment_list))
context.comment_text = len(context.comment_list)

def load_feedback(self, context):
user = frappe.session.user
if user == 'Guest':
user = ''

feedback = frappe.get_all('Feedback',
fields=['feedback', 'rating'],
fields=['like'],
filters=dict(
reference_doctype=self.doctype,
reference_name=self.name,
ip_address=frappe.local.request_ip,
owner=user
)
)

like_count = 0

if frappe.db.count('Feedback'):
like_count = frappe.db.count('Feedback',
filters = dict(
reference_doctype = self.doctype,
reference_name = self.name,
like = True
)
)

context.user_feedback = feedback[0] if feedback else ''
context.like_count = like_count

def set_read_time(self):
content = self.content or self.content_html or ''


+ 12
- 12
frappe/website/doctype/blog_post/templates/blog_post.html 查看文件

@@ -43,21 +43,26 @@
) }}
{%- endif -%}
<div class="blog-footer">
<div>
{{ _('Published on') }} <time datetime="{{ published_on }}">{{ frappe.format_date(published_on) }}</time>
<div class="blog-feedback">
{% if not disable_feedback %}
{% include 'templates/includes/feedback/feedback.html' %}
{% endif %}
</div>
<div>
{% if social_links %}
{% if social_links %}
<div>
{% for link in social_links %}
<a href="{{ link.link }}" class="text-muted ml-2 fa fa-{{ link.icon }}" target="_blank"></a>
{% endfor %}
{% endif %}
</div>
{% endif %}
<div>
{{ _('Published on') }} <time datetime="{{ published_on }}">{{ frappe.format_date(published_on) }}</time>
</div>
</div>

{% if blogger_info %}
<hr class="my-5">
{% include "templates/includes/blog/blogger.html" %}
<hr class="mt-2 mb-5">
{% include "templates/includes/blog/blogger.html" %}
{% endif %}

{% if not disable_comments %}
@@ -65,11 +70,6 @@
{% include 'templates/includes/comments/comments.html' %}
</div>
{% endif %}
{% if not disable_feedback %}
<div class="blog-feedback">
{% include 'templates/includes/feedback/feedback.html' %}
</div>
{% endif %}

</div>
<script>


+ 23
- 3
frappe/website/doctype/blog_settings/blog_settings.json 查看文件

@@ -10,6 +10,7 @@
"column_break",
"enable_social_sharing",
"show_cta_in_blog",
"allow_guest_to_comment",
"cta_section",
"title",
"subtitle",
@@ -17,7 +18,9 @@
"cta_label",
"cta_url",
"section_break_12",
"feedback_limit"
"feedback_limit",
"column_break_14",
"comment_limit"
],
"fields": [
{
@@ -86,18 +89,35 @@
"fieldtype": "Section Break"
},
{
"default": "1",
"default": "5",
"description": "Feedback limit per hour",
"fieldname": "feedback_limit",
"fieldtype": "Int",
"label": "Feedback limit"
},
{
"default": "5",
"description": "Comment limit per hour",
"fieldname": "comment_limit",
"fieldtype": "Int",
"label": "Comment limit"
},
{
"fieldname": "column_break_14",
"fieldtype": "Column Break"
},
{
"default": "1",
"fieldname": "allow_guest_to_comment",
"fieldtype": "Check",
"label": "Allow guest to comment"
}
],
"icon": "fa fa-cog",
"idx": 1,
"issingle": 1,
"links": [],
"modified": "2021-09-30 13:00:18.887103",
"modified": "2021-10-28 20:44:44.143193",
"modified_by": "Administrator",
"module": "Website",
"name": "Blog Settings",


+ 4
- 1
frappe/website/doctype/blog_settings/blog_settings.py 查看文件

@@ -15,4 +15,7 @@ class BlogSettings(Document):
clear_cache("writers")

def get_feedback_limit():
return frappe.db.get_single_value("Blog Settings", "feedback_limit") or 0
return frappe.db.get_single_value("Blog Settings", "feedback_limit") or 5

def get_comment_limit():
return frappe.db.get_single_value("Blog Settings", "comment_limit") or 5

+ 7
- 0
frappe/website/doctype/website_theme/website_theme_template.scss 查看文件

@@ -43,5 +43,12 @@ body {
--text-color: #{$body-text-color};
--text-light: #{$body-text-color};
{% endif -%}
{% if not button_rounded_corners %}
--border-radius-sm: 0px;
--border-radius: 0px;
--border-radius-md: 0px;
--border-radius-lg: 0px;
--border-radius-full: 0px;
{% endif -%}
}


+ 1
- 1
frappe/www/app.html 查看文件

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


Loading…
取消
儲存