Merge branch 'develop' of github.com:frappe/frappe into multistep_webforms

This commit is contained in:
hrwx 2021-11-25 15:29:21 +00:00
commit c7c46a55de
102 changed files with 1584 additions and 764 deletions

View file

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

View file

@ -14,6 +14,7 @@ concurrency:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
@ -128,4 +129,4 @@ jobs:
fail_ci_if_error: true
files: /home/runner/frappe-bench/sites/coverage.xml
verbose: true
flags: server
flags: server

View file

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

View file

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

View file

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

View file

@ -7,6 +7,8 @@ context('Report View', () => {
cy.visit('/app/website');
cy.insert_doc('DocType', custom_submittable_doctype, true);
cy.clear_cache();
});
it('Field with enabled allow_on_submit should be editable.', () => {
cy.insert_doc(doctype_name, {
'title': 'Doc 1',
'description': 'Random Text',
@ -14,8 +16,6 @@ context('Report View', () => {
// submit document
'docstatus': 1
}, true).as('doc');
});
it('Field with enabled allow_on_submit should be editable.', () => {
cy.intercept('POST', 'api/method/frappe.client.set_value').as('value-update');
cy.visit(`/app/List/${doctype_name}/Report`);
// check status column added from docstatus

View file

@ -1523,8 +1523,8 @@ def format(*args, **kwargs):
import frappe.utils.formatters
return frappe.utils.formatters.format_value(*args, **kwargs)
def get_print(doctype=None, name=None, print_format=None, style=None,
html=None, as_pdf=False, doc=None, output=None, no_letterhead=0, password=None):
def get_print(doctype=None, name=None, print_format=None, style=None, html=None,
as_pdf=False, doc=None, output=None, no_letterhead=0, password=None, pdf_options=None):
"""Get Print Format for given document.
:param doctype: DocType of document.
@ -1543,15 +1543,15 @@ def get_print(doctype=None, name=None, print_format=None, style=None,
local.form_dict.doc = doc
local.form_dict.no_letterhead = no_letterhead
options = None
pdf_options = pdf_options or {}
if password:
options = {'password': password}
pdf_options['password'] = password
if not html:
html = get_response_content("printview")
if as_pdf:
return get_pdf(html, output = output, options = options)
return get_pdf(html, options=pdf_options, output=output)
else:
return html

View file

@ -120,6 +120,8 @@ def init_request(request):
else:
frappe.connect(set_admin_as_user=False)
request.max_content_length = frappe.local.conf.get('max_file_size') or 10 * 1024 * 1024
make_form_dict(request)
if request.method != "OPTIONS":

View file

@ -461,6 +461,7 @@ def migrate(context, skip_failing=False, skip_search_index=False):
skip_search_index=skip_search_index
)
finally:
print()
frappe.destroy()
if not context.sites:
raise SiteNotSpecifiedError

View file

@ -791,10 +791,11 @@ def request(context, args=None, path=None):
@click.command('make-app')
@click.argument('destination')
@click.argument('app_name')
def make_app(destination, app_name):
@click.option('--no-git', is_flag=True, default=False, help='Do not initialize git repository for the app')
def make_app(destination, app_name, no_git=False):
"Creates a boilerplate app"
from frappe.utils.boilerplate import make_boilerplate
make_boilerplate(destination, app_name)
make_boilerplate(destination, app_name, no_git=no_git)
@click.command('set-config')

View file

@ -11,11 +11,26 @@ class AccessLog(Document):
@frappe.whitelist()
def make_access_log(
doctype=None,
document=None,
method=None,
file_type=None,
report_name=None,
filters=None,
page=None,
columns=None,
):
_make_access_log(
doctype, document, method, file_type, report_name, filters, page, columns,
)
@frappe.write_only()
@retry(
stop=stop_after_attempt(3), retry=retry_if_exception_type(frappe.DuplicateEntryError)
)
def make_access_log(
def _make_access_log(
doctype=None,
document=None,
method=None,
@ -42,6 +57,7 @@ def make_access_log(
}).db_insert()
# `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview`
# dont commit in test mode
# dont commit in test mode. It must be tempting to put this block along with the in_request in the
# whitelisted method...yeah, don't do it. That part would be executed possibly on a read only DB conn
if not frappe.flags.in_test or in_request:
frappe.db.commit()

View file

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

View file

@ -146,25 +146,43 @@ def add_attachments(name, attachments):
})
_file.save(ignore_permissions=True)
@frappe.whitelist(allow_guest=True)
def mark_email_as_seen(name=None):
@frappe.whitelist(allow_guest=True, methods=("GET",))
def mark_email_as_seen(name: str = None):
try:
if name and frappe.db.exists("Communication", name) and not frappe.db.get_value("Communication", name, "read_by_recipient"):
frappe.db.set_value("Communication", name, "read_by_recipient", 1)
frappe.db.set_value("Communication", name, "delivery_status", "Read")
frappe.db.set_value("Communication", name, "read_by_recipient_on", get_datetime())
frappe.db.commit()
update_communication_as_read(name)
frappe.db.commit() # nosemgrep: this will be called in a GET request
except Exception:
frappe.log_error(frappe.get_traceback())
finally:
# Return image as response under all circumstances
from PIL import Image
import io
im = Image.new('RGBA', (1, 1))
im.putdata([(255,255,255,0)])
buffered_obj = io.BytesIO()
im.save(buffered_obj, format="PNG")
frappe.response["type"] = 'binary'
frappe.response["filename"] = "imaginary_pixel.png"
frappe.response["filecontent"] = buffered_obj.getvalue()
finally:
frappe.response.update({
"type": "binary",
"filename": "imaginary_pixel.png",
"filecontent": (
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00"
b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\r"
b"IDATx\x9cc\xf8\xff\xff?\x03\x00\x08\xfc\x02\xfe\xa7\x9a\xa0"
b"\xa0\x00\x00\x00\x00IEND\xaeB`\x82"
)
})
def update_communication_as_read(name):
if not name or not isinstance(name, str):
return
communication = frappe.db.get_value(
"Communication",
name,
"read_by_recipient",
as_dict=True
)
if not communication or communication.read_by_recipient:
return
frappe.db.set_value("Communication", name, {
"read_by_recipient": 1,
"delivery_status": "Read",
"read_by_recipient_on": get_datetime()
})

View file

@ -191,7 +191,7 @@ class Exporter:
[format_column_name(df) for df in self.fields if df.parent == child_table_doctype]
)
)
data = frappe.db.get_list(
data = frappe.db.get_all(
child_table_doctype,
filters={
"parent": ("in", parent_names),

View file

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

View file

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

View file

@ -716,13 +716,11 @@ def delete_file(path):
os.remove(path)
@frappe.whitelist()
def get_max_file_size():
return cint(conf.get('max_file_size')) or 10485760
def has_permission(doc, ptype=None, user=None):
has_access = False
user = user or frappe.session.user

View file

@ -13,6 +13,9 @@ class NavbarSettings(Document):
def validate_standard_navbar_items(self):
doc_before_save = self.get_doc_before_save()
if not doc_before_save:
return
before_save_items = [item for item in \
doc_before_save.help_dropdown + doc_before_save.settings_dropdown if item.is_standard]
@ -32,7 +35,3 @@ def get_app_logo():
def get_navbar_settings():
navbar_settings = frappe.get_single('Navbar Settings')
return navbar_settings

View file

@ -51,6 +51,14 @@ class Report(Document):
and not frappe.flags.in_patch):
frappe.throw(_("You are not allowed to delete Standard Report"))
delete_custom_role('report', self.name)
self.delete_prepared_reports()
def delete_prepared_reports(self):
prepared_reports = frappe.get_all("Prepared Report", filters={'ref_report_doctype': self.name}, pluck='name')
for report in prepared_reports:
frappe.delete_doc("Prepared Report", report, ignore_missing=True, force=True,
delete_permanently=True)
def get_columns(self):
return [d.as_dict(no_default_fields = True) for d in self.columns]

View file

@ -2,15 +2,22 @@
# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
desk_properties = ("search_bar", "notifications", "list_sidebar",
"bulk_actions", "view_switcher", "form_sidebar", "timeline", "dashboard")
STANDARD_ROLES = (
"Administrator",
"System Manager",
"Script Manager",
"All",
"Guest"
)
class Role(Document):
def before_rename(self, old, new, merge=False):
if old in ("Guest", "Administrator", "System Manager", "All"):
if old in STANDARD_ROLES:
frappe.throw(frappe._("Standard roles cannot be renamed"))
def after_insert(self):
@ -23,7 +30,7 @@ class Role(Document):
self.set_desk_properties()
def disable_role(self):
if self.name in ("Guest", "Administrator", "System Manager", "All"):
if self.name in STANDARD_ROLES:
frappe.throw(frappe._("Standard roles cannot be disabled"))
else:
self.remove_roles()

View file

@ -10,6 +10,13 @@ frappe.ui.form.on('Server Script', {
frm.dashboard.hide();
}
if (!frm.is_new()) {
frm.add_custom_button(__('Compare Versions'), () => {
new frappe.ui.DiffView("Server Script", "script", frm.doc.name);
});
}
frm.call('get_autocompletion_items')
.then(r => r.message)
.then(items => {

View file

@ -599,7 +599,7 @@
"fieldname": "desk_theme",
"fieldtype": "Select",
"label": "Desk Theme",
"options": "Light\nDark"
"options": "Light\nDark\nAutomatic"
},
{
"fieldname": "module_profile",
@ -669,7 +669,7 @@
}
],
"max_attachments": 5,
"modified": "2021-10-27 17:17:16.098457",
"modified": "2021-11-17 17:17:16.098457",
"modified_by": "Administrator",
"module": "Core",
"name": "User",

View file

@ -1046,7 +1046,7 @@ def generate_keys(user):
@frappe.whitelist()
def switch_theme(theme):
if theme in ["Dark", "Light"]:
if theme in ["Dark", "Light", "Automatic"]:
frappe.db.set_value("User", frappe.session.user, "desk_theme", theme)
def get_enabled_users():

View file

@ -43,6 +43,12 @@ frappe.ui.form.on('Client Script', {
d.show();
});
});
if (!frm.is_new()) {
frm.add_custom_button(__('Compare Versions'), () => {
new frappe.ui.DiffView("Client Script", "script", frm.doc.name);
});
}
}
frm.set_query('dt', {

View file

@ -568,11 +568,10 @@ class Database(object):
def _get_value_for_many_names(self, doctype, names, field, debug=False, run=True):
names = list(filter(None, names))
if names:
return self.get_all(doctype,
fields=['name', field],
filters=[['name', 'in', names]],
fields=field,
filters=names,
debug=debug, as_list=1, run=run)
else:
return {}

View file

@ -53,7 +53,7 @@
},
{
"fieldname": "subject",
"fieldtype": "Data",
"fieldtype": "Small Text",
"in_global_search": 1,
"in_list_view": 1,
"label": "Subject",
@ -277,10 +277,11 @@
"icon": "fa fa-calendar",
"idx": 1,
"links": [],
"modified": "2020-01-14 21:47:15.825287",
"modified": "2021-11-18 05:06:24.881742",
"modified_by": "Administrator",
"module": "Desk",
"name": "Event",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{

View file

@ -11,6 +11,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils.user import get_enabled_system_users
from frappe.desk.reportview import get_filters_cond
from frappe.desk.doctype.notification_settings.notification_settings import is_email_notifications_enabled_for_type
weekdays = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"]
communication_mapping = {"": "Event", "Event": "Event", "Meeting": "Meeting", "Call": "Phone", "Sent/Received Email": "Email", "Other": "Other"}
@ -141,7 +142,12 @@ def has_permission(doc, user):
def send_event_digest():
today = nowdate()
for user in get_enabled_system_users():
# select only those users that have event reminder email notifications enabled
users = [user for user in get_enabled_system_users() if
is_email_notifications_enabled_for_type(user.name, 'Event Reminders')]
for user in users:
events = get_events(today, today, user.name, for_reminder=True)
if events:
frappe.set_user_lang(user.name, user.language)

View file

@ -14,8 +14,11 @@
"enable_email_assignment",
"enable_email_energy_point",
"enable_email_share",
"enable_email_event_reminders",
"user",
"seen"
"seen",
"system_notifications_section",
"energy_points_system_notifications"
],
"fields": [
{
@ -84,15 +87,34 @@
"fieldtype": "Check",
"hidden": 1,
"label": "Seen"
},
{
"fieldname": "system_notifications_section",
"fieldtype": "Section Break",
"label": "System Notifications"
},
{
"default": "1",
"fieldname": "energy_points_system_notifications",
"fieldtype": "Check",
"label": "Energy Points"
},
{
"default": "1",
"depends_on": "enable_email_notifications",
"fieldname": "enable_email_event_reminders",
"fieldtype": "Check",
"label": "Event Reminders"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-11-04 12:54:57.989317",
"modified": "2021-11-24 14:45:31.931154",
"modified_by": "Administrator",
"module": "Desk",
"name": "Notification Settings",
"naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{

View file

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

6
frappe/email/doctype/newsletter/newsletter.py Executable file → Normal file
View file

@ -159,11 +159,10 @@ class Newsletter(WebsiteGenerator):
def send_newsletter(self, emails: List[str]):
"""Trigger email generation for `emails` and add it in Email Queue.
"""
# TODO: get rid of this maybe?
message = self.get_message()
attachments = self.get_newsletter_attachments()
sender = self.send_from or frappe.utils.get_formatted_email(self.owner)
args = {"message": message, "name": self.name}
args = self.as_dict()
args["message"] = self.get_message()
is_auto_commit_set = bool(frappe.db.auto_commit_on_many_writes)
frappe.db.auto_commit_on_many_writes = not frappe.flags.in_test
@ -172,7 +171,6 @@ class Newsletter(WebsiteGenerator):
subject=self.subject,
sender=sender,
recipients=emails,
message=message,
attachments=attachments,
template="newsletter",
add_unsubscribe_link=self.send_unsubscribe_link,

View file

@ -240,6 +240,7 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
if not dry_run:
remove_from_installed_apps(app_name)
frappe.get_single('Installed Applications').update_versions()
frappe.db.commit()
click.secho(f"Uninstalled App {app_name} from Site {site}", fg="green")

View file

@ -175,6 +175,8 @@ def parse_naming_series(parts, doctype='', doc=''):
part = today.strftime("%d")
elif e == 'YYYY':
part = today.strftime('%Y')
elif e == 'WW':
part = determine_consecutive_week_number(today)
elif e == 'timestamp':
part = str(today)
elif e == 'FY':
@ -193,6 +195,19 @@ def parse_naming_series(parts, doctype='', doc=''):
return n
def determine_consecutive_week_number(datetime):
"""Determines the consecutive calendar week"""
m = datetime.month
# ISO 8601 calandar week
w = datetime.strftime('%V')
# Ensure consecutiveness for the first and last days of a year
if m == 1 and int(w) >= 52:
w = '00'
elif m == 12 and int(w) <= 1:
w = '53'
return w
def getseries(key, digits):
# series created ?
# Using frappe.qb as frappe.get_values does not allow order_by=None

View file

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

View file

@ -10,6 +10,8 @@
"repeat_header_footer",
"column_break_4",
"pdf_page_size",
"pdf_page_height",
"pdf_page_width",
"view_link_in_email",
"with_letterhead",
"allow_print_for_draft",
@ -56,7 +58,7 @@
"fieldname": "pdf_page_size",
"fieldtype": "Select",
"label": "PDF Page Size",
"options": "A4\nLetter"
"options": "A0\nA1\nA2\nA3\nA4\nA5\nA6\nA7\nA8\nA9\nB0\nB1\nB2\nB3\nB4\nB5\nB6\nB7\nB8\nB9\nB10\nC5E\nComm10E\nDLE\nExecutive\nFolio\nLedger\nLegal\nLetter\nTabloid\nCustom"
},
{
"fieldname": "view_link_in_email",
@ -156,6 +158,18 @@
"fieldname": "font_size",
"fieldtype": "Float",
"label": "Font Size"
},
{
"depends_on": "eval:doc.pdf_page_size == \"Custom\"",
"fieldname": "pdf_page_height",
"fieldtype": "Float",
"label": "PDF Page Height (in mm)"
},
{
"depends_on": "eval:doc.pdf_page_size == \"Custom\"",
"fieldname": "pdf_page_width",
"fieldtype": "Float",
"label": "PDF Page Width (in mm)"
}
],
"icon": "fa fa-cog",

View file

@ -8,14 +8,23 @@ from frappe.utils import cint
from frappe.model.document import Document
class PrintSettings(Document):
def validate(self):
if self.pdf_page_size == "Custom" and not (
self.pdf_page_height and self.pdf_page_width
):
frappe.throw(_("Page height and width cannot be zero"))
def on_update(self):
frappe.clear_cache()
@frappe.whitelist()
def is_print_server_enabled():
if not hasattr(frappe.local, 'enable_print_server'):
frappe.local.enable_print_server = cint(frappe.db.get_single_value('Print Settings',
'enable_print_server'))
if not hasattr(frappe.local, "enable_print_server"):
frappe.local.enable_print_server = cint(
frappe.db.get_single_value("Print Settings", "enable_print_server")
)
return frappe.local.enable_print_server

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View file

@ -62,6 +62,7 @@ import "./frappe/utils/help_links.js";
import "./frappe/utils/address_and_contact.js";
import "./frappe/utils/preview_email.js";
import "./frappe/utils/file_manager.js";
import "./frappe/utils/diffview";
import "./frappe/upload.js";
import "./frappe/ui/tree.js";

View file

@ -64,6 +64,19 @@ frappe.Application = class Application {
}
});
frappe.ui.add_system_theme_switch_listener();
const root = document.documentElement;
const observer = new MutationObserver(() => {
frappe.ui.set_theme();
});
observer.observe(root, {
attributes: true,
attributeFilter: ['data-theme-mode']
});
frappe.ui.set_theme();
// page container
this.make_page_container();
this.set_route();

View file

@ -29,21 +29,26 @@
</span>
</div>
<label v-if="is_optimizable" class="optimize-checkbox"><input type="checkbox" :checked="optimize" @change="$emit('toggle_optimize')">Optimize</label>
<div>
<span v-if="file.error_message" class="file-error text-danger">
{{ file.error_message }}
</span>
</div>
</div>
<div class="file-actions">
<ProgressRing
v-show="file.uploading && !uploaded"
v-show="file.uploading && !uploaded && !file.failed"
primary="var(--primary-color)"
secondary="var(--gray-200)"
radius="24"
:radius="24"
:progress="progress"
stroke="3"
:stroke="3"
/>
<div v-if="uploaded" v-html="frappe.utils.icon('solid-success', 'lg')"></div>
<div v-if="file.failed" v-html="frappe.utils.icon('solid-red', 'lg')"></div>
<div v-if="file.failed" v-html="frappe.utils.icon('solid-error', 'lg')"></div>
<div class="file-action-buttons">
<button v-if="is_cropable" class="btn btn-crop muted" @click="$emit('toggle_image_cropper')" v-html="frappe.utils.icon('crop', 'md')"></button>
<button v-if="!uploaded && !file.uploading" class="btn muted" @click="$emit('remove')" v-html="frappe.utils.icon('delete', 'md')"></button>
<button v-if="!uploaded && !file.uploading && !file.failed" class="btn muted" @click="$emit('remove')" v-html="frappe.utils.icon('delete', 'md')"></button>
</div>
</div>
</div>
@ -89,18 +94,18 @@ export default {
return this.file.doc ? this.file.doc.is_private : this.file.private;
},
uploaded() {
return this.file.total && this.file.total === this.file.progress && !this.file.failed;
return this.file.request_succeeded;
},
is_image() {
return this.file.file_obj.type.startsWith('image');
},
is_optimizable() {
let is_svg = this.file.file_obj.type == 'image/svg+xml';
return this.is_image && !is_svg;
return this.is_image && !is_svg && !this.uploaded && !this.file.failed;
},
is_cropable() {
let croppable_types = ['image/jpeg', 'image/png'];
return !this.uploaded && !this.file.uploading && croppable_types.includes(this.file.file_obj.type);
return !this.uploaded && !this.file.uploading && !this.file.failed && croppable_types.includes(this.file.file_obj.type);
},
progress() {
let value = Math.round((this.file.progress * 100) / this.file.total);
@ -208,4 +213,9 @@ export default {
align-items: center;
padding-top: 0.25rem;
}
.file-error {
font-size: var(--text-sm);
font-weight: var(--text-bold);
}
</style>

View file

@ -197,6 +197,7 @@ export default {
show_image_cropper: false,
crop_image_with_index: -1,
trigger_upload: false,
close_dialog: false,
hide_dialog_footer: false,
allow_take_photo: false,
allow_web_link: true,
@ -218,6 +219,12 @@ export default {
}
});
}
if (this.restrictions.max_file_size == null) {
frappe.call('frappe.core.doctype.file.file.get_max_file_size')
.then(res => {
this.restrictions.max_file_size = Number(res.message);
});
}
},
watch: {
files(newvalue, oldvalue) {
@ -289,6 +296,8 @@ export default {
progress: 0,
total: 0,
failed: false,
request_succeeded: false,
error_message: null,
uploading: false,
private: !is_image
}
@ -329,9 +338,17 @@ export default {
if (!is_correct_type) {
console.warn('File skipped because of invalid file type', file);
frappe.show_alert({
message: __('File "{0}" was skipped because of invalid file type', [file.name]),
indicator: 'orange'
});
}
if (!valid_file_size) {
console.warn('File skipped because of invalid file size', file.size, file);
frappe.show_alert({
message: __('File "{0}" was skipped because size exceeds {1} MB', [file.name, max_file_size / (1024 * 1024)]),
indicator: 'orange'
});
}
return is_correct_type && valid_file_size;
@ -357,9 +374,10 @@ export default {
let selected_file = this.$refs.file_browser.selected_node;
if (!selected_file.value) {
frappe.msgprint(__('Click on a file to select it.'));
this.close_dialog = true;
return Promise.reject();
}
this.close_dialog = true;
return this.upload_file({
file_url: selected_file.file_url
});
@ -368,9 +386,11 @@ export default {
let file_url = this.$refs.web_link.url;
if (!file_url) {
frappe.msgprint(__('Invalid URL'));
this.close_dialog = true;
return Promise.reject();
}
file_url = decodeURI(file_url)
this.close_dialog = true;
return this.upload_file({
file_url
});
@ -383,6 +403,7 @@ export default {
this.on_success && this.on_success(file);
})
);
this.close_dialog = true;
return Promise.all(promises);
},
upload_file(file, i) {
@ -410,6 +431,7 @@ export default {
xhr.onreadystatechange = () => {
if (xhr.readyState == XMLHttpRequest.DONE) {
if (xhr.status === 200) {
file.request_succeeded = true;
let r = null;
let file_doc = null;
try {
@ -426,15 +448,24 @@ export default {
if (this.on_success) {
this.on_success(file_doc, r);
}
if (i == this.files.length - 1 && this.files.every(file => file.request_succeeded)) {
this.close_dialog = true;
}
} else if (xhr.status === 403) {
file.failed = true;
let response = JSON.parse(xhr.responseText);
frappe.msgprint({
title: __('Not permitted'),
indicator: 'red',
message: response._error_message
});
file.error_message = `Not permitted. ${response._error_message || ''}`;
} else if (xhr.status === 413) {
file.failed = true;
file.error_message = 'Size exceeds the maximum allowed file size.';
} else {
file.failed = true;
file.error_message = xhr.status === 0 ? 'XMLHttpRequest Error' : `${xhr.status} : ${xhr.statusText}`;
let error = null;
try {
error = JSON.parse(xhr.responseText);

View file

@ -67,6 +67,12 @@ export default class FileUploader {
}
});
this.uploader.$watch('close_dialog', (close_dialog) => {
if (close_dialog) {
this.dialog && this.dialog.hide();
}
});
this.uploader.$watch('hide_dialog_footer', (hide_dialog_footer) => {
if (hide_dialog_footer) {
this.dialog && this.dialog.footer.addClass('hide');
@ -84,10 +90,8 @@ export default class FileUploader {
upload_files() {
this.dialog && this.dialog.get_primary_btn().prop('disabled', true);
return this.uploader.upload_files()
.then(() => {
this.dialog && this.dialog.hide();
});
this.dialog && this.dialog.get_secondary_btn().prop('disabled', true);
return this.uploader.upload_files();
}
make_dialog() {

View file

@ -3,6 +3,11 @@ frappe.provide('frappe.utils.utils');
frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.form.ControlData {
static horizontal = false
async make() {
await frappe.require(this.required_libs);
super.make();
}
make_wrapper() {
// Create the elements for map area
super.make_wrapper();
@ -196,4 +201,17 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
this.editableLayers.removeLayer(l);
});
}
get required_libs() {
return [
"assets/frappe/js/lib/leaflet/easy-button.css",
"assets/frappe/js/lib/leaflet/L.Control.Locate.css",
"assets/frappe/js/lib/leaflet/leaflet.draw.css",
"assets/frappe/js/lib/leaflet/leaflet.css",
"assets/frappe/js/lib/leaflet/leaflet.js",
"assets/frappe/js/lib/leaflet/easy-button.js",
"assets/frappe/js/lib/leaflet/leaflet.draw.js",
"assets/frappe/js/lib/leaflet/L.Control.Locate.js",
];
}
};

View file

@ -14,6 +14,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
this.progress_area = this.make_section({
css_class: 'progress-area',
hidden: 1,
collapsible: 1,
is_dashboard_section: 1,
});
@ -21,6 +22,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
label: __("Overview"),
css_class: 'form-heatmap',
hidden: 1,
collapsible: 1,
is_dashboard_section: 1,
body_html: `
<div id="heatmap-${frappe.model.scrub(this.frm.doctype)}" class="heatmap"></div>
@ -32,6 +34,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
label: __("Graph"),
css_class: 'form-graph',
hidden: 1,
collapsible: 1,
is_dashboard_section: 1
});
@ -40,6 +43,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
label: __("Stats"),
css_class: 'form-stats',
hidden: 1,
collapsible: 1,
is_dashboard_section: 1,
body_html: this.stats_area_row
});
@ -50,6 +54,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
label: __("Connections"),
css_class: 'form-links',
hidden: 1,
collapsible: 1,
is_dashboard_section: 1,
body_html: this.transactions_area
});
@ -84,9 +89,10 @@ frappe.ui.form.Dashboard = class FormDashboard {
hidden,
body_html,
make_card: true,
collapsible: 1,
is_dashboard_section: 1
};
return new Section(this.frm.layout.wrapper, options).body;
return new Section(this.parent, options).body;
}
add_progress(title, percent, message) {
@ -203,7 +209,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
after_refresh() {
// show / hide new buttons (if allowed)
this.links_area.body.find('.btn-new').each((i, el) => {
if (this.frm.can_create($(this).attr('data-doctype'))) {
if (this.frm.can_create($(el).attr('data-doctype'))) {
$(el).removeClass('hidden');
}
});

View file

@ -156,8 +156,11 @@ frappe.ui.form.Form = class FrappeForm {
let dashboard_parent = $('<div class="form-dashboard">');
let main_page = this.layout.tabs.length ? this.layout.tabs[0].wrapper : this.layout.wrapper;
main_page.prepend(dashboard_parent);
if (this.layout.tabs.length) {
this.layout.tabs[0].wrapper.prepend(dashboard_parent);
} else {
dashboard_parent.insertAfter(this.layout.wrapper.find('.form-message'));
}
this.dashboard = new frappe.ui.form.Dashboard(dashboard_parent, this);
this.tour = new frappe.ui.form.FormTour({

View file

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

View file

@ -245,7 +245,7 @@ frappe.ui.form.Layout = class Layout {
}
make_section(df) {
this.section = new Section(this.current_tab ? this.current_tab.wrapper : this.page, df, this.card_layout);
this.section = new Section(this.current_tab ? this.current_tab.wrapper : this.page, df, this.card_layout, this);
// append to layout fields
if (df) {

View file

@ -267,7 +267,7 @@ frappe.ui.form.QuickEntryForm = class QuickEntryForm {
render_edit_in_full_page_link() {
var me = this;
this.dialog.add_custom_action(
`${frappe.utils.icon('edit', 'xs')} ${__("Edit in full page")}`,
`${__("Edit in full page")}`,
() => me.open_doc(true)
);
}

View file

@ -1,5 +1,6 @@
export default class Section {
constructor(parent, df, card_layout) {
constructor(parent, df, card_layout, layout) {
this.layout = layout;
this.card_layout = card_layout;
this.parent = parent;
this.df = df || {};
@ -25,6 +26,7 @@ export default class Section {
${this.df.is_dashboard_section ? "form-dashboard-section" : "form-section"}
${ make_card ? "card-section" : "" }">
`).appendTo(this.parent);
this.layout && this.layout.sections.push(this);
if (this.df) {
if (this.df.label) {

View file

@ -24,51 +24,84 @@ export default class BulkOperations {
return;
}
if (valid_docs.length > 0) {
const dialog = new frappe.ui.Dialog({
title: __('Print Documents'),
fields: [
{
'fieldtype': 'Select',
'label': __('Letter Head'),
'fieldname': 'letter_sel',
'default': __('No Letterhead'),
options: this.get_letterhead_options()
},
{
'fieldtype': 'Select',
'label': __('Print Format'),
'fieldname': 'print_sel',
options: frappe.meta.get_print_formats(this.doctype)
}
]
});
dialog.set_primary_action(__('Print'), args => {
if (!args) return;
const default_print_format = frappe.get_meta(this.doctype).default_print_format;
const with_letterhead = args.letter_sel == __("No Letterhead") ? 0 : 1;
const print_format = args.print_sel ? args.print_sel : default_print_format;
const json_string = JSON.stringify(valid_docs);
const letterhead = args.letter_sel;
const w = window.open('/api/method/frappe.utils.print_format.download_multi_pdf?' +
'doctype=' + encodeURIComponent(this.doctype) +
'&name=' + encodeURIComponent(json_string) +
'&format=' + encodeURIComponent(print_format) +
'&no_letterhead=' + (with_letterhead ? '0' : '1') +
'&letterhead=' + encodeURIComponent(letterhead)
);
if (!w) {
frappe.msgprint(__('Please enable pop-ups'));
return;
}
});
dialog.show();
} else {
if (valid_docs.length === 0) {
frappe.msgprint(__('Select atleast 1 record for printing'));
return;
}
const dialog = new frappe.ui.Dialog({
title: __('Print Documents'),
fields: [{
fieldtype: 'Select',
label: __('Letter Head'),
fieldname: 'letter_sel',
default: __('No Letterhead'),
options: this.get_letterhead_options()
},
{
fieldtype: 'Select',
label: __('Print Format'),
fieldname: 'print_sel',
options: frappe.meta.get_print_formats(this.doctype)
},
{
fieldtype: 'Select',
label: __('Page Size'),
fieldname: 'page_size',
options: frappe.meta.get_print_sizes(),
default: print_settings.pdf_page_size
},
{
fieldtype: 'Float',
label: __('Page Height (in mm)'),
fieldname: 'page_height',
depends_on: 'eval:doc.page_size == "Custom"',
default: print_settings.pdf_page_height
},
{
fieldtype: 'Float',
label: __('Page Width (in mm)'),
fieldname: 'page_width',
depends_on: 'eval:doc.page_size == "Custom"',
default: print_settings.pdf_page_width
}]
});
dialog.set_primary_action(__('Print'), args => {
if (!args) return;
const default_print_format = frappe.get_meta(this.doctype).default_print_format;
const with_letterhead = args.letter_sel == __("No Letterhead") ? 0 : 1;
const print_format = args.print_sel ? args.print_sel : default_print_format;
const json_string = JSON.stringify(valid_docs);
const letterhead = args.letter_sel;
let pdf_options;
if (args.page_size === "Custom") {
if (args.page_height === 0 || args.page_width === 0) {
frappe.throw(__('Page height and width cannot be zero'));
}
pdf_options = JSON.stringify({ "page-height": args.page_height, "page-width": args.page_width });
} else {
pdf_options = JSON.stringify({ "page-size": args.page_size });
}
const w = window.open(
'/api/method/frappe.utils.print_format.download_multi_pdf?' +
'doctype=' + encodeURIComponent(this.doctype) +
'&name=' + encodeURIComponent(json_string) +
'&format=' + encodeURIComponent(print_format) +
'&no_letterhead=' + (with_letterhead ? '0' : '1') +
'&letterhead=' + encodeURIComponent(letterhead) +
'&options=' + encodeURIComponent(pdf_options)
);
if (!w) {
frappe.msgprint(__('Please enable pop-ups'));
return;
}
});
dialog.show();
}
get_letterhead_options () {

View file

@ -307,6 +307,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}
update_checkbox(target) {
if (!this.$checkbox_actions) return;
let $check_all_checkbox = this.$checkbox_actions.find(".list-check-all");
if ($check_all_checkbox.prop("checked") && target && !target.prop("checked")) {

View file

@ -192,6 +192,15 @@ $.extend(frappe.meta, {
}
},
get_print_sizes: function() {
return [
"A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9",
"B0", "B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", "B9", "B10",
"C5E", "Comm10E", "DLE", "Executive", "Folio", "Ledger", "Legal",
"Letter", "Tabloid", "Custom"
];
},
get_print_formats: function(doctype) {
var print_format_list = ["Standard"];
var default_print_format = locals.DocType[doctype].default_print_format;

View file

@ -42,7 +42,7 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
}
refresh() {
this.current_theme = document.documentElement.getAttribute("data-theme") || "light";
this.current_theme = document.documentElement.getAttribute("data-theme-mode") || "light";
this.fetch_themes().then(() => {
this.render();
});
@ -54,10 +54,17 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
{
name: "light",
label: __("Frappe Light"),
info: __("Light Theme")
},
{
name: "dark",
label: __("Timeless Night"),
info: __("Dark Theme")
},
{
name: "automatic",
label: __("Automatic"),
info: __("Uses system's theme to switch between light and dark mode")
}
];
@ -74,11 +81,15 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
}
get_preview_html(theme) {
const is_auto_theme = theme.name === "automatic";
const preview = $(`<div class="${this.current_theme == theme.name ? "selected" : "" }">
<div data-theme=${theme.name}>
<div data-theme=${is_auto_theme ? "light" : theme.name}
data-is-auto-theme="${is_auto_theme}" title="${theme.info}">
<div class="background">
<div>
<div class="preview-check">${frappe.utils.icon('tick', 'xs')}</div>
<div class="preview-check" data-theme=${is_auto_theme ? "dark" : theme.name}>
${frappe.utils.icon('tick', 'xs')}
</div>
</div>
<div class="navbar"></div>
<div class="p-2">
@ -112,13 +123,14 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
toggle_theme(theme) {
this.current_theme = theme.toLowerCase();
document.documentElement.setAttribute("data-theme", this.current_theme);
document.documentElement.setAttribute("data-theme-mode", this.current_theme);
frappe.show_alert("Theme Changed", 3);
frappe.xcall("frappe.core.doctype.user.user.switch_theme", {
theme: toTitle(theme)
});
}
show() {
this.dialog.show();
}
@ -127,3 +139,22 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
this.dialog.hide();
}
};
frappe.ui.add_system_theme_switch_listener = () => {
frappe.ui.dark_theme_media_query.addEventListener('change', () => {
frappe.ui.set_theme();
});
};
frappe.ui.dark_theme_media_query = window.matchMedia("(prefers-color-scheme: dark)");
frappe.ui.set_theme = (theme) => {
const root = document.documentElement;
let theme_mode = root.getAttribute("data-theme-mode");
if (!theme) {
if (theme_mode === "automatic") {
theme = frappe.ui.dark_theme_media_query.matches ? 'dark' : 'light';
}
}
root.setAttribute("data-theme", theme || theme_mode);
};

View file

@ -305,7 +305,7 @@ frappe.search.AwesomeBar = class AwesomeBar {
index: 80,
default: "Calculator",
onclick: function() {
frappe.msgprint(formatted_value, "Result");
frappe.msgprint(formatted_value, __("Result"));
}
});
} catch(e) {
@ -317,10 +317,10 @@ frappe.search.AwesomeBar = class AwesomeBar {
make_random(txt) {
if(txt.toLowerCase().includes('random')) {
this.options.push({
label: "Generate Random Password",
label: __("Generate Random Password"),
value: frappe.utils.get_random(16),
onclick: function() {
frappe.msgprint(frappe.utils.get_random(16), "Result");
frappe.msgprint(frappe.utils.get_random(16), __("Result"));
}
})
}

View file

@ -0,0 +1,100 @@
frappe.provide("frappe.ui");
frappe.ui.DiffView = class DiffView {
constructor(doctype, fieldname, docname) {
this.dialog = null;
this.handler = null;
this.doctype = doctype;
this.fieldname = fieldname;
this.docname = docname;
this.dialog = this.make_dialog();
this.set_empty_state();
this.dialog.show();
}
make_dialog() {
const get_query = () => ({
query: "frappe.utils.diff.version_query",
filters: { docname: this.docname, ref_doctype: this.doctype },
});
const onchange = () => this.compute_diff();
let dialog = new frappe.ui.Dialog({
title: __("Compare Versions"),
fields: [
{
label: __("From version"),
fieldtype: "Link",
fieldname: "from_version",
options: "Version",
reqd: 1,
get_query,
onchange,
},
{
fieldtype: "Column Break",
fieldname: "cb",
},
{
label: __("To version"),
fieldtype: "Link",
fieldname: "to_version",
options: "Version",
reqd: 1,
get_query,
onchange,
},
{
fieldtype: "Section Break",
fieldname: "sb",
},
{
label: __("Diff"),
fieldtype: "HTML",
fieldname: "diff",
},
],
size: "large",
});
return dialog;
}
compute_diff() {
const from_version = this.dialog.get_value("from_version");
const to_version = this.dialog.get_value("to_version");
const fieldname = this.fieldname;
if (from_version && to_version) {
frappe
.xcall("frappe.utils.diff.get_version_diff", {
from_version,
to_version,
fieldname,
})
.then((data) => {
this.dialog.set_value("diff", this.prettify_diff(data));
});
} else {
this.set_empty_state();
}
}
prettify_diff(diff) {
let html = ``;
diff.forEach((line) => {
let line_class = "";
if (line.startsWith("+")) {
line_class = "insert";
} else if (line.startsWith("-")) {
line_class = "delete";
}
html += `<div class=${line_class}>${line}</div>`;
});
return `<div class='diffview'>${html}</div>`;
}
set_empty_state() {
this.dialog.set_value("diff", __("Select two versions to view the diff."));
}
};

View file

@ -129,7 +129,7 @@ function format_currency(v, currency, decimals) {
}
if (symbol)
return symbol + " " + format_number(v, format, decimals);
return __(symbol) + " " + format_number(v, format, decimals);
else
return format_number(v, format, decimals);
}

View file

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

View file

@ -42,7 +42,6 @@ frappe.views.Container = class Container {
cur_page = this;
if(this.page && this.page.label === label) {
$(this.page).trigger('show');
return;
}
var me = this;

View file

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

View file

@ -634,6 +634,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
this.render_datatable();
this.add_chart_buttons_to_toolbar(true);
this.add_card_button_to_toolbar();
this.$report.show();
} else {
this.data = [];
this.toggle_nothing_to_show(true);
@ -882,7 +883,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
hide_loading_screen() {
this.$loading.hide();
this.$report.show();
}
get_chart_options(data) {
@ -1789,6 +1789,19 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
this.$chart.toggle(flag);
this.$summary.toggle(flag);
}
get_checked_items(only_docnames) {
const indexes = this.datatable.rowmanager.getCheckedRows();
return indexes.reduce((items, i) => {
if (i === undefined) return items;
const item = this.data[i];
items.push(only_docnames ? item.name : item);
return items;
}, []);
}
// backward compatibility
get get_values() {
return this.get_filter_values;

View file

@ -106,6 +106,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
get_args() {
const args = super.get_args();
delete args.group_by;
this.group_by_control.set_args(args);
return args;
@ -387,6 +388,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
this.$charts_wrapper.addClass('hidden');
this.save_view_user_settings(
{ chart_args: null });
this.chart_args = null;
}
}

View file

@ -211,7 +211,7 @@ export default class NumberCardWidget extends Widget {
const symbol = number_parts[1] || '';
const formatted_number = $(frappe.format(number_parts[0], df)).text();
this.formatted_number = formatted_number + ' ' + symbol;
this.formatted_number = formatted_number + ' ' + __(symbol);
}
render_number() {

View file

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

View file

@ -574,6 +574,31 @@ details > summary:focus {
}
}
.diffview {
font-family: monospace;
white-space: pre;
word-wrap: break-word;
color: var(--text-color);
}
.diffview .insert {
background-color: var(--green-100);
}
.diffview .delete {
background-color: var(--red-100);
}
[data-theme="dark"] {
.diffview .insert {
background-color: var(--green-800);
}
.diffview .delete {
background-color: var(--red-800);
}
}
// REDESIGN TODO: Handling of broken images?
// img.no-image:before {
// .img-background();
@ -603,4 +628,4 @@ details > summary:focus {
.chart-container {
direction: ltr;
}
*/
*/

View file

@ -1,6 +1,6 @@
.modal-body .theme-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-gap: 18px;
.background {
@ -9,7 +9,7 @@
border-radius: var(--border-radius-lg);
overflow: hidden;
cursor: pointer;
height: 160px;
height: 120px;
position: relative;
&:hover {
@ -28,6 +28,7 @@
margin-right: var(--margin-sm);
border-radius: var(--border-radius-full);
z-index: 1;
}
}
@ -72,6 +73,7 @@
border-radius: var(--border-radius-sm);
height: 10px;
width: 20px;
z-index: 1;
}
.text {
@ -80,4 +82,17 @@
height: 10px;
width: 40px;
}
}
// TODO: Replace with better alternative
[data-is-auto-theme="true"] {
.background::after {
content: "";
top: 0;
right: 0;
height: 100%;
width: 50%;
background: var(--gray-900);
position: absolute;
}
}

View file

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

View file

@ -0,0 +1,18 @@
.error-page {
text-align: center;
.img-404 {
width: 40%;
margin: var(--margin-2xl) auto;
@include media-breakpoint-down(sm) {
width: 80%
}
}
.back-to-home {
font-size: var(--text-base);
}
}

View file

@ -26,6 +26,7 @@
@import 'doc';
@import 'navbar';
@import 'footer';
@import 'error-state';
.ql-editor.read-mode {
padding: 0;

View file

@ -158,6 +158,8 @@ def get():
bootinfo["setup_complete"] = cint(frappe.db.get_single_value('System Settings', 'setup_complete'))
bootinfo["is_first_startup"] = cint(frappe.db.get_single_value('System Settings', 'is_first_startup'))
bootinfo['desk_theme'] = frappe.db.get_value("User", frappe.session.user, "desk_theme") or 'Light'
return bootinfo
@frappe.whitelist()

View file

@ -32,7 +32,9 @@ class EnergyPointLog(Document):
frappe.cache().hdel('energy_points', self.user)
frappe.publish_realtime('update_points', after_commit=True)
if self.type != 'Review':
if self.type != 'Review' and \
frappe.get_cached_value('Notification Settings', self.user, 'energy_points_system_notifications'):
reference_user = self.user if self.type == 'Auto' else self.owner
notification_doc = {
'type': 'Energy Point',

View file

@ -8,6 +8,18 @@ from frappe.utils.testutils import add_custom_field, clear_custom_fields
from frappe.desk.form.assign_to import add as assign_to
class TestEnergyPointLog(unittest.TestCase):
@classmethod
def setUpClass(cls):
settings = frappe.get_single('Energy Point Settings')
settings.enabled = 1
settings.save()
@classmethod
def tearDownClass(cls):
settings = frappe.get_single('Energy Point Settings')
settings.enabled = 0
settings.save()
def setUp(self):
frappe.cache().delete_value('energy_point_rule_map')
@ -336,4 +348,4 @@ def assign_users_to_todo(todo_name, users):
'assign_to': [user],
'doctype': 'ToDo',
'name': todo_name
})
})

View file

@ -1,229 +1,70 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"actions": [],
"creation": "2019-03-19 13:17:51.710241",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enabled",
"section_break_2",
"review_levels",
"point_allocation_periodicity",
"last_point_allocation_date"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fetch_if_empty": 0,
"default": "0",
"fieldname": "enabled",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Enabled",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Enabled"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "enabled",
"fetch_if_empty": 0,
"fieldname": "section_break_2",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "review_levels",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Review Levels",
"length": 0,
"no_copy": 0,
"options": "Review Level",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "Review Level"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Weekly",
"fetch_if_empty": 0,
"fieldname": "point_allocation_periodicity",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Point Allocation Periodicity",
"length": 0,
"no_copy": 0,
"options": "Daily\nWeekly\nMonthly",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "Daily\nWeekly\nMonthly"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "last_point_allocation_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Last Point Allocation Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
}
],
"has_web_view": 0,
"hide_toolbar": 1,
"idx": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2019-03-26 19:10:14.087840",
"links": [],
"modified": "2021-11-16 23:24:01.366928",
"modified_by": "Administrator",
"module": "Social",
"name": "Energy Point Settings",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
"track_changes": 1
}

View file

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

View file

@ -3,8 +3,11 @@
{{ message }}
</div>
</div>
{% if published and send_webview_link %}
<div style="font-size: 12px; line-height: 20px;">
<div>
Open in <a style="color: #687178; text-decoration: underline;" href="/newsletters/{{ name }}" target="_blank">web</a>
</div>
</div>
</div>
{% endif %}

View file

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

View file

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

View file

@ -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 %}
{% 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>
{% endif %}
<hr class="add-comment-hr my-5">
<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" %}
<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>
{% endfor %}
</div>
</div>
{% 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>
<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 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);
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);
}
$('.start-discussion').removeClass('hidden');
}
var n_comments = $(".comment-row").length;
$('.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) {
$(".no_comment").toggle(false);
}
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>

View file

@ -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)
@ -25,16 +28,6 @@ def add_comment(comment, comment_email, comment_by, reference_doctype, reference
frappe.msgprint(_('Comments cannot have links or email addresses'))
return False
comments_count = frappe.db.count("Comment", {
"comment_type": "Comment",
"comment_email": comment_email,
"creation": (">", add_to_date(now(), hours=-1))
})
if comments_count > 20:
frappe.msgprint(_('Hourly comment limit reached for: {0}').format(frappe.bold(comment_email)))
return False
comment = doc.add_comment(
text=comment,
comment_email=comment_email,
@ -51,14 +44,17 @@ def add_comment(comment, comment_email, comment_by, reference_doctype, reference
comment.name,
_("View Comment")))
# notify creator
frappe.sendmail(
recipients=frappe.db.get_value('User', doc.owner, 'email') or doc.owner,
subject=_('New Comment on {0}: {1}').format(doc.doctype, doc.name),
message=content,
reference_doctype=doc.doctype,
reference_name=doc.name
)
if doc.doctype == "Blog Post" and not doc.enable_email_notification:
pass
else:
# notify creator
frappe.sendmail(
recipients=frappe.db.get_value('User', doc.owner, 'email') or doc.owner,
subject=_('New Comment on {0}: {1}').format(doc.doctype, doc.name),
message=content,
reference_doctype=doc.doctype,
reference_name=doc.name
)
# revert with template if all clear (no backlinks)
template = frappe.get_template("templates/includes/comments/comment.html")

View file

@ -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"));
let like = parseInt("{{ user_feedback.like or 0 }}");
let like_count = parseInt("{{ like_count or 0 }}");
if (frappe.is_user_logged_in()) {
if (feedback) {
$("[name='feedback']").val(feedback);
toggle_feedback();
set_rating(rating);
}
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());
let toggle_like_icon = function(active) {
active ? $('.like-icon').addClass('gray') : $('.like-icon').removeClass('gray');
}
$('.like-icon').append(frappe.utils.icon('heart', 'md'))
toggle_like_icon(like);
$('.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');
});
});
$('.like-count').text(like_count);
$('.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;
}
if (!args.feedback || !args.feedback.trim()) {
frappe.msgprint("{{ _("Please add a valid feedback.") }}");
return false;
}
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-icon').click(() => {
update_like();
update_feedback();
})
$('#toggle-feedback').click(() => {
toggle_feedback();
return false;
})
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>

View file

@ -10,27 +10,10 @@ 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):
doc = frappe.get_doc(reference_doctype, reference_name)
if doc.disable_feedback == 1:
def give_feedback(reference_doctype, reference_name, like):
like = frappe.parse_json(like)
ref_doc = frappe.get_doc(reference_doctype, reference_name)
if ref_doc.disable_feedback == 1:
return
filters = {
@ -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)
send_mail(doc, subject)
subject = _('Feedback on {0}: {1}').format(reference_doctype, reference_name)
ref_doc.enable_email_notification and 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(

View file

@ -195,9 +195,9 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}"
{% else %}
<div class="print-heading">
<h2>
<div>{{ doc.select_print_heading or (doc.print_heading if doc.print_heading != None
<div>{{ _(doc.select_print_heading) or (_(doc.print_heading) if doc.print_heading != None
else _(doc.doctype)) }}</div>
<small class="sub-heading">{{ doc.sub_heading if doc.sub_heading != None
<small class="sub-heading">{{ _(doc.sub_heading) if doc.sub_heading != None
else doc.name }}</small>
</h2>
</div>

View file

@ -11,14 +11,7 @@ from frappe.utils.boilerplate import make_boilerplate
class TestBoilerPlate(unittest.TestCase):
@classmethod
def tearDownClass(cls):
bench_path = frappe.utils.get_bench_path()
test_app_dir = os.path.join(bench_path, "apps", "test_app")
if os.path.exists(test_app_dir):
shutil.rmtree(test_app_dir)
def test_create_app(self):
def setUpClass(cls):
title = "Test App"
description = "This app's description contains 'single quotes' and \"double quotes\"."
publisher = "Test Publisher"
@ -27,7 +20,7 @@ class TestBoilerPlate(unittest.TestCase):
color = ""
app_license = "MIT"
user_input = [
cls.user_input = [
title,
description,
publisher,
@ -37,22 +30,21 @@ class TestBoilerPlate(unittest.TestCase):
app_license,
]
bench_path = frappe.utils.get_bench_path()
apps_dir = os.path.join(bench_path, "apps")
app_name = "test_app"
cls.bench_path = frappe.utils.get_bench_path()
cls.apps_dir = os.path.join(cls.bench_path, "apps")
cls.app_names = ("test_app", "test_app_no_git")
cls.gitignore_file = ".gitignore"
cls.git_folder = ".git"
with patch("builtins.input", side_effect=user_input):
make_boilerplate(apps_dir, app_name)
root_paths = [
app_name,
cls.root_paths = [
"requirements.txt",
"README.md",
"setup.py",
"license.txt",
".git",
cls.git_folder,
cls.gitignore_file
]
paths_inside_app = [
cls.paths_inside_app = [
"__init__.py",
"hooks.py",
"patches.txt",
@ -60,25 +52,68 @@ class TestBoilerPlate(unittest.TestCase):
"www",
"config",
"modules.txt",
"public",
app_name,
"public"
]
new_app_dir = os.path.join(bench_path, apps_dir, app_name)
@classmethod
def tearDownClass(cls):
test_app_dirs = (os.path.join(cls.bench_path, "apps", app_name) for app_name in cls.app_names)
for test_app_dir in test_app_dirs:
if os.path.exists(test_app_dir):
shutil.rmtree(test_app_dir)
def test_create_app(self):
with patch("builtins.input", side_effect=self.user_input):
make_boilerplate(self.apps_dir, self.app_names[0])
new_app_dir = os.path.join(self.bench_path, self.apps_dir, self.app_names[0])
paths = self.get_paths(new_app_dir, self.app_names[0])
for path in paths:
self.assertTrue(
os.path.exists(path),
msg=f"{path} should exist in {self.app_names[0]} app"
)
self.check_parsable_python_files(new_app_dir)
def test_create_app_without_git_init(self):
with patch("builtins.input", side_effect=self.user_input):
make_boilerplate(self.apps_dir, self.app_names[1], no_git=True)
new_app_dir = os.path.join(self.apps_dir, self.app_names[1])
paths = self.get_paths(new_app_dir, self.app_names[1])
for path in paths:
if os.path.basename(path) in (self.git_folder, self.gitignore_file):
self.assertFalse(
os.path.exists(path),
msg=f"{path} shouldn't exist in {self.app_names[1]} app"
)
else:
self.assertTrue(
os.path.exists(path),
msg=f"{path} should exist in {self.app_names[1]} app"
)
self.check_parsable_python_files(new_app_dir)
def get_paths(self, app_dir, app_name):
all_paths = list()
for path in root_paths:
all_paths.append(os.path.join(new_app_dir, path))
for path in self.root_paths:
all_paths.append(os.path.join(app_dir, path))
for path in paths_inside_app:
all_paths.append(os.path.join(new_app_dir, app_name, path))
all_paths.append(os.path.join(app_dir, app_name))
for path in all_paths:
self.assertTrue(os.path.exists(path), msg=f"{path} should exist in new app")
for path in self.paths_inside_app:
all_paths.append(os.path.join(app_dir, app_name, path))
return all_paths
def check_parsable_python_files(self, app_dir):
# check if python files are parsable
python_files = glob.glob(new_app_dir + "**/*.py", recursive=True)
python_files = glob.glob(app_dir + "**/*.py", recursive=True)
for python_file in python_files:
with open(python_file) as p:

View file

@ -5,6 +5,7 @@ import gzip
import json
import os
import shlex
import shutil
import subprocess
import sys
import unittest
@ -102,14 +103,24 @@ def exists_in_backup(doctypes, file):
class BaseTestCommands(unittest.TestCase):
def execute(self, command, kwargs=None):
site = {"site": frappe.local.site}
cmd_input = None
if kwargs:
cmd_input = kwargs.get("cmd_input", None)
if cmd_input:
if not isinstance(cmd_input, bytes):
raise Exception(
f"The input should be of type bytes, not {type(cmd_input).__name__}"
)
del kwargs["cmd_input"]
kwargs.update(site)
else:
kwargs = site
self.command = " ".join(command.split()).format(**kwargs)
print("{0}$ {1}{2}".format(color.silver, self.command, color.nc))
command = shlex.split(self.command)
self._proc = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self._proc = subprocess.run(command, input=cmd_input, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self.stdout = clean(self._proc.stdout)
self.stderr = clean(self._proc.stderr)
self.returncode = clean(self._proc.returncode)
@ -466,6 +477,28 @@ class TestCommands(BaseTestCommands):
self.assertEqual(self.returncode, 0)
self.assertEqual(check_password('Administrator', 'test2'), 'Administrator')
def test_make_app(self):
user_input = [
b"Test App", # title
b"This app's description contains 'single quotes' and \"double quotes\".", # description
b"Test Publisher", # publisher
b"example@example.org", # email
b"", # icon
b"", # color
b"MIT" # app_license
]
app_name = "testapp0"
apps_path = os.path.join(frappe.utils.get_bench_path(), "apps")
test_app_path = os.path.join(apps_path, app_name)
self.execute(f"bench make-app {apps_path} {app_name}", {"cmd_input": b'\n'.join(user_input)})
self.assertEqual(self.returncode, 0)
self.assertTrue(
os.path.exists(test_app_path)
)
# cleanup
shutil.rmtree(test_app_path)
class RemoveAppUnitTests(unittest.TestCase):
def test_delete_modules(self):

View file

@ -34,7 +34,21 @@ class TestDB(unittest.TestCase):
self.assertEqual(frappe.db.sql("""SELECT name FROM `tabUser` WHERE name >= 't' ORDER BY MODIFIED DESC""")[0][0],
frappe.db.get_value("User", {"name": [">=", "t"]}))
self.assertIn("concat_ws", frappe.db.get_value("User", filters={"name": "Administrator"}, fieldname=Concat_ws(" ", "LastName"), run=False).lower())
self.assertIn(
"concat_ws",
frappe.db.get_value(
"User",
filters={"name": "Administrator"},
fieldname=Concat_ws(" ", "LastName"),
run=False,
).lower(),
)
self.assertEqual(
frappe.db.sql("select email from tabUser where name='Administrator' order by modified DESC"),
frappe.db.get_values(
"User", filters=[["name", "=", "Administrator"]], fieldname="email"
),
)
def test_set_value(self):
todo1 = frappe.get_doc(dict(doctype='ToDo', description = 'test_set_value 1')).insert()

View file

@ -157,7 +157,7 @@ class TestDocument(unittest.TestCase):
def test_varchar_length(self):
d = self.test_insert()
d.subject = "abcde"*100
d.sender = "abcde"*100 + "@user.com"
self.assertRaises(frappe.CharacterLengthExceededError, d.save)
def test_xss_filter(self):
@ -251,4 +251,4 @@ class TestDocument(unittest.TestCase):
'doctype': 'Test Formatted',
'currency': 100000
})
self.assertEquals(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00')
self.assertEquals(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00')

View file

@ -7,6 +7,7 @@ from frappe.utils import now_datetime
from frappe.model.naming import getseries
from frappe.model.naming import append_number_if_name_exists, revert_series_if_last
from frappe.model.naming import determine_consecutive_week_number, parse_naming_series
class TestNaming(unittest.TestCase):
def tearDown(self):
@ -60,6 +61,34 @@ class TestNaming(unittest.TestCase):
self.assertEqual(todo.name, 'TODO-{month}-{status}-{series}'.format(
month=now_datetime().strftime('%m'), status=todo.status, series=series))
def test_format_autoname_for_consecutive_week_number(self):
'''
Test if braced params are replaced for consecutive week number in format autoname
'''
doctype = 'ToDo'
todo_doctype = frappe.get_doc('DocType', doctype)
todo_doctype.autoname = 'format:TODO-{WW}-{##}'
todo_doctype.save()
description = 'Format'
todo = frappe.new_doc(doctype)
todo.description = description
todo.insert()
series = getseries('', 2)
series = str(int(series)-1)
if len(series) < 2:
series = '0' + series
week = determine_consecutive_week_number(now_datetime())
self.assertEqual(todo.name, 'TODO-{week}-{series}'.format(
week=week, series=series))
def test_revert_series(self):
from datetime import datetime
year = datetime.now().year
@ -150,3 +179,32 @@ class TestNaming(unittest.TestCase):
self.assertEqual(amended_doc.name, "{}-CANC-1".format(original_name))
submittable_doctype.delete()
def test_parse_naming_series_for_consecutive_week_number(self):
week = determine_consecutive_week_number(now_datetime())
name = parse_naming_series('PREFIX-.WW.-SUFFIX')
expected_name = 'PREFIX-{}-SUFFIX'.format(week)
self.assertEqual(name, expected_name)
def test_determine_consecutive_week_number(self):
from datetime import datetime
dt = datetime.fromisoformat("2019-12-31")
w = determine_consecutive_week_number(dt)
self.assertEqual(w, "53")
dt = datetime.fromisoformat("2020-01-01")
w = determine_consecutive_week_number(dt)
self.assertEqual(w, "01")
dt = datetime.fromisoformat("2020-01-15")
w = determine_consecutive_week_number(dt)
self.assertEqual(w, "03")
dt = datetime.fromisoformat("2021-01-01")
w = determine_consecutive_week_number(dt)
self.assertEqual(w, "00")
dt = datetime.fromisoformat("2021-12-31")
w = determine_consecutive_week_number(dt)
self.assertEqual(w, "52")

View file

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

View file

@ -7,6 +7,7 @@ from frappe.utils import evaluate_filters, money_in_words, scrub_urls, get_url
from frappe.utils import validate_url, validate_email_address
from frappe.utils import ceil, floor
from frappe.utils.data import cast, validate_python_code
from frappe.utils.diff import get_version_diff, version_query, _get_value_from_version
from PIL import Image
from frappe.utils.image import strip_exif_data, optimize_image
@ -269,3 +270,39 @@ class TestPythonExpressions(unittest.TestCase):
]
for expr in invalid_expressions:
self.assertRaises(frappe.ValidationError, validate_python_code, expr)
class TestDiffUtils(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.doc = frappe.get_doc(doctype="Client Script", dt="Client Script")
cls.doc.save(ignore_version=False)
cls.doc.script = "2;"
cls.doc.save(ignore_version=False)
cls.doc.script = "42;"
cls.doc.save(ignore_version=False)
cls.versions = version_query(doctype="Version", txt="", searchfield="name", start=0,
page_len=20, filters={"ref_doctype": cls.doc.doctype, "docname": cls.doc.name})
@classmethod
def tearDownClass(cls):
cls.doc.delete()
def test_version_query(self):
self.assertGreaterEqual(len(self.versions), 2)
def test_get_field_value_from_version(self):
latest_version = self.versions[0][0]
self.assertEqual("42;", _get_value_from_version(latest_version, fieldname="script")[0])
old_version = self.versions[1][0]
self.assertEqual("2;", _get_value_from_version(old_version, fieldname="script")[0])
def test_get_version_diff(self):
old_version = self.versions[1][0]
latest_version = self.versions[0][0]
diff = get_version_diff(old_version, latest_version)
self.assertIn('-2;', diff)
self.assertIn('+42;', diff)

View file

@ -3,7 +3,7 @@
import frappe, os, re, git
from frappe.utils import touch_file, cstr
def make_boilerplate(dest, app_name):
def make_boilerplate(dest, app_name, no_git=False):
if not os.path.exists(dest):
print("Destination directory does not exist")
return
@ -63,9 +63,6 @@ def make_boilerplate(dest, app_name):
with open(os.path.join(dest, hooks.app_name, "MANIFEST.in"), "w") as f:
f.write(frappe.as_unicode(manifest_template.format(**hooks)))
with open(os.path.join(dest, hooks.app_name, ".gitignore"), "w") as f:
f.write(frappe.as_unicode(gitignore_template.format(app_name = hooks.app_name)))
with open(os.path.join(dest, hooks.app_name, "requirements.txt"), "w") as f:
f.write("# frappe -- https://github.com/frappe/frappe is installed via 'bench init'")
@ -98,11 +95,16 @@ def make_boilerplate(dest, app_name):
with open(os.path.join(dest, hooks.app_name, hooks.app_name, "config", "docs.py"), "w") as f:
f.write(frappe.as_unicode(docs_template.format(**hooks)))
# initialize git repository
app_directory = os.path.join(dest, hooks.app_name)
app_repo = git.Repo.init(app_directory)
app_repo.git.add(A=True)
app_repo.index.commit("feat: Initialize App")
if not no_git:
with open(os.path.join(dest, hooks.app_name, ".gitignore"), "w") as f:
f.write(frappe.as_unicode(gitignore_template.format(app_name = hooks.app_name)))
# initialize git repository
app_repo = git.Repo.init(app_directory)
app_repo.git.add(A=True)
app_repo.index.commit("feat: Initialize App")
print("'{app}' created at {path}".format(app=app_name, path=app_directory))

View file

@ -868,7 +868,7 @@ def fmt_money(amount, precision=None, currency=None, format=None):
if currency and frappe.defaults.get_global_default("hide_currency_symbol") != "Yes":
symbol = frappe.db.get_value("Currency", currency, "symbol", cache=True) or currency
amount = symbol + " " + amount
amount = frappe._(symbol) + " " + amount
return amount

61
frappe/utils/diff.py Normal file
View file

@ -0,0 +1,61 @@
import json
from difflib import unified_diff
from typing import List
import frappe
from frappe.utils import pretty_date
@frappe.whitelist()
def get_version_diff(
from_version: str, to_version: str, fieldname: str = "script"
) -> List[str]:
before, before_timestamp = _get_value_from_version(from_version, fieldname)
after, after_timestamp = _get_value_from_version(to_version, fieldname)
if not (before and after):
return ["Values not available for diff"]
before = before.split("\n")
after = after.split("\n")
diff = unified_diff(
before,
after,
fromfile=from_version,
tofile=to_version,
fromfiledate=before_timestamp,
tofiledate=after_timestamp,
)
return list(diff)
def _get_value_from_version(version_name: str, fieldname: str):
version = frappe.get_list(
"Version", fields=["data", "modified"], filters={"name": version_name}
)
if version:
data = json.loads(version[0].data)
changed_fields = data.get("changed", [])
# data structure of field: [fieldname, before_save, after_save]
for field in changed_fields:
if field[0] == fieldname:
return field[2], str(version[0].modified)
return None, None
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def version_query(doctype, txt, searchfield, start, page_len, filters):
results = frappe.get_list(
"Version",
fields=["name", "modified"],
filters=filters,
limit_start=start,
limit_page_length=page_len,
order_by="modified desc",
)
return [(d.name, pretty_date(d.modified), d.modified) for d in results]

View file

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

View file

@ -95,7 +95,7 @@ def prepare_options(html, options):
'quiet': None,
# 'no-outline': None,
'encoding': "UTF-8",
#'load-error-handling': 'ignore'
# 'load-error-handling': 'ignore'
})
if not options.get("margin-right"):
@ -111,8 +111,21 @@ def prepare_options(html, options):
options.update(get_cookie_options())
# page size
if not options.get("page-size"):
options['page-size'] = frappe.db.get_single_value("Print Settings", "pdf_page_size") or "A4"
pdf_page_size = (
options.get("page-size")
or frappe.db.get_single_value("Print Settings", "pdf_page_size")
or "A4"
)
if pdf_page_size == "Custom":
options["page-height"] = options.get("page-height") or frappe.db.get_single_value(
"Print Settings", "pdf_page_height"
)
options["page-width"] = options.get("page-width") or frappe.db.get_single_value(
"Print Settings", "pdf_page_width"
)
else:
options["page-size"] = pdf_page_size
return html, options

View file

@ -11,7 +11,7 @@ base_template_path = "www/printview.html"
standard_format = "templates/print_formats/standard.html"
@frappe.whitelist()
def download_multi_pdf(doctype, name, format=None, no_letterhead=0):
def download_multi_pdf(doctype, name, format=None, no_letterhead=False, options=None):
"""
Concatenate multiple docs as PDF .
@ -54,18 +54,21 @@ def download_multi_pdf(doctype, name, format=None, no_letterhead=0):
import json
output = PdfFileWriter()
if isinstance(options, str):
options = json.loads(options)
if not isinstance(doctype, dict):
result = json.loads(name)
# Concatenating pdf files
for i, ss in enumerate(result):
output = frappe.get_print(doctype, ss, format, as_pdf = True, output = output, no_letterhead=no_letterhead)
output = frappe.get_print(doctype, ss, format, as_pdf=True, output=output, no_letterhead=no_letterhead, pdf_options=options)
frappe.local.response.filename = "{doctype}.pdf".format(doctype=doctype.replace(" ", "-").replace("/", "-"))
else:
for doctype_name in doctype:
for doc_name in doctype[doctype_name]:
try:
output = frappe.get_print(doctype_name, doc_name, format, as_pdf = True, output = output, no_letterhead=no_letterhead)
output = frappe.get_print(doctype_name, doc_name, format, as_pdf=True, output=output, no_letterhead=no_letterhead, pdf_options=options)
except Exception:
frappe.log_error("Permission Error on doc {} of doctype {}".format(doc_name, doctype_name))
frappe.local.response.filename = "{}.pdf".format(name)

View file

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

View file

@ -17,6 +17,7 @@
"published",
"featured",
"hide_cta",
"enable_email_notification",
"disable_comments",
"disable_feedback",
"section_break_5",
@ -197,6 +198,13 @@
"fieldname": "disable_feedback",
"fieldtype": "Check",
"label": "Disable Feedback"
},
{
"default": "1",
"description": "Enable email notification for any comment or feedback on your Blog Post.",
"fieldname": "enable_email_notification",
"fieldtype": "Check",
"label": "Enable Email Notification"
}
],
"has_web_view": 1,
@ -206,7 +214,7 @@
"is_published_field": "published",
"links": [],
"max_attachments": 5,
"modified": "2021-09-13 17:19:35.436045",
"modified": "2021-11-23 10:42:01.759723",
"modified_by": "Administrator",
"module": "Website",
"name": "Blog Post",
@ -240,4 +248,4 @@
"sort_order": "ASC",
"title_field": "title",
"track_changes": 1
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -112,7 +112,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-02-20 13:33:44.011509",
"modified": "2021-11-22 17:56:40.495232",
"modified_by": "Administrator",
"module": "Workflow",
"name": "Workflow State",
@ -137,6 +137,10 @@
"share": 1,
"submit": 0,
"write": 1
},
{
"role": "All",
"select": 1
}
],
"quick_entry": 1,

Some files were not shown because too many files have changed in this diff Show more