@@ -10,6 +10,7 @@ concurrency: | |||
jobs: | |||
test: | |||
runs-on: ubuntu-latest | |||
timeout-minutes: 60 | |||
name: Patch Test | |||
@@ -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 |
@@ -13,6 +13,7 @@ concurrency: | |||
jobs: | |||
test: | |||
runs-on: ubuntu-latest | |||
timeout-minutes: 60 | |||
strategy: | |||
fail-fast: false | |||
@@ -13,6 +13,7 @@ concurrency: | |||
jobs: | |||
test: | |||
runs-on: ubuntu-latest | |||
timeout-minutes: 60 | |||
strategy: | |||
fail-fast: false | |||
@@ -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'); | |||
@@ -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 | |||
@@ -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, | |||
@@ -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() | |||
}) |
@@ -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", | |||
@@ -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"}) | |||
@@ -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", | |||
@@ -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(): | |||
@@ -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 {} | |||
@@ -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 | |||
} | |||
} |
@@ -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) |
@@ -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(); | |||
@@ -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'); | |||
} | |||
}); | |||
@@ -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({ | |||
@@ -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(); | |||
@@ -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) { | |||
@@ -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) { | |||
@@ -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")) { | |||
@@ -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); | |||
}; |
@@ -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]; | |||
@@ -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}, | |||
@@ -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) { | |||
@@ -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; | |||
@@ -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, | |||
@@ -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; | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} |
@@ -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; | |||
@@ -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() | |||
@@ -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', | |||
@@ -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 | |||
}) | |||
}) |
@@ -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 | |||
} |
@@ -0,0 +1,8 @@ | |||
# Copyright (c) 2021, Frappe Technologies and Contributors | |||
# See license.txt | |||
# import frappe | |||
import unittest | |||
class TestEnergyPointSettings(unittest.TestCase): | |||
pass |
@@ -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> |
@@ -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> |
@@ -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,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) | |||
@@ -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> |
@@ -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( | |||
@@ -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> | |||
@@ -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() | |||
@@ -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"])) |
@@ -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 | |||
@@ -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) |
@@ -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 '' | |||
@@ -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> | |||
@@ -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", | |||
@@ -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 |
@@ -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,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"> | |||