@@ -0,0 +1,35 @@ | |||||
context('List Paging', () => { | |||||
before(() => { | |||||
cy.login(); | |||||
cy.visit('/app/website'); | |||||
return cy.window().its('frappe').then(frappe => { | |||||
return frappe.call("frappe.tests.ui_test_helpers.create_multiple_todo_records"); | |||||
}); | |||||
}); | |||||
it('test load more with count selection buttons', () => { | |||||
cy.visit('/app/todo/view/report'); | |||||
cy.get('.list-paging-area .list-count').should('contain.text', '20 of'); | |||||
cy.get('.list-paging-area .btn-more').click(); | |||||
cy.get('.list-paging-area .list-count').should('contain.text', '40 of'); | |||||
cy.get('.list-paging-area .btn-more').click(); | |||||
cy.get('.list-paging-area .list-count').should('contain.text', '60 of'); | |||||
cy.get('.list-paging-area .btn-group .btn-paging[data-value="100"]').click(); | |||||
cy.get('.list-paging-area .list-count').should('contain.text', '100 of'); | |||||
cy.get('.list-paging-area .btn-more').click(); | |||||
cy.get('.list-paging-area .list-count').should('contain.text', '200 of'); | |||||
cy.get('.list-paging-area .btn-more').click(); | |||||
cy.get('.list-paging-area .list-count').should('contain.text', '300 of'); | |||||
// check if refresh works after load more | |||||
cy.get('.page-head .standard-actions [data-original-title="Refresh"]').click(); | |||||
cy.get('.list-paging-area .list-count').should('contain.text', '300 of'); | |||||
cy.get('.list-paging-area .btn-group .btn-paging[data-value="500"]').click(); | |||||
cy.get('.list-paging-area .list-count').should('contain.text', '500 of'); | |||||
}); | |||||
}); |
@@ -0,0 +1,22 @@ | |||||
context('Number Card', () => { | |||||
before(() => { | |||||
cy.login(); | |||||
cy.visit('/app/website'); | |||||
}); | |||||
it('Check filter populate for child table doctype', () => { | |||||
cy.visit('/app/number-card/new-number-card-1'); | |||||
cy.get('[data-fieldname="parent_document_type"]').should('have.css', 'display', 'none'); | |||||
cy.get_field('document_type', 'Link'); | |||||
cy.fill_field('document_type', 'Workspace Link', 'Link').focus().blur(); | |||||
cy.get_field('document_type', 'Link').should('have.value', 'Workspace Link'); | |||||
cy.fill_field('label', 'Test Number Card', 'Data'); | |||||
cy.get('[data-fieldname="filters_json"]').click().wait(200); | |||||
cy.get('.modal-body .filter-action-buttons .add-filter').click(); | |||||
cy.get('.modal-body .fieldname-select-area').click(); | |||||
cy.get('.modal-actions .btn-modal-close').click(); | |||||
}); | |||||
}); |
@@ -13,9 +13,6 @@ context('Report View', () => { | |||||
'enabled': 0, | 'enabled': 0, | ||||
'docstatus': 1 // submit document | 'docstatus': 1 // submit document | ||||
}, true); | }, true); | ||||
return cy.window().its('frappe').then(frappe => { | |||||
return frappe.call("frappe.tests.ui_test_helpers.create_multiple_contact_records"); | |||||
}); | |||||
}); | }); | ||||
it('Field with enabled allow_on_submit should be editable.', () => { | it('Field with enabled allow_on_submit should be editable.', () => { | ||||
@@ -43,32 +40,4 @@ context('Report View', () => { | |||||
expect(r.message.enabled).to.equals(1); | expect(r.message.enabled).to.equals(1); | ||||
}); | }); | ||||
}); | }); | ||||
it('test load more with count selection buttons', () => { | |||||
cy.visit('/app/contact/view/report'); | |||||
cy.get('.list-paging-area .list-count').should('contain.text', '20 of'); | |||||
cy.get('.list-paging-area .btn-more').click(); | |||||
cy.get('.list-paging-area .list-count').should('contain.text', '40 of'); | |||||
cy.get('.list-paging-area .btn-more').click(); | |||||
cy.get('.list-paging-area .list-count').should('contain.text', '60 of'); | |||||
cy.get('.list-paging-area .btn-group .btn-paging[data-value="100"]').click(); | |||||
cy.get('.list-paging-area .list-count').should('contain.text', '100 of'); | |||||
cy.get('.list-paging-area .btn-more').click(); | |||||
cy.get('.list-paging-area .list-count').should('contain.text', '200 of'); | |||||
cy.get('.list-paging-area .btn-more').click(); | |||||
cy.get('.list-paging-area .list-count').should('contain.text', '300 of'); | |||||
// check if refresh works after load more | |||||
cy.get('.page-head .standard-actions [data-original-title="Refresh"]').click(); | |||||
cy.get('.list-paging-area .list-count').should('contain.text', '300 of'); | |||||
cy.get('.list-paging-area .btn-group .btn-paging[data-value="500"]').click(); | |||||
cy.get('.list-paging-area .list-count').should('contain.text', '500 of'); | |||||
cy.get('.list-paging-area .btn-more').click(); | |||||
cy.get('.list-paging-area .list-count').should('contain.text', '1000 of'); | |||||
}); | |||||
}); | }); |
@@ -9,7 +9,7 @@ const cliui = require("cliui")(); | |||||
const chalk = require("chalk"); | const chalk = require("chalk"); | ||||
const html_plugin = require("./frappe-html"); | const html_plugin = require("./frappe-html"); | ||||
const rtlcss = require('rtlcss'); | const rtlcss = require('rtlcss'); | ||||
const postCssPlugin = require("esbuild-plugin-postcss2").default; | |||||
const postCssPlugin = require("@frappe/esbuild-plugin-postcss2").default; | |||||
const ignore_assets = require("./ignore-assets"); | const ignore_assets = require("./ignore-assets"); | ||||
const sass_options = require("./sass_options"); | const sass_options = require("./sass_options"); | ||||
const build_cleanup_plugin = require("./build-cleanup"); | const build_cleanup_plugin = require("./build-cleanup"); | ||||
@@ -20,7 +20,8 @@ module.exports = { | |||||
.then(content => { | .then(content => { | ||||
content = scrub_html_template(content); | content = scrub_html_template(content); | ||||
return { | return { | ||||
contents: `\n\tfrappe.templates['${filename}'] = \`${content}\`;\n` | |||||
contents: `\n\tfrappe.templates['${filename}'] = \`${content}\`;\n`, | |||||
watchFiles: [filepath] | |||||
}; | }; | ||||
}) | }) | ||||
.catch(() => { | .catch(() => { | ||||
@@ -35,6 +35,7 @@ from frappe.query_builder import ( | |||||
patch_query_execute, | patch_query_execute, | ||||
patch_query_aggregation, | patch_query_aggregation, | ||||
) | ) | ||||
from frappe.utils.data import cstr | |||||
__version__ = '14.0.0-dev' | __version__ = '14.0.0-dev' | ||||
@@ -214,6 +215,7 @@ def init(site, sites_path=None, new_site=False): | |||||
local.cache = {} | local.cache = {} | ||||
local.document_cache = {} | local.document_cache = {} | ||||
local.meta_cache = {} | local.meta_cache = {} | ||||
local.autoincremented_status_map = {site: -1} | |||||
local.form_dict = _dict() | local.form_dict = _dict() | ||||
local.session = _dict() | local.session = _dict() | ||||
local.dev_server = _dev_server | local.dev_server = _dev_server | ||||
@@ -850,8 +852,7 @@ def set_value(doctype, docname, fieldname, value=None): | |||||
return frappe.client.set_value(doctype, docname, fieldname, value) | return frappe.client.set_value(doctype, docname, fieldname, value) | ||||
def get_cached_doc(*args, **kwargs): | def get_cached_doc(*args, **kwargs): | ||||
if args and len(args) > 1 and isinstance(args[1], str): | |||||
key = get_document_cache_key(args[0], args[1]) | |||||
if key := can_cache_doc(args): | |||||
# local cache | # local cache | ||||
doc = local.document_cache.get(key) | doc = local.document_cache.get(key) | ||||
if doc: | if doc: | ||||
@@ -869,8 +870,24 @@ def get_cached_doc(*args, **kwargs): | |||||
return doc | return doc | ||||
def can_cache_doc(args): | |||||
""" | |||||
Determine if document should be cached based on get_doc params. | |||||
Returns cache key if doc can be cached, None otherwise. | |||||
""" | |||||
if not args: | |||||
return | |||||
doctype = args[0] | |||||
name = doctype if len(args) == 1 else args[1] | |||||
# Only cache if both doctype and name are strings | |||||
if isinstance(doctype, str) and isinstance(name, str): | |||||
return get_document_cache_key(doctype, name) | |||||
def get_document_cache_key(doctype, name): | def get_document_cache_key(doctype, name): | ||||
return '{0}::{1}'.format(doctype, name) | |||||
return f'{doctype}::{name}' | |||||
def clear_document_cache(doctype, name): | def clear_document_cache(doctype, name): | ||||
cache().hdel("last_modified", doctype) | cache().hdel("last_modified", doctype) | ||||
@@ -911,8 +928,7 @@ def get_doc(*args, **kwargs): | |||||
doc = frappe.model.document.get_doc(*args, **kwargs) | doc = frappe.model.document.get_doc(*args, **kwargs) | ||||
# set in cache | # set in cache | ||||
if args and len(args) > 1: | |||||
key = get_document_cache_key(args[0], args[1]) | |||||
if key := can_cache_doc(args): | |||||
local.document_cache[key] = doc | local.document_cache[key] = doc | ||||
cache().hset('document_cache', key, doc.as_dict()) | cache().hset('document_cache', key, doc.as_dict()) | ||||
@@ -1001,7 +1017,7 @@ def get_module(modulename): | |||||
def scrub(txt): | def scrub(txt): | ||||
"""Returns sluggified string. e.g. `Sales Order` becomes `sales_order`.""" | """Returns sluggified string. e.g. `Sales Order` becomes `sales_order`.""" | ||||
return txt.replace(' ', '_').replace('-', '_').lower() | |||||
return cstr(txt).replace(' ', '_').replace('-', '_').lower() | |||||
def unscrub(txt): | def unscrub(txt): | ||||
"""Returns titlified string. e.g. `sales_order` becomes `Sales Order`.""" | """Returns titlified string. e.g. `sales_order` becomes `Sales Order`.""" | ||||
@@ -325,6 +325,7 @@ def get_desk_settings(): | |||||
def get_notification_settings(): | def get_notification_settings(): | ||||
return frappe.get_cached_doc('Notification Settings', frappe.session.user) | return frappe.get_cached_doc('Notification Settings', frappe.session.user) | ||||
@frappe.whitelist() | |||||
def get_link_title_doctypes(): | def get_link_title_doctypes(): | ||||
dts = frappe.get_all("DocType", {"show_title_field_in_link": 1}) | dts = frappe.get_all("DocType", {"show_title_field_in_link": 1}) | ||||
custom_dts = frappe.get_all( | custom_dts = frappe.get_all( | ||||
@@ -1,7 +1,7 @@ | |||||
# imports - standard imports | # imports - standard imports | ||||
import os | import os | ||||
import sys | |||||
import shutil | import shutil | ||||
import sys | |||||
# imports - third party imports | # imports - third party imports | ||||
import click | import click | ||||
@@ -65,11 +65,11 @@ def restore(context, sql_file_path, encryption_key=None, db_root_username=None, | |||||
"Restore site database from an sql file" | "Restore site database from an sql file" | ||||
from frappe.installer import ( | from frappe.installer import ( | ||||
_new_site, | _new_site, | ||||
extract_sql_from_archive, | |||||
extract_files, | extract_files, | ||||
extract_sql_from_archive, | |||||
is_downgrade, | is_downgrade, | ||||
is_partial, | is_partial, | ||||
validate_database_sql | |||||
validate_database_sql, | |||||
) | ) | ||||
from frappe.utils.backups import Backup | from frappe.utils.backups import Backup | ||||
if not os.path.exists(sql_file_path): | if not os.path.exists(sql_file_path): | ||||
@@ -207,7 +207,7 @@ def restore(context, sql_file_path, encryption_key=None, db_root_username=None, | |||||
@click.option('--encryption-key', help='Backup encryption key') | @click.option('--encryption-key', help='Backup encryption key') | ||||
@pass_context | @pass_context | ||||
def partial_restore(context, sql_file_path, verbose, encryption_key=None): | def partial_restore(context, sql_file_path, verbose, encryption_key=None): | ||||
from frappe.installer import partial_restore, extract_sql_from_archive | |||||
from frappe.installer import extract_sql_from_archive, partial_restore | |||||
from frappe.utils.backups import Backup | from frappe.utils.backups import Backup | ||||
if not os.path.exists(sql_file_path): | if not os.path.exists(sql_file_path): | ||||
@@ -545,7 +545,7 @@ def _use(site, sites_path='.'): | |||||
def use(site, sites_path='.'): | def use(site, sites_path='.'): | ||||
if os.path.exists(os.path.join(sites_path, site)): | if os.path.exists(os.path.join(sites_path, site)): | ||||
with open(os.path.join(sites_path, "currentsite.txt"), "w") as sitefile: | |||||
with open(os.path.join(sites_path, "currentsite.txt"), "w") as sitefile: | |||||
sitefile.write(site) | sitefile.write(site) | ||||
print("Current Site set to {}".format(site)) | print("Current Site set to {}".format(site)) | ||||
else: | else: | ||||
@@ -751,6 +751,7 @@ def set_admin_password(context, admin_password=None, logout_all_sessions=False): | |||||
def set_user_password(site, user, password, logout_all_sessions=False): | def set_user_password(site, user, password, logout_all_sessions=False): | ||||
import getpass | import getpass | ||||
from frappe.utils.password import update_password | from frappe.utils.password import update_password | ||||
try: | try: | ||||
@@ -881,15 +882,16 @@ def stop_recording(context): | |||||
raise SiteNotSpecifiedError | raise SiteNotSpecifiedError | ||||
@click.command('ngrok') | @click.command('ngrok') | ||||
@click.option('--bind-tls', is_flag=True, default=False, help='Returns a reference to the https tunnel.') | |||||
@pass_context | @pass_context | ||||
def start_ngrok(context): | |||||
def start_ngrok(context, bind_tls): | |||||
from pyngrok import ngrok | from pyngrok import ngrok | ||||
site = get_site(context) | site = get_site(context) | ||||
frappe.init(site=site) | frappe.init(site=site) | ||||
port = frappe.conf.http_port or frappe.conf.webserver_port | port = frappe.conf.http_port or frappe.conf.webserver_port | ||||
tunnel = ngrok.connect(addr=str(port), host_header=site) | |||||
tunnel = ngrok.connect(addr=str(port), host_header=site, bind_tls=bind_tls) | |||||
print(f'Public URL: {tunnel.public_url}') | print(f'Public URL: {tunnel.public_url}') | ||||
print('Inspect logs at http://localhost:4040') | print('Inspect logs at http://localhost:4040') | ||||
@@ -18,6 +18,7 @@ from urllib.parse import unquote | |||||
from frappe.utils.user import is_system_user | from frappe.utils.user import is_system_user | ||||
from frappe.contacts.doctype.contact.contact import get_contact_name | from frappe.contacts.doctype.contact.contact import get_contact_name | ||||
from frappe.automation.doctype.assignment_rule.assignment_rule import apply as apply_assignment_rule | from frappe.automation.doctype.assignment_rule.assignment_rule import apply as apply_assignment_rule | ||||
from parse import compile | |||||
exclude_from_linked_with = True | exclude_from_linked_with = True | ||||
@@ -114,6 +115,44 @@ class Communication(Document, CommunicationEmailMixin): | |||||
frappe.publish_realtime('new_message', self.as_dict(), | frappe.publish_realtime('new_message', self.as_dict(), | ||||
user=self.reference_name, after_commit=True) | user=self.reference_name, after_commit=True) | ||||
def set_signature_in_email_content(self): | |||||
"""Set sender's User.email_signature or default outgoing's EmailAccount.signature to the email | |||||
""" | |||||
if not self.content: | |||||
return | |||||
quill_parser = compile('<div class="ql-editor read-mode">{}</div>') | |||||
email_body = quill_parser.parse(self.content) | |||||
if not email_body: | |||||
return | |||||
email_body = email_body[0] | |||||
user_email_signature = frappe.db.get_value( | |||||
"User", | |||||
self.sender, | |||||
"email_signature", | |||||
) if self.sender else None | |||||
signature = user_email_signature or frappe.db.get_value( | |||||
"Email Account", | |||||
{"default_outgoing": 1, "add_signature": 1}, | |||||
"signature", | |||||
) | |||||
if not signature: | |||||
return | |||||
_signature = quill_parser.parse(signature)[0] if "ql-editor" in signature else None | |||||
if (_signature or signature) not in self.content: | |||||
self.content = f'{self.content}</p><br><p class="signature">{signature}' | |||||
def before_save(self): | |||||
if not self.flags.skip_add_signature: | |||||
self.set_signature_in_email_content() | |||||
def on_update(self): | def on_update(self): | ||||
# add to _comment property of the doctype, so it shows up in | # add to _comment property of the doctype, so it shows up in | ||||
# comments count for the list view | # comments count for the list view | ||||
@@ -22,12 +22,30 @@ OUTGOING_EMAIL_ACCOUNT_MISSING = _(""" | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent", | |||||
sender=None, sender_full_name=None, recipients=None, communication_medium="Email", send_email=False, | |||||
print_html=None, print_format=None, attachments='[]', send_me_a_copy=False, cc=None, bcc=None, | |||||
flags=None, read_receipt=None, print_letterhead=True, email_template=None, communication_type=None, | |||||
ignore_permissions=False) -> Dict[str, str]: | |||||
"""Make a new communication. | |||||
def make( | |||||
doctype=None, | |||||
name=None, | |||||
content=None, | |||||
subject=None, | |||||
sent_or_received="Sent", | |||||
sender=None, | |||||
sender_full_name=None, | |||||
recipients=None, | |||||
communication_medium="Email", | |||||
send_email=False, | |||||
print_html=None, | |||||
print_format=None, | |||||
attachments="[]", | |||||
send_me_a_copy=False, | |||||
cc=None, | |||||
bcc=None, | |||||
read_receipt=None, | |||||
print_letterhead=True, | |||||
email_template=None, | |||||
communication_type=None, | |||||
**kwargs, | |||||
) -> Dict[str, str]: | |||||
"""Make a new communication. Checks for email permissions for specified Document. | |||||
:param doctype: Reference DocType. | :param doctype: Reference DocType. | ||||
:param name: Reference Document name. | :param name: Reference Document name. | ||||
@@ -44,17 +62,71 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = | |||||
:param send_me_a_copy: Send a copy to the sender (default **False**). | :param send_me_a_copy: Send a copy to the sender (default **False**). | ||||
:param email_template: Template which is used to compose mail . | :param email_template: Template which is used to compose mail . | ||||
""" | """ | ||||
is_error_report = (doctype=="User" and name==frappe.session.user and subject=="Error Report") | |||||
send_me_a_copy = cint(send_me_a_copy) | |||||
if kwargs: | |||||
from frappe.utils.commands import warn | |||||
warn( | |||||
f"Options {kwargs} used in frappe.core.doctype.communication.email.make " | |||||
"are deprecated or unsupported", | |||||
category=DeprecationWarning | |||||
) | |||||
if doctype and name and not frappe.has_permission(doctype=doctype, ptype="email", doc=name): | |||||
raise frappe.PermissionError( | |||||
f"You are not allowed to send emails related to: {doctype} {name}" | |||||
) | |||||
return _make( | |||||
doctype=doctype, | |||||
name=name, | |||||
content=content, | |||||
subject=subject, | |||||
sent_or_received=sent_or_received, | |||||
sender=sender, | |||||
sender_full_name=sender_full_name, | |||||
recipients=recipients, | |||||
communication_medium=communication_medium, | |||||
send_email=send_email, | |||||
print_html=print_html, | |||||
print_format=print_format, | |||||
attachments=attachments, | |||||
send_me_a_copy=cint(send_me_a_copy), | |||||
cc=cc, | |||||
bcc=bcc, | |||||
read_receipt=read_receipt, | |||||
print_letterhead=print_letterhead, | |||||
email_template=email_template, | |||||
communication_type=communication_type, | |||||
add_signature=False, | |||||
) | |||||
if not ignore_permissions: | |||||
if doctype and name and not is_error_report and not frappe.has_permission(doctype, "email", name) and not (flags or {}).get('ignore_doctype_permissions'): | |||||
raise frappe.PermissionError("You are not allowed to send emails related to: {doctype} {name}".format( | |||||
doctype=doctype, name=name)) | |||||
if not sender: | |||||
sender = get_formatted_email(frappe.session.user) | |||||
def _make( | |||||
doctype=None, | |||||
name=None, | |||||
content=None, | |||||
subject=None, | |||||
sent_or_received="Sent", | |||||
sender=None, | |||||
sender_full_name=None, | |||||
recipients=None, | |||||
communication_medium="Email", | |||||
send_email=False, | |||||
print_html=None, | |||||
print_format=None, | |||||
attachments="[]", | |||||
send_me_a_copy=False, | |||||
cc=None, | |||||
bcc=None, | |||||
read_receipt=None, | |||||
print_letterhead=True, | |||||
email_template=None, | |||||
communication_type=None, | |||||
add_signature=True, | |||||
) -> Dict[str, str]: | |||||
"""Internal method to make a new communication that ignores Permission checks. | |||||
""" | |||||
sender = sender or get_formatted_email(frappe.session.user) | |||||
recipients = list_to_str(recipients) if isinstance(recipients, list) else recipients | recipients = list_to_str(recipients) if isinstance(recipients, list) else recipients | ||||
cc = list_to_str(cc) if isinstance(cc, list) else cc | cc = list_to_str(cc) if isinstance(cc, list) else cc | ||||
bcc = list_to_str(bcc) if isinstance(bcc, list) else bcc | bcc = list_to_str(bcc) if isinstance(bcc, list) else bcc | ||||
@@ -77,7 +149,9 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = | |||||
"read_receipt":read_receipt, | "read_receipt":read_receipt, | ||||
"has_attachment": 1 if attachments else 0, | "has_attachment": 1 if attachments else 0, | ||||
"communication_type": communication_type, | "communication_type": communication_type, | ||||
}).insert(ignore_permissions=True) | |||||
}) | |||||
comm.flags.skip_add_signature = not add_signature | |||||
comm.insert(ignore_permissions=True) | |||||
# if not committed, delayed task doesn't find the communication | # if not committed, delayed task doesn't find the communication | ||||
if attachments: | if attachments: | ||||
@@ -87,17 +161,21 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = | |||||
if cint(send_email): | if cint(send_email): | ||||
if not comm.get_outgoing_email_account(): | if not comm.get_outgoing_email_account(): | ||||
frappe.throw(msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError) | |||||
frappe.throw( | |||||
msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError | |||||
) | |||||
comm.send_email(print_html=print_html, print_format=print_format, | |||||
send_me_a_copy=send_me_a_copy, print_letterhead=print_letterhead) | |||||
comm.send_email( | |||||
print_html=print_html, | |||||
print_format=print_format, | |||||
send_me_a_copy=send_me_a_copy, | |||||
print_letterhead=print_letterhead, | |||||
) | |||||
emails_not_sent_to = comm.exclude_emails_list(include_sender=send_me_a_copy) | emails_not_sent_to = comm.exclude_emails_list(include_sender=send_me_a_copy) | ||||
return { | |||||
"name": comm.name, | |||||
"emails_not_sent_to": ", ".join(emails_not_sent_to) | |||||
} | |||||
return {"name": comm.name, "emails_not_sent_to": ", ".join(emails_not_sent_to)} | |||||
def validate_email(doc: "Communication") -> None: | def validate_email(doc: "Communication") -> None: | ||||
"""Validate Email Addresses of Recipients and CC""" | """Validate Email Addresses of Recipients and CC""" | ||||
@@ -324,7 +324,7 @@ class DataExporter: | |||||
d = doc.copy() | d = doc.copy() | ||||
meta = frappe.get_meta(dt) | meta = frappe.get_meta(dt) | ||||
if self.all_doctypes: | if self.all_doctypes: | ||||
d.name = '"'+ d.name+'"' | |||||
d.name = f'"{d.name}"' | |||||
if len(rows) < rowidx + 1: | if len(rows) < rowidx + 1: | ||||
rows.append([""] * (len(self.columns) + 1)) | rows.append([""] * (len(self.columns) + 1)) | ||||
@@ -61,6 +61,13 @@ frappe.ui.form.on('DocType', { | |||||
frm.events.set_naming_rule_description(frm); | frm.events.set_naming_rule_description(frm); | ||||
}, | }, | ||||
istable: (frm) => { | |||||
if (frm.doc.istable && frm.is_new()) { | |||||
frm.set_value('autoname', 'autoincrement'); | |||||
frm.set_value('allow_rename', 0); | |||||
} | |||||
}, | |||||
naming_rule: function(frm) { | naming_rule: function(frm) { | ||||
// set the "autoname" property based on naming_rule | // set the "autoname" property based on naming_rule | ||||
if (frm.doc.naming_rule && !frm.__from_autoname) { | if (frm.doc.naming_rule && !frm.__from_autoname) { | ||||
@@ -70,6 +77,10 @@ frappe.ui.form.on('DocType', { | |||||
if (frm.doc.naming_rule=='Set by user') { | if (frm.doc.naming_rule=='Set by user') { | ||||
frm.set_value('autoname', 'Prompt'); | frm.set_value('autoname', 'Prompt'); | ||||
} else if (frm.doc.naming_rule === 'Autoincrement') { | |||||
frm.set_value('autoname', 'autoincrement'); | |||||
// set allow rename to be false when using autoincrement | |||||
frm.set_value('allow_rename', 0); | |||||
} else if (frm.doc.naming_rule=='By fieldname') { | } else if (frm.doc.naming_rule=='By fieldname') { | ||||
frm.set_value('autoname', 'field:'); | frm.set_value('autoname', 'field:'); | ||||
} else if (frm.doc.naming_rule=='By "Naming Series" field') { | } else if (frm.doc.naming_rule=='By "Naming Series" field') { | ||||
@@ -91,6 +102,7 @@ frappe.ui.form.on('DocType', { | |||||
set_naming_rule_description(frm) { | set_naming_rule_description(frm) { | ||||
let naming_rule_description = { | let naming_rule_description = { | ||||
'Set by user': '', | 'Set by user': '', | ||||
'Autoincrement': 'Uses Auto Increment feature of database.<br><b>WARNING: After using this option, any other naming option will not be accessible.</b>', | |||||
'By fieldname': 'Format: <code>field:[fieldname]</code>. Valid fieldname must exist', | 'By fieldname': 'Format: <code>field:[fieldname]</code>. Valid fieldname must exist', | ||||
'By "Naming Series" field': 'Format: <code>naming_series:[fieldname]</code>. Fieldname called <code>naming_series</code> must exist', | 'By "Naming Series" field': 'Format: <code>naming_series:[fieldname]</code>. Fieldname called <code>naming_series</code> must exist', | ||||
'Expression': 'Format: <code>format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####}</code> - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.', | 'Expression': 'Format: <code>format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####}</code> - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.', | ||||
@@ -111,6 +123,8 @@ frappe.ui.form.on('DocType', { | |||||
frm.__from_autoname = true; | frm.__from_autoname = true; | ||||
if (frm.doc.autoname.toLowerCase() === 'prompt') { | if (frm.doc.autoname.toLowerCase() === 'prompt') { | ||||
frm.set_value('naming_rule', 'Set by user'); | frm.set_value('naming_rule', 'Set by user'); | ||||
} else if (frm.doc.autoname.toLowerCase() === 'autoincrement') { | |||||
frm.set_value('naming_rule', 'Autoincrement'); | |||||
} else if (frm.doc.autoname.startsWith('field:')) { | } else if (frm.doc.autoname.startsWith('field:')) { | ||||
frm.set_value('naming_rule', 'By fieldname'); | frm.set_value('naming_rule', 'By fieldname'); | ||||
} else if (frm.doc.autoname.startsWith('naming_series:')) { | } else if (frm.doc.autoname.startsWith('naming_series:')) { | ||||
@@ -208,7 +208,7 @@ | |||||
"label": "Naming" | "label": "Naming" | ||||
}, | }, | ||||
{ | { | ||||
"description": "Naming Options:\n<ol><li><b>field:[fieldname]</b> - By Field</li><li><b>naming_series:</b> - By Naming Series (field called naming_series must be present</li><li><b>Prompt</b> - Prompt user for a name</li><li><b>[series]</b> - Series by prefix (separated by a dot); for example PRE.#####</li>\n<li><b>format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####}</b> - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.</li></ol>", | |||||
"description": "Naming Options:\n<ol><li><b>field:[fieldname]</b> - By Field</li><li><b>autoincrement</b> - Uses Databases' Auto Increment feature</li><li><b>naming_series:</b> - By Naming Series (field called naming_series must be present</li><li><b>Prompt</b> - Prompt user for a name</li><li><b>[series]</b> - Series by prefix (separated by a dot); for example PRE.#####</li>\n<li><b>format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####}</b> - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.</li></ol>", | |||||
"fieldname": "autoname", | "fieldname": "autoname", | ||||
"fieldtype": "Data", | "fieldtype": "Data", | ||||
"label": "Auto Name", | "label": "Auto Name", | ||||
@@ -216,6 +216,7 @@ | |||||
"oldfieldtype": "Data" | "oldfieldtype": "Data" | ||||
}, | }, | ||||
{ | { | ||||
"depends_on": "eval:doc.naming_rule !== \"Autoincrement\"", | |||||
"fieldname": "name_case", | "fieldname": "name_case", | ||||
"fieldtype": "Select", | "fieldtype": "Select", | ||||
"label": "Name Case", | "label": "Name Case", | ||||
@@ -282,6 +283,7 @@ | |||||
}, | }, | ||||
{ | { | ||||
"default": "1", | "default": "1", | ||||
"depends_on": "eval:doc.naming_rule !== \"Autoincrement\"", | |||||
"fieldname": "allow_rename", | "fieldname": "allow_rename", | ||||
"fieldtype": "Check", | "fieldtype": "Check", | ||||
"label": "Allow Rename", | "label": "Allow Rename", | ||||
@@ -565,7 +567,7 @@ | |||||
"fieldtype": "Select", | "fieldtype": "Select", | ||||
"label": "Naming Rule", | "label": "Naming Rule", | ||||
"length": 40, | "length": 40, | ||||
"options": "\nSet by user\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script" | |||||
"options": "\nSet by user\nAutoincrement\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script" | |||||
}, | }, | ||||
{ | { | ||||
"fieldname": "migration_hash", | "fieldname": "migration_hash", | ||||
@@ -593,6 +595,7 @@ | |||||
], | ], | ||||
"icon": "fa fa-bolt", | "icon": "fa fa-bolt", | ||||
"idx": 6, | "idx": 6, | ||||
"index_web_pages_for_search": 1, | |||||
"links": [ | "links": [ | ||||
{ | { | ||||
"group": "Views", | "group": "Views", | ||||
@@ -670,10 +673,11 @@ | |||||
"link_fieldname": "reference_doctype" | "link_fieldname": "reference_doctype" | ||||
} | } | ||||
], | ], | ||||
"modified": "2022-01-07 16:07:06.196534", | |||||
"modified": "2022-02-15 21:47:16.467217", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Core", | "module": "Core", | ||||
"name": "DocType", | "name": "DocType", | ||||
"naming_rule": "Set by user", | |||||
"owner": "Administrator", | "owner": "Administrator", | ||||
"permissions": [ | "permissions": [ | ||||
{ | { | ||||
@@ -703,5 +707,6 @@ | |||||
"show_name_in_global_search": 1, | "show_name_in_global_search": 1, | ||||
"sort_field": "modified", | "sort_field": "modified", | ||||
"sort_order": "DESC", | "sort_order": "DESC", | ||||
"states": [], | |||||
"track_changes": 1 | "track_changes": 1 | ||||
} | } |
@@ -60,6 +60,7 @@ class DocType(Document): | |||||
self.check_developer_mode() | self.check_developer_mode() | ||||
self.validate_autoname() | |||||
self.validate_name() | self.validate_name() | ||||
self.set_defaults_for_single_and_table() | self.set_defaults_for_single_and_table() | ||||
@@ -714,6 +715,18 @@ class DocType(Document): | |||||
self.name) | self.name) | ||||
return max_idx and max_idx[0][0] or 0 | return max_idx and max_idx[0][0] or 0 | ||||
def validate_autoname(self): | |||||
if not self.is_new(): | |||||
doc_before_save = self.get_doc_before_save() | |||||
if doc_before_save: | |||||
if (self.autoname == "autoincrement" and doc_before_save.autoname != "autoincrement") \ | |||||
or (self.autoname != "autoincrement" and doc_before_save.autoname == "autoincrement"): | |||||
frappe.throw(_("Cannot change to/from Autoincrement naming rule")) | |||||
else: | |||||
if self.autoname == "autoincrement": | |||||
self.allow_rename = 0 | |||||
def validate_name(self, name=None): | def validate_name(self, name=None): | ||||
if not name: | if not name: | ||||
name = self.name | name = self.name | ||||
@@ -732,9 +745,12 @@ class DocType(Document): | |||||
frappe.throw(_("DocType's name should not start or end with whitespace"), frappe.NameError) | frappe.throw(_("DocType's name should not start or end with whitespace"), frappe.NameError) | ||||
# a DocType's name should not start with a number or underscore | # a DocType's name should not start with a number or underscore | ||||
# and should only contain letters, numbers and underscore | |||||
if not re.match(r"^(?![\W])[^\d_\s][\w ]+$", name, **flags): | |||||
frappe.throw(_("DocType's name should start with a letter and it can only consist of letters, numbers, spaces and underscores"), frappe.NameError) | |||||
# and should only contain letters, numbers, underscore, and hyphen | |||||
if not re.match(r"^(?![\W])[^\d_\s][\w -]+$", name, **flags): | |||||
frappe.throw(_( | |||||
"A DocType's name should start with a letter and can only " | |||||
"consist of letters, numbers, spaces, underscores and hyphens" | |||||
), frappe.NameError, title="Invalid Name") | |||||
validate_route_conflict(self.doctype, self.name) | validate_route_conflict(self.doctype, self.name) | ||||
@@ -24,7 +24,7 @@ class TestDocType(unittest.TestCase): | |||||
self.assertRaises(frappe.NameError, new_doctype("8Some DocType").insert) | self.assertRaises(frappe.NameError, new_doctype("8Some DocType").insert) | ||||
self.assertRaises(frappe.NameError, new_doctype("Some (DocType)").insert) | self.assertRaises(frappe.NameError, new_doctype("Some (DocType)").insert) | ||||
self.assertRaises(frappe.NameError, new_doctype("Some Doctype with a name whose length is more than 61 characters").insert) | self.assertRaises(frappe.NameError, new_doctype("Some Doctype with a name whose length is more than 61 characters").insert) | ||||
for name in ("Some DocType", "Some_DocType"): | |||||
for name in ("Some DocType", "Some_DocType", "Some-DocType"): | |||||
if frappe.db.exists("DocType", name): | if frappe.db.exists("DocType", name): | ||||
frappe.delete_doc("DocType", name) | frappe.delete_doc("DocType", name) | ||||
@@ -505,7 +505,23 @@ class TestDocType(unittest.TestCase): | |||||
dt.delete() | dt.delete() | ||||
def new_doctype(name, unique=0, depends_on='', fields=None): | |||||
def test_autoincremented_doctype_transition(self): | |||||
frappe.delete_doc("testy_autoinc_dt") | |||||
dt = new_doctype("testy_autoinc_dt", autoincremented=True).insert(ignore_permissions=True) | |||||
dt.autoname = "hash" | |||||
try: | |||||
dt.save(ignore_permissions=True) | |||||
except frappe.ValidationError as e: | |||||
self.assertEqual(e.args[0], "Cannot change to/from Autoincrement naming rule") | |||||
else: | |||||
self.fail("Shouldnt be possible to transition autoincremented doctype to any other naming rule") | |||||
finally: | |||||
# cleanup | |||||
dt.delete(ignore_permissions=True) | |||||
def new_doctype(name, unique=0, depends_on='', fields=None, autoincremented=False): | |||||
doc = frappe.get_doc({ | doc = frappe.get_doc({ | ||||
"doctype": "DocType", | "doctype": "DocType", | ||||
"module": "Core", | "module": "Core", | ||||
@@ -521,7 +537,8 @@ def new_doctype(name, unique=0, depends_on='', fields=None): | |||||
"role": "System Manager", | "role": "System Manager", | ||||
"read": 1, | "read": 1, | ||||
}], | }], | ||||
"name": name | |||||
"name": name, | |||||
"autoname": "autoincrement" if autoincremented else "" | |||||
}) | }) | ||||
if fields: | if fields: | ||||
@@ -61,7 +61,7 @@ class Role(Document): | |||||
def get_info_based_on_role(role, field='email'): | def get_info_based_on_role(role, field='email'): | ||||
''' Get information of all users that have been assigned this role ''' | ''' Get information of all users that have been assigned this role ''' | ||||
users = frappe.get_list("Has Role", filters={"role": role, "parenttype": "User"}, | |||||
users = frappe.get_list("Has Role", filters={"role": role}, parent_doctype="User", | |||||
fields=["parent as user_name"]) | fields=["parent as user_name"]) | ||||
return get_user_info(users, field) | return get_user_info(users, field) | ||||
@@ -112,7 +112,10 @@ class TestServerScript(unittest.TestCase): | |||||
self.assertEqual(frappe.get_doc('Server Script', 'test_return_value').execute_method(), 'hello') | self.assertEqual(frappe.get_doc('Server Script', 'test_return_value').execute_method(), 'hello') | ||||
def test_permission_query(self): | def test_permission_query(self): | ||||
self.assertTrue('where (1 = 1)' in frappe.db.get_list('ToDo', run=False)) | |||||
if frappe.conf.db_type == "mariadb": | |||||
self.assertTrue('where (1 = 1)' in frappe.db.get_list('ToDo', run=False)) | |||||
else: | |||||
self.assertTrue('where (1 = \'1\')' in frappe.db.get_list('ToDo', run=False)) | |||||
self.assertTrue(isinstance(frappe.db.get_list('ToDo'), list)) | self.assertTrue(isinstance(frappe.db.get_list('ToDo'), list)) | ||||
def test_attribute_error(self): | def test_attribute_error(self): | ||||
@@ -668,8 +668,7 @@ | |||||
"link_fieldname": "user" | "link_fieldname": "user" | ||||
} | } | ||||
], | ], | ||||
"max_attachments": 5, | |||||
"modified": "2022-01-03 11:53:25.250822", | |||||
"modified": "2022-03-09 01:47:56.745069", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Core", | "module": "Core", | ||||
"name": "User", | "name": "User", | ||||
@@ -142,8 +142,6 @@ class Database(object): | |||||
self.log_query(query, values, debug, explain) | self.log_query(query, values, debug, explain) | ||||
if values!=(): | if values!=(): | ||||
if isinstance(values, dict): | |||||
values = dict(values) | |||||
# MySQL-python==1.2.5 hack! | # MySQL-python==1.2.5 hack! | ||||
if not isinstance(values, (dict, tuple, list)): | if not isinstance(values, (dict, tuple, list)): | ||||
@@ -181,7 +179,7 @@ class Database(object): | |||||
print(e) | print(e) | ||||
raise | raise | ||||
if ignore_ddl and (self.is_missing_column(e) or self.is_missing_table(e) or self.cant_drop_field_or_key(e)): | |||||
if ignore_ddl and (self.is_missing_column(e) or self.is_table_missing(e) or self.cant_drop_field_or_key(e)): | |||||
pass | pass | ||||
else: | else: | ||||
raise | raise | ||||
@@ -1028,7 +1026,7 @@ class Database(object): | |||||
return [] | return [] | ||||
def is_missing_table_or_column(self, e): | def is_missing_table_or_column(self, e): | ||||
return self.is_missing_column(e) or self.is_missing_table(e) | |||||
return self.is_missing_column(e) or self.is_table_missing(e) | |||||
def multisql(self, sql_dict, values=(), **kwargs): | def multisql(self, sql_dict, values=(), **kwargs): | ||||
current_dialect = frappe.db.db_type or 'mariadb' | current_dialect = frappe.db.db_type or 'mariadb' | ||||
@@ -154,6 +154,10 @@ class MariaDBDatabase(Database): | |||||
def is_table_missing(e): | def is_table_missing(e): | ||||
return e.args[0] == ER.NO_SUCH_TABLE | return e.args[0] == ER.NO_SUCH_TABLE | ||||
@staticmethod | |||||
def is_missing_table(e): | |||||
return MariaDBDatabase.is_table_missing(e) | |||||
@staticmethod | @staticmethod | ||||
def is_missing_column(e): | def is_missing_column(e): | ||||
return e.args[0] == ER.BAD_FIELD_ERROR | return e.args[0] == ER.BAD_FIELD_ERROR | ||||
@@ -1,12 +1,16 @@ | |||||
import frappe | import frappe | ||||
from frappe import _ | from frappe import _ | ||||
from frappe.database.schema import DBTable | from frappe.database.schema import DBTable | ||||
from frappe.database.sequence import create_sequence | |||||
from frappe.model import log_types | |||||
class MariaDBTable(DBTable): | class MariaDBTable(DBTable): | ||||
def create(self): | def create(self): | ||||
additional_definitions = "" | additional_definitions = "" | ||||
engine = self.meta.get("engine") or "InnoDB" | engine = self.meta.get("engine") or "InnoDB" | ||||
varchar_len = frappe.db.VARCHAR_LEN | varchar_len = frappe.db.VARCHAR_LEN | ||||
name_column = f"name varchar({varchar_len}) primary key" | |||||
# columns | # columns | ||||
column_defs = self.get_column_definitions() | column_defs = self.get_column_definitions() | ||||
@@ -29,9 +33,27 @@ class MariaDBTable(DBTable): | |||||
) | ) | ||||
) + ',\n' | ) + ',\n' | ||||
# creating sequence(s) | |||||
if (not self.meta.issingle and self.meta.autoname == "autoincrement")\ | |||||
or self.doctype in log_types: | |||||
# NOTE: using a very small cache - as during backup, if the sequence was used in anyform, | |||||
# it drops the cache and uses the next non cached value in setval func and | |||||
# puts that in the backup file, which will start the counter | |||||
# from that value when inserting any new record in the doctype. | |||||
# By default the cache is 1000 which will mess up the sequence when | |||||
# using the system after a restore. | |||||
# issue link: https://jira.mariadb.org/browse/MDEV-21786 | |||||
create_sequence(self.doctype, check_not_exists=True, cache=50) | |||||
# NOTE: not used nextval func as default as the ability to restore | |||||
# database with sequences has bugs in mariadb and gives a scary error. | |||||
# issue link: https://jira.mariadb.org/browse/MDEV-21786 | |||||
name_column = "name bigint primary key" | |||||
# create table | # create table | ||||
query = f"""create table `{self.table_name}` ( | query = f"""create table `{self.table_name}` ( | ||||
name varchar({varchar_len}) not null primary key, | |||||
{name_column}, | |||||
creation datetime(6), | creation datetime(6), | ||||
modified datetime(6), | modified datetime(6), | ||||
modified_by varchar({varchar_len}), | modified_by varchar({varchar_len}), | ||||
@@ -99,16 +99,13 @@ class PostgresDatabase(Database): | |||||
return db_size[0].get('database_size') | return db_size[0].get('database_size') | ||||
# pylint: disable=W0221 | # pylint: disable=W0221 | ||||
def sql(self, *args, **kwargs): | |||||
if args: | |||||
# since tuple is immutable | |||||
args = list(args) | |||||
args[0] = modify_query(args[0]) | |||||
args = tuple(args) | |||||
elif kwargs.get('query'): | |||||
kwargs['query'] = modify_query(kwargs.get('query')) | |||||
return super(PostgresDatabase, self).sql(*args, **kwargs) | |||||
def sql(self, query, values=(), *args, **kwargs): | |||||
return super(PostgresDatabase, self).sql( | |||||
modify_query(query), | |||||
modify_values(values), | |||||
*args, | |||||
**kwargs | |||||
) | |||||
def get_tables(self, cached=True): | def get_tables(self, cached=True): | ||||
return [d[0] for d in self.sql("""select table_name | return [d[0] for d in self.sql("""select table_name | ||||
@@ -153,6 +150,10 @@ class PostgresDatabase(Database): | |||||
def is_table_missing(e): | def is_table_missing(e): | ||||
return getattr(e, 'pgcode', None) == '42P01' | return getattr(e, 'pgcode', None) == '42P01' | ||||
@staticmethod | |||||
def is_missing_table(e): | |||||
return PostgresDatabase.is_table_missing(e) | |||||
@staticmethod | @staticmethod | ||||
def is_missing_column(e): | def is_missing_column(e): | ||||
return getattr(e, 'pgcode', None) == '42703' | return getattr(e, 'pgcode', None) == '42703' | ||||
@@ -335,12 +336,47 @@ def modify_query(query): | |||||
query = replace_locate_with_strpos(query) | query = replace_locate_with_strpos(query) | ||||
# select from requires "" | # select from requires "" | ||||
if re.search('from tab', query, flags=re.IGNORECASE): | if re.search('from tab', query, flags=re.IGNORECASE): | ||||
query = re.sub('from tab([a-zA-Z]*)', r'from "tab\1"', query, flags=re.IGNORECASE) | |||||
query = re.sub(r'from tab([\w-]*)', r'from "tab\1"', query, flags=re.IGNORECASE) | |||||
# only find int (with/without signs), ignore decimals (with/without signs), ignore hashes (which start with numbers), | |||||
# drop .0 from decimals and add quotes around them | |||||
# | |||||
# >>> query = "c='abcd' , a >= 45, b = -45.0, c = 40, d=4500.0, e=3500.53, f=40psdfsd, g=9092094312, h=12.00023" | |||||
# >>> re.sub(r"([=><]+)\s*(?!\d+[a-zA-Z])(?![+-]?\d+\.\d\d+)([+-]?\d+)(\.0)?", r"\1 '\2'", query) | |||||
# "c='abcd' , a >= '45', b = '-45', c = '40', d= '4500', e=3500.53, f=40psdfsd, g= '9092094312', h=12.00023 | |||||
query = re.sub(r"([=><]+)\s*(?!\d+[a-zA-Z])(?![+-]?\d+\.\d\d+)([+-]?\d+)(\.0)?", r"\1 '\2'", query) | |||||
return query | return query | ||||
def modify_values(values): | |||||
def stringify_value(value): | |||||
if isinstance(value, int): | |||||
value = str(value) | |||||
elif isinstance(value, float): | |||||
truncated_float = int(value) | |||||
if value == truncated_float: | |||||
value = str(truncated_float) | |||||
return value | |||||
if not values: | |||||
return values | |||||
if isinstance(values, dict): | |||||
for k, v in values.items(): | |||||
values[k] = stringify_value(v) | |||||
elif isinstance(values, (tuple, list)): | |||||
new_values = [] | |||||
for val in values: | |||||
new_values.append(stringify_value(val)) | |||||
values = new_values | |||||
else: | |||||
values = stringify_value(values) | |||||
return values | |||||
def replace_locate_with_strpos(query): | def replace_locate_with_strpos(query): | ||||
# strpos is the locate equivalent in postgres | # strpos is the locate equivalent in postgres | ||||
if re.search(r'locate\(', query, flags=re.IGNORECASE): | if re.search(r'locate\(', query, flags=re.IGNORECASE): | ||||
query = re.sub(r'locate\(([^,]+),([^)]+)\)', r'strpos(\2, \1)', query, flags=re.IGNORECASE) | |||||
query = re.sub(r'locate\(([^,]+),([^)]+)(\)?)\)', r'strpos(\2\3, \1)', query, flags=re.IGNORECASE) | |||||
return query | return query |
@@ -2,10 +2,14 @@ import frappe | |||||
from frappe import _ | from frappe import _ | ||||
from frappe.utils import cint, flt | from frappe.utils import cint, flt | ||||
from frappe.database.schema import DBTable, get_definition | from frappe.database.schema import DBTable, get_definition | ||||
from frappe.database.sequence import create_sequence | |||||
from frappe.model import log_types | |||||
class PostgresTable(DBTable): | class PostgresTable(DBTable): | ||||
def create(self): | def create(self): | ||||
varchar_len = frappe.db.VARCHAR_LEN | varchar_len = frappe.db.VARCHAR_LEN | ||||
name_column = f"name varchar({varchar_len}) primary key" | |||||
additional_definitions = "" | additional_definitions = "" | ||||
# columns | # columns | ||||
@@ -26,9 +30,21 @@ class PostgresTable(DBTable): | |||||
) | ) | ||||
) | ) | ||||
# creating sequence(s) | |||||
if (not self.meta.issingle and self.meta.autoname == "autoincrement")\ | |||||
or self.doctype in log_types: | |||||
# The sequence cache is per connection. | |||||
# Since we're opening and closing connections for every transaction this results in skipping the cache | |||||
# to the next non-cached value hence not using cache in postgres. | |||||
# ref: https://stackoverflow.com/questions/21356375/postgres-9-0-4-sequence-skipping-numbers | |||||
create_sequence(self.doctype, check_not_exists=True) | |||||
name_column = "name bigint primary key" | |||||
# TODO: set docstatus length | |||||
# create table | # create table | ||||
frappe.db.sql(f"""create table `{self.table_name}` ( | frappe.db.sql(f"""create table `{self.table_name}` ( | ||||
name varchar({varchar_len}) not null primary key, | |||||
{name_column}, | |||||
creation timestamp(6), | creation timestamp(6), | ||||
modified timestamp(6), | modified timestamp(6), | ||||
modified_by varchar({varchar_len}), | modified_by varchar({varchar_len}), | ||||
@@ -0,0 +1,80 @@ | |||||
from frappe import db, scrub | |||||
def create_sequence( | |||||
doctype_name: str, | |||||
*, | |||||
slug: str = "_id_seq", | |||||
check_not_exists: bool = False, | |||||
cycle: bool = False, | |||||
cache: int = 0, | |||||
start_value: int = 0, | |||||
increment_by: int = 0, | |||||
min_value: int = 0, | |||||
max_value: int = 0 | |||||
) -> str: | |||||
query = "create sequence" | |||||
sequence_name = scrub(doctype_name + slug) | |||||
if check_not_exists: | |||||
query += " if not exists" | |||||
query += f" {sequence_name}" | |||||
if cache: | |||||
query += f" cache {cache}" | |||||
else: | |||||
# in postgres, the default is cache 1 | |||||
if db.db_type == "mariadb": | |||||
query += " nocache" | |||||
if start_value: | |||||
# default is 1 | |||||
query += f" start with {start_value}" | |||||
if increment_by: | |||||
# default is 1 | |||||
query += f" increment by {increment_by}" | |||||
if min_value: | |||||
# default is 1 | |||||
query += f" min value {min_value}" | |||||
if max_value: | |||||
query += f" max value {max_value}" | |||||
if not cycle: | |||||
if db.db_type == "mariadb": | |||||
query += " nocycle" | |||||
else: | |||||
query += " cycle" | |||||
db.sql(query) | |||||
return sequence_name | |||||
def get_next_val(doctype_name: str, slug: str = "_id_seq") -> int: | |||||
if db.db_type == "postgres": | |||||
return db.sql(f"select nextval(\'\"{scrub(doctype_name + slug)}\"\')")[0][0] | |||||
return db.sql(f"select nextval(`{scrub(doctype_name + slug)}`)")[0][0] | |||||
def set_next_val( | |||||
doctype_name: str, | |||||
next_val: int, | |||||
*, | |||||
slug: str = "_id_seq", | |||||
is_val_used :bool = False | |||||
) -> None: | |||||
if not is_val_used: | |||||
is_val_used = 0 if db.db_type == "mariadb" else "f" | |||||
else: | |||||
is_val_used = 1 if db.db_type == "mariadb" else "t" | |||||
if db.db_type == "postgres": | |||||
db.sql(f"SELECT SETVAL('\"{scrub(doctype_name + slug)}\"', {next_val}, '{is_val_used}')") | |||||
else: | |||||
db.sql(f"SELECT SETVAL(`{scrub(doctype_name + slug)}`, {next_val}, {is_val_used})") |
@@ -28,6 +28,7 @@ frappe.ui.form.on('Number Card', { | |||||
frm.trigger('render_filters_table'); | frm.trigger('render_filters_table'); | ||||
} | } | ||||
frm.trigger('create_add_to_dashboard_button'); | frm.trigger('create_add_to_dashboard_button'); | ||||
frm.trigger('set_parent_document_type'); | |||||
}, | }, | ||||
create_add_to_dashboard_button: function(frm) { | create_add_to_dashboard_button: function(frm) { | ||||
@@ -141,7 +142,9 @@ frappe.ui.form.on('Number Card', { | |||||
frm.set_value('filters_json', '[]'); | frm.set_value('filters_json', '[]'); | ||||
frm.set_value('dynamic_filters_json', '[]'); | frm.set_value('dynamic_filters_json', '[]'); | ||||
frm.set_value('aggregate_function_based_on', ''); | frm.set_value('aggregate_function_based_on', ''); | ||||
frm.set_value('parent_document_type', ''); | |||||
frm.trigger('set_options'); | frm.trigger('set_options'); | ||||
frm.trigger('set_parent_document_type'); | |||||
}, | }, | ||||
set_options: function(frm) { | set_options: function(frm) { | ||||
@@ -317,6 +320,7 @@ frappe.ui.form.on('Number Card', { | |||||
frm.filter_group = new frappe.ui.FilterGroup({ | frm.filter_group = new frappe.ui.FilterGroup({ | ||||
parent: dialog.get_field('filter_area').$wrapper, | parent: dialog.get_field('filter_area').$wrapper, | ||||
doctype: frm.doc.document_type, | doctype: frm.doc.document_type, | ||||
parent_doctype: frm.doc.parent_document_type, | |||||
on_change: () => {}, | on_change: () => {}, | ||||
}); | }); | ||||
filters && frm.filter_group.add_filters_to_filter_group(filters); | filters && frm.filter_group.add_filters_to_filter_group(filters); | ||||
@@ -436,6 +440,36 @@ frappe.ui.form.on('Number Card', { | |||||
frm.dynamic_filter_table.find('tbody').html(filter_rows); | frm.dynamic_filter_table.find('tbody').html(filter_rows); | ||||
} | } | ||||
}, | |||||
set_parent_document_type: async function(frm) { | |||||
let document_type = frm.doc.document_type; | |||||
let doc_is_table = document_type && | |||||
(await frappe.db.get_value('DocType', document_type, 'istable')).message.istable; | |||||
frm.set_df_property('parent_document_type', 'hidden', !doc_is_table); | |||||
if (document_type && doc_is_table) { | |||||
let parent = await frappe.db.get_list('DocField', { | |||||
filters: { | |||||
'fieldtype': 'Table', | |||||
'options': document_type | |||||
}, | |||||
fields: ['parent'] | |||||
}); | |||||
parent && frm.set_query('parent_document_type', function() { | |||||
return { | |||||
filters: { | |||||
"name": ['in', parent.map(({ parent }) => parent)] | |||||
} | |||||
}; | |||||
}); | |||||
if (parent.length === 1) { | |||||
frm.set_value('parent_document_type', parent[0].parent); | |||||
} | |||||
} | |||||
} | } | ||||
}); | }); |
@@ -16,6 +16,7 @@ | |||||
"aggregate_function_based_on", | "aggregate_function_based_on", | ||||
"column_break_2", | "column_break_2", | ||||
"document_type", | "document_type", | ||||
"parent_document_type", | |||||
"report_field", | "report_field", | ||||
"report_function", | "report_function", | ||||
"is_public", | "is_public", | ||||
@@ -188,10 +189,17 @@ | |||||
"label": "Function", | "label": "Function", | ||||
"mandatory_depends_on": "eval: doc.type == 'Report'", | "mandatory_depends_on": "eval: doc.type == 'Report'", | ||||
"options": "Sum\nAverage\nMinimum\nMaximum" | "options": "Sum\nAverage\nMinimum\nMaximum" | ||||
}, | |||||
{ | |||||
"description": "The document type selected is a child table, so the parent document type is required.", | |||||
"fieldname": "parent_document_type", | |||||
"fieldtype": "Link", | |||||
"label": "Parent Document Type", | |||||
"options": "DocType" | |||||
} | } | ||||
], | ], | ||||
"links": [], | "links": [], | ||||
"modified": "2020-07-23 11:11:03.391719", | |||||
"modified": "2022-03-10 15:34:38.210910", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Desk", | "module": "Desk", | ||||
"name": "Number Card", | "name": "Number Card", | ||||
@@ -234,6 +242,7 @@ | |||||
"search_fields": "label, document_type", | "search_fields": "label, document_type", | ||||
"sort_field": "modified", | "sort_field": "modified", | ||||
"sort_order": "DESC", | "sort_order": "DESC", | ||||
"states": [], | |||||
"title_field": "label", | "title_field": "label", | ||||
"track_changes": 1 | "track_changes": 1 | ||||
} | } |
@@ -3,6 +3,7 @@ | |||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | import frappe | ||||
from frappe import _ | |||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe.utils import cint | from frappe.utils import cint | ||||
from frappe.model.naming import append_number_if_name_exists | from frappe.model.naming import append_number_if_name_exists | ||||
@@ -17,6 +18,13 @@ class NumberCard(Document): | |||||
if frappe.db.exists("Number Card", self.name): | if frappe.db.exists("Number Card", self.name): | ||||
self.name = append_number_if_name_exists('Number Card', self.name) | self.name = append_number_if_name_exists('Number Card', self.name) | ||||
def validate(self): | |||||
if not self.document_type: | |||||
frappe.throw(_("Document type is required to create a number card")) | |||||
if self.document_type and frappe.get_meta(self.document_type).istable and not self.parent_document_type: | |||||
frappe.throw(_("Parent document type is required to create a number card")) | |||||
def on_update(self): | def on_update(self): | ||||
if frappe.conf.developer_mode and self.is_standard: | if frappe.conf.developer_mode and self.is_standard: | ||||
export_to_files(record_list=[['Number Card', self.name]], record_module=self.module) | export_to_files(record_list=[['Number Card', self.name]], record_module=self.module) | ||||
@@ -88,15 +88,16 @@ frappe.ui.form.on('System Console', { | |||||
<td>${row.Progress}</td> | <td>${row.Progress}</td> | ||||
</tr>` | </tr>` | ||||
} | } | ||||
frm.get_field('processlist').html(` | frm.get_field('processlist').html(` | ||||
<p class='text-muted'>Requested on: ${timestamp}</p> | <p class='text-muted'>Requested on: ${timestamp}</p> | ||||
<table class='table-bordered' style='width: 100%'> | <table class='table-bordered' style='width: 100%'> | ||||
<thead><tr> | <thead><tr> | ||||
<th width='10%'>Id</ht> | |||||
<th width='5%'>Id</ht> | |||||
<th width='10%'>Time</ht> | <th width='10%'>Time</ht> | ||||
<th width='10%'>State</ht> | <th width='10%'>State</ht> | ||||
<th width='60%'>Info</ht> | <th width='60%'>Info</ht> | ||||
<th width='10%'>Progress</ht> | |||||
<th width='15%'>Progress / Wait Event</ht> | |||||
</tr></thead> | </tr></thead> | ||||
<tbody>${rows}</thead>`); | <tbody>${rows}</thead>`); | ||||
}); | }); | ||||
@@ -41,4 +41,14 @@ def execute_code(doc): | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def show_processlist(): | def show_processlist(): | ||||
frappe.only_for('System Manager') | frappe.only_for('System Manager') | ||||
return frappe.db.sql('show full processlist', as_dict=1) | |||||
return frappe.db.multisql({ | |||||
"postgres": """ | |||||
SELECT pid AS "Id", | |||||
query_start AS "Time", | |||||
state AS "State", | |||||
query AS "Info", | |||||
wait_event AS "Progress" | |||||
FROM pg_stat_activity""", | |||||
"mariadb": "show full processlist" | |||||
}, as_dict=True) |
@@ -277,6 +277,7 @@ def sort_page(workspace_pages, pages): | |||||
doc = frappe.get_doc('Workspace', page.name) | doc = frappe.get_doc('Workspace', page.name) | ||||
doc.sequence_id = seq + 1 | doc.sequence_id = seq + 1 | ||||
doc.parent_page = d.get('parent_page') or "" | doc.parent_page = d.get('parent_page') or "" | ||||
doc.flags.ignore_links = True | |||||
doc.save(ignore_permissions=True) | doc.save(ignore_permissions=True) | ||||
break | break | ||||
@@ -1,9 +1,10 @@ | |||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors | |||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import json | import json | ||||
from collections import defaultdict | from collections import defaultdict | ||||
import itertools | import itertools | ||||
from typing import List | |||||
from typing import Dict, List, Optional | |||||
import frappe | import frappe | ||||
import frappe.desk.form.load | import frappe.desk.form.load | ||||
@@ -367,7 +368,7 @@ def get_exempted_doctypes(): | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None): | |||||
def get_linked_docs(doctype: str, name: str, linkinfo: Optional[Dict] = None) -> Dict[str, List]: | |||||
if isinstance(linkinfo, str): | if isinstance(linkinfo, str): | ||||
# additional fields are added in linkinfo | # additional fields are added in linkinfo | ||||
linkinfo = json.loads(linkinfo) | linkinfo = json.loads(linkinfo) | ||||
@@ -377,23 +378,21 @@ def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None): | |||||
if not linkinfo: | if not linkinfo: | ||||
return results | return results | ||||
if for_doctype: | |||||
links = frappe.get_doc(doctype, name).get_link_filters(for_doctype) | |||||
if links: | |||||
linkinfo = links | |||||
if for_doctype in linkinfo: | |||||
# only get linked with for this particular doctype | |||||
linkinfo = { for_doctype: linkinfo.get(for_doctype) } | |||||
else: | |||||
return results | |||||
for dt, link in linkinfo.items(): | for dt, link in linkinfo.items(): | ||||
filters = [] | filters = [] | ||||
link["doctype"] = dt | link["doctype"] = dt | ||||
link_meta_bundle = frappe.desk.form.load.get_meta_bundle(dt) | |||||
try: | |||||
link_meta_bundle = frappe.desk.form.load.get_meta_bundle(dt) | |||||
except Exception as e: | |||||
if isinstance(e, frappe.DoesNotExistError): | |||||
if frappe.local.message_log: | |||||
frappe.local.message_log.pop() | |||||
continue | |||||
linkmeta = link_meta_bundle[0] | linkmeta = link_meta_bundle[0] | ||||
if not linkmeta.has_permission(): | |||||
continue | |||||
if not linkmeta.get("issingle"): | if not linkmeta.get("issingle"): | ||||
fields = [d.fieldname for d in linkmeta.get("fields", { | fields = [d.fieldname for d in linkmeta.get("fields", { | ||||
"in_list_view": 1, | "in_list_view": 1, | ||||
@@ -456,6 +455,13 @@ def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None): | |||||
return results | return results | ||||
@frappe.whitelist() | |||||
def get(doctype, docname): | |||||
linked_doctypes = get_linked_doctypes(doctype=doctype) | |||||
return get_linked_docs(doctype=doctype, name=docname, linkinfo=linked_doctypes) | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False): | def get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False): | ||||
"""add list of doctypes this doctype is 'linked' with. | """add list of doctypes this doctype is 'linked' with. | ||||
@@ -470,6 +476,7 @@ def get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False): | |||||
else: | else: | ||||
return frappe.cache().hget("linked_doctypes", doctype, lambda: _get_linked_doctypes(doctype)) | return frappe.cache().hget("linked_doctypes", doctype, lambda: _get_linked_doctypes(doctype)) | ||||
def _get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False): | def _get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False): | ||||
ret = {} | ret = {} | ||||
# find fields where this doctype is linked | # find fields where this doctype is linked | ||||
@@ -499,6 +506,7 @@ def _get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False) | |||||
return ret | return ret | ||||
def get_linked_fields(doctype, without_ignore_user_permissions_enabled=False): | def get_linked_fields(doctype, without_ignore_user_permissions_enabled=False): | ||||
filters = [['fieldtype','=', 'Link'], ['options', '=', doctype]] | filters = [['fieldtype','=', 'Link'], ['options', '=', doctype]] | ||||
@@ -529,6 +537,7 @@ def get_linked_fields(doctype, without_ignore_user_permissions_enabled=False): | |||||
return ret | return ret | ||||
def get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled=False): | def get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled=False): | ||||
ret = {} | ret = {} | ||||
@@ -10,6 +10,7 @@ import frappe.desk.form.meta | |||||
from frappe.model.utils.user_settings import get_user_settings | from frappe.model.utils.user_settings import get_user_settings | ||||
from frappe.permissions import get_doc_permissions | from frappe.permissions import get_doc_permissions | ||||
from frappe.desk.form.document_follow import is_document_followed | from frappe.desk.form.document_follow import is_document_followed | ||||
from frappe.utils.data import cstr | |||||
from frappe import _ | from frappe import _ | ||||
from frappe import _dict | from frappe import _dict | ||||
from urllib.parse import quote | from urllib.parse import quote | ||||
@@ -124,7 +125,6 @@ def get_docinfo(doc=None, doctype=None, name=None): | |||||
update_user_info(docinfo) | update_user_info(docinfo) | ||||
frappe.response["docinfo"] = docinfo | frappe.response["docinfo"] = docinfo | ||||
return docinfo | |||||
def add_comments(doc, docinfo): | def add_comments(doc, docinfo): | ||||
# divide comments into separate lists | # divide comments into separate lists | ||||
@@ -356,7 +356,7 @@ def get_document_email(doctype, name): | |||||
return None | return None | ||||
email = email.split("@") | email = email.split("@") | ||||
return "{0}+{1}+{2}@{3}".format(email[0], quote(doctype), quote(name), email[1]) | |||||
return "{0}+{1}+{2}@{3}".format(email[0], quote(doctype), quote(cstr(name)), email[1]) | |||||
def get_automatic_email_link(): | def get_automatic_email_link(): | ||||
return frappe.db.get_value("Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id") | return frappe.db.get_value("Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id") | ||||
@@ -352,14 +352,10 @@ def export_query(): | |||||
) | ) | ||||
return | return | ||||
columns = get_columns_dict(data.columns) | |||||
from frappe.utils.xlsxutils import make_xlsx | from frappe.utils.xlsxutils import make_xlsx | ||||
data["result"] = handle_duration_fieldtype_values( | |||||
data.get("result"), data.get("columns") | |||||
) | |||||
xlsx_data, column_widths = build_xlsx_data(columns, data, visible_idx, include_indentation) | |||||
format_duration_fields(data) | |||||
xlsx_data, column_widths = build_xlsx_data(data, visible_idx, include_indentation) | |||||
xlsx_file = make_xlsx(xlsx_data, "Query Report", column_widths=column_widths) | xlsx_file = make_xlsx(xlsx_data, "Query Report", column_widths=column_widths) | ||||
frappe.response["filename"] = report_name + ".xlsx" | frappe.response["filename"] = report_name + ".xlsx" | ||||
@@ -367,39 +363,18 @@ def export_query(): | |||||
frappe.response["type"] = "binary" | frappe.response["type"] = "binary" | ||||
def handle_duration_fieldtype_values(result, columns): | |||||
for i, col in enumerate(columns): | |||||
fieldtype = None | |||||
if isinstance(col, str): | |||||
col = col.split(":") | |||||
if len(col) > 1: | |||||
if col[1]: | |||||
fieldtype = col[1] | |||||
if "/" in fieldtype: | |||||
fieldtype, options = fieldtype.split("/") | |||||
else: | |||||
fieldtype = "Data" | |||||
else: | |||||
fieldtype = col.get("fieldtype") | |||||
if fieldtype == "Duration": | |||||
for entry in range(0, len(result)): | |||||
row = result[entry] | |||||
if isinstance(row, dict): | |||||
val_in_seconds = row[col.fieldname] | |||||
if val_in_seconds: | |||||
duration_val = format_duration(val_in_seconds) | |||||
row[col.fieldname] = duration_val | |||||
else: | |||||
val_in_seconds = row[i] | |||||
if val_in_seconds: | |||||
duration_val = format_duration(val_in_seconds) | |||||
row[i] = duration_val | |||||
def format_duration_fields(data: frappe._dict) -> None: | |||||
for i, col in enumerate(data.columns): | |||||
if col.get("fieldtype") != "Duration": | |||||
continue | |||||
return result | |||||
for row in data.result: | |||||
index = col.fieldname if isinstance(row, dict) else i | |||||
if row[index]: | |||||
row[index] = format_duration(row[index]) | |||||
def build_xlsx_data(columns, data, visible_idx, include_indentation, ignore_visible_idx=False): | |||||
def build_xlsx_data(data, visible_idx, include_indentation, ignore_visible_idx=False): | |||||
result = [[]] | result = [[]] | ||||
column_widths = [] | column_widths = [] | ||||
@@ -257,7 +257,7 @@ def scrub_custom_query(query, key, txt): | |||||
def relevance_sorter(key, query, as_dict): | def relevance_sorter(key, query, as_dict): | ||||
value = _(key.name if as_dict else key[0]) | value = _(key.name if as_dict else key[0]) | ||||
return ( | return ( | ||||
value.lower().startswith(query.lower()) is not True, | |||||
cstr(value).lower().startswith(query.lower()) is not True, | |||||
value | value | ||||
) | ) | ||||
@@ -104,7 +104,7 @@ class AutoEmailReport(Document): | |||||
report_data['columns'] = columns | report_data['columns'] = columns | ||||
report_data['result'] = data | report_data['result'] = data | ||||
xlsx_data, column_widths = build_xlsx_data(columns, report_data, [], 1, ignore_visible_idx=True) | |||||
xlsx_data, column_widths = build_xlsx_data(report_data, [], 1, ignore_visible_idx=True) | |||||
xlsx_file = make_xlsx(xlsx_data, "Auto Email Report", column_widths=column_widths) | xlsx_file = make_xlsx(xlsx_data, "Auto Email Report", column_widths=column_widths) | ||||
return xlsx_file.getvalue() | return xlsx_file.getvalue() | ||||
@@ -113,7 +113,7 @@ class AutoEmailReport(Document): | |||||
report_data['columns'] = columns | report_data['columns'] = columns | ||||
report_data['result'] = data | report_data['result'] = data | ||||
xlsx_data, column_widths = build_xlsx_data(columns, report_data, [], 1, ignore_visible_idx=True) | |||||
xlsx_data, column_widths = build_xlsx_data(report_data, [], 1, ignore_visible_idx=True) | |||||
return to_csv(xlsx_data) | return to_csv(xlsx_data) | ||||
else: | else: | ||||
@@ -236,8 +236,7 @@ | |||||
"index_web_pages_for_search": 1, | "index_web_pages_for_search": 1, | ||||
"is_published_field": "published", | "is_published_field": "published", | ||||
"links": [], | "links": [], | ||||
"max_attachments": 3, | |||||
"modified": "2021-12-06 20:09:37.963141", | |||||
"modified": "2022-03-09 01:48:16.741603", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Email", | "module": "Email", | ||||
"name": "Newsletter", | "name": "Newsletter", | ||||
@@ -51,7 +51,7 @@ class TestNewsletterMixin: | |||||
"reference_name": newsletter, | "reference_name": newsletter, | ||||
}) | }) | ||||
frappe.delete_doc("Newsletter", newsletter) | frappe.delete_doc("Newsletter", newsletter) | ||||
frappe.db.delete("Newsletter Email Group", newsletter) | |||||
frappe.db.delete("Newsletter Email Group", {"parent": newsletter}) | |||||
newsletters.remove(newsletter) | newsletters.remove(newsletter) | ||||
def setup_email_group(self): | def setup_email_group(self): | ||||
@@ -186,7 +186,7 @@ def get_context(context): | |||||
def send_an_email(self, doc, context): | def send_an_email(self, doc, context): | ||||
from email.utils import formataddr | from email.utils import formataddr | ||||
from frappe.core.doctype.communication.email import make as make_communication | |||||
from frappe.core.doctype.communication.email import _make as make_communication | |||||
subject = self.subject | subject = self.subject | ||||
if "{" in subject: | if "{" in subject: | ||||
subject = frappe.render_template(self.subject, context) | subject = frappe.render_template(self.subject, context) | ||||
@@ -216,7 +216,8 @@ def get_context(context): | |||||
# Add mail notification to communication list | # Add mail notification to communication list | ||||
# No need to add if it is already a communication. | # No need to add if it is already a communication. | ||||
if doc.doctype != 'Communication': | if doc.doctype != 'Communication': | ||||
make_communication(doctype=doc.doctype, | |||||
make_communication( | |||||
doctype=doc.doctype, | |||||
name=doc.name, | name=doc.name, | ||||
content=message, | content=message, | ||||
subject=subject, | subject=subject, | ||||
@@ -228,7 +229,7 @@ def get_context(context): | |||||
cc=cc, | cc=cc, | ||||
bcc=bcc, | bcc=bcc, | ||||
communication_type='Automated Message', | communication_type='Automated Message', | ||||
ignore_permissions=True) | |||||
) | |||||
def send_a_slack_msg(self, doc, context): | def send_a_slack_msg(self, doc, context): | ||||
send_slack_message( | send_slack_message( | ||||
@@ -259,17 +259,12 @@ def get_formatted_html(subject, message, footer=None, print_html=None, | |||||
email_account = email_account or EmailAccount.find_outgoing(match_by_email=sender) | email_account = email_account or EmailAccount.find_outgoing(match_by_email=sender) | ||||
signature = None | |||||
if "<!-- signature-included -->" not in message: | |||||
signature = get_signature(email_account) | |||||
rendered_email = frappe.get_template("templates/emails/standard.html").render({ | rendered_email = frappe.get_template("templates/emails/standard.html").render({ | ||||
"brand_logo": get_brand_logo(email_account) if with_container or header else None, | "brand_logo": get_brand_logo(email_account) if with_container or header else None, | ||||
"with_container": with_container, | "with_container": with_container, | ||||
"site_url": get_url(), | "site_url": get_url(), | ||||
"header": get_header(header), | "header": get_header(header), | ||||
"content": message, | "content": message, | ||||
"signature": signature, | |||||
"footer": get_footer(email_account, footer), | "footer": get_footer(email_account, footer), | ||||
"title": subject, | "title": subject, | ||||
"print_html": print_html, | "print_html": print_html, | ||||
@@ -281,8 +276,7 @@ def get_formatted_html(subject, message, footer=None, print_html=None, | |||||
if unsubscribe_link: | if unsubscribe_link: | ||||
html = html.replace("<!--unsubscribe link here-->", unsubscribe_link.html) | html = html.replace("<!--unsubscribe link here-->", unsubscribe_link.html) | ||||
html = inline_style_in_html(html) | |||||
return html | |||||
return inline_style_in_html(html) | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_email_html(template, args, subject, header=None, with_container=False): | def get_email_html(template, args, subject, header=None, with_container=False): | ||||
@@ -203,12 +203,17 @@ def get_unread_update_logs(consumer_name, dt, dn): | |||||
SELECT | SELECT | ||||
update_log.name | update_log.name | ||||
FROM `tabEvent Update Log` update_log | FROM `tabEvent Update Log` update_log | ||||
JOIN `tabEvent Update Log Consumer` consumer ON consumer.parent = update_log.name | |||||
JOIN `tabEvent Update Log Consumer` consumer ON consumer.parent = %(log_name)s | |||||
WHERE | WHERE | ||||
consumer.consumer = %(consumer)s | consumer.consumer = %(consumer)s | ||||
AND update_log.ref_doctype = %(dt)s | AND update_log.ref_doctype = %(dt)s | ||||
AND update_log.docname = %(dn)s | AND update_log.docname = %(dn)s | ||||
""", {'consumer': consumer_name, "dt": dt, "dn": dn}, as_dict=0)] | |||||
""", { | |||||
"consumer": consumer_name, | |||||
"dt": dt, | |||||
"dn": dn, | |||||
"log_name": "update_log.name" if frappe.conf.db_type == "mariadb" else "CAST(update_log.name AS VARCHAR)" | |||||
}, as_dict=0)] | |||||
logs = frappe.get_all( | logs = frappe.get_all( | ||||
'Event Update Log', | 'Event Update Log', | ||||
@@ -7,6 +7,7 @@ import json | |||||
import requests | import requests | ||||
import frappe | import frappe | ||||
from frappe.utils.data import cstr | |||||
class AuthError(Exception): | class AuthError(Exception): | ||||
@@ -122,7 +123,7 @@ class FrappeClient(object): | |||||
'''Update a remote document | '''Update a remote document | ||||
:param doc: dict or Document object to be updated remotely. `name` is mandatory for this''' | :param doc: dict or Document object to be updated remotely. `name` is mandatory for this''' | ||||
url = self.url + "/api/resource/" + doc.get("doctype") + "/" + doc.get("name") | |||||
url = self.url + "/api/resource/" + doc.get("doctype") + "/" + cstr(doc.get("name")) | |||||
res = self.session.put(url, data={"data":frappe.as_json(doc)}, verify=self.verify, headers=self.headers) | res = self.session.put(url, data={"data":frappe.as_json(doc)}, verify=self.verify, headers=self.headers) | ||||
return frappe._dict(self.post_process(res)) | return frappe._dict(self.post_process(res)) | ||||
@@ -207,7 +208,7 @@ class FrappeClient(object): | |||||
if fields: | if fields: | ||||
params["fields"] = json.dumps(fields) | params["fields"] = json.dumps(fields) | ||||
res = self.session.get(self.url + "/api/resource/" + doctype + "/" + name, | |||||
res = self.session.get(self.url + "/api/resource/" + doctype + "/" + cstr(name), | |||||
params=params, verify=self.verify, headers=self.headers) | params=params, verify=self.verify, headers=self.headers) | ||||
return self.post_process(res) | return self.post_process(res) | ||||
@@ -611,7 +611,7 @@ def is_downgrade(sql_file_path, verbose=False): | |||||
downgrade = backup_version > current_version | downgrade = backup_version > current_version | ||||
if verbose and downgrade: | if verbose and downgrade: | ||||
print("Your site will be downgraded from Frappe {0} to {1}".format(current_version, backup_version)) | |||||
print(f"Your site will be downgraded from Frappe {backup_version} to {current_version}") | |||||
return downgrade | return downgrade | ||||
@@ -475,7 +475,7 @@ class BaseDocument(object): | |||||
d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in DOCTYPES_FOR_DOCTYPE) | d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in DOCTYPES_FOR_DOCTYPE) | ||||
# don't update name, as case might've been changed | # don't update name, as case might've been changed | ||||
name = d['name'] | |||||
name = cstr(d['name']) | |||||
del d['name'] | del d['name'] | ||||
columns = list(d) | columns = list(d) | ||||
@@ -164,7 +164,8 @@ class DatabaseQuery(object): | |||||
# left join parent, child tables | # left join parent, child tables | ||||
for child in self.tables[1:]: | for child in self.tables[1:]: | ||||
args.tables += f" {self.join} {child} on ({child}.parent = {self.tables[0]}.name)" | |||||
parent_name = self.cast_name(f"{self.tables[0]}.name") | |||||
args.tables += f" {self.join} {child} on ({child}.parent = {parent_name})" | |||||
if self.grouped_or_conditions: | if self.grouped_or_conditions: | ||||
self.conditions.append(f"({' or '.join(self.grouped_or_conditions)})") | self.conditions.append(f"({' or '.join(self.grouped_or_conditions)})") | ||||
@@ -318,21 +319,60 @@ class DatabaseQuery(object): | |||||
] | ] | ||||
# add tables from fields | # add tables from fields | ||||
if self.fields: | if self.fields: | ||||
for field in self.fields: | |||||
if not ("tab" in field and "." in field) or any(x for x in sql_functions if x in field): | |||||
for i, field in enumerate(self.fields): | |||||
# add cast in locate/strpos | |||||
func_found = False | |||||
for func in sql_functions: | |||||
if func in field.lower(): | |||||
self.fields[i] = self.cast_name(field, func) | |||||
func_found = True | |||||
break | |||||
if func_found or not ("tab" in field and "." in field): | |||||
continue | continue | ||||
table_name = field.split('.')[0] | table_name = field.split('.')[0] | ||||
if table_name.lower().startswith('group_concat('): | if table_name.lower().startswith('group_concat('): | ||||
table_name = table_name[13:] | table_name = table_name[13:] | ||||
if table_name.lower().startswith('ifnull('): | |||||
table_name = table_name[7:] | |||||
if not table_name[0]=='`': | if not table_name[0]=='`': | ||||
table_name = f"`{table_name}`" | table_name = f"`{table_name}`" | ||||
if table_name not in self.tables: | if table_name not in self.tables: | ||||
self.append_table(table_name) | self.append_table(table_name) | ||||
def cast_name(self, column: str, sql_function: str = "",) -> str: | |||||
if frappe.db.db_type == "postgres": | |||||
if "name" in column.lower(): | |||||
if "cast(" not in column.lower() or "::" not in column: | |||||
if not sql_function: | |||||
return f"cast({column} as varchar)" | |||||
elif sql_function == "locate(": | |||||
return re.sub( | |||||
r'locate\(([^,]+),([^)]+)\)', | |||||
r'locate(\1, cast(\2 as varchar))', | |||||
column, | |||||
flags=re.IGNORECASE | |||||
) | |||||
elif sql_function == "strpos(": | |||||
return re.sub( | |||||
r'strpos\(([^,]+),([^)]+)\)', | |||||
r'strpos(cast(\1 as varchar), \2)', | |||||
column, | |||||
flags=re.IGNORECASE | |||||
) | |||||
elif sql_function == "ifnull(": | |||||
return re.sub( | |||||
r"ifnull\(([^,]+)", | |||||
r"ifnull(cast(\1 as varchar)", | |||||
column, | |||||
flags=re.IGNORECASE | |||||
) | |||||
return column | |||||
def append_table(self, table_name): | def append_table(self, table_name): | ||||
self.tables.append(table_name) | self.tables.append(table_name) | ||||
doctype = table_name[4:-1] | doctype = table_name[4:-1] | ||||
@@ -423,6 +463,8 @@ class DatabaseQuery(object): | |||||
ifnull(`tabDocType`.`fieldname`, fallback) operator "value" | ifnull(`tabDocType`.`fieldname`, fallback) operator "value" | ||||
""" | """ | ||||
# TODO: refactor | |||||
from frappe.boot import get_additional_filters_from_hooks | from frappe.boot import get_additional_filters_from_hooks | ||||
additional_filters_config = get_additional_filters_from_hooks() | additional_filters_config = get_additional_filters_from_hooks() | ||||
f = get_filter(self.doctype, f, additional_filters_config) | f = get_filter(self.doctype, f, additional_filters_config) | ||||
@@ -432,15 +474,16 @@ class DatabaseQuery(object): | |||||
self.append_table(tname) | self.append_table(tname) | ||||
if 'ifnull(' in f.fieldname: | if 'ifnull(' in f.fieldname: | ||||
column_name = f.fieldname | |||||
column_name = self.cast_name(f.fieldname, "ifnull(") | |||||
else: | else: | ||||
column_name = f"{tname}.{f.fieldname}" | |||||
can_be_null = True | |||||
column_name = self.cast_name(f"{tname}.{f.fieldname}") | |||||
if f.operator.lower() in additional_filters_config: | if f.operator.lower() in additional_filters_config: | ||||
f.update(get_additional_filter_field(additional_filters_config, f, f.value)) | f.update(get_additional_filter_field(additional_filters_config, f, f.value)) | ||||
meta = frappe.get_meta(f.doctype) | |||||
can_be_null = True | |||||
# prepare in condition | # prepare in condition | ||||
if f.operator.lower() in ('ancestors of', 'descendants of', 'not ancestors of', 'not descendants of'): | if f.operator.lower() in ('ancestors of', 'descendants of', 'not ancestors of', 'not descendants of'): | ||||
values = f.value or '' | values = f.value or '' | ||||
@@ -449,12 +492,8 @@ class DatabaseQuery(object): | |||||
# if not isinstance(values, (list, tuple)): | # if not isinstance(values, (list, tuple)): | ||||
# values = values.split(",") | # values = values.split(",") | ||||
ref_doctype = f.doctype | |||||
if frappe.get_meta(f.doctype).get_field(f.fieldname) is not None : | |||||
ref_doctype = frappe.get_meta(f.doctype).get_field(f.fieldname).options | |||||
result=[] | |||||
field = meta.get_field(f.fieldname) | |||||
ref_doctype = field.options if field else f.doctype | |||||
lft, rgt = '', '' | lft, rgt = '', '' | ||||
if f.value: | if f.value: | ||||
@@ -474,29 +513,30 @@ class DatabaseQuery(object): | |||||
}, order_by='`lft` DESC') | }, order_by='`lft` DESC') | ||||
fallback = "''" | fallback = "''" | ||||
value = [frappe.db.escape((v.name or '').strip(), percent=False) for v in result] | |||||
value = [frappe.db.escape((cstr(v.name) or '').strip(), percent=False) for v in result] | |||||
if len(value): | if len(value): | ||||
value = f"({', '.join(value)})" | value = f"({', '.join(value)})" | ||||
else: | else: | ||||
value = "('')" | value = "('')" | ||||
# changing operator to IN as the above code fetches all the parent / child values and convert into tuple | # changing operator to IN as the above code fetches all the parent / child values and convert into tuple | ||||
# which can be directly used with IN operator to query. | # which can be directly used with IN operator to query. | ||||
f.operator = 'not in' if f.operator.lower() in ('not ancestors of', 'not descendants of') else 'in' | f.operator = 'not in' if f.operator.lower() in ('not ancestors of', 'not descendants of') else 'in' | ||||
elif f.operator.lower() in ('in', 'not in'): | elif f.operator.lower() in ('in', 'not in'): | ||||
values = f.value or '' | values = f.value or '' | ||||
if isinstance(values, str): | if isinstance(values, str): | ||||
values = values.split(",") | values = values.split(",") | ||||
fallback = "''" | fallback = "''" | ||||
value = [frappe.db.escape((v or '').strip(), percent=False) for v in values] | |||||
value = [frappe.db.escape((cstr(v) or '').strip(), percent=False) for v in values] | |||||
if len(value): | if len(value): | ||||
value = f"({', '.join(value)})" | value = f"({', '.join(value)})" | ||||
else: | else: | ||||
value = "('')" | value = "('')" | ||||
else: | else: | ||||
df = frappe.get_meta(f.doctype).get("fields", {"fieldname": f.fieldname}) | |||||
df = meta.get("fields", {"fieldname": f.fieldname}) | |||||
df = df[0] if df else None | df = df[0] if df else None | ||||
if df and df.fieldtype in ("Check", "Float", "Int", "Currency", "Percent"): | if df and df.fieldtype in ("Check", "Float", "Int", "Currency", "Percent"): | ||||
@@ -513,7 +553,8 @@ class DatabaseQuery(object): | |||||
fallback = "'0001-01-01 00:00:00'" | fallback = "'0001-01-01 00:00:00'" | ||||
elif f.operator.lower() in ('between') and \ | elif f.operator.lower() in ('between') and \ | ||||
(f.fieldname in ('creation', 'modified') or (df and (df.fieldtype=="Date" or df.fieldtype=="Datetime"))): | |||||
(f.fieldname in ('creation', 'modified') or | |||||
(df and (df.fieldtype=="Date" or df.fieldtype=="Datetime"))): | |||||
value = get_between_date_filter(f.value, df) | value = get_between_date_filter(f.value, df) | ||||
fallback = "'0001-01-01 00:00:00'" | fallback = "'0001-01-01 00:00:00'" | ||||
@@ -528,7 +569,7 @@ class DatabaseQuery(object): | |||||
fallback = "''" | fallback = "''" | ||||
can_be_null = True | can_be_null = True | ||||
if 'ifnull' not in column_name: | |||||
if 'ifnull' not in column_name.lower(): | |||||
column_name = f'ifnull({column_name}, {fallback})' | column_name = f'ifnull({column_name}, {fallback})' | ||||
elif df and df.fieldtype=="Date": | elif df and df.fieldtype=="Date": | ||||
@@ -570,7 +611,7 @@ class DatabaseQuery(object): | |||||
value = f"{tname}.{quote}{f.value.name}{quote}" | value = f"{tname}.{quote}{f.value.name}{quote}" | ||||
# escape value | # escape value | ||||
elif isinstance(value, str) and not f.operator.lower() == 'between': | |||||
elif isinstance(value, str) and f.operator.lower() != 'between': | |||||
value = f"{frappe.db.escape(value, percent=False)}" | value = f"{frappe.db.escape(value, percent=False)}" | ||||
if ( | if ( | ||||
@@ -158,7 +158,7 @@ def update_naming_series(doc): | |||||
and getattr(doc, "naming_series", None): | and getattr(doc, "naming_series", None): | ||||
revert_series_if_last(doc.naming_series, doc.name, doc) | revert_series_if_last(doc.naming_series, doc.name, doc) | ||||
elif doc.meta.autoname.split(":")[0] not in ("Prompt", "field", "hash"): | |||||
elif doc.meta.autoname.split(":")[0] not in ("Prompt", "field", "hash", "autoincrement"): | |||||
revert_series_if_last(doc.meta.autoname, doc.name, doc) | revert_series_if_last(doc.meta.autoname, doc.name, doc) | ||||
def delete_from_table(doctype, name, ignore_doctypes, doc): | def delete_from_table(doctype, name, ignore_doctypes, doc): | ||||
@@ -1,14 +1,18 @@ | |||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
from typing import Optional | |||||
from typing import Optional, TYPE_CHECKING, Union | |||||
import frappe | import frappe | ||||
from frappe import _ | from frappe import _ | ||||
from frappe.database.sequence import get_next_val, set_next_val | |||||
from frappe.utils import now_datetime, cint, cstr | from frappe.utils import now_datetime, cint, cstr | ||||
import re | import re | ||||
from frappe.model import log_types | from frappe.model import log_types | ||||
from frappe.query_builder import DocType | from frappe.query_builder import DocType | ||||
if TYPE_CHECKING: | |||||
from frappe.model.meta import Meta | |||||
def set_new_name(doc): | def set_new_name(doc): | ||||
""" | """ | ||||
@@ -24,11 +28,16 @@ def set_new_name(doc): | |||||
doc.run_method("before_naming") | doc.run_method("before_naming") | ||||
autoname = frappe.get_meta(doc.doctype).autoname or "" | |||||
meta = frappe.get_meta(doc.doctype) | |||||
autoname = meta.autoname or "" | |||||
if autoname.lower() != "prompt" and not frappe.flags.in_import: | if autoname.lower() != "prompt" and not frappe.flags.in_import: | ||||
doc.name = None | doc.name = None | ||||
if is_autoincremented(doc.doctype, meta): | |||||
doc.name = get_next_val(doc.doctype) | |||||
return | |||||
if getattr(doc, "amended_from", None): | if getattr(doc, "amended_from", None): | ||||
_set_amended_name(doc) | _set_amended_name(doc) | ||||
return | return | ||||
@@ -64,9 +73,37 @@ def set_new_name(doc): | |||||
doc.name = validate_name( | doc.name = validate_name( | ||||
doc.doctype, | doc.doctype, | ||||
doc.name, | doc.name, | ||||
frappe.get_meta(doc.doctype).get_field("name_case") | |||||
meta.get_field("name_case") | |||||
) | ) | ||||
def is_autoincremented(doctype: str, meta: "Meta" = None): | |||||
if doctype in log_types: | |||||
if frappe.local.autoincremented_status_map.get(frappe.local.site) is None or \ | |||||
frappe.local.autoincremented_status_map[frappe.local.site] == -1: | |||||
if frappe.db.sql( | |||||
f"""select data_type FROM information_schema.columns | |||||
where column_name = 'name' and table_name = 'tab{doctype}'""" | |||||
)[0][0] == "bigint": | |||||
frappe.local.autoincremented_status_map[frappe.local.site] = 1 | |||||
return True | |||||
else: | |||||
frappe.local.autoincremented_status_map[frappe.local.site] = 0 | |||||
elif frappe.local.autoincremented_status_map[frappe.local.site]: | |||||
return True | |||||
else: | |||||
if not meta: | |||||
meta = frappe.get_meta(doctype) | |||||
if getattr(meta, "issingle", False): | |||||
return False | |||||
if meta.autoname == "autoincrement": | |||||
return True | |||||
return False | |||||
def set_name_from_naming_options(autoname, doc): | def set_name_from_naming_options(autoname, doc): | ||||
""" | """ | ||||
Get a name based on the autoname field option | Get a name based on the autoname field option | ||||
@@ -284,9 +321,19 @@ def get_default_naming_series(doctype): | |||||
return None | return None | ||||
def validate_name(doctype: str, name: str, case: Optional[str] = None): | |||||
def validate_name(doctype: str, name: Union[int, str], case: Optional[str] = None): | |||||
if not name: | if not name: | ||||
frappe.throw(_("No Name Specified for {0}").format(doctype)) | frappe.throw(_("No Name Specified for {0}").format(doctype)) | ||||
if isinstance(name, int): | |||||
if is_autoincremented(doctype): | |||||
# this will set the sequence val to be the provided name and set it to be used | |||||
# so that the sequence will start from the next val of the setted val(name) | |||||
set_next_val(doctype, name, is_val_used=True) | |||||
return name | |||||
frappe.throw(_("Invalid name type (integer) for varchar name column"), frappe.NameError) | |||||
if name.startswith("New "+doctype): | if name.startswith("New "+doctype): | ||||
frappe.throw(_("There were some errors setting the name, please contact the administrator"), frappe.NameError) | frappe.throw(_("There were some errors setting the name, please contact the administrator"), frappe.NameError) | ||||
if case == "Title Case": | if case == "Title Case": | ||||
@@ -43,8 +43,8 @@ def update_document_title( | |||||
title_field = doc.meta.get_title_field() | title_field = doc.meta.get_title_field() | ||||
title_updated = (title_field != "name") and (updated_title != doc.get(title_field)) | |||||
name_updated = updated_name != doc.name | |||||
title_updated = updated_title and (title_field != "name") and (updated_title != doc.get(title_field)) | |||||
name_updated = updated_name and (updated_name != doc.name) | |||||
if name_updated: | if name_updated: | ||||
docname = rename_doc(doctype=doctype, old=docname, new=updated_name, merge=merge) | docname = rename_doc(doctype=doctype, old=docname, new=updated_name, merge=merge) | ||||
@@ -11,7 +11,7 @@ from frappe.query_builder import DocType | |||||
from frappe.utils import get_datetime, now | from frappe.utils import get_datetime, now | ||||
def caclulate_hash(path: str) -> str: | |||||
def calculate_hash(path: str) -> str: | |||||
"""Calculate md5 hash of the file in binary mode | """Calculate md5 hash of the file in binary mode | ||||
Args: | Args: | ||||
@@ -99,7 +99,7 @@ def import_file_by_path(path: str,force: bool = False,data_import: bool = False, | |||||
print(f"{path} missing") | print(f"{path} missing") | ||||
return | return | ||||
calculated_hash = caclulate_hash(path) | |||||
calculated_hash = calculate_hash(path) | |||||
if docs: | if docs: | ||||
if not isinstance(docs, list): | if not isinstance(docs, list): | ||||
@@ -24,7 +24,7 @@ ul.tree-children { | |||||
} | } | ||||
.tree-link .node-parent, | .tree-link .node-parent, | ||||
.tree-link .node-leaf { | .tree-link .node-leaf { | ||||
margin-right: 5px; | |||||
margin-right: 8px; | |||||
} | } | ||||
.tree-link.active i { | .tree-link.active i { | ||||
color: #5e64ff; | color: #5e64ff; | ||||
@@ -166,6 +166,9 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui | |||||
} | } | ||||
parse_options(options) { | parse_options(options) { | ||||
if (typeof options === 'string' && options[0] === '[') { | |||||
options = frappe.utils.parse_json(options); | |||||
} | |||||
if (typeof options === 'string') { | if (typeof options === 'string') { | ||||
options = options.split('\n'); | options = options.split('\n'); | ||||
} | } | ||||
@@ -248,7 +248,7 @@ frappe.ui.form.Form = class FrappeForm { | |||||
// on main doc | // on main doc | ||||
frappe.model.on(me.doctype, "*", function(fieldname, value, doc) { | frappe.model.on(me.doctype, "*", function(fieldname, value, doc) { | ||||
// set input | // set input | ||||
if(doc.name===me.docname) { | |||||
if (cstr(doc.name) === me.docname) { | |||||
me.dirty(); | me.dirty(); | ||||
let field = me.fields_dict[fieldname]; | let field = me.fields_dict[fieldname]; | ||||
@@ -501,9 +501,9 @@ export default class Grid { | |||||
} | } | ||||
set_column_disp(fieldname, show) { | set_column_disp(fieldname, show) { | ||||
if ($.isArray(fieldname)) { | |||||
if (Array.isArray(fieldname)) { | |||||
for (let field of fieldname) { | for (let field of fieldname) { | ||||
this.update_docfield_property(field, "hidden", show); | |||||
this.update_docfield_property(field, "hidden", show ? 0 : 1); | |||||
this.set_editable_grid_column_disp(field, show); | this.set_editable_grid_column_disp(field, show); | ||||
} | } | ||||
} else { | } else { | ||||
@@ -1,9 +1,8 @@ | |||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||||
// MIT License. See license.txt | |||||
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors | |||||
// MIT License. See LICENSE | |||||
frappe.ui.form.LinkedWith = class LinkedWith { | frappe.ui.form.LinkedWith = class LinkedWith { | ||||
constructor(opts) { | constructor(opts) { | ||||
$.extend(this, opts); | $.extend(this, opts); | ||||
} | } | ||||
@@ -21,29 +20,23 @@ frappe.ui.form.LinkedWith = class LinkedWith { | |||||
} | } | ||||
make_dialog() { | make_dialog() { | ||||
this.dialog = new frappe.ui.Dialog({ | this.dialog = new frappe.ui.Dialog({ | ||||
title: __("Linked With") | title: __("Linked With") | ||||
}); | }); | ||||
this.dialog.on_page_show = () => { | this.dialog.on_page_show = () => { | ||||
// execute ajax calls sequentially | |||||
// 1. get linked doctypes | |||||
// 2. load all doctypes | |||||
// 3. load linked docs | |||||
this.get_linked_doctypes() | |||||
.then(() => this.load_doctypes()) | |||||
.then(() => this.links_not_permitted_or_missing()) | |||||
.then(() => this.get_linked_docs()) | |||||
.then(() => this.make_html()); | |||||
frappe.xcall( | |||||
"frappe.desk.form.linked_with.get", | |||||
{"doctype": cur_frm.doctype, "docname": cur_frm.docname}, | |||||
).then(r => { | |||||
this.frm.__linked_docs = r; | |||||
}).then(() => this.make_html()); | |||||
}; | }; | ||||
} | } | ||||
make_html() { | make_html() { | ||||
const linked_docs = this.frm.__linked_docs; | |||||
let html = ''; | let html = ''; | ||||
const linked_docs = this.frm.__linked_docs; | |||||
const linked_doctypes = Object.keys(linked_docs); | const linked_doctypes = Object.keys(linked_docs); | ||||
if (linked_doctypes.length === 0) { | if (linked_doctypes.length === 0) { | ||||
@@ -63,88 +56,6 @@ frappe.ui.form.LinkedWith = class LinkedWith { | |||||
$(this.dialog.body).html(html); | $(this.dialog.body).html(html); | ||||
} | } | ||||
load_doctypes() { | |||||
const already_loaded = Object.keys(locals.DocType); | |||||
let doctypes_to_load = []; | |||||
if (this.frm.__linked_doctypes) { | |||||
doctypes_to_load = | |||||
Object.keys(this.frm.__linked_doctypes) | |||||
.filter(doctype => !already_loaded.includes(doctype)); | |||||
} | |||||
// load all doctypes asynchronously using with_doctype | |||||
const promises = doctypes_to_load.map(dt => { | |||||
return frappe.model.with_doctype(dt, () => { | |||||
if(frappe.listview_settings[dt]) { | |||||
// add additional fields to __linked_doctypes | |||||
this.frm.__linked_doctypes[dt].add_fields = | |||||
frappe.listview_settings[dt].add_fields; | |||||
} | |||||
}); | |||||
}); | |||||
return Promise.all(promises); | |||||
} | |||||
links_not_permitted_or_missing() { | |||||
let links = null; | |||||
if (this.frm.__linked_doctypes) { | |||||
links = | |||||
Object.keys(this.frm.__linked_doctypes) | |||||
.filter(frappe.model.can_get_report); | |||||
} | |||||
let flag; | |||||
if(!links) { | |||||
$(this.dialog.body).html(`${this.frm.__linked_doctypes | |||||
? __("Not enough permission to see links") | |||||
: __("Not Linked to any record")}`); | |||||
flag = true; | |||||
} | |||||
flag = false; | |||||
// reject Promise if not_permitted or missing | |||||
return new Promise( | |||||
(resolve, reject) => flag ? reject() : resolve() | |||||
); | |||||
} | |||||
get_linked_doctypes() { | |||||
return new Promise((resolve) => { | |||||
if (this.frm.__linked_doctypes) { | |||||
resolve(); | |||||
} | |||||
frappe.call({ | |||||
method: "frappe.desk.form.linked_with.get_linked_doctypes", | |||||
args: { | |||||
doctype: this.frm.doctype | |||||
}, | |||||
callback: (r) => { | |||||
this.frm.__linked_doctypes = r.message; | |||||
resolve(); | |||||
} | |||||
}); | |||||
}); | |||||
} | |||||
get_linked_docs() { | |||||
return frappe.call({ | |||||
method: "frappe.desk.form.linked_with.get_linked_docs", | |||||
args: { | |||||
doctype: this.frm.doctype, | |||||
name: this.frm.docname, | |||||
linkinfo: this.frm.__linked_doctypes, | |||||
for_doctype: this.for_doctype | |||||
}, | |||||
callback: (r) => { | |||||
this.frm.__linked_docs = r.message || {}; | |||||
} | |||||
}); | |||||
} | |||||
make_doc_head(heading) { | make_doc_head(heading) { | ||||
return ` | return ` | ||||
<header class="level list-row list-row-head text-muted small"> | <header class="level list-row list-row-head text-muted small"> | ||||
@@ -44,8 +44,17 @@ frappe.ui.form.Attachments = class Attachments { | |||||
// add attachment objects | // add attachment objects | ||||
var attachments = this.get_attachments(); | var attachments = this.get_attachments(); | ||||
if(attachments.length) { | if(attachments.length) { | ||||
attachments.forEach(function(attachment) { | |||||
me.add_attachment(attachment) | |||||
let exists = {}; | |||||
let unique_attachments = attachments.filter(attachment => { | |||||
return Object.prototype.hasOwnProperty.call( | |||||
exists, | |||||
attachment.file_name | |||||
) | |||||
? false | |||||
: (exists[attachment.file_name] = true); | |||||
}); | |||||
unique_attachments.forEach(attachment => { | |||||
me.add_attachment(attachment); | |||||
}); | }); | ||||
} else { | } else { | ||||
this.attachments_label.removeClass("has-attachments"); | this.attachments_label.removeClass("has-attachments"); | ||||
@@ -75,7 +84,19 @@ frappe.ui.form.Attachments = class Attachments { | |||||
remove_action = function(target_id) { | remove_action = function(target_id) { | ||||
frappe.confirm(__("Are you sure you want to delete the attachment?"), | frappe.confirm(__("Are you sure you want to delete the attachment?"), | ||||
function() { | function() { | ||||
me.remove_attachment(target_id); | |||||
let target_attachment = me | |||||
.get_attachments() | |||||
.find(attachment => attachment.name === target_id); | |||||
let to_be_removed = me | |||||
.get_attachments() | |||||
.filter( | |||||
attachment => | |||||
attachment.file_name === | |||||
target_attachment.file_name | |||||
); | |||||
to_be_removed.forEach(attachment => | |||||
me.remove_attachment(attachment.name) | |||||
); | |||||
} | } | ||||
); | ); | ||||
return false; | return false; | ||||
@@ -760,6 +760,10 @@ class FilterArea { | |||||
const doctype_fields = this.list_view.meta.fields; | const doctype_fields = this.list_view.meta.fields; | ||||
const title_field = this.list_view.meta.title_field; | const title_field = this.list_view.meta.title_field; | ||||
const has_existing_filters = ( | |||||
this.list_view.filters | |||||
&& this.list_view.filters.length > 0 | |||||
); | |||||
fields = fields.concat( | fields = fields.concat( | ||||
doctype_fields | doctype_fields | ||||
@@ -794,13 +798,17 @@ class FilterArea { | |||||
options = options.join("\n"); | options = options.join("\n"); | ||||
} | } | ||||
} | } | ||||
let default_value = | |||||
fieldtype === "Link" | |||||
? frappe.defaults.get_user_default(options) | |||||
: null; | |||||
let default_value; | |||||
if (fieldtype === "Link" && !has_existing_filters) { | |||||
default_value = frappe.defaults.get_user_default(options); | |||||
} | |||||
if (["__default", "__global"].includes(default_value)) { | if (["__default", "__global"].includes(default_value)) { | ||||
default_value = null; | default_value = null; | ||||
} | } | ||||
return { | return { | ||||
fieldtype: fieldtype, | fieldtype: fieldtype, | ||||
label: __(df.label), | label: __(df.label), | ||||
@@ -6,8 +6,8 @@ frappe.provide('frappe.views.list_view'); | |||||
window.cur_list = null; | window.cur_list = null; | ||||
frappe.views.ListFactory = class ListFactory extends frappe.views.Factory { | frappe.views.ListFactory = class ListFactory extends frappe.views.Factory { | ||||
make (route) { | make (route) { | ||||
var me = this; | |||||
var doctype = route[1]; | |||||
const me = this; | |||||
const doctype = route[1]; | |||||
// List / Gantt / Kanban / etc | // List / Gantt / Kanban / etc | ||||
// File is a special view | // File is a special view | ||||
@@ -21,60 +21,58 @@ frappe.views.ListFactory = class ListFactory extends frappe.views.Factory { | |||||
} | } | ||||
frappe.provide('frappe.views.list_view.' + doctype); | frappe.provide('frappe.views.list_view.' + doctype); | ||||
const page_name = frappe.get_route_str(); | |||||
if (!frappe.views.list_view[page_name]) { | |||||
frappe.views.list_view[page_name] = new view_class({ | |||||
doctype: doctype, | |||||
parent: me.make_page(true, page_name) | |||||
}); | |||||
} else { | |||||
frappe.container.change_to(page_name); | |||||
} | |||||
me.set_cur_list(); | |||||
frappe.views.list_view[me.page_name] = new view_class({ | |||||
doctype: doctype, | |||||
parent: me.make_page(true, me.page_name) | |||||
}); | |||||
me.set_cur_list(); | |||||
} | } | ||||
show() { | |||||
before_show() { | |||||
if (this.re_route_to_view()) { | if (this.re_route_to_view()) { | ||||
return; | |||||
return false; | |||||
} | } | ||||
this.set_module_breadcrumb(); | this.set_module_breadcrumb(); | ||||
super.show(); | |||||
} | |||||
on_show() { | |||||
this.set_cur_list(); | this.set_cur_list(); | ||||
cur_list && cur_list.show(); | |||||
if (cur_list) cur_list.show(); | |||||
} | } | ||||
re_route_to_view() { | re_route_to_view() { | ||||
var route = frappe.get_route(); | |||||
var doctype = route[1]; | |||||
var last_route = frappe.route_history.slice(-2)[0]; | |||||
if (route[0] === 'List' && route.length === 2 && frappe.views.list_view[doctype]) { | |||||
if(last_route && last_route[0]==='List' && last_route[1]===doctype) { | |||||
// last route same as this route, so going back. | |||||
// this happens because /app/List/Item will redirect to /app/List/Item/List | |||||
// while coming from back button, the last 2 routes will be same, so | |||||
// we know user is coming in the reverse direction (via back button) | |||||
const doctype = this.route[1]; | |||||
const last_route = frappe.route_history.slice(-2)[0]; | |||||
if ( | |||||
this.route[0] === 'List' && | |||||
this.route.length === 2 && | |||||
frappe.views.list_view[doctype] && | |||||
last_route && | |||||
last_route[0] === 'List' && | |||||
last_route[1] === doctype | |||||
) { | |||||
// last route same as this route, so going back. | |||||
// this happens because /app/List/Item will redirect to /app/List/Item/List | |||||
// while coming from back button, the last 2 routes will be same, so | |||||
// we know user is coming in the reverse direction (via back button) | |||||
// example: | |||||
// Step 1: /app/List/Item redirects to /app/List/Item/List | |||||
// Step 2: User hits "back" comes back to /app/List/Item | |||||
// Step 3: Now we cannot send the user back to /app/List/Item/List so go back one more step | |||||
window.history.go(-1); | |||||
return true; | |||||
} else { | |||||
return false; | |||||
} | |||||
// example: | |||||
// Step 1: /app/List/Item redirects to /app/List/Item/List | |||||
// Step 2: User hits "back" comes back to /app/List/Item | |||||
// Step 3: Now we cannot send the user back to /app/List/Item/List so go back one more step | |||||
window.history.go(-1); | |||||
return true; | |||||
} | } | ||||
} | } | ||||
set_module_breadcrumb() { | set_module_breadcrumb() { | ||||
if (frappe.route_history.length > 1) { | if (frappe.route_history.length > 1) { | ||||
var prev_route = frappe.route_history[frappe.route_history.length - 2]; | |||||
const prev_route = frappe.route_history[frappe.route_history.length - 2]; | |||||
if (prev_route[0] === 'modules') { | if (prev_route[0] === 'modules') { | ||||
var doctype = frappe.get_route()[1], | |||||
module = prev_route[1]; | |||||
const doctype = this.route[1], module = prev_route[1]; | |||||
if (frappe.module_links[module] && frappe.module_links[module].includes(doctype)) { | if (frappe.module_links[module] && frappe.module_links[module].includes(doctype)) { | ||||
// save the last page from the breadcrumb was accessed | // save the last page from the breadcrumb was accessed | ||||
frappe.breadcrumbs.set_doctype_module(doctype, module); | frappe.breadcrumbs.set_doctype_module(doctype, module); | ||||
@@ -84,10 +82,8 @@ frappe.views.ListFactory = class ListFactory extends frappe.views.Factory { | |||||
} | } | ||||
set_cur_list() { | set_cur_list() { | ||||
var route = frappe.get_route(); | |||||
var page_name = frappe.get_route_str(); | |||||
cur_list = frappe.views.list_view[page_name]; | |||||
if (cur_list && cur_list.doctype !== route[1]) { | |||||
cur_list = frappe.views.list_view[this.page_name]; | |||||
if (cur_list && cur_list.doctype !== this.route[1]) { | |||||
// changing... | // changing... | ||||
window.cur_list = null; | window.cur_list = null; | ||||
} | } | ||||
@@ -83,32 +83,15 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||||
this.sort_by = this.view_user_settings.sort_by || "modified"; | this.sort_by = this.view_user_settings.sort_by || "modified"; | ||||
this.sort_order = this.view_user_settings.sort_order || "desc"; | this.sort_order = this.view_user_settings.sort_order || "desc"; | ||||
// set filters from user_settings or list_settings | |||||
if ( | |||||
this.view_user_settings.filters && | |||||
this.view_user_settings.filters.length | |||||
) { | |||||
// Priority 1: user_settings | |||||
const saved_filters = this.view_user_settings.filters; | |||||
this.filters = this.validate_filters(saved_filters); | |||||
} else { | |||||
// Priority 2: filters in listview_settings | |||||
this.filters = (this.settings.filters || []).map((f) => { | |||||
if (f.length === 3) { | |||||
f = [this.doctype, f[0], f[1], f[2]]; | |||||
} | |||||
return f; | |||||
}); | |||||
} | |||||
// build menu items | // build menu items | ||||
this.menu_items = this.menu_items.concat(this.get_menu_items()); | this.menu_items = this.menu_items.concat(this.get_menu_items()); | ||||
// set filters from view_user_settings or list_settings | |||||
if ( | if ( | ||||
this.view_user_settings.filters && | this.view_user_settings.filters && | ||||
this.view_user_settings.filters.length | this.view_user_settings.filters.length | ||||
) { | ) { | ||||
// Priority 1: saved filters | |||||
// Priority 1: view_user_settings | |||||
const saved_filters = this.view_user_settings.filters; | const saved_filters = this.view_user_settings.filters; | ||||
this.filters = this.validate_filters(saved_filters); | this.filters = this.validate_filters(saved_filters); | ||||
} else { | } else { | ||||
@@ -932,7 +915,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||||
return this.settings.get_form_link(doc); | return this.settings.get_form_link(doc); | ||||
} | } | ||||
const docname = doc.name.match(/[%'"#\s]/) | |||||
const docname = cstr(doc.name).match(/[%'"#\s]/) | |||||
? encodeURIComponent(doc.name) | ? encodeURIComponent(doc.name) | ||||
: doc.name; | : doc.name; | ||||
@@ -1757,8 +1740,12 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||||
const docnames = this.get_checked_items(true).map( | const docnames = this.get_checked_items(true).map( | ||||
(docname) => docname.toString() | (docname) => docname.toString() | ||||
); | ); | ||||
let message = __("Delete {0} item permanently?", [docnames.length], "Title of confirmation dialog"); | |||||
if (docnames.length > 1) { | |||||
message = __("Delete {0} items permanently?", [docnames.length], "Title of confirmation dialog"); | |||||
} | |||||
frappe.confirm( | frappe.confirm( | ||||
__("Delete {0} items permanently?", [docnames.length], "Title of confirmation dialog"), | |||||
message, | |||||
() => { | () => { | ||||
this.disable_list_update = true; | this.disable_list_update = true; | ||||
bulk_operations.delete(docnames, () => { | bulk_operations.delete(docnames, () => { | ||||
@@ -138,6 +138,7 @@ frappe.render_tree = function(opts) { | |||||
opts.base_url = frappe.urllib.get_base_url(); | opts.base_url = frappe.urllib.get_base_url(); | ||||
opts.landscape = false; | opts.landscape = false; | ||||
opts.print_css = frappe.boot.print_css; | opts.print_css = frappe.boot.print_css; | ||||
opts.print_format_css_path = frappe.assets.bundled_asset('print_format.bundle.css'); | |||||
var tree = frappe.render_template("print_tree", opts); | var tree = frappe.render_template("print_tree", opts); | ||||
var w = window.open(); | var w = window.open(); | ||||
@@ -577,13 +577,15 @@ $.extend(frappe.model, { | |||||
}, | }, | ||||
delete_doc: function(doctype, docname, callback) { | delete_doc: function(doctype, docname, callback) { | ||||
var title = docname; | |||||
var title_field = frappe.get_meta(doctype).title_field; | |||||
let title = docname; | |||||
const title_field = frappe.get_meta(doctype).title_field; | |||||
if (frappe.get_meta(doctype).autoname == "hash" && title_field) { | if (frappe.get_meta(doctype).autoname == "hash" && title_field) { | ||||
var title = frappe.model.get_value(doctype, docname, title_field); | |||||
title += " (" + docname + ")"; | |||||
const value = frappe.model.get_value(doctype, docname, title_field); | |||||
if (value) { | |||||
title = `${value} (${docname})`; | |||||
} | |||||
} | } | ||||
frappe.confirm(__("Permanently delete {0}?", [title]), function() { | |||||
frappe.confirm(__("Permanently delete {0}?", [title.bold()]), function() { | |||||
return frappe.call({ | return frappe.call({ | ||||
method: 'frappe.client.delete', | method: 'frappe.client.delete', | ||||
args: { | args: { | ||||
@@ -134,7 +134,17 @@ frappe.msgprint = function(msg, title, is_minimizable) { | |||||
} | } | ||||
if(data.message instanceof Array) { | if(data.message instanceof Array) { | ||||
data.message.forEach(function(m) { | |||||
let messages = data.message; | |||||
const exceptions = messages | |||||
.map(m => JSON.parse(m)) | |||||
.filter(m => m.raise_exception); | |||||
// only show exceptions if any exceptions exist | |||||
if (exceptions.length) { | |||||
messages = exceptions; | |||||
} | |||||
messages.forEach(function(m) { | |||||
frappe.msgprint(m); | frappe.msgprint(m); | ||||
}); | }); | ||||
return; | return; | ||||
@@ -196,6 +196,15 @@ Object.assign(frappe.utils, { | |||||
} | } | ||||
return true; | return true; | ||||
}, | }, | ||||
parse_json: function(str) { | |||||
let parsed_json = ''; | |||||
try { | |||||
parsed_json = JSON.parse(str); | |||||
} catch (e) { | |||||
return str; | |||||
} | |||||
return parsed_json; | |||||
}, | |||||
strip_whitespace: function(html) { | strip_whitespace: function(html) { | ||||
return (html || "").replace(/<p>\s*<\/p>/g, "").replace(/<br>(\s*<br>\s*)+/g, "<br><br>"); | return (html || "").replace(/<p>\s*<\/p>/g, "").replace(/<br>(\s*<br>\s*)+/g, "<br><br>"); | ||||
}, | }, | ||||
@@ -10,20 +10,21 @@ frappe.views.Factory = class Factory { | |||||
} | } | ||||
show() { | show() { | ||||
var page_name = frappe.get_route_str(), | |||||
me = this; | |||||
this.route = frappe.get_route(); | |||||
this.page_name = frappe.get_route_str(); | |||||
if (frappe.pages[page_name]) { | |||||
frappe.container.change_to(page_name); | |||||
if(me.on_show) { | |||||
me.on_show(); | |||||
if (this.before_show && this.before_show() === false) return; | |||||
if (frappe.pages[this.page_name]) { | |||||
frappe.container.change_to(this.page_name); | |||||
if (this.on_show) { | |||||
this.on_show(); | |||||
} | } | ||||
} else { | } else { | ||||
var route = frappe.get_route(); | |||||
if(route[1]) { | |||||
me.make(route); | |||||
if (this.route[1]) { | |||||
this.make(this.route); | |||||
} else { | } else { | ||||
frappe.show_not_found(route); | |||||
frappe.show_not_found(this.route); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
@@ -34,15 +35,17 @@ frappe.views.Factory = class Factory { | |||||
} | } | ||||
frappe.make_page = function(double_column, page_name) { | frappe.make_page = function(double_column, page_name) { | ||||
if(!page_name) { | |||||
var page_name = frappe.get_route_str(); | |||||
if (!page_name) { | |||||
page_name = frappe.get_route_str(); | |||||
} | } | ||||
var page = frappe.container.add_page(page_name); | |||||
const page = frappe.container.add_page(page_name); | |||||
frappe.ui.make_app_page({ | frappe.ui.make_app_page({ | ||||
parent: page, | parent: page, | ||||
single_column: !double_column | single_column: !double_column | ||||
}); | }); | ||||
frappe.container.change_to(page_name); | frappe.container.change_to(page_name); | ||||
return page; | return page; | ||||
} | } |
@@ -1,91 +1,106 @@ | |||||
<!DOCTYPE html> | <!DOCTYPE html> | ||||
<html lang="en"> | <html lang="en"> | ||||
<head> | |||||
<meta charset="utf-8"> | |||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |||||
<meta name="viewport" content="width=device-width, initial-scale=1"> | |||||
<meta name="description" content=""> | |||||
<meta name="author" content=""> | |||||
<title>{{ title }}</title> | |||||
<link href="{{ base_url }}/assets/frappe/css/bootstrap.css" rel="stylesheet"> | |||||
<link type="text/css" rel="stylesheet" | |||||
href="{{ base_url }}/assets/frappe/css/font-awesome.css"> | |||||
<link rel="stylesheet" type="text/css" href="{{ base_url }}/assets/frappe/css/tree.css"> | |||||
<style> | |||||
{{ print_css }} | |||||
</style> | |||||
<style> | |||||
.tree.opened::before, | |||||
.tree-node.opened::before, | |||||
.tree:last-child::after, | |||||
.tree-node:last-child::after { | |||||
z-index: 1; | |||||
border-left: 1px solid #d1d8dd; | |||||
background: none; | |||||
} | |||||
.tree a, | |||||
.tree-link { | |||||
text-decoration: none; | |||||
cursor: default; | |||||
} | |||||
.tree.opened > .tree-children > .tree-node > .tree-link::before, | |||||
.tree-node.opened > .tree-children > .tree-node > .tree-link::before { | |||||
border-top: 1px solid #d1d8dd; | |||||
z-index: 1; | |||||
background: none; | |||||
} | |||||
i.fa.fa-fw.fa-folder { | |||||
z-index: 2; | |||||
position: relative; | |||||
} | |||||
.tree:last-child::after, .tree-node:last-child::after { | |||||
display: none; | |||||
} | |||||
.tree-node-toolbar { | |||||
display: none; | |||||
} | |||||
i.octicon.octicon-primitive-dot.text-extra-muted { | |||||
width: 7px; | |||||
height: 7px; | |||||
border-radius: 50%; | |||||
background: #d1d8dd; | |||||
display: inline-block; | |||||
position: relative; | |||||
z-index: 2; | |||||
} | |||||
<head> | |||||
<meta charset="utf-8"> | |||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |||||
<meta name="viewport" content="width=device-width, initial-scale=1"> | |||||
<meta name="description" content=""> | |||||
<meta name="author" content=""> | |||||
<title>{{ title }}</title> | |||||
<link href="{{ base_url }}/assets/frappe/css/bootstrap.css" rel="stylesheet"> | |||||
<link type="text/css" rel="stylesheet" | |||||
href="{{ base_url }}/assets/frappe/css/font-awesome.css"> | |||||
<link rel="stylesheet" type="text/css" href="{{ base_url }}/assets/frappe/css/tree.css"> | |||||
<link rel="stylesheet" type="text/css" href="{{ base_url }}{{ print_format_css_path }}"> | |||||
<style> | |||||
{{ print_css }} | |||||
</style> | |||||
<style> | |||||
.tree.opened::before, | |||||
.tree-node.opened::before, | |||||
.tree:last-child::after, | |||||
.tree-node:last-child::after { | |||||
z-index: 1; | |||||
border-left: 1px solid #d1d8dd; | |||||
background: none; | |||||
} | |||||
.tree a, | |||||
.tree-link { | |||||
text-decoration: none; | |||||
cursor: default; | |||||
} | |||||
.tree.opened > .tree-children > .tree-node > .tree-link::before, | |||||
.tree-node.opened > .tree-children > .tree-node > .tree-link::before { | |||||
border-top: 1px solid #d1d8dd; | |||||
z-index: 1; | |||||
background: none; | |||||
} | |||||
i.fa.fa-fw.fa-folder { | |||||
z-index: 2; | |||||
position: relative; | |||||
} | |||||
.tree:last-child::after, .tree-node:last-child::after { | |||||
display: none; | |||||
} | |||||
.tree-node-toolbar { | |||||
display: none; | |||||
} | |||||
i.octicon.octicon-primitive-dot.text-extra-muted { | |||||
width: 7px; | |||||
height: 7px; | |||||
border-radius: 50%; | |||||
background: #d1d8dd; | |||||
display: inline-block; | |||||
position: relative; | |||||
z-index: 2; | |||||
} | |||||
@media (max-width: 767px) { | |||||
ul.tree-children { | |||||
padding-left: 20px; | |||||
@media (max-width: 767px) { | |||||
ul.tree-children { | |||||
padding-left: 20px; | |||||
} | |||||
} | } | ||||
} | |||||
</style> | |||||
</head> | |||||
<body> | |||||
<div class="print-format-gutter"> | |||||
{% if print_settings.repeat_header_footer %} | |||||
<div id="footer-html" class="visible-pdf"> | |||||
{% if print_settings.letter_head && print_settings.letter_head.footer %} | |||||
<div class="letter-head-footer"> | |||||
{{ print_settings.letter_head.footer }} | |||||
</div> | |||||
{% endif %} | |||||
<p class="text-center small page-number visible-pdf"> | |||||
{{ __("Page {0} of {1}", [`<span class="page"></span>`, `<span class="topage"></span>`]) }} | |||||
</p> | |||||
</div> | |||||
{% endif %} | |||||
</style> | |||||
</head> | |||||
<body> | |||||
<svg id="frappe-symbols" aria-hidden="true" style="position: absolute; width: 0; height: 0; overflow: hidden;" class="d-block" xmlns="http://www.w3.org/2000/svg"> | |||||
<symbol viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" id="icon-primitive-dot"> | |||||
<path d="M9.5 6a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0z"></path> | |||||
</symbol> | |||||
<div class="print-format {% if landscape %} landscape {% endif %}"> | |||||
{% if print_settings.letter_head %} | |||||
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}> | |||||
<div class="letter-head">{{ print_settings.letter_head.header }}</div> | |||||
</div> | |||||
{% endif %} | |||||
<div class="tree opened"> | |||||
{{ tree }} | |||||
<symbol viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" id="icon-folder-open"> | |||||
<path d="M8.024 6.5H3a.5.5 0 0 0-.5.5v8a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V9.5A.5.5 0 0 0 17 9h-6.783a.5.5 0 0 1-.417-.224L8.441 6.724a.5.5 0 0 0-.417-.224z" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="square"></path> | |||||
<path d="M3.88 4.5v-1a.5.5 0 0 1 .5-.5h11.24a.5.5 0 0 1 .5.5V7" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"></path> | |||||
</symbol> | |||||
<symbol viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" id="icon-folder-normal"> | |||||
<path d="M2.5 4v10a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V6.5a1 1 0 0 0-1-1h-6.283a.5.5 0 0 1-.417-.224L8.441 3.224A.5.5 0 0 0 8.024 3H3.5a1 1 0 0 0-1 1z" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="square"></path> | |||||
</symbol> | |||||
</svg> | |||||
<div class="print-format-gutter"> | |||||
{% if print_settings.repeat_header_footer %} | |||||
<div id="footer-html" class="visible-pdf"> | |||||
{% if print_settings.letter_head && print_settings.letter_head.footer %} | |||||
<div class="letter-head-footer"> | |||||
{{ print_settings.letter_head.footer }} | |||||
</div> | |||||
{% endif %} | |||||
<p class="text-center small page-number visible-pdf"> | |||||
{{ __("Page {0} of {1}", [`<span class="page"></span>`, `<span class="topage"></span>`]) }} | |||||
</p> | |||||
</div> | </div> | ||||
</div> | |||||
</div> | |||||
</body> | |||||
{% endif %} | |||||
<div class="print-format {% if landscape %} landscape {% endif %}"> | |||||
{% if print_settings.letter_head %} | |||||
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}> | |||||
<div class="letter-head">{{ print_settings.letter_head.header }}</div> | |||||
</div> | |||||
{% endif %} | |||||
<div class="tree opened"> | |||||
{{ tree }} | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</body> | |||||
</html> | </html> |
@@ -125,11 +125,12 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { | |||||
} | } | ||||
after_render() { | after_render() { | ||||
if (this.report_doc) { | |||||
this.set_dirty_state_for_custom_report(); | |||||
} else { | |||||
if (!this.report_doc) { | |||||
this.save_report_settings(); | this.save_report_settings(); | ||||
} else if (!$.isEmptyObject(this.report_doc.json)) { | |||||
this.set_dirty_state_for_custom_report(); | |||||
} | } | ||||
if (!this.group_by) { | if (!this.group_by) { | ||||
this.init_chart(); | this.init_chart(); | ||||
} | } | ||||
@@ -1,5 +1,5 @@ | |||||
from pypika.functions import * | from pypika.functions import * | ||||
from pypika.terms import Function | |||||
from pypika.terms import Function, CustomFunction, ArithmeticExpression, Arithmetic | |||||
from frappe.query_builder.utils import ImportMapper, db_type_is | from frappe.query_builder.utils import ImportMapper, db_type_is | ||||
from frappe.query_builder.custom import GROUP_CONCAT, STRING_AGG, MATCH, TO_TSVECTOR | from frappe.query_builder.custom import GROUP_CONCAT, STRING_AGG, MATCH, TO_TSVECTOR | ||||
from frappe.database.query import Query | from frappe.database.query import Query | ||||
@@ -25,6 +25,24 @@ Match = ImportMapper( | |||||
} | } | ||||
) | ) | ||||
class _PostgresTimestamp(ArithmeticExpression): | |||||
def __init__(self, datepart, timepart, alias=None): | |||||
if isinstance(datepart, str): | |||||
datepart = Cast(datepart, "date") | |||||
if isinstance(timepart, str): | |||||
timepart = Cast(timepart, "time") | |||||
super().__init__(operator=Arithmetic.add, | |||||
left=datepart, right=timepart, alias=alias) | |||||
CombineDatetime = ImportMapper( | |||||
{ | |||||
db_type_is.MARIADB: CustomFunction("TIMESTAMP", ["date", "time"]), | |||||
db_type_is.POSTGRES: _PostgresTimestamp, | |||||
} | |||||
) | |||||
def _aggregate(function, dt, fieldname, filters, **kwargs): | def _aggregate(function, dt, fieldname, filters, **kwargs): | ||||
return ( | return ( | ||||
@@ -46,4 +64,4 @@ def _avg(dt, fieldname, filters=None, **kwargs): | |||||
return _aggregate(Avg, dt, fieldname, filters, **kwargs) | return _aggregate(Avg, dt, fieldname, filters, **kwargs) | ||||
def _sum(dt, fieldname, filters=None, **kwargs): | def _sum(dt, fieldname, filters=None, **kwargs): | ||||
return _aggregate(Sum, dt, fieldname, filters, **kwargs) | |||||
return _aggregate(Sum, dt, fieldname, filters, **kwargs) |
@@ -2,6 +2,7 @@ | |||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | import frappe | ||||
from frappe.utils.data import cstr | |||||
import os | import os | ||||
import redis | import redis | ||||
@@ -118,7 +119,7 @@ def get_user_info(): | |||||
} | } | ||||
def get_doc_room(doctype, docname): | def get_doc_room(doctype, docname): | ||||
return ''.join([frappe.local.site, ':doc:', doctype, '/', docname]) | |||||
return ''.join([frappe.local.site, ':doc:', doctype, '/', cstr(docname)]) | |||||
def get_user_room(user): | def get_user_room(user): | ||||
return ''.join([frappe.local.site, ':user:', user]) | return ''.join([frappe.local.site, ':user:', user]) | ||||
@@ -37,7 +37,6 @@ | |||||
<tr> | <tr> | ||||
<td valign="top"> | <td valign="top"> | ||||
<p>{{ content }}</p> | <p>{{ content }}</p> | ||||
<p class="signature">{{ signature }}</p> | |||||
</td> | </td> | ||||
</tr> | </tr> | ||||
</table> | </table> | ||||
@@ -562,3 +562,50 @@ class TestDDLCommandsPost(unittest.TestCase): | |||||
""", | """, | ||||
) | ) | ||||
self.assertEquals(len(indexs_in_table), 1) | self.assertEquals(len(indexs_in_table), 1) | ||||
@run_only_if(db_type_is.POSTGRES) | |||||
def test_modify_query(self): | |||||
from frappe.database.postgres.database import modify_query | |||||
query = "select * from `tabtree b` where lft > 13 and rgt <= 16 and name =1.0 and parent = 4134qrsdc and isgroup = 1.00045" | |||||
self.assertEqual( | |||||
"select * from \"tabtree b\" where lft > \'13\' and rgt <= '16' and name = '1' and parent = 4134qrsdc and isgroup = 1.00045", | |||||
modify_query(query) | |||||
) | |||||
query = "select locate(\".io\", \"frappe.io\"), locate(\"3\", cast(3 as varchar)), locate(\"3\", 3::varchar)" | |||||
self.assertEqual( | |||||
"select strpos( \"frappe.io\", \".io\"), strpos( cast(3 as varchar), \"3\"), strpos( 3::varchar, \"3\")", | |||||
modify_query(query) | |||||
) | |||||
@run_only_if(db_type_is.POSTGRES) | |||||
def test_modify_values(self): | |||||
from frappe.database.postgres.database import modify_values | |||||
self.assertEqual( | |||||
{"abcd": "23", "efgh": "23", "ijkl": 23.0345, "mnop": "wow"}, | |||||
modify_values({"abcd": 23, "efgh": 23.0, "ijkl": 23.0345, "mnop": "wow"}) | |||||
) | |||||
self.assertEqual( | |||||
["23", "23", 23.00004345, "wow"], | |||||
modify_values((23, 23.0, 23.00004345, "wow")) | |||||
) | |||||
def test_sequence_table_creation(self): | |||||
from frappe.core.doctype.doctype.test_doctype import new_doctype | |||||
dt = new_doctype("autoinc_dt_seq_test", autoincremented=True).insert(ignore_permissions=True) | |||||
if frappe.db.db_type == "postgres": | |||||
self.assertTrue( | |||||
frappe.db.sql("""select sequence_name FROM information_schema.sequences | |||||
where sequence_name ilike 'autoinc_dt_seq_test%'""")[0][0] | |||||
) | |||||
else: | |||||
self.assertTrue( | |||||
frappe.db.sql("""select data_type FROM information_schema.tables | |||||
where table_type = 'SEQUENCE' and table_name like 'autoinc_dt_seq_test%'""")[0][0] | |||||
) | |||||
dt.delete(ignore_permissions=True) |
@@ -494,6 +494,27 @@ class TestReportview(unittest.TestCase): | |||||
response = execute_cmd("frappe.desk.reportview.get") | response = execute_cmd("frappe.desk.reportview.get") | ||||
self.assertListEqual(response["keys"], ["field_label", "field_name", "_aggregate_column", 'columns']) | self.assertListEqual(response["keys"], ["field_label", "field_name", "_aggregate_column", 'columns']) | ||||
def test_cast_name(self): | |||||
from frappe.core.doctype.doctype.test_doctype import new_doctype | |||||
dt = new_doctype("autoinc_dt_test", autoincremented=True).insert(ignore_permissions=True) | |||||
query = DatabaseQuery("autoinc_dt_test").execute( | |||||
fields=["locate('1', `tabautoinc_dt_test`.`name`)", "`tabautoinc_dt_test`.`name`"], | |||||
filters={"name": 1}, | |||||
run=False | |||||
) | |||||
if frappe.db.db_type == "postgres": | |||||
self.assertTrue("strpos( cast( \"tabautoinc_dt_test\".\"name\" as varchar), \'1\')" in query) | |||||
self.assertTrue("where cast(\"tabautoinc_dt_test\".name as varchar) = \'1\'" in query) | |||||
else: | |||||
self.assertTrue("locate(\'1\', `tabautoinc_dt_test`.`name`)" in query) | |||||
self.assertTrue("where `tabautoinc_dt_test`.name = 1" in query) | |||||
dt.delete(ignore_permissions=True) | |||||
def add_child_table_to_blog_post(): | def add_child_table_to_blog_post(): | ||||
child_table = frappe.get_doc({ | child_table = frappe.get_doc({ | ||||
'doctype': 'DocType', | 'doctype': 'DocType', | ||||
@@ -168,8 +168,8 @@ class TestFormLoad(unittest.TestCase): | |||||
"reference_name": note.name, | "reference_name": note.name, | ||||
}).insert() | }).insert() | ||||
docinfo = get_docinfo(note) | |||||
get_docinfo(note) | |||||
docinfo = frappe.response["docinfo"] | |||||
self.assertEqual(len(docinfo.comments), 1) | self.assertEqual(len(docinfo.comments), 1) | ||||
self.assertIn("test", docinfo.comments[0].content) | self.assertIn("test", docinfo.comments[0].content) | ||||
@@ -245,6 +245,17 @@ class TestNaming(unittest.TestCase): | |||||
}) | }) | ||||
self.assertRaises(frappe.ValidationError, tag.insert) | self.assertRaises(frappe.ValidationError, tag.insert) | ||||
def test_autoincremented_naming(self): | |||||
from frappe.core.doctype.doctype.test_doctype import new_doctype | |||||
doctype = "autoinc_doctype" + frappe.generate_hash(length=5) | |||||
dt = new_doctype(doctype, autoincremented=True).insert(ignore_permissions=True) | |||||
for i in range(1, 20): | |||||
self.assertEqual(frappe.new_doc(doctype).save(ignore_permissions=True).name, i) | |||||
dt.delete(ignore_permissions=True) | |||||
def make_invalid_todo(): | def make_invalid_todo(): | ||||
frappe.get_doc({ | frappe.get_doc({ | ||||
@@ -3,7 +3,7 @@ from typing import Callable | |||||
import frappe | import frappe | ||||
from frappe.query_builder.custom import ConstantColumn | from frappe.query_builder.custom import ConstantColumn | ||||
from frappe.query_builder.functions import Coalesce, GroupConcat, Match | |||||
from frappe.query_builder.functions import Coalesce, GroupConcat, Match, CombineDatetime | |||||
from frappe.query_builder.utils import db_type_is | from frappe.query_builder.utils import db_type_is | ||||
from frappe.query_builder import Case | from frappe.query_builder import Case | ||||
@@ -32,6 +32,27 @@ class TestCustomFunctionsMariaDB(unittest.TestCase): | |||||
query.get_sql(), "SELECT `name`,'John' `User` FROM `tabDocType`" | query.get_sql(), "SELECT `name`,'John' `User` FROM `tabDocType`" | ||||
) | ) | ||||
def test_timestamp(self): | |||||
note = frappe.qb.DocType("Note") | |||||
self.assertEqual("TIMESTAMP(posting_date,posting_time)", CombineDatetime(note.posting_date, note.posting_time).get_sql()) | |||||
self.assertEqual("TIMESTAMP('2021-01-01','00:00:21')", CombineDatetime("2021-01-01", "00:00:21").get_sql()) | |||||
todo = frappe.qb.DocType("ToDo") | |||||
select_query = (frappe.qb | |||||
.from_(note) | |||||
.join(todo).on(todo.refernce_name == note.name) | |||||
.select(CombineDatetime(note.posting_date, note.posting_time))) | |||||
self.assertIn("select timestamp(`tabnote`.`posting_date`,`tabnote`.`posting_time`)", str(select_query).lower()) | |||||
select_query = select_query.orderby(CombineDatetime(note.posting_date, note.posting_time)) | |||||
self.assertIn("order by timestamp(`tabnote`.`posting_date`,`tabnote`.`posting_time`)", str(select_query).lower()) | |||||
select_query = select_query.where(CombineDatetime(note.posting_date, note.posting_time) >= CombineDatetime("2021-01-01", "00:00:01")) | |||||
self.assertIn("timestamp(`tabnote`.`posting_date`,`tabnote`.`posting_time`)>=timestamp('2021-01-01','00:00:01')", str(select_query).lower()) | |||||
select_query = select_query.select(CombineDatetime(note.posting_date, note.posting_time, alias="timestamp")) | |||||
self.assertIn("timestamp(`tabnote`.`posting_date`,`tabnote`.`posting_time`) `timestamp`", str(select_query).lower()) | |||||
@run_only_if(db_type_is.POSTGRES) | @run_only_if(db_type_is.POSTGRES) | ||||
class TestCustomFunctionsPostgres(unittest.TestCase): | class TestCustomFunctionsPostgres(unittest.TestCase): | ||||
@@ -52,6 +73,30 @@ class TestCustomFunctionsPostgres(unittest.TestCase): | |||||
query.get_sql(), 'SELECT "name",\'John\' "User" FROM "tabDocType"' | query.get_sql(), 'SELECT "name",\'John\' "User" FROM "tabDocType"' | ||||
) | ) | ||||
def test_timestamp(self): | |||||
note = frappe.qb.DocType("Note") | |||||
self.assertEqual("posting_date+posting_time", CombineDatetime(note.posting_date, note.posting_time).get_sql()) | |||||
self.assertEqual("CAST('2021-01-01' AS DATE)+CAST('00:00:21' AS TIME)", CombineDatetime("2021-01-01", "00:00:21").get_sql()) | |||||
todo = frappe.qb.DocType("ToDo") | |||||
select_query = (frappe.qb | |||||
.from_(note) | |||||
.join(todo).on(todo.refernce_name == note.name) | |||||
.select(CombineDatetime(note.posting_date, note.posting_time))) | |||||
self.assertIn('select "tabnote"."posting_date"+"tabnote"."posting_time"', str(select_query).lower()) | |||||
select_query = select_query.orderby(CombineDatetime(note.posting_date, note.posting_time)) | |||||
self.assertIn('order by "tabnote"."posting_date"+"tabnote"."posting_time"', str(select_query).lower()) | |||||
select_query = select_query.where( | |||||
CombineDatetime(note.posting_date, note.posting_time) >= CombineDatetime('2021-01-01', '00:00:01') | |||||
) | |||||
self.assertIn("""where "tabnote"."posting_date"+"tabnote"."posting_time">=cast('2021-01-01' as date)+cast('00:00:01' as time)""", | |||||
str(select_query).lower()) | |||||
select_query = select_query.select(CombineDatetime(note.posting_date, note.posting_time, alias="timestamp")) | |||||
self.assertIn('"tabnote"."posting_date"+"tabnote"."posting_time" "timestamp"', str(select_query).lower()) | |||||
class TestBuilderBase(object): | class TestBuilderBase(object): | ||||
def test_adding_tabs(self): | def test_adding_tabs(self): | ||||
@@ -12,37 +12,30 @@ class TestQueryReport(unittest.TestCase): | |||||
def test_xlsx_data_with_multiple_datatypes(self): | def test_xlsx_data_with_multiple_datatypes(self): | ||||
"""Test exporting report using rows with multiple datatypes (list, dict)""" | """Test exporting report using rows with multiple datatypes (list, dict)""" | ||||
# Describe the columns | |||||
columns = { | |||||
0: {"label": "Column A", "fieldname": "column_a"}, | |||||
1: {"label": "Column B", "fieldname": "column_b"}, | |||||
2: {"label": "Column C", "fieldname": "column_c"} | |||||
} | |||||
# Create mock data | # Create mock data | ||||
data = frappe._dict() | data = frappe._dict() | ||||
data.columns = [ | data.columns = [ | ||||
{"label": "Column A", "fieldname": "column_a"}, | |||||
{"label": "Column B", "fieldname": "column_b", "width": 150}, | |||||
{"label": "Column C", "fieldname": "column_c", "width": 100} | |||||
{"label": "Column A", "fieldname": "column_a", "fieldtype": "Float"}, | |||||
{"label": "Column B", "fieldname": "column_b", "width": 100, "fieldtype": "Float"}, | |||||
{"label": "Column C", "fieldname": "column_c", "width": 150, "fieldtype": "Duration"}, | |||||
] | ] | ||||
data.result = [ | data.result = [ | ||||
[1.0, 3.0, 5.5], | |||||
{"column_a": 22.1, "column_b": 21.8, "column_c": 30.2}, | |||||
{"column_b": 5.1, "column_c": 9.5, "column_a": 11.1}, | |||||
[3.0, 1.5, 7.5], | |||||
[1.0, 3.0, 600], | |||||
{"column_a": 22.1, "column_b": 21.8, "column_c": 86412}, | |||||
{"column_b": 5.1, "column_c": 53234, "column_a": 11.1}, | |||||
[3.0, 1.5, 333], | |||||
] | ] | ||||
# Define the visible rows | # Define the visible rows | ||||
visible_idx = [0, 2, 3] | visible_idx = [0, 2, 3] | ||||
# Build the result | # Build the result | ||||
xlsx_data, column_widths = build_xlsx_data(columns, data, visible_idx, include_indentation=0) | |||||
xlsx_data, column_widths = build_xlsx_data(data, visible_idx, include_indentation=0) | |||||
self.assertEqual(type(xlsx_data), list) | self.assertEqual(type(xlsx_data), list) | ||||
self.assertEqual(len(xlsx_data), 4) # columns + data | self.assertEqual(len(xlsx_data), 4) # columns + data | ||||
# column widths are divided by 10 to match the scale that is supported by openpyxl | # column widths are divided by 10 to match the scale that is supported by openpyxl | ||||
self.assertListEqual(column_widths, [0, 15, 10]) | |||||
self.assertListEqual(column_widths, [0, 10, 15]) | |||||
for row in xlsx_data: | for row in xlsx_data: | ||||
self.assertEqual(type(row), list) | self.assertEqual(type(row), list) |
@@ -135,11 +135,14 @@ def create_contact_records(): | |||||
insert_contact('Test Form Contact 3', '12345') | insert_contact('Test Form Contact 3', '12345') | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def create_multiple_contact_records(): | |||||
if frappe.db.get_all('Contact', {'first_name': 'Multiple Contact 1'}): | |||||
def create_multiple_todo_records(): | |||||
if frappe.db.get_all('ToDo', {'description': 'Multiple ToDo 1'}): | |||||
return | return | ||||
for index in range(1001): | |||||
insert_contact('Multiple Contact {}'.format(index+1), '12345{}'.format(index+1)) | |||||
for index in range(501): | |||||
frappe.get_doc({ | |||||
'doctype': 'ToDo', | |||||
'description': 'Multiple ToDo {}'.format(index+1) | |||||
}).insert() | |||||
def insert_contact(first_name, phone_number): | def insert_contact(first_name, phone_number): | ||||
doc = frappe.get_doc({ | doc = frappe.get_doc({ | ||||
@@ -146,7 +146,7 @@ Monthly,Monatlich, | |||||
More,Weiter, | More,Weiter, | ||||
More Information,Mehr Informationen, | More Information,Mehr Informationen, | ||||
More...,Mehr..., | More...,Mehr..., | ||||
Move,Bewegen, | |||||
Move,Verschieben, | |||||
My Account,Mein Konto, | My Account,Mein Konto, | ||||
My Profile,Mein Profil, | My Profile,Mein Profil, | ||||
My Settings,Meine Einstellungen, | My Settings,Meine Einstellungen, | ||||
@@ -175,7 +175,7 @@ Payment Gateway,Zahlungs-Gateways, | |||||
Payment Gateway Name,Name des Zahlungsgateways, | Payment Gateway Name,Name des Zahlungsgateways, | ||||
Payments,Zahlungen, | Payments,Zahlungen, | ||||
Period,Periode, | Period,Periode, | ||||
Pincode,Postleitzahl (PLZ), | |||||
Pincode,Postleitzahl, | |||||
Plan Name,Planname, | Plan Name,Planname, | ||||
Please enable pop-ups,Bitte Pop-ups aktivieren, | Please enable pop-ups,Bitte Pop-ups aktivieren, | ||||
Please select Company,Bitte Unternehmen auswählen, | Please select Company,Bitte Unternehmen auswählen, | ||||
@@ -1486,7 +1486,7 @@ Linked,Verknüpft, | |||||
Linked With,Verknüpft mit, | Linked With,Verknüpft mit, | ||||
Linked with {0},Verknüpft mit {0}, | Linked with {0},Verknüpft mit {0}, | ||||
Links,Verknüpfungen, | Links,Verknüpfungen, | ||||
List,Listenansicht, | |||||
List,Liste, | |||||
List Filter,Listenfilter, | List Filter,Listenfilter, | ||||
List View,Listenansicht, | List View,Listenansicht, | ||||
List View Setting,Einstellungen zu Listenansicht, | List View Setting,Einstellungen zu Listenansicht, | ||||
@@ -2427,7 +2427,7 @@ Sum,Summe, | |||||
Sum of {0},Summe von {0}, | Sum of {0},Summe von {0}, | ||||
Support Email Address Not Specified,Support-E-Mail-Adresse nicht angegeben, | Support Email Address Not Specified,Support-E-Mail-Adresse nicht angegeben, | ||||
Suspend Sending,Senden unterbrechen, | Suspend Sending,Senden unterbrechen, | ||||
Switch To Desk,Switch To Desk, | |||||
Switch To Desk,Zum Desk wechseln, | |||||
Symbol,Symbol, | Symbol,Symbol, | ||||
Sync,Synchronisieren, | Sync,Synchronisieren, | ||||
Sync on Migrate,Sync auf Migrate, | Sync on Migrate,Sync auf Migrate, | ||||
@@ -2870,8 +2870,8 @@ bullhorn,Megafon, | |||||
ca-central-1,ca-central-1, | ca-central-1,ca-central-1, | ||||
camera,Kamera, | camera,Kamera, | ||||
cancelled this document,brach die Arbeit an diesem Dokument ab, | cancelled this document,brach die Arbeit an diesem Dokument ab, | ||||
changed value of {0},Wert von {0} geändert, | |||||
changed values for {0},Werte von {0} geändert, | |||||
changed value of {0},hat den Wert von {0} geändert, | |||||
changed values for {0},hat die Werte von {0} geändert, | |||||
chevron-down,Winkel nach unten, | chevron-down,Winkel nach unten, | ||||
chevron-left,Winkel nach links, | chevron-left,Winkel nach links, | ||||
chevron-right,Winkel nach rechts, | chevron-right,Winkel nach rechts, | ||||
@@ -3431,7 +3431,7 @@ Mandatory Depends On,Obligatorisch Hängt von ab, | |||||
Map Columns,Spalten zuordnen, | Map Columns,Spalten zuordnen, | ||||
Map columns from {0} to fields in {1},Ordnen Sie Spalten von {0} Feldern in {1} zu., | Map columns from {0} to fields in {1},Ordnen Sie Spalten von {0} Feldern in {1} zu., | ||||
Mapping column {0} to field {1},Spalte {0} dem Feld {1} zuordnen, | Mapping column {0} to field {1},Spalte {0} dem Feld {1} zuordnen, | ||||
Mark all as Read,Markiere alle als gelesen, | |||||
Mark all as Read,Alle als gelesen markieren, | |||||
Maximum Points,Maximale Punkte, | Maximum Points,Maximale Punkte, | ||||
Maximum points allowed after multiplying points with the multiplier value\n(Note: For no limit leave this field empty or set 0),Maximal zulässige Punkte nach Multiplikation der Punkte mit dem Multiplikatorwert (Hinweis: Für unbegrenzte Anzahl lassen Sie dieses Feld leer oder setzen Sie 0), | Maximum points allowed after multiplying points with the multiplier value\n(Note: For no limit leave this field empty or set 0),Maximal zulässige Punkte nach Multiplikation der Punkte mit dem Multiplikatorwert (Hinweis: Für unbegrenzte Anzahl lassen Sie dieses Feld leer oder setzen Sie 0), | ||||
Me,Mir, | Me,Mir, | ||||
@@ -3485,7 +3485,7 @@ Page Shortcuts,Seitenkürzel, | |||||
Parent Field (Tree),Elternfeld (Baum), | Parent Field (Tree),Elternfeld (Baum), | ||||
Parent Field must be a valid fieldname,Das übergeordnete Feld muss ein gültiger Feldname sein, | Parent Field must be a valid fieldname,Das übergeordnete Feld muss ein gültiger Feldname sein, | ||||
Pin Globally,Global anheften, | Pin Globally,Global anheften, | ||||
Places,Setzt, | |||||
Places,Orte, | |||||
Please check the filter values set for Dashboard Chart: {},Bitte überprüfen Sie die für das Dashboard-Diagramm festgelegten Filterwerte: {}, | Please check the filter values set for Dashboard Chart: {},Bitte überprüfen Sie die für das Dashboard-Diagramm festgelegten Filterwerte: {}, | ||||
Please enable pop-ups in your browser,Bitte aktivieren Sie Popups in Ihrem Browser, | Please enable pop-ups in your browser,Bitte aktivieren Sie Popups in Ihrem Browser, | ||||
Please find attached {0}: {1},Im Anhang finden Sie {0}: {1}, | Please find attached {0}: {1},Im Anhang finden Sie {0}: {1}, | ||||
@@ -3541,7 +3541,7 @@ Select Filters,Wählen Sie Filter, | |||||
Select Google Calendar to which event should be synced.,"Wählen Sie Google Kalender aus, mit dem das Ereignis synchronisiert werden soll.", | Select Google Calendar to which event should be synced.,"Wählen Sie Google Kalender aus, mit dem das Ereignis synchronisiert werden soll.", | ||||
Select Google Contacts to which contact should be synced.,"Wählen Sie Google-Kontakte aus, mit denen der Kontakt synchronisiert werden soll.", | Select Google Contacts to which contact should be synced.,"Wählen Sie Google-Kontakte aus, mit denen der Kontakt synchronisiert werden soll.", | ||||
Select Group By...,Wählen Sie Gruppieren nach ..., | Select Group By...,Wählen Sie Gruppieren nach ..., | ||||
Select Mandatory,Wählen Pflicht, | |||||
Select Mandatory,Verpflichtende auswählen, | |||||
Select atleast 2 actions,Wählen Sie mindestens 2 Aktionen aus, | Select atleast 2 actions,Wählen Sie mindestens 2 Aktionen aus, | ||||
Select list item,Listenelement auswählen, | Select list item,Listenelement auswählen, | ||||
Select multiple list items,Wählen Sie mehrere Listenelemente aus, | Select multiple list items,Wählen Sie mehrere Listenelemente aus, | ||||
@@ -3664,8 +3664,8 @@ You need to install pycups to use this feature!,"Sie müssen Pycups installieren | |||||
Your Target,Dein Ziel, | Your Target,Dein Ziel, | ||||
"browse,","Durchsuche,", | "browse,","Durchsuche,", | ||||
cancelled this document {0},stornierte dieses Dokument {0}, | cancelled this document {0},stornierte dieses Dokument {0}, | ||||
changed value of {0} {1},geänderter Wert von {0} {1}, | |||||
changed values for {0} {1},geänderte Werte für {0} {1}, | |||||
changed value of {0} {1},hat den Wert von {0} {1} geändert, | |||||
changed values for {0} {1},hat die Werte von {0} {1} geändert, | |||||
choose an,wähle ein, | choose an,wähle ein, | ||||
empty,leeren, | empty,leeren, | ||||
of,von, | of,von, | ||||
@@ -3789,14 +3789,14 @@ Reset,Zurücksetzen, | |||||
Review,Rezension, | Review,Rezension, | ||||
Room,Zimmer, | Room,Zimmer, | ||||
Room Type,Zimmertyp, | Room Type,Zimmertyp, | ||||
Save,speichern, | |||||
Save,Speichern, | |||||
Search results for,Suchergebnisse für, | Search results for,Suchergebnisse für, | ||||
Select All,Alles auswählen, | Select All,Alles auswählen, | ||||
Send,Absenden, | Send,Absenden, | ||||
Sending,Versand, | Sending,Versand, | ||||
Server Error,Serverfehler, | Server Error,Serverfehler, | ||||
Set,Menge, | Set,Menge, | ||||
Setup,Einstellungen, | |||||
Setup,Einrichtung, | |||||
Setup Wizard,Setup-Assistent, | Setup Wizard,Setup-Assistent, | ||||
Size,Größe, | Size,Größe, | ||||
Sr,Pos, | Sr,Pos, | ||||
@@ -3819,7 +3819,7 @@ Warehouse,Lager, | |||||
Welcome to {0},Willkommen auf {0}, | Welcome to {0},Willkommen auf {0}, | ||||
Year,Jahr, | Year,Jahr, | ||||
Yearly,Jährlich, | Yearly,Jährlich, | ||||
You,Benutzer, | |||||
You,Sie, | |||||
You can also copy-paste this link in your browser,Sie können diese Verknüpfung in Ihren Browser kopieren, | You can also copy-paste this link in your browser,Sie können diese Verknüpfung in Ihren Browser kopieren, | ||||
and,und, | and,und, | ||||
{0} Name,{0} Name, | {0} Name,{0} Name, | ||||
@@ -3953,7 +3953,7 @@ lock,sperren, | |||||
logged in,Angemeldet, | logged in,Angemeldet, | ||||
message,Mitteilung, | message,Mitteilung, | ||||
module,Modul, | module,Modul, | ||||
move,Bewegung, | |||||
move,verschieben, | |||||
music,Musik, | music,Musik, | ||||
new,Neu, | new,Neu, | ||||
now,jetzt, | now,jetzt, | ||||
@@ -4135,9 +4135,9 @@ Using this console may allow attackers to impersonate you and steal your informa | |||||
yesterday,gestern, | yesterday,gestern, | ||||
{0} years ago,Vor {0} Jahren, | {0} years ago,Vor {0} Jahren, | ||||
New Chart,Neues Diagramm, | New Chart,Neues Diagramm, | ||||
New Shortcut,Neue Verknüpfung, | |||||
New Shortcut,Neuer Schnellzugriff, | |||||
Edit Chart,Diagramm bearbeiten, | Edit Chart,Diagramm bearbeiten, | ||||
Edit Shortcut,Verknüpfung bearbeiten, | |||||
Edit Shortcut,Schnellzugriff bearbeiten, | |||||
Couldn't Load Desk,Schreibtisch konnte nicht geladen werden, | Couldn't Load Desk,Schreibtisch konnte nicht geladen werden, | ||||
"Something went wrong while loading Desk. <b>Please relaod the page</b>. If the problem persists, contact the Administrator","Beim Laden von Desk ist ein Fehler aufgetreten. <b>Bitte überarbeiten Sie die Seite</b> . Wenn das Problem weiterhin besteht, wenden Sie sich an den Administrator", | "Something went wrong while loading Desk. <b>Please relaod the page</b>. If the problem persists, contact the Administrator","Beim Laden von Desk ist ein Fehler aufgetreten. <b>Bitte überarbeiten Sie die Seite</b> . Wenn das Problem weiterhin besteht, wenden Sie sich an den Administrator", | ||||
Customize Workspace,Arbeitsbereich anpassen, | Customize Workspace,Arbeitsbereich anpassen, | ||||
@@ -4228,7 +4228,7 @@ since last month,seit letztem Monat, | |||||
since last year,seit letztem Jahr, | since last year,seit letztem Jahr, | ||||
Show,Show, | Show,Show, | ||||
New Number Card,Neue Zahlenkarte, | New Number Card,Neue Zahlenkarte, | ||||
Your Shortcuts,Ihre Verknüpfungen, | |||||
Your Shortcuts,Ihre Schnellzugriffe, | |||||
You haven't added any Dashboard Charts or Number Cards yet.,Sie haben noch keine Dashboard-Diagramme oder Zahlenkarten hinzugefügt., | You haven't added any Dashboard Charts or Number Cards yet.,Sie haben noch keine Dashboard-Diagramme oder Zahlenkarten hinzugefügt., | ||||
Click On Customize to add your first widget,"Klicken Sie auf Anpassen, um Ihr erstes Widget hinzuzufügen", | Click On Customize to add your first widget,"Klicken Sie auf Anpassen, um Ihr erstes Widget hinzuzufügen", | ||||
Are you sure you want to reset all customizations?,Möchten Sie wirklich alle Anpassungen zurücksetzen?, | Are you sure you want to reset all customizations?,Möchten Sie wirklich alle Anpassungen zurücksetzen?, | ||||
@@ -4650,7 +4650,7 @@ Not permitted to view {0},{0} darf nicht angezeigt werden, | |||||
Camera,Kamera, | Camera,Kamera, | ||||
Invalid filter: {0},Ungültiger Filter: {0}, | Invalid filter: {0},Ungültiger Filter: {0}, | ||||
Let's Get Started,Lass uns anfangen, | Let's Get Started,Lass uns anfangen, | ||||
Reports & Masters,Berichte & Meister, | |||||
Reports & Masters,Berichte & Stammdaten, | |||||
New {0} {1} added to Dashboard {2},Neues {0} {1} zum Dashboard hinzugefügt {2}, | New {0} {1} added to Dashboard {2},Neues {0} {1} zum Dashboard hinzugefügt {2}, | ||||
New {0} {1} created,Neue {0} {1} erstellt, | New {0} {1} created,Neue {0} {1} erstellt, | ||||
New {0} Created,Neu {0} erstellt, | New {0} Created,Neu {0} erstellt, | ||||
@@ -4715,3 +4715,67 @@ Reset sorting,Sortierung zurücksetzen, | |||||
Sort Ascending,Aufsteigend sortieren, | Sort Ascending,Aufsteigend sortieren, | ||||
Sort Descending,Absteigend sortieren, | Sort Descending,Absteigend sortieren, | ||||
Remove column,Spalte entfernen, | Remove column,Spalte entfernen, | ||||
Set all public,Alle als öffentlich setzen, | |||||
Set all private,Alle als privat setzen, | |||||
Library,Bibliothek, | |||||
My Device,Mein Gerät, | |||||
Drag and drop files here or upload from,Ziehen Sie Dateien hierher oder laden Sie sie von, | |||||
days,Tage, | |||||
seconds,Sekunden, | |||||
minutes,Minuten, | |||||
Copy,Kopieren, | |||||
{} Assigned,{} Zugewiesen, | |||||
Hide Saved,Gespeicherte ausblenden, | |||||
Show Saved,Gespeicherte anzeigen, | |||||
{0} created this {1},{0} erstellte dies {1}, | |||||
{0} edited this {1},{0} bearbeitete dies {1}, | |||||
Toggle Full Width,Toggle Volle Breite, | |||||
Documentation,Dokumentation, | |||||
About,Über, | |||||
Search or type a command (Ctrl + G),Suchen oder Befehl eingeben (Strg + G), | |||||
{} Pending,{} Ausstehend, | |||||
{} Available,{} Verfügbar, | |||||
{} Open,{} Offen, | |||||
Password set,Passwort gesetzt, | |||||
Your new password has been set successfully.,Ihr Passwort wurde erfolgreich aktualisiert., | |||||
You hit the rate limit because of too many requests. Please try after sometime.,Sie haben die maximale Anzahl an Anfragen erreicht. Bitte versuchen Sie es später noch einmal., | |||||
"You need {0} permission to fetch values from {1} {2}","Sie benötigen eine {0}-Berechtigung, um die Werte von {1} {2} abzurufen", | |||||
Cannot Fetch Values,Werte können nicht abgerufen werden, | |||||
You do not have Read or Select Permissions for {},Sie haben keine Lese- oder Auswahlberechtigung für {}, | |||||
Or,Oder, | |||||
{0} changed values for {1},{0} hat die Werte von {1} geändert, | |||||
{0} changed values for {1} {2},{0} hat die Werte von {1} {2} geändert, | |||||
{0} cancelled this document,{0} dieses Dokument storniert, | |||||
{0} cancelled this document {1},{0} dieses Dokument storniert {1}, | |||||
{0} submitted this document,{0} hat dieses Dokument eingereicht, | |||||
{0} submitted this document {1},{0} hat das Dokument {1} eingereicht, | |||||
Customizations Discarded,Anpassungen verworfen, | |||||
No filters selected,Keine Filter ausgewählt, | |||||
You haven't created a {0} yet,Sie haben noch kein(en) {0} erstellt, | |||||
No Data...,Keine Daten..., | |||||
Don't have an account?,Sie haben noch kein Benutzerkonto?, | |||||
{0} changed value of {1},{0} hat den Wert von {1} geändert, | |||||
Basic Info,Grundlegende Informationen, | |||||
No.,Nr.,number | |||||
No.,Nein.,opposite of yes | |||||
There are no upcoming events for you.,Es sind keine Termine für Sie geplant., | |||||
No Upcoming Events,Keine bevorstehenden Termine, | |||||
"Looks like you haven’t received any notifications.","Sieht aus, als hätten Sie keine Benachrichtigungen erhalten.", | |||||
No New notifications,Keine neuen Benachrichtigungen, | |||||
Overview,Übersicht, | |||||
Connections,Verknüpfungen, | |||||
Save Customizations,Anpassungen speichern, | |||||
Apply Filters,Filter anwenden, | |||||
Add a Filter,Filter hinzufügen, | |||||
Reset Customizations,Anpassungen zurücksetzen, | |||||
{} wants to access the following details from your account,{} möchte Zugriff auf die folgenden Angaben von Ihrem Account, | |||||
{0} is not a field of doctype {1},{0} ist kein Feld in Doctype {1}, | |||||
{0} from {1} to {2} in row #{3},{0} von {1} zu/bis {2} in Zeile #{3}, | |||||
{0} from {1} to {2},{0} von {1} zu/bis {2}, | |||||
{0} changed {1} to {2},{0} wurde von {1} zu {2} geändert, | |||||
{0} Map,{0} Karte, | |||||
Use HTML,HTML verwenden, | |||||
Submit on Creation,Nach Erstellung buchen, | |||||
Show Absolute Values,Absolutwerte anzeigen, | |||||
Row #{0}: Could not find field {1} in {2} DocType,Zeile #{0}: Feld {1} existiert nicht in DocType {2}, | |||||
Repeat on Days,An Tagen wiederholen, |
@@ -57,7 +57,11 @@ def enqueue(method, queue='default', timeout=None, event=None, | |||||
# To handle older implementations | # To handle older implementations | ||||
is_async = kwargs.pop('async', is_async) | is_async = kwargs.pop('async', is_async) | ||||
if now or frappe.flags.in_migrate: | |||||
if not is_async and not frappe.flags.in_test: | |||||
print(_("Using enqueue with is_async=False outside of tests is not recommended, use now=True instead.")) | |||||
call_directly = now or frappe.flags.in_migrate or (not is_async and not frappe.flags.in_test) | |||||
if call_directly: | |||||
return frappe.call(method, **kwargs) | return frappe.call(method, **kwargs) | ||||
q = get_queue(queue, is_async=is_async) | q = get_queue(queue, is_async=is_async) | ||||
@@ -183,8 +183,6 @@ class BackupGenerator: | |||||
False, | False, | ||||
) | ) | ||||
self.todays_date = now_datetime().strftime("%Y%m%d_%H%M%S") | |||||
if not ( | if not ( | ||||
self.backup_path_conf | self.backup_path_conf | ||||
and self.backup_path_db | and self.backup_path_db | ||||
@@ -212,7 +210,7 @@ class BackupGenerator: | |||||
partial = "-partial" if self.partial else "" | partial = "-partial" if self.partial else "" | ||||
ext = "tgz" if self.compress_files else "tar" | ext = "tgz" if self.compress_files else "tar" | ||||
enc = "-enc" if frappe.get_system_settings("encrypt_backup") else "" | enc = "-enc" if frappe.get_system_settings("encrypt_backup") else "" | ||||
self.todays_date = now_datetime().strftime("%Y%m%d_%H%M%S") | |||||
for_conf = f"{self.todays_date}-{self.site_slug}-site_config_backup{enc}.json" | for_conf = f"{self.todays_date}-{self.site_slug}-site_config_backup{enc}.json" | ||||
for_db = f"{self.todays_date}-{self.site_slug}{partial}-database{enc}.sql.gz" | for_db = f"{self.todays_date}-{self.site_slug}{partial}-database{enc}.sql.gz" | ||||
@@ -1494,7 +1494,7 @@ def expand_relative_urls(html): | |||||
return html | return html | ||||
def quoted(url): | def quoted(url): | ||||
return cstr(quote(encode(url), safe=b"~@#$&()*!+=:;,.?/'")) | |||||
return cstr(quote(encode(cstr(url)), safe=b"~@#$&()*!+=:;,.?/'")) | |||||
def quote_urls(html): | def quote_urls(html): | ||||
def _quote_url(match): | def _quote_url(match): | ||||
@@ -1,14 +1,15 @@ | |||||
import json | import json | ||||
from difflib import unified_diff | from difflib import unified_diff | ||||
from typing import List | |||||
from typing import List, Union | |||||
import frappe | import frappe | ||||
from frappe.utils import pretty_date | from frappe.utils import pretty_date | ||||
from frappe.utils.data import cstr | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_version_diff( | def get_version_diff( | ||||
from_version: str, to_version: str, fieldname: str = "script" | |||||
from_version: Union[int, str], to_version: Union[int, str], fieldname: str = "script" | |||||
) -> List[str]: | ) -> List[str]: | ||||
before, before_timestamp = _get_value_from_version(from_version, fieldname) | before, before_timestamp = _get_value_from_version(from_version, fieldname) | ||||
@@ -23,15 +24,15 @@ def get_version_diff( | |||||
diff = unified_diff( | diff = unified_diff( | ||||
before, | before, | ||||
after, | after, | ||||
fromfile=from_version, | |||||
tofile=to_version, | |||||
fromfile=cstr(from_version), | |||||
tofile=cstr(to_version), | |||||
fromfiledate=before_timestamp, | fromfiledate=before_timestamp, | ||||
tofiledate=after_timestamp, | tofiledate=after_timestamp, | ||||
) | ) | ||||
return list(diff) | return list(diff) | ||||
def _get_value_from_version(version_name: str, fieldname: str): | |||||
def _get_value_from_version(version_name: Union[int, str], fieldname: str): | |||||
version = frappe.get_list( | version = frappe.get_list( | ||||
"Version", fields=["data", "modified"], filters={"name": version_name} | "Version", fields=["data", "modified"], filters={"name": version_name} | ||||
) | ) | ||||
@@ -9,6 +9,8 @@ import os | |||||
from frappe.utils import cint, strip_html_tags | from frappe.utils import cint, strip_html_tags | ||||
from frappe.utils.html_utils import unescape_html | from frappe.utils.html_utils import unescape_html | ||||
from frappe.model.base_document import get_controller | from frappe.model.base_document import get_controller | ||||
from frappe.utils.data import cstr | |||||
def setup_global_search_table(): | def setup_global_search_table(): | ||||
""" | """ | ||||
@@ -251,7 +253,7 @@ def update_global_search(doc): | |||||
if hasattr(doc, 'is_website_published') and doc.meta.allow_guest_to_view: | if hasattr(doc, 'is_website_published') and doc.meta.allow_guest_to_view: | ||||
published = 1 if doc.is_website_published() else 0 | published = 1 if doc.is_website_published() else 0 | ||||
title = (doc.get_title() or '')[:int(frappe.db.VARCHAR_LEN)] | |||||
title = (cstr(doc.get_title()) or '')[:int(frappe.db.VARCHAR_LEN)] | |||||
route = doc.get('route') if doc else '' | route = doc.get('route') if doc else '' | ||||
value = dict( | value = dict( | ||||
@@ -213,8 +213,7 @@ | |||||
"index_web_pages_for_search": 1, | "index_web_pages_for_search": 1, | ||||
"is_published_field": "published", | "is_published_field": "published", | ||||
"links": [], | "links": [], | ||||
"max_attachments": 5, | |||||
"modified": "2021-11-23 10:42:01.759723", | |||||
"modified": "2022-03-09 01:48:25.227295", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Website", | "module": "Website", | ||||
"name": "Blog Post", | "name": "Blog Post", | ||||
@@ -82,7 +82,8 @@ frappe.boot = { | |||||
time_zone: { | time_zone: { | ||||
system: "{{ frappe.utils.get_time_zone() }}", | system: "{{ frappe.utils.get_time_zone() }}", | ||||
user: "{{ frappe.db.get_value('User', frappe.session.user, 'time_zone') or frappe.utils.get_time_zone() }}" | user: "{{ frappe.db.get_value('User', frappe.session.user, 'time_zone') or frappe.utils.get_time_zone() }}" | ||||
} | |||||
}, | |||||
link_title_doctypes: `{{ frappe.call('frappe.boot.get_link_title_doctypes') }}` | |||||
}; | }; | ||||
// for backward compatibility of some libs | // for backward compatibility of some libs | ||||
frappe.sys_defaults = frappe.boot.sysdefaults; | frappe.sys_defaults = frappe.boot.sysdefaults; | ||||
@@ -598,13 +598,24 @@ def get_link_options(web_form_name, doctype, allow_read_on_all_link_options=Fals | |||||
break | break | ||||
if doctype_validated: | if doctype_validated: | ||||
link_options = [] | |||||
link_options, filters = [], {} | |||||
if limited_to_user: | if limited_to_user: | ||||
link_options = "\n".join([doc.name for doc in frappe.get_all(doctype, filters = {"owner":frappe.session.user})]) | |||||
else: | |||||
link_options = "\n".join([doc.name for doc in frappe.get_all(doctype)]) | |||||
filters = {"owner":frappe.session.user} | |||||
fields = ['name as value'] | |||||
return link_options | |||||
title_field = frappe.db.get_value('DocType', doctype, 'title_field', cache=1) | |||||
show_title_field_in_link = frappe.db.get_value('DocType', doctype, 'show_title_field_in_link', cache=1) == 1 | |||||
if title_field and show_title_field_in_link: | |||||
fields.append(f'{title_field} as label') | |||||
link_options = frappe.get_all(doctype, filters, fields) | |||||
if title_field and show_title_field_in_link: | |||||
return json.dumps(link_options, default=str) | |||||
else: | |||||
return "\n".join([doc.value for doc in link_options]) | |||||
else: | else: | ||||
raise frappe.PermissionError('Not Allowed, {0}'.format(doctype)) | raise frappe.PermissionError('Not Allowed, {0}'.format(doctype)) |
@@ -338,8 +338,7 @@ | |||||
"index_web_pages_for_search": 1, | "index_web_pages_for_search": 1, | ||||
"is_published_field": "published", | "is_published_field": "published", | ||||
"links": [], | "links": [], | ||||
"max_attachments": 20, | |||||
"modified": "2022-01-03 13:01:48.182645", | |||||
"modified": "2022-03-09 01:45:28.548671", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Website", | "module": "Website", | ||||
"name": "Web Page", | "name": "Web Page", | ||||
@@ -420,8 +420,7 @@ | |||||
"index_web_pages_for_search": 1, | "index_web_pages_for_search": 1, | ||||
"issingle": 1, | "issingle": 1, | ||||
"links": [], | "links": [], | ||||
"max_attachments": 10, | |||||
"modified": "2022-02-24 15:37:22.360138", | |||||
"modified": "2022-03-09 01:47:31.094462", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Website", | "module": "Website", | ||||
"name": "Website Settings", | "name": "Website Settings", | ||||
@@ -8,5 +8,5 @@ def get_context(context): | |||||
if frappe.flags.in_migrate: return | if frappe.flags.in_migrate: return | ||||
context.http_status_code = 500 | context.http_status_code = 500 | ||||
print(frappe.get_traceback().encode("utf-8")) | |||||
print(frappe.get_traceback()) | |||||
return {"error": frappe.get_traceback().replace("<", "<").replace(">", ">") } | return {"error": frappe.get_traceback().replace("<", "<").replace(">", ">") } |
@@ -24,7 +24,6 @@ | |||||
"@editorjs/editorjs": "2.20.0", | "@editorjs/editorjs": "2.20.0", | ||||
"ace-builds": "^1.4.8", | "ace-builds": "^1.4.8", | ||||
"air-datepicker": "github:frappe/air-datepicker", | "air-datepicker": "github:frappe/air-datepicker", | ||||
"autoprefixer": "^9.8.6", | |||||
"awesomplete": "^1.1.5", | "awesomplete": "^1.1.5", | ||||
"bootstrap": "4.5.0", | "bootstrap": "4.5.0", | ||||
"cliui": "^7.0.4", | "cliui": "^7.0.4", | ||||
@@ -66,14 +65,17 @@ | |||||
"vuedraggable": "^2.24.3" | "vuedraggable": "^2.24.3" | ||||
}, | }, | ||||
"devDependencies": { | "devDependencies": { | ||||
"@frappe/esbuild-plugin-postcss2": "^0.1.3", | |||||
"autoprefixer": "10", | |||||
"chalk": "^2.3.2", | "chalk": "^2.3.2", | ||||
"esbuild": "^0.11.21", | "esbuild": "^0.11.21", | ||||
"esbuild-plugin-postcss2": "^0.0.9", | |||||
"esbuild-vue": "^0.2.0", | "esbuild-vue": "^0.2.0", | ||||
"fast-glob": "^3.2.5", | "fast-glob": "^3.2.5", | ||||
"launch-editor": "^2.2.1", | "launch-editor": "^2.2.1", | ||||
"md5": "^2.3.0", | "md5": "^2.3.0", | ||||
"postcss": "8", | |||||
"rtlcss": "^3.2.1", | "rtlcss": "^3.2.1", | ||||
"sass": "^1.49.9", | |||||
"yargs": "^16.2.0" | "yargs": "^16.2.0" | ||||
}, | }, | ||||
"snyk": true, | "snyk": true, | ||||
@@ -29,6 +29,7 @@ maxminddb-geolite2==2018.703 | |||||
num2words~=0.5.10 | num2words~=0.5.10 | ||||
oauthlib~=3.1.0 | oauthlib~=3.1.0 | ||||
openpyxl~=3.0.7 | openpyxl~=3.0.7 | ||||
parse~=1.19.0 | |||||
passlib~=1.7.4 | passlib~=1.7.4 | ||||
paytmchecksum~=1.7.0 | paytmchecksum~=1.7.0 | ||||
pdfkit~=0.6.1 | pdfkit~=0.6.1 | ||||
@@ -36,6 +36,20 @@ | |||||
codex-notifier "^1.1.2" | codex-notifier "^1.1.2" | ||||
codex-tooltip "^1.0.1" | codex-tooltip "^1.0.1" | ||||
"@frappe/esbuild-plugin-postcss2@^0.1.3": | |||||
version "0.1.3" | |||||
resolved "https://registry.yarnpkg.com/@frappe/esbuild-plugin-postcss2/-/esbuild-plugin-postcss2-0.1.3.tgz#523a5cc32788f184bb78c7b946c9f132ef386508" | |||||
integrity sha512-/kPz/NJki2GFFtcgTnvdkkjgPEU1uHmaN7/OI2Ysc2tEZ7dcL7FYEEV72a5Fv8cniJbmH8UUjItZmHixFCT1Dg== | |||||
dependencies: | |||||
autoprefixer "^10.2.5" | |||||
fs-extra "^9.1.0" | |||||
less "^4.x" | |||||
postcss-modules "^4.0.0" | |||||
resolve-file "^0.3.0" | |||||
sass "^1.x" | |||||
stylus "^0.x" | |||||
tmp "^0.2.1" | |||||
"@nodelib/fs.scandir@2.1.4": | "@nodelib/fs.scandir@2.1.4": | ||||
version "2.1.4" | version "2.1.4" | ||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69" | resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69" | ||||
@@ -344,6 +358,18 @@ atob@^2.1.2: | |||||
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" | resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" | ||||
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== | integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== | ||||
autoprefixer@10: | |||||
version "10.4.2" | |||||
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.2.tgz#25e1df09a31a9fba5c40b578936b90d35c9d4d3b" | |||||
integrity sha512-9fOPpHKuDW1w/0EKfRmVnxTDt8166MAnLI3mgZ1JCnhNtYWxcJ6Ud5CO/AVOZi/AvFa8DY9RTy3h3+tFBlrrdQ== | |||||
dependencies: | |||||
browserslist "^4.19.1" | |||||
caniuse-lite "^1.0.30001297" | |||||
fraction.js "^4.1.2" | |||||
normalize-range "^0.1.2" | |||||
picocolors "^1.0.0" | |||||
postcss-value-parser "^4.2.0" | |||||
autoprefixer@^10.2.5: | autoprefixer@^10.2.5: | ||||
version "10.2.5" | version "10.2.5" | ||||
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.2.5.tgz#096a0337dbc96c0873526d7fef5de4428d05382d" | resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.2.5.tgz#096a0337dbc96c0873526d7fef5de4428d05382d" | ||||
@@ -356,19 +382,6 @@ autoprefixer@^10.2.5: | |||||
normalize-range "^0.1.2" | normalize-range "^0.1.2" | ||||
postcss-value-parser "^4.1.0" | postcss-value-parser "^4.1.0" | ||||
autoprefixer@^9.8.6: | |||||
version "9.8.6" | |||||
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" | |||||
integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg== | |||||
dependencies: | |||||
browserslist "^4.12.0" | |||||
caniuse-lite "^1.0.30001109" | |||||
colorette "^1.2.1" | |||||
normalize-range "^0.1.2" | |||||
num2fraction "^1.2.2" | |||||
postcss "^7.0.32" | |||||
postcss-value-parser "^4.1.0" | |||||
available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2: | available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2: | ||||
version "1.0.2" | version "1.0.2" | ||||
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5" | resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5" | ||||
@@ -512,7 +525,7 @@ braces@^3.0.1, braces@~3.0.2: | |||||
dependencies: | dependencies: | ||||
fill-range "^7.0.1" | fill-range "^7.0.1" | ||||
browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.16.0, browserslist@^4.16.3: | |||||
browserslist@^4.0.0, browserslist@^4.16.0, browserslist@^4.16.3: | |||||
version "4.16.6" | version "4.16.6" | ||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2" | resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2" | ||||
integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ== | integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ== | ||||
@@ -523,6 +536,17 @@ browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.16.0, browserslist@^4 | |||||
escalade "^3.1.1" | escalade "^3.1.1" | ||||
node-releases "^1.1.71" | node-releases "^1.1.71" | ||||
browserslist@^4.19.1: | |||||
version "4.20.0" | |||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.0.tgz#35951e3541078c125d36df76056e94738a52ebe9" | |||||
integrity sha512-bnpOoa+DownbciXj0jVGENf8VYQnE2LNWomhYuCsMmmx9Jd9lwq0WXODuwpSsp8AVdKM2/HorrzxAfbKvWTByQ== | |||||
dependencies: | |||||
caniuse-lite "^1.0.30001313" | |||||
electron-to-chromium "^1.4.76" | |||||
escalade "^3.1.1" | |||||
node-releases "^2.0.2" | |||||
picocolors "^1.0.0" | |||||
bytes@3.1.0: | bytes@3.1.0: | ||||
version "3.1.0" | version "3.1.0" | ||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" | ||||
@@ -570,11 +594,16 @@ caniuse-api@^3.0.0: | |||||
lodash.memoize "^4.1.2" | lodash.memoize "^4.1.2" | ||||
lodash.uniq "^4.5.0" | lodash.uniq "^4.5.0" | ||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001196, caniuse-lite@^1.0.30001219: | |||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001196, caniuse-lite@^1.0.30001219: | |||||
version "1.0.30001296" | version "1.0.30001296" | ||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001296.tgz" | resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001296.tgz" | ||||
integrity sha512-WfrtPEoNSoeATDlf4y3QvkwiELl9GyPLISV5GejTbbQRtQx4LhsXmc9IQ6XCL2d7UxCyEzToEZNMeqR79OUw8Q== | integrity sha512-WfrtPEoNSoeATDlf4y3QvkwiELl9GyPLISV5GejTbbQRtQx4LhsXmc9IQ6XCL2d7UxCyEzToEZNMeqR79OUw8Q== | ||||
caniuse-lite@^1.0.30001297, caniuse-lite@^1.0.30001313: | |||||
version "1.0.30001316" | |||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001316.tgz#b44a1f419f82d2e119aa0bbdab5ec15471796358" | |||||
integrity sha512-JgUdNoZKxPZFzbzJwy4hDSyGuH/gXz2rN51QmoR8cBQsVo58llD3A0vlRKKRt8FGf5u69P9eQyIH8/z9vN/S0Q== | |||||
caseless@~0.12.0: | caseless@~0.12.0: | ||||
version "0.12.0" | version "0.12.0" | ||||
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" | resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" | ||||
@@ -736,7 +765,7 @@ colord@^2.0.0: | |||||
resolved "https://registry.yarnpkg.com/colord/-/colord-2.0.0.tgz#f8c19f2526b7dc5b22d6e57ef102f03a2a43a3d8" | resolved "https://registry.yarnpkg.com/colord/-/colord-2.0.0.tgz#f8c19f2526b7dc5b22d6e57ef102f03a2a43a3d8" | ||||
integrity sha512-WMDFJfoY3wqPZNpKUFdse3HhD5BHCbE9JCdxRzoVH+ywRITGOeWAHNkGEmyxLlErEpN9OLMWgdM9dWQtDk5dog== | integrity sha512-WMDFJfoY3wqPZNpKUFdse3HhD5BHCbE9JCdxRzoVH+ywRITGOeWAHNkGEmyxLlErEpN9OLMWgdM9dWQtDk5dog== | ||||
colorette@^1.2.1, colorette@^1.2.2: | |||||
colorette@^1.2.2: | |||||
version "1.2.2" | version "1.2.2" | ||||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" | resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" | ||||
integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== | integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== | ||||
@@ -1220,6 +1249,11 @@ electron-to-chromium@^1.3.723: | |||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.736.tgz#f632d900a1f788dab22fec9c62ec5c9c8f0c4052" | resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.736.tgz#f632d900a1f788dab22fec9c62ec5c9c8f0c4052" | ||||
integrity sha512-DY8dA7gR51MSo66DqitEQoUMQ0Z+A2DSXFi7tK304bdTVqczCAfUuyQw6Wdg8hIoo5zIxkU1L24RQtUce1Ioig== | integrity sha512-DY8dA7gR51MSo66DqitEQoUMQ0Z+A2DSXFi7tK304bdTVqczCAfUuyQw6Wdg8hIoo5zIxkU1L24RQtUce1Ioig== | ||||
electron-to-chromium@^1.4.76: | |||||
version "1.4.84" | |||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.84.tgz#2700befbcb49c42c4ee162e137ff392c07658249" | |||||
integrity sha512-b+DdcyOiZtLXHdgEG8lncYJdxbdJWJvclPNMg0eLUDcSOSO876WA/pYjdSblUTd7eJdIs4YdIxHWGazx7UPSJw== | |||||
emoji-regex@^7.0.1: | emoji-regex@^7.0.1: | ||||
version "7.0.3" | version "7.0.3" | ||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" | ||||
@@ -1348,21 +1382,6 @@ es-to-primitive@^1.2.1: | |||||
is-date-object "^1.0.1" | is-date-object "^1.0.1" | ||||
is-symbol "^1.0.2" | is-symbol "^1.0.2" | ||||
esbuild-plugin-postcss2@^0.0.9: | |||||
version "0.0.9" | |||||
resolved "https://registry.yarnpkg.com/esbuild-plugin-postcss2/-/esbuild-plugin-postcss2-0.0.9.tgz#b889af46f703990988d47885632108901948673e" | |||||
integrity sha512-iDKxWohm9aD2s+++ihb6GJVcddebsxOaC+Oz8TV0xJnKy0yHz/xazX96HyP45cS6+SFvZwr+SzG+QHbMOuXfMg== | |||||
dependencies: | |||||
autoprefixer "^10.2.5" | |||||
fs-extra "^9.1.0" | |||||
less "^4.x" | |||||
postcss "8.x" | |||||
postcss-modules "^4.0.0" | |||||
resolve-file "^0.3.0" | |||||
sass "^1.x" | |||||
stylus "^0.x" | |||||
tmp "^0.2.1" | |||||
esbuild-vue@^0.2.0: | esbuild-vue@^0.2.0: | ||||
version "0.2.0" | version "0.2.0" | ||||
resolved "https://registry.yarnpkg.com/esbuild-vue/-/esbuild-vue-0.2.0.tgz#8a3fde404bda57fe32b80e24917d14036e242bd3" | resolved "https://registry.yarnpkg.com/esbuild-vue/-/esbuild-vue-0.2.0.tgz#8a3fde404bda57fe32b80e24917d14036e242bd3" | ||||
@@ -1630,6 +1649,11 @@ fraction.js@^4.0.13: | |||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.0.13.tgz#3c1c315fa16b35c85fffa95725a36fa729c69dfe" | resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.0.13.tgz#3c1c315fa16b35c85fffa95725a36fa729c69dfe" | ||||
integrity sha512-E1fz2Xs9ltlUp+qbiyx9wmt2n9dRzPsS11Jtdb8D2o+cC7wr9xkkKsVKJuBX0ST+LVS+LhLO+SbLJNtfWcJvXA== | integrity sha512-E1fz2Xs9ltlUp+qbiyx9wmt2n9dRzPsS11Jtdb8D2o+cC7wr9xkkKsVKJuBX0ST+LVS+LhLO+SbLJNtfWcJvXA== | ||||
fraction.js@^4.1.2: | |||||
version "4.2.0" | |||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" | |||||
integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== | |||||
frappe-charts@^2.0.0-rc13: | frappe-charts@^2.0.0-rc13: | ||||
version "2.0.0-rc13" | version "2.0.0-rc13" | ||||
resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-2.0.0-rc13.tgz#fdb251d7ae311c41e38f90a3ae108070ec6b9072" | resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-2.0.0-rc13.tgz#fdb251d7ae311c41e38f90a3ae108070ec6b9072" | ||||
@@ -2082,6 +2106,11 @@ immediate@~3.0.5: | |||||
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" | resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" | ||||
integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= | integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= | ||||
immutable@^4.0.0: | |||||
version "4.0.0" | |||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23" | |||||
integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw== | |||||
import-fresh@^3.2.1: | import-fresh@^3.2.1: | ||||
version "3.3.0" | version "3.3.0" | ||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" | resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" | ||||
@@ -2873,11 +2902,16 @@ nan@^2.13.2: | |||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" | resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" | ||||
integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== | integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== | ||||
nanoid@^3.1.22, nanoid@^3.1.23: | |||||
nanoid@^3.1.23: | |||||
version "3.2.0" | version "3.2.0" | ||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" | ||||
integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== | integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== | ||||
nanoid@^3.3.1: | |||||
version "3.3.1" | |||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" | |||||
integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== | |||||
native-request@^1.0.5: | native-request@^1.0.5: | ||||
version "1.0.8" | version "1.0.8" | ||||
resolved "https://registry.yarnpkg.com/native-request/-/native-request-1.0.8.tgz#8f66bf606e0f7ea27c0e5995eb2f5d03e33ae6fb" | resolved "https://registry.yarnpkg.com/native-request/-/native-request-1.0.8.tgz#8f66bf606e0f7ea27c0e5995eb2f5d03e33ae6fb" | ||||
@@ -2962,6 +2996,11 @@ node-releases@^1.1.71: | |||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.72.tgz#14802ab6b1039a79a0c7d662b610a5bbd76eacbe" | resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.72.tgz#14802ab6b1039a79a0c7d662b610a5bbd76eacbe" | ||||
integrity sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw== | integrity sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw== | ||||
node-releases@^2.0.2: | |||||
version "2.0.2" | |||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.2.tgz#7139fe71e2f4f11b47d4d2986aaf8c48699e0c01" | |||||
integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg== | |||||
node-sass@^7.0.0: | node-sass@^7.0.0: | ||||
version "7.0.0" | version "7.0.0" | ||||
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-7.0.0.tgz#33ee7c2df299d51f682f13d79f3d2a562225788e" | resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-7.0.0.tgz#33ee7c2df299d51f682f13d79f3d2a562225788e" | ||||
@@ -3059,11 +3098,6 @@ nth-check@^2.0.0: | |||||
dependencies: | dependencies: | ||||
boolbase "^1.0.0" | boolbase "^1.0.0" | ||||
num2fraction@^1.2.2: | |||||
version "1.2.2" | |||||
resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" | |||||
integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= | |||||
number-is-nan@^1.0.0: | number-is-nan@^1.0.0: | ||||
version "1.0.1" | version "1.0.1" | ||||
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" | resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" | ||||
@@ -3280,6 +3314,11 @@ performance-now@^2.1.0: | |||||
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" | resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" | ||||
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= | integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= | ||||
picocolors@^1.0.0: | |||||
version "1.0.0" | |||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" | |||||
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== | |||||
picomatch@^2.0.4: | picomatch@^2.0.4: | ||||
version "2.2.3" | version "2.2.3" | ||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.3.tgz#465547f359ccc206d3c48e46a1bcb89bf7ee619d" | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.3.tgz#465547f359ccc206d3c48e46a1bcb89bf7ee619d" | ||||
@@ -3627,14 +3666,19 @@ postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: | |||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" | resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" | ||||
integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== | integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== | ||||
postcss@8.x: | |||||
version "8.2.10" | |||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.10.tgz#ca7a042aa8aff494b334d0ff3e9e77079f6f702b" | |||||
integrity sha512-b/h7CPV7QEdrqIxtAf2j31U5ef05uBDuvoXv6L51Q4rcS1jdlXAVKJv+atCFdUXYl9dyTHGyoMzIepwowRJjFw== | |||||
postcss-value-parser@^4.2.0: | |||||
version "4.2.0" | |||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" | |||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== | |||||
postcss@8: | |||||
version "8.4.8" | |||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.8.tgz#dad963a76e82c081a0657d3a2f3602ce10c2e032" | |||||
integrity sha512-2tXEqGxrjvAO6U+CJzDL2Fk2kPHTv1jQsYkSoMeOis2SsYaXRO2COxTdQp99cYvif9JTXaAk9lYGc3VhJt7JPQ== | |||||
dependencies: | dependencies: | ||||
colorette "^1.2.2" | |||||
nanoid "^3.1.22" | |||||
source-map "^0.6.1" | |||||
nanoid "^3.3.1" | |||||
picocolors "^1.0.0" | |||||
source-map-js "^1.0.2" | |||||
postcss@^5.2.5: | postcss@^5.2.5: | ||||
version "5.2.18" | version "5.2.18" | ||||
@@ -3664,15 +3708,6 @@ postcss@^7.0.14: | |||||
source-map "^0.6.1" | source-map "^0.6.1" | ||||
supports-color "^6.1.0" | supports-color "^6.1.0" | ||||
postcss@^7.0.32: | |||||
version "7.0.32" | |||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d" | |||||
integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw== | |||||
dependencies: | |||||
chalk "^2.4.2" | |||||
source-map "^0.6.1" | |||||
supports-color "^6.1.0" | |||||
postcss@^8.2.4: | postcss@^8.2.4: | ||||
version "8.3.5" | version "8.3.5" | ||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.5.tgz#982216b113412bc20a86289e91eb994952a5b709" | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.5.tgz#982216b113412bc20a86289e91eb994952a5b709" | ||||
@@ -4242,6 +4277,15 @@ sass@^1.18.0, sass@^1.x: | |||||
dependencies: | dependencies: | ||||
chokidar ">=3.0.0 <4.0.0" | chokidar ">=3.0.0 <4.0.0" | ||||
sass@^1.49.9: | |||||
version "1.49.9" | |||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.49.9.tgz#b15a189ecb0ca9e24634bae5d1ebc191809712f9" | |||||
integrity sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A== | |||||
dependencies: | |||||
chokidar ">=3.0.0 <4.0.0" | |||||
immutable "^4.0.0" | |||||
source-map-js ">=0.6.2 <2.0.0" | |||||
sax@^1.2.4, sax@~1.2.4: | sax@^1.2.4, sax@~1.2.4: | ||||
version "1.2.4" | version "1.2.4" | ||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" | resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" | ||||
@@ -4422,6 +4466,11 @@ sortablejs@^1.7.0: | |||||
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.8.3.tgz#5ae908ef96300966e95440a143340f5dd565a0df" | resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.8.3.tgz#5ae908ef96300966e95440a143340f5dd565a0df" | ||||
integrity sha512-AftvD4hdKcR5QlGi7L/JST506zGNGrysE8/QohDpwKXJarHWqCt+TUlrtoMk/wkECB607Q019/OZlJViyWiD6A== | integrity sha512-AftvD4hdKcR5QlGi7L/JST506zGNGrysE8/QohDpwKXJarHWqCt+TUlrtoMk/wkECB607Q019/OZlJViyWiD6A== | ||||
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: | |||||
version "1.0.2" | |||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" | |||||
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== | |||||
source-map-js@^0.6.2: | source-map-js@^0.6.2: | ||||
version "0.6.2" | version "0.6.2" | ||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" | resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" | ||||