@@ -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, | |||
'docstatus': 1 // submit document | |||
}, 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.', () => { | |||
@@ -43,32 +40,4 @@ context('Report View', () => { | |||
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 html_plugin = require("./frappe-html"); | |||
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 sass_options = require("./sass_options"); | |||
const build_cleanup_plugin = require("./build-cleanup"); | |||
@@ -20,7 +20,8 @@ module.exports = { | |||
.then(content => { | |||
content = scrub_html_template(content); | |||
return { | |||
contents: `\n\tfrappe.templates['${filename}'] = \`${content}\`;\n` | |||
contents: `\n\tfrappe.templates['${filename}'] = \`${content}\`;\n`, | |||
watchFiles: [filepath] | |||
}; | |||
}) | |||
.catch(() => { | |||
@@ -35,6 +35,7 @@ from frappe.query_builder import ( | |||
patch_query_execute, | |||
patch_query_aggregation, | |||
) | |||
from frappe.utils.data import cstr | |||
__version__ = '14.0.0-dev' | |||
@@ -214,6 +215,7 @@ def init(site, sites_path=None, new_site=False): | |||
local.cache = {} | |||
local.document_cache = {} | |||
local.meta_cache = {} | |||
local.autoincremented_status_map = {site: -1} | |||
local.form_dict = _dict() | |||
local.session = _dict() | |||
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) | |||
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 | |||
doc = local.document_cache.get(key) | |||
if doc: | |||
@@ -869,8 +870,24 @@ def get_cached_doc(*args, **kwargs): | |||
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): | |||
return '{0}::{1}'.format(doctype, name) | |||
return f'{doctype}::{name}' | |||
def clear_document_cache(doctype, name): | |||
cache().hdel("last_modified", doctype) | |||
@@ -911,8 +928,7 @@ def get_doc(*args, **kwargs): | |||
doc = frappe.model.document.get_doc(*args, **kwargs) | |||
# 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 | |||
cache().hset('document_cache', key, doc.as_dict()) | |||
@@ -1001,7 +1017,7 @@ def get_module(modulename): | |||
def scrub(txt): | |||
"""Returns sluggified string. e.g. `Sales Order` becomes `sales_order`.""" | |||
return txt.replace(' ', '_').replace('-', '_').lower() | |||
return cstr(txt).replace(' ', '_').replace('-', '_').lower() | |||
def unscrub(txt): | |||
"""Returns titlified string. e.g. `sales_order` becomes `Sales Order`.""" | |||
@@ -325,6 +325,7 @@ def get_desk_settings(): | |||
def get_notification_settings(): | |||
return frappe.get_cached_doc('Notification Settings', frappe.session.user) | |||
@frappe.whitelist() | |||
def get_link_title_doctypes(): | |||
dts = frappe.get_all("DocType", {"show_title_field_in_link": 1}) | |||
custom_dts = frappe.get_all( | |||
@@ -1,7 +1,7 @@ | |||
# imports - standard imports | |||
import os | |||
import sys | |||
import shutil | |||
import sys | |||
# imports - third party imports | |||
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" | |||
from frappe.installer import ( | |||
_new_site, | |||
extract_sql_from_archive, | |||
extract_files, | |||
extract_sql_from_archive, | |||
is_downgrade, | |||
is_partial, | |||
validate_database_sql | |||
validate_database_sql, | |||
) | |||
from frappe.utils.backups import Backup | |||
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') | |||
@pass_context | |||
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 | |||
if not os.path.exists(sql_file_path): | |||
@@ -545,7 +545,7 @@ def _use(site, sites_path='.'): | |||
def use(site, sites_path='.'): | |||
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) | |||
print("Current Site set to {}".format(site)) | |||
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): | |||
import getpass | |||
from frappe.utils.password import update_password | |||
try: | |||
@@ -881,15 +882,16 @@ def stop_recording(context): | |||
raise SiteNotSpecifiedError | |||
@click.command('ngrok') | |||
@click.option('--bind-tls', is_flag=True, default=False, help='Returns a reference to the https tunnel.') | |||
@pass_context | |||
def start_ngrok(context): | |||
def start_ngrok(context, bind_tls): | |||
from pyngrok import ngrok | |||
site = get_site(context) | |||
frappe.init(site=site) | |||
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('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.contacts.doctype.contact.contact import get_contact_name | |||
from frappe.automation.doctype.assignment_rule.assignment_rule import apply as apply_assignment_rule | |||
from parse import compile | |||
exclude_from_linked_with = True | |||
@@ -114,6 +115,44 @@ class Communication(Document, CommunicationEmailMixin): | |||
frappe.publish_realtime('new_message', self.as_dict(), | |||
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): | |||
# add to _comment property of the doctype, so it shows up in | |||
# comments count for the list view | |||
@@ -22,12 +22,30 @@ OUTGOING_EMAIL_ACCOUNT_MISSING = _(""" | |||
@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 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 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 | |||
cc = list_to_str(cc) if isinstance(cc, list) else cc | |||
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, | |||
"has_attachment": 1 if attachments else 0, | |||
"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 attachments: | |||
@@ -87,17 +161,21 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = | |||
if cint(send_email): | |||
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) | |||
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: | |||
"""Validate Email Addresses of Recipients and CC""" | |||
@@ -324,7 +324,7 @@ class DataExporter: | |||
d = doc.copy() | |||
meta = frappe.get_meta(dt) | |||
if self.all_doctypes: | |||
d.name = '"'+ d.name+'"' | |||
d.name = f'"{d.name}"' | |||
if len(rows) < rowidx + 1: | |||
rows.append([""] * (len(self.columns) + 1)) | |||
@@ -61,6 +61,13 @@ frappe.ui.form.on('DocType', { | |||
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) { | |||
// set the "autoname" property based on naming_rule | |||
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') { | |||
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') { | |||
frm.set_value('autoname', '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) { | |||
let naming_rule_description = { | |||
'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 "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.', | |||
@@ -111,6 +123,8 @@ frappe.ui.form.on('DocType', { | |||
frm.__from_autoname = true; | |||
if (frm.doc.autoname.toLowerCase() === 'prompt') { | |||
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:')) { | |||
frm.set_value('naming_rule', 'By fieldname'); | |||
} else if (frm.doc.autoname.startsWith('naming_series:')) { | |||
@@ -208,7 +208,7 @@ | |||
"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", | |||
"fieldtype": "Data", | |||
"label": "Auto Name", | |||
@@ -216,6 +216,7 @@ | |||
"oldfieldtype": "Data" | |||
}, | |||
{ | |||
"depends_on": "eval:doc.naming_rule !== \"Autoincrement\"", | |||
"fieldname": "name_case", | |||
"fieldtype": "Select", | |||
"label": "Name Case", | |||
@@ -282,6 +283,7 @@ | |||
}, | |||
{ | |||
"default": "1", | |||
"depends_on": "eval:doc.naming_rule !== \"Autoincrement\"", | |||
"fieldname": "allow_rename", | |||
"fieldtype": "Check", | |||
"label": "Allow Rename", | |||
@@ -565,7 +567,7 @@ | |||
"fieldtype": "Select", | |||
"label": "Naming Rule", | |||
"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", | |||
@@ -593,6 +595,7 @@ | |||
], | |||
"icon": "fa fa-bolt", | |||
"idx": 6, | |||
"index_web_pages_for_search": 1, | |||
"links": [ | |||
{ | |||
"group": "Views", | |||
@@ -670,10 +673,11 @@ | |||
"link_fieldname": "reference_doctype" | |||
} | |||
], | |||
"modified": "2022-01-07 16:07:06.196534", | |||
"modified": "2022-02-15 21:47:16.467217", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "DocType", | |||
"naming_rule": "Set by user", | |||
"owner": "Administrator", | |||
"permissions": [ | |||
{ | |||
@@ -703,5 +707,6 @@ | |||
"show_name_in_global_search": 1, | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"states": [], | |||
"track_changes": 1 | |||
} |
@@ -60,6 +60,7 @@ class DocType(Document): | |||
self.check_developer_mode() | |||
self.validate_autoname() | |||
self.validate_name() | |||
self.set_defaults_for_single_and_table() | |||
@@ -714,6 +715,18 @@ class DocType(Document): | |||
self.name) | |||
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): | |||
if not 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) | |||
# 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) | |||
@@ -24,7 +24,7 @@ class TestDocType(unittest.TestCase): | |||
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 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): | |||
frappe.delete_doc("DocType", name) | |||
@@ -505,7 +505,23 @@ class TestDocType(unittest.TestCase): | |||
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({ | |||
"doctype": "DocType", | |||
"module": "Core", | |||
@@ -521,7 +537,8 @@ def new_doctype(name, unique=0, depends_on='', fields=None): | |||
"role": "System Manager", | |||
"read": 1, | |||
}], | |||
"name": name | |||
"name": name, | |||
"autoname": "autoincrement" if autoincremented else "" | |||
}) | |||
if fields: | |||
@@ -61,7 +61,7 @@ class Role(Document): | |||
def get_info_based_on_role(role, field='email'): | |||
''' 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"]) | |||
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') | |||
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)) | |||
def test_attribute_error(self): | |||
@@ -668,8 +668,7 @@ | |||
"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", | |||
"module": "Core", | |||
"name": "User", | |||
@@ -142,8 +142,6 @@ class Database(object): | |||
self.log_query(query, values, debug, explain) | |||
if values!=(): | |||
if isinstance(values, dict): | |||
values = dict(values) | |||
# MySQL-python==1.2.5 hack! | |||
if not isinstance(values, (dict, tuple, list)): | |||
@@ -181,7 +179,7 @@ class Database(object): | |||
print(e) | |||
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 | |||
else: | |||
raise | |||
@@ -1028,7 +1026,7 @@ class Database(object): | |||
return [] | |||
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): | |||
current_dialect = frappe.db.db_type or 'mariadb' | |||
@@ -154,6 +154,10 @@ class MariaDBDatabase(Database): | |||
def is_table_missing(e): | |||
return e.args[0] == ER.NO_SUCH_TABLE | |||
@staticmethod | |||
def is_missing_table(e): | |||
return MariaDBDatabase.is_table_missing(e) | |||
@staticmethod | |||
def is_missing_column(e): | |||
return e.args[0] == ER.BAD_FIELD_ERROR | |||
@@ -1,12 +1,16 @@ | |||
import frappe | |||
from frappe import _ | |||
from frappe.database.schema import DBTable | |||
from frappe.database.sequence import create_sequence | |||
from frappe.model import log_types | |||
class MariaDBTable(DBTable): | |||
def create(self): | |||
additional_definitions = "" | |||
engine = self.meta.get("engine") or "InnoDB" | |||
varchar_len = frappe.db.VARCHAR_LEN | |||
name_column = f"name varchar({varchar_len}) primary key" | |||
# columns | |||
column_defs = self.get_column_definitions() | |||
@@ -29,9 +33,27 @@ class MariaDBTable(DBTable): | |||
) | |||
) + ',\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 | |||
query = f"""create table `{self.table_name}` ( | |||
name varchar({varchar_len}) not null primary key, | |||
{name_column}, | |||
creation datetime(6), | |||
modified datetime(6), | |||
modified_by varchar({varchar_len}), | |||
@@ -99,16 +99,13 @@ class PostgresDatabase(Database): | |||
return db_size[0].get('database_size') | |||
# 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): | |||
return [d[0] for d in self.sql("""select table_name | |||
@@ -153,6 +150,10 @@ class PostgresDatabase(Database): | |||
def is_table_missing(e): | |||
return getattr(e, 'pgcode', None) == '42P01' | |||
@staticmethod | |||
def is_missing_table(e): | |||
return PostgresDatabase.is_table_missing(e) | |||
@staticmethod | |||
def is_missing_column(e): | |||
return getattr(e, 'pgcode', None) == '42703' | |||
@@ -335,12 +336,47 @@ def modify_query(query): | |||
query = replace_locate_with_strpos(query) | |||
# select from requires "" | |||
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 | |||
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): | |||
# strpos is the locate equivalent in postgres | |||
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 |
@@ -2,10 +2,14 @@ import frappe | |||
from frappe import _ | |||
from frappe.utils import cint, flt | |||
from frappe.database.schema import DBTable, get_definition | |||
from frappe.database.sequence import create_sequence | |||
from frappe.model import log_types | |||
class PostgresTable(DBTable): | |||
def create(self): | |||
varchar_len = frappe.db.VARCHAR_LEN | |||
name_column = f"name varchar({varchar_len}) primary key" | |||
additional_definitions = "" | |||
# 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 | |||
frappe.db.sql(f"""create table `{self.table_name}` ( | |||
name varchar({varchar_len}) not null primary key, | |||
{name_column}, | |||
creation timestamp(6), | |||
modified timestamp(6), | |||
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('create_add_to_dashboard_button'); | |||
frm.trigger('set_parent_document_type'); | |||
}, | |||
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('dynamic_filters_json', '[]'); | |||
frm.set_value('aggregate_function_based_on', ''); | |||
frm.set_value('parent_document_type', ''); | |||
frm.trigger('set_options'); | |||
frm.trigger('set_parent_document_type'); | |||
}, | |||
set_options: function(frm) { | |||
@@ -317,6 +320,7 @@ frappe.ui.form.on('Number Card', { | |||
frm.filter_group = new frappe.ui.FilterGroup({ | |||
parent: dialog.get_field('filter_area').$wrapper, | |||
doctype: frm.doc.document_type, | |||
parent_doctype: frm.doc.parent_document_type, | |||
on_change: () => {}, | |||
}); | |||
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); | |||
} | |||
}, | |||
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", | |||
"column_break_2", | |||
"document_type", | |||
"parent_document_type", | |||
"report_field", | |||
"report_function", | |||
"is_public", | |||
@@ -188,10 +189,17 @@ | |||
"label": "Function", | |||
"mandatory_depends_on": "eval: doc.type == 'Report'", | |||
"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": [], | |||
"modified": "2020-07-23 11:11:03.391719", | |||
"modified": "2022-03-10 15:34:38.210910", | |||
"modified_by": "Administrator", | |||
"module": "Desk", | |||
"name": "Number Card", | |||
@@ -234,6 +242,7 @@ | |||
"search_fields": "label, document_type", | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"states": [], | |||
"title_field": "label", | |||
"track_changes": 1 | |||
} |
@@ -3,6 +3,7 @@ | |||
# License: MIT. See LICENSE | |||
import frappe | |||
from frappe import _ | |||
from frappe.model.document import Document | |||
from frappe.utils import cint | |||
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): | |||
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): | |||
if frappe.conf.developer_mode and self.is_standard: | |||
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> | |||
</tr>` | |||
} | |||
frm.get_field('processlist').html(` | |||
<p class='text-muted'>Requested on: ${timestamp}</p> | |||
<table class='table-bordered' style='width: 100%'> | |||
<thead><tr> | |||
<th width='10%'>Id</ht> | |||
<th width='5%'>Id</ht> | |||
<th width='10%'>Time</ht> | |||
<th width='10%'>State</ht> | |||
<th width='60%'>Info</ht> | |||
<th width='10%'>Progress</ht> | |||
<th width='15%'>Progress / Wait Event</ht> | |||
</tr></thead> | |||
<tbody>${rows}</thead>`); | |||
}); | |||
@@ -41,4 +41,14 @@ def execute_code(doc): | |||
@frappe.whitelist() | |||
def show_processlist(): | |||
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.sequence_id = seq + 1 | |||
doc.parent_page = d.get('parent_page') or "" | |||
doc.flags.ignore_links = True | |||
doc.save(ignore_permissions=True) | |||
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 | |||
import json | |||
from collections import defaultdict | |||
import itertools | |||
from typing import List | |||
from typing import Dict, List, Optional | |||
import frappe | |||
import frappe.desk.form.load | |||
@@ -367,7 +368,7 @@ def get_exempted_doctypes(): | |||
@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): | |||
# additional fields are added in linkinfo | |||
linkinfo = json.loads(linkinfo) | |||
@@ -377,23 +378,21 @@ def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None): | |||
if not linkinfo: | |||
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(): | |||
filters = [] | |||
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] | |||
if not linkmeta.has_permission(): | |||
continue | |||
if not linkmeta.get("issingle"): | |||
fields = [d.fieldname for d in linkmeta.get("fields", { | |||
"in_list_view": 1, | |||
@@ -456,6 +455,13 @@ def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None): | |||
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() | |||
def get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False): | |||
"""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: | |||
return frappe.cache().hget("linked_doctypes", doctype, lambda: _get_linked_doctypes(doctype)) | |||
def _get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False): | |||
ret = {} | |||
# find fields where this doctype is linked | |||
@@ -499,6 +506,7 @@ def _get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False) | |||
return ret | |||
def get_linked_fields(doctype, without_ignore_user_permissions_enabled=False): | |||
filters = [['fieldtype','=', 'Link'], ['options', '=', doctype]] | |||
@@ -529,6 +537,7 @@ def get_linked_fields(doctype, without_ignore_user_permissions_enabled=False): | |||
return ret | |||
def get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled=False): | |||
ret = {} | |||
@@ -10,6 +10,7 @@ import frappe.desk.form.meta | |||
from frappe.model.utils.user_settings import get_user_settings | |||
from frappe.permissions import get_doc_permissions | |||
from frappe.desk.form.document_follow import is_document_followed | |||
from frappe.utils.data import cstr | |||
from frappe import _ | |||
from frappe import _dict | |||
from urllib.parse import quote | |||
@@ -124,7 +125,6 @@ def get_docinfo(doc=None, doctype=None, name=None): | |||
update_user_info(docinfo) | |||
frappe.response["docinfo"] = docinfo | |||
return docinfo | |||
def add_comments(doc, docinfo): | |||
# divide comments into separate lists | |||
@@ -356,7 +356,7 @@ def get_document_email(doctype, name): | |||
return None | |||
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(): | |||
return frappe.db.get_value("Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id") | |||
@@ -352,14 +352,10 @@ def export_query(): | |||
) | |||
return | |||
columns = get_columns_dict(data.columns) | |||
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) | |||
frappe.response["filename"] = report_name + ".xlsx" | |||
@@ -367,39 +363,18 @@ def export_query(): | |||
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 = [[]] | |||
column_widths = [] | |||
@@ -257,7 +257,7 @@ def scrub_custom_query(query, key, txt): | |||
def relevance_sorter(key, query, as_dict): | |||
value = _(key.name if as_dict else key[0]) | |||
return ( | |||
value.lower().startswith(query.lower()) is not True, | |||
cstr(value).lower().startswith(query.lower()) is not True, | |||
value | |||
) | |||
@@ -104,7 +104,7 @@ class AutoEmailReport(Document): | |||
report_data['columns'] = columns | |||
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) | |||
return xlsx_file.getvalue() | |||
@@ -113,7 +113,7 @@ class AutoEmailReport(Document): | |||
report_data['columns'] = columns | |||
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) | |||
else: | |||
@@ -236,8 +236,7 @@ | |||
"index_web_pages_for_search": 1, | |||
"is_published_field": "published", | |||
"links": [], | |||
"max_attachments": 3, | |||
"modified": "2021-12-06 20:09:37.963141", | |||
"modified": "2022-03-09 01:48:16.741603", | |||
"modified_by": "Administrator", | |||
"module": "Email", | |||
"name": "Newsletter", | |||
@@ -51,7 +51,7 @@ class TestNewsletterMixin: | |||
"reference_name": newsletter, | |||
}) | |||
frappe.delete_doc("Newsletter", newsletter) | |||
frappe.db.delete("Newsletter Email Group", newsletter) | |||
frappe.db.delete("Newsletter Email Group", {"parent": newsletter}) | |||
newsletters.remove(newsletter) | |||
def setup_email_group(self): | |||
@@ -186,7 +186,7 @@ def get_context(context): | |||
def send_an_email(self, doc, context): | |||
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 | |||
if "{" in subject: | |||
subject = frappe.render_template(self.subject, context) | |||
@@ -216,7 +216,8 @@ def get_context(context): | |||
# Add mail notification to communication list | |||
# No need to add if it is already a communication. | |||
if doc.doctype != 'Communication': | |||
make_communication(doctype=doc.doctype, | |||
make_communication( | |||
doctype=doc.doctype, | |||
name=doc.name, | |||
content=message, | |||
subject=subject, | |||
@@ -228,7 +229,7 @@ def get_context(context): | |||
cc=cc, | |||
bcc=bcc, | |||
communication_type='Automated Message', | |||
ignore_permissions=True) | |||
) | |||
def send_a_slack_msg(self, doc, context): | |||
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) | |||
signature = None | |||
if "<!-- signature-included -->" not in message: | |||
signature = get_signature(email_account) | |||
rendered_email = frappe.get_template("templates/emails/standard.html").render({ | |||
"brand_logo": get_brand_logo(email_account) if with_container or header else None, | |||
"with_container": with_container, | |||
"site_url": get_url(), | |||
"header": get_header(header), | |||
"content": message, | |||
"signature": signature, | |||
"footer": get_footer(email_account, footer), | |||
"title": subject, | |||
"print_html": print_html, | |||
@@ -281,8 +276,7 @@ def get_formatted_html(subject, message, footer=None, print_html=None, | |||
if unsubscribe_link: | |||
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() | |||
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 | |||
update_log.name | |||
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 | |||
consumer.consumer = %(consumer)s | |||
AND update_log.ref_doctype = %(dt)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( | |||
'Event Update Log', | |||
@@ -7,6 +7,7 @@ import json | |||
import requests | |||
import frappe | |||
from frappe.utils.data import cstr | |||
class AuthError(Exception): | |||
@@ -122,7 +123,7 @@ class FrappeClient(object): | |||
'''Update a remote document | |||
: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) | |||
return frappe._dict(self.post_process(res)) | |||
@@ -207,7 +208,7 @@ class FrappeClient(object): | |||
if 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) | |||
return self.post_process(res) | |||
@@ -611,7 +611,7 @@ def is_downgrade(sql_file_path, verbose=False): | |||
downgrade = backup_version > current_version | |||
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 | |||
@@ -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) | |||
# don't update name, as case might've been changed | |||
name = d['name'] | |||
name = cstr(d['name']) | |||
del d['name'] | |||
columns = list(d) | |||
@@ -164,7 +164,8 @@ class DatabaseQuery(object): | |||
# left join parent, child tables | |||
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: | |||
self.conditions.append(f"({' or '.join(self.grouped_or_conditions)})") | |||
@@ -318,21 +319,60 @@ class DatabaseQuery(object): | |||
] | |||
# add tables from 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 | |||
table_name = field.split('.')[0] | |||
if table_name.lower().startswith('group_concat('): | |||
table_name = table_name[13:] | |||
if table_name.lower().startswith('ifnull('): | |||
table_name = table_name[7:] | |||
if not table_name[0]=='`': | |||
table_name = f"`{table_name}`" | |||
if table_name not in self.tables: | |||
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): | |||
self.tables.append(table_name) | |||
doctype = table_name[4:-1] | |||
@@ -423,6 +463,8 @@ class DatabaseQuery(object): | |||
ifnull(`tabDocType`.`fieldname`, fallback) operator "value" | |||
""" | |||
# TODO: refactor | |||
from frappe.boot import get_additional_filters_from_hooks | |||
additional_filters_config = get_additional_filters_from_hooks() | |||
f = get_filter(self.doctype, f, additional_filters_config) | |||
@@ -432,15 +474,16 @@ class DatabaseQuery(object): | |||
self.append_table(tname) | |||
if 'ifnull(' in f.fieldname: | |||
column_name = f.fieldname | |||
column_name = self.cast_name(f.fieldname, "ifnull(") | |||
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: | |||
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 | |||
if f.operator.lower() in ('ancestors of', 'descendants of', 'not ancestors of', 'not descendants of'): | |||
values = f.value or '' | |||
@@ -449,12 +492,8 @@ class DatabaseQuery(object): | |||
# if not isinstance(values, (list, tuple)): | |||
# 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 = '', '' | |||
if f.value: | |||
@@ -474,29 +513,30 @@ class DatabaseQuery(object): | |||
}, order_by='`lft` DESC') | |||
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): | |||
value = f"({', '.join(value)})" | |||
else: | |||
value = "('')" | |||
# 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. | |||
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'): | |||
values = f.value or '' | |||
if isinstance(values, str): | |||
values = values.split(",") | |||
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): | |||
value = f"({', '.join(value)})" | |||
else: | |||
value = "('')" | |||
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 | |||
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'" | |||
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) | |||
fallback = "'0001-01-01 00:00:00'" | |||
@@ -528,7 +569,7 @@ class DatabaseQuery(object): | |||
fallback = "''" | |||
can_be_null = True | |||
if 'ifnull' not in column_name: | |||
if 'ifnull' not in column_name.lower(): | |||
column_name = f'ifnull({column_name}, {fallback})' | |||
elif df and df.fieldtype=="Date": | |||
@@ -570,7 +611,7 @@ class DatabaseQuery(object): | |||
value = f"{tname}.{quote}{f.value.name}{quote}" | |||
# 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)}" | |||
if ( | |||
@@ -158,7 +158,7 @@ def update_naming_series(doc): | |||
and getattr(doc, "naming_series", None): | |||
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) | |||
def delete_from_table(doctype, name, ignore_doctypes, doc): | |||
@@ -1,14 +1,18 @@ | |||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
from typing import Optional | |||
from typing import Optional, TYPE_CHECKING, Union | |||
import frappe | |||
from frappe import _ | |||
from frappe.database.sequence import get_next_val, set_next_val | |||
from frappe.utils import now_datetime, cint, cstr | |||
import re | |||
from frappe.model import log_types | |||
from frappe.query_builder import DocType | |||
if TYPE_CHECKING: | |||
from frappe.model.meta import Meta | |||
def set_new_name(doc): | |||
""" | |||
@@ -24,11 +28,16 @@ def set_new_name(doc): | |||
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: | |||
doc.name = None | |||
if is_autoincremented(doc.doctype, meta): | |||
doc.name = get_next_val(doc.doctype) | |||
return | |||
if getattr(doc, "amended_from", None): | |||
_set_amended_name(doc) | |||
return | |||
@@ -64,9 +73,37 @@ def set_new_name(doc): | |||
doc.name = validate_name( | |||
doc.doctype, | |||
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): | |||
""" | |||
Get a name based on the autoname field option | |||
@@ -284,9 +321,19 @@ def get_default_naming_series(doctype): | |||
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: | |||
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): | |||
frappe.throw(_("There were some errors setting the name, please contact the administrator"), frappe.NameError) | |||
if case == "Title Case": | |||
@@ -43,8 +43,8 @@ def update_document_title( | |||
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: | |||
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 | |||
def caclulate_hash(path: str) -> str: | |||
def calculate_hash(path: str) -> str: | |||
"""Calculate md5 hash of the file in binary mode | |||
Args: | |||
@@ -99,7 +99,7 @@ def import_file_by_path(path: str,force: bool = False,data_import: bool = False, | |||
print(f"{path} missing") | |||
return | |||
calculated_hash = caclulate_hash(path) | |||
calculated_hash = calculate_hash(path) | |||
if docs: | |||
if not isinstance(docs, list): | |||
@@ -24,7 +24,7 @@ ul.tree-children { | |||
} | |||
.tree-link .node-parent, | |||
.tree-link .node-leaf { | |||
margin-right: 5px; | |||
margin-right: 8px; | |||
} | |||
.tree-link.active i { | |||
color: #5e64ff; | |||
@@ -166,6 +166,9 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui | |||
} | |||
parse_options(options) { | |||
if (typeof options === 'string' && options[0] === '[') { | |||
options = frappe.utils.parse_json(options); | |||
} | |||
if (typeof options === 'string') { | |||
options = options.split('\n'); | |||
} | |||
@@ -248,7 +248,7 @@ frappe.ui.form.Form = class FrappeForm { | |||
// on main doc | |||
frappe.model.on(me.doctype, "*", function(fieldname, value, doc) { | |||
// set input | |||
if(doc.name===me.docname) { | |||
if (cstr(doc.name) === me.docname) { | |||
me.dirty(); | |||
let field = me.fields_dict[fieldname]; | |||
@@ -501,9 +501,9 @@ export default class Grid { | |||
} | |||
set_column_disp(fieldname, show) { | |||
if ($.isArray(fieldname)) { | |||
if (Array.isArray(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); | |||
} | |||
} 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 { | |||
constructor(opts) { | |||
$.extend(this, opts); | |||
} | |||
@@ -21,29 +20,23 @@ frappe.ui.form.LinkedWith = class LinkedWith { | |||
} | |||
make_dialog() { | |||
this.dialog = new frappe.ui.Dialog({ | |||
title: __("Linked With") | |||
}); | |||
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() { | |||
const linked_docs = this.frm.__linked_docs; | |||
let html = ''; | |||
const linked_docs = this.frm.__linked_docs; | |||
const linked_doctypes = Object.keys(linked_docs); | |||
if (linked_doctypes.length === 0) { | |||
@@ -63,88 +56,6 @@ frappe.ui.form.LinkedWith = class LinkedWith { | |||
$(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) { | |||
return ` | |||
<header class="level list-row list-row-head text-muted small"> | |||
@@ -44,8 +44,17 @@ frappe.ui.form.Attachments = class Attachments { | |||
// add attachment objects | |||
var attachments = this.get_attachments(); | |||
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 { | |||
this.attachments_label.removeClass("has-attachments"); | |||
@@ -75,7 +84,19 @@ frappe.ui.form.Attachments = class Attachments { | |||
remove_action = function(target_id) { | |||
frappe.confirm(__("Are you sure you want to delete the attachment?"), | |||
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; | |||
@@ -760,6 +760,10 @@ class FilterArea { | |||
const doctype_fields = this.list_view.meta.fields; | |||
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( | |||
doctype_fields | |||
@@ -794,13 +798,17 @@ class FilterArea { | |||
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)) { | |||
default_value = null; | |||
} | |||
return { | |||
fieldtype: fieldtype, | |||
label: __(df.label), | |||
@@ -6,8 +6,8 @@ frappe.provide('frappe.views.list_view'); | |||
window.cur_list = null; | |||
frappe.views.ListFactory = class ListFactory extends frappe.views.Factory { | |||
make (route) { | |||
var me = this; | |||
var doctype = route[1]; | |||
const me = this; | |||
const doctype = route[1]; | |||
// List / Gantt / Kanban / etc | |||
// 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); | |||
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()) { | |||
return; | |||
return false; | |||
} | |||
this.set_module_breadcrumb(); | |||
super.show(); | |||
} | |||
on_show() { | |||
this.set_cur_list(); | |||
cur_list && cur_list.show(); | |||
if (cur_list) cur_list.show(); | |||
} | |||
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() { | |||
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') { | |||
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)) { | |||
// save the last page from the breadcrumb was accessed | |||
frappe.breadcrumbs.set_doctype_module(doctype, module); | |||
@@ -84,10 +82,8 @@ frappe.views.ListFactory = class ListFactory extends frappe.views.Factory { | |||
} | |||
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... | |||
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_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 | |||
this.menu_items = this.menu_items.concat(this.get_menu_items()); | |||
// set filters from view_user_settings or list_settings | |||
if ( | |||
this.view_user_settings.filters && | |||
this.view_user_settings.filters.length | |||
) { | |||
// Priority 1: saved filters | |||
// Priority 1: view_user_settings | |||
const saved_filters = this.view_user_settings.filters; | |||
this.filters = this.validate_filters(saved_filters); | |||
} else { | |||
@@ -932,7 +915,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
return this.settings.get_form_link(doc); | |||
} | |||
const docname = doc.name.match(/[%'"#\s]/) | |||
const docname = cstr(doc.name).match(/[%'"#\s]/) | |||
? encodeURIComponent(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( | |||
(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( | |||
__("Delete {0} items permanently?", [docnames.length], "Title of confirmation dialog"), | |||
message, | |||
() => { | |||
this.disable_list_update = true; | |||
bulk_operations.delete(docnames, () => { | |||
@@ -138,6 +138,7 @@ frappe.render_tree = function(opts) { | |||
opts.base_url = frappe.urllib.get_base_url(); | |||
opts.landscape = false; | |||
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 w = window.open(); | |||
@@ -577,13 +577,15 @@ $.extend(frappe.model, { | |||
}, | |||
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) { | |||
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({ | |||
method: 'frappe.client.delete', | |||
args: { | |||
@@ -134,7 +134,17 @@ frappe.msgprint = function(msg, title, is_minimizable) { | |||
} | |||
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); | |||
}); | |||
return; | |||
@@ -196,6 +196,15 @@ Object.assign(frappe.utils, { | |||
} | |||
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) { | |||
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() { | |||
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 { | |||
var route = frappe.get_route(); | |||
if(route[1]) { | |||
me.make(route); | |||
if (this.route[1]) { | |||
this.make(this.route); | |||
} 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) { | |||
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({ | |||
parent: page, | |||
single_column: !double_column | |||
}); | |||
frappe.container.change_to(page_name); | |||
return page; | |||
} |
@@ -1,91 +1,106 @@ | |||
<!DOCTYPE html> | |||
<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> | |||
</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> |
@@ -125,11 +125,12 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { | |||
} | |||
after_render() { | |||
if (this.report_doc) { | |||
this.set_dirty_state_for_custom_report(); | |||
} else { | |||
if (!this.report_doc) { | |||
this.save_report_settings(); | |||
} else if (!$.isEmptyObject(this.report_doc.json)) { | |||
this.set_dirty_state_for_custom_report(); | |||
} | |||
if (!this.group_by) { | |||
this.init_chart(); | |||
} | |||
@@ -1,5 +1,5 @@ | |||
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.custom import GROUP_CONCAT, STRING_AGG, MATCH, TO_TSVECTOR | |||
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): | |||
return ( | |||
@@ -46,4 +64,4 @@ def _avg(dt, fieldname, filters=None, **kwargs): | |||
return _aggregate(Avg, dt, fieldname, filters, **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 | |||
import frappe | |||
from frappe.utils.data import cstr | |||
import os | |||
import redis | |||
@@ -118,7 +119,7 @@ def get_user_info(): | |||
} | |||
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): | |||
return ''.join([frappe.local.site, ':user:', user]) | |||
@@ -37,7 +37,6 @@ | |||
<tr> | |||
<td valign="top"> | |||
<p>{{ content }}</p> | |||
<p class="signature">{{ signature }}</p> | |||
</td> | |||
</tr> | |||
</table> | |||
@@ -562,3 +562,50 @@ class TestDDLCommandsPost(unittest.TestCase): | |||
""", | |||
) | |||
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") | |||
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(): | |||
child_table = frappe.get_doc({ | |||
'doctype': 'DocType', | |||
@@ -168,8 +168,8 @@ class TestFormLoad(unittest.TestCase): | |||
"reference_name": note.name, | |||
}).insert() | |||
docinfo = get_docinfo(note) | |||
get_docinfo(note) | |||
docinfo = frappe.response["docinfo"] | |||
self.assertEqual(len(docinfo.comments), 1) | |||
self.assertIn("test", docinfo.comments[0].content) | |||
@@ -245,6 +245,17 @@ class TestNaming(unittest.TestCase): | |||
}) | |||
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(): | |||
frappe.get_doc({ | |||
@@ -3,7 +3,7 @@ from typing import Callable | |||
import frappe | |||
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 import Case | |||
@@ -32,6 +32,27 @@ class TestCustomFunctionsMariaDB(unittest.TestCase): | |||
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) | |||
class TestCustomFunctionsPostgres(unittest.TestCase): | |||
@@ -52,6 +73,30 @@ class TestCustomFunctionsPostgres(unittest.TestCase): | |||
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): | |||
def test_adding_tabs(self): | |||
@@ -12,37 +12,30 @@ class TestQueryReport(unittest.TestCase): | |||
def test_xlsx_data_with_multiple_datatypes(self): | |||
"""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 | |||
data = frappe._dict() | |||
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 = [ | |||
[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 | |||
visible_idx = [0, 2, 3] | |||
# 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(len(xlsx_data), 4) # columns + data | |||
# 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: | |||
self.assertEqual(type(row), list) |
@@ -135,11 +135,14 @@ def create_contact_records(): | |||
insert_contact('Test Form Contact 3', '12345') | |||
@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 | |||
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): | |||
doc = frappe.get_doc({ | |||
@@ -146,7 +146,7 @@ Monthly,Monatlich, | |||
More,Weiter, | |||
More Information,Mehr Informationen, | |||
More...,Mehr..., | |||
Move,Bewegen, | |||
Move,Verschieben, | |||
My Account,Mein Konto, | |||
My Profile,Mein Profil, | |||
My Settings,Meine Einstellungen, | |||
@@ -175,7 +175,7 @@ Payment Gateway,Zahlungs-Gateways, | |||
Payment Gateway Name,Name des Zahlungsgateways, | |||
Payments,Zahlungen, | |||
Period,Periode, | |||
Pincode,Postleitzahl (PLZ), | |||
Pincode,Postleitzahl, | |||
Plan Name,Planname, | |||
Please enable pop-ups,Bitte Pop-ups aktivieren, | |||
Please select Company,Bitte Unternehmen auswählen, | |||
@@ -1486,7 +1486,7 @@ Linked,Verknüpft, | |||
Linked With,Verknüpft mit, | |||
Linked with {0},Verknüpft mit {0}, | |||
Links,Verknüpfungen, | |||
List,Listenansicht, | |||
List,Liste, | |||
List Filter,Listenfilter, | |||
List View,Listenansicht, | |||
List View Setting,Einstellungen zu Listenansicht, | |||
@@ -2427,7 +2427,7 @@ Sum,Summe, | |||
Sum of {0},Summe von {0}, | |||
Support Email Address Not Specified,Support-E-Mail-Adresse nicht angegeben, | |||
Suspend Sending,Senden unterbrechen, | |||
Switch To Desk,Switch To Desk, | |||
Switch To Desk,Zum Desk wechseln, | |||
Symbol,Symbol, | |||
Sync,Synchronisieren, | |||
Sync on Migrate,Sync auf Migrate, | |||
@@ -2870,8 +2870,8 @@ bullhorn,Megafon, | |||
ca-central-1,ca-central-1, | |||
camera,Kamera, | |||
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-left,Winkel nach links, | |||
chevron-right,Winkel nach rechts, | |||
@@ -3431,7 +3431,7 @@ Mandatory Depends On,Obligatorisch Hängt von ab, | |||
Map Columns,Spalten zuordnen, | |||
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, | |||
Mark all as Read,Markiere alle als gelesen, | |||
Mark all as Read,Alle als gelesen markieren, | |||
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), | |||
Me,Mir, | |||
@@ -3485,7 +3485,7 @@ Page Shortcuts,Seitenkürzel, | |||
Parent Field (Tree),Elternfeld (Baum), | |||
Parent Field must be a valid fieldname,Das übergeordnete Feld muss ein gültiger Feldname sein, | |||
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 enable pop-ups in your browser,Bitte aktivieren Sie Popups in Ihrem Browser, | |||
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 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 Mandatory,Wählen Pflicht, | |||
Select Mandatory,Verpflichtende auswählen, | |||
Select atleast 2 actions,Wählen Sie mindestens 2 Aktionen aus, | |||
Select list item,Listenelement auswählen, | |||
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, | |||
"browse,","Durchsuche,", | |||
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, | |||
empty,leeren, | |||
of,von, | |||
@@ -3789,14 +3789,14 @@ Reset,Zurücksetzen, | |||
Review,Rezension, | |||
Room,Zimmer, | |||
Room Type,Zimmertyp, | |||
Save,speichern, | |||
Save,Speichern, | |||
Search results for,Suchergebnisse für, | |||
Select All,Alles auswählen, | |||
Send,Absenden, | |||
Sending,Versand, | |||
Server Error,Serverfehler, | |||
Set,Menge, | |||
Setup,Einstellungen, | |||
Setup,Einrichtung, | |||
Setup Wizard,Setup-Assistent, | |||
Size,Größe, | |||
Sr,Pos, | |||
@@ -3819,7 +3819,7 @@ Warehouse,Lager, | |||
Welcome to {0},Willkommen auf {0}, | |||
Year,Jahr, | |||
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, | |||
and,und, | |||
{0} Name,{0} Name, | |||
@@ -3953,7 +3953,7 @@ lock,sperren, | |||
logged in,Angemeldet, | |||
message,Mitteilung, | |||
module,Modul, | |||
move,Bewegung, | |||
move,verschieben, | |||
music,Musik, | |||
new,Neu, | |||
now,jetzt, | |||
@@ -4135,9 +4135,9 @@ Using this console may allow attackers to impersonate you and steal your informa | |||
yesterday,gestern, | |||
{0} years ago,Vor {0} Jahren, | |||
New Chart,Neues Diagramm, | |||
New Shortcut,Neue Verknüpfung, | |||
New Shortcut,Neuer Schnellzugriff, | |||
Edit Chart,Diagramm bearbeiten, | |||
Edit Shortcut,Verknüpfung bearbeiten, | |||
Edit Shortcut,Schnellzugriff bearbeiten, | |||
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", | |||
Customize Workspace,Arbeitsbereich anpassen, | |||
@@ -4228,7 +4228,7 @@ since last month,seit letztem Monat, | |||
since last year,seit letztem Jahr, | |||
Show,Show, | |||
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., | |||
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?, | |||
@@ -4650,7 +4650,7 @@ Not permitted to view {0},{0} darf nicht angezeigt werden, | |||
Camera,Kamera, | |||
Invalid filter: {0},Ungültiger Filter: {0}, | |||
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} created,Neue {0} {1} erstellt, | |||
New {0} Created,Neu {0} erstellt, | |||
@@ -4715,3 +4715,67 @@ Reset sorting,Sortierung zurücksetzen, | |||
Sort Ascending,Aufsteigend sortieren, | |||
Sort Descending,Absteigend sortieren, | |||
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 | |||
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) | |||
q = get_queue(queue, is_async=is_async) | |||
@@ -183,8 +183,6 @@ class BackupGenerator: | |||
False, | |||
) | |||
self.todays_date = now_datetime().strftime("%Y%m%d_%H%M%S") | |||
if not ( | |||
self.backup_path_conf | |||
and self.backup_path_db | |||
@@ -212,7 +210,7 @@ class BackupGenerator: | |||
partial = "-partial" if self.partial else "" | |||
ext = "tgz" if self.compress_files else "tar" | |||
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_db = f"{self.todays_date}-{self.site_slug}{partial}-database{enc}.sql.gz" | |||
@@ -1494,7 +1494,7 @@ def expand_relative_urls(html): | |||
return html | |||
def quoted(url): | |||
return cstr(quote(encode(url), safe=b"~@#$&()*!+=:;,.?/'")) | |||
return cstr(quote(encode(cstr(url)), safe=b"~@#$&()*!+=:;,.?/'")) | |||
def quote_urls(html): | |||
def _quote_url(match): | |||
@@ -1,14 +1,15 @@ | |||
import json | |||
from difflib import unified_diff | |||
from typing import List | |||
from typing import List, Union | |||
import frappe | |||
from frappe.utils import pretty_date | |||
from frappe.utils.data import cstr | |||
@frappe.whitelist() | |||
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]: | |||
before, before_timestamp = _get_value_from_version(from_version, fieldname) | |||
@@ -23,15 +24,15 @@ def get_version_diff( | |||
diff = unified_diff( | |||
before, | |||
after, | |||
fromfile=from_version, | |||
tofile=to_version, | |||
fromfile=cstr(from_version), | |||
tofile=cstr(to_version), | |||
fromfiledate=before_timestamp, | |||
tofiledate=after_timestamp, | |||
) | |||
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", fields=["data", "modified"], filters={"name": version_name} | |||
) | |||
@@ -9,6 +9,8 @@ import os | |||
from frappe.utils import cint, strip_html_tags | |||
from frappe.utils.html_utils import unescape_html | |||
from frappe.model.base_document import get_controller | |||
from frappe.utils.data import cstr | |||
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: | |||
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 '' | |||
value = dict( | |||
@@ -213,8 +213,7 @@ | |||
"index_web_pages_for_search": 1, | |||
"is_published_field": "published", | |||
"links": [], | |||
"max_attachments": 5, | |||
"modified": "2021-11-23 10:42:01.759723", | |||
"modified": "2022-03-09 01:48:25.227295", | |||
"modified_by": "Administrator", | |||
"module": "Website", | |||
"name": "Blog Post", | |||
@@ -82,7 +82,8 @@ frappe.boot = { | |||
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() }}" | |||
} | |||
}, | |||
link_title_doctypes: `{{ frappe.call('frappe.boot.get_link_title_doctypes') }}` | |||
}; | |||
// for backward compatibility of some libs | |||
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 | |||
if doctype_validated: | |||
link_options = [] | |||
link_options, filters = [], {} | |||
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: | |||
raise frappe.PermissionError('Not Allowed, {0}'.format(doctype)) |
@@ -338,8 +338,7 @@ | |||
"index_web_pages_for_search": 1, | |||
"is_published_field": "published", | |||
"links": [], | |||
"max_attachments": 20, | |||
"modified": "2022-01-03 13:01:48.182645", | |||
"modified": "2022-03-09 01:45:28.548671", | |||
"modified_by": "Administrator", | |||
"module": "Website", | |||
"name": "Web Page", | |||
@@ -420,8 +420,7 @@ | |||
"index_web_pages_for_search": 1, | |||
"issingle": 1, | |||
"links": [], | |||
"max_attachments": 10, | |||
"modified": "2022-02-24 15:37:22.360138", | |||
"modified": "2022-03-09 01:47:31.094462", | |||
"modified_by": "Administrator", | |||
"module": "Website", | |||
"name": "Website Settings", | |||
@@ -8,5 +8,5 @@ def get_context(context): | |||
if frappe.flags.in_migrate: return | |||
context.http_status_code = 500 | |||
print(frappe.get_traceback().encode("utf-8")) | |||
print(frappe.get_traceback()) | |||
return {"error": frappe.get_traceback().replace("<", "<").replace(">", ">") } |
@@ -24,7 +24,6 @@ | |||
"@editorjs/editorjs": "2.20.0", | |||
"ace-builds": "^1.4.8", | |||
"air-datepicker": "github:frappe/air-datepicker", | |||
"autoprefixer": "^9.8.6", | |||
"awesomplete": "^1.1.5", | |||
"bootstrap": "4.5.0", | |||
"cliui": "^7.0.4", | |||
@@ -66,14 +65,17 @@ | |||
"vuedraggable": "^2.24.3" | |||
}, | |||
"devDependencies": { | |||
"@frappe/esbuild-plugin-postcss2": "^0.1.3", | |||
"autoprefixer": "10", | |||
"chalk": "^2.3.2", | |||
"esbuild": "^0.11.21", | |||
"esbuild-plugin-postcss2": "^0.0.9", | |||
"esbuild-vue": "^0.2.0", | |||
"fast-glob": "^3.2.5", | |||
"launch-editor": "^2.2.1", | |||
"md5": "^2.3.0", | |||
"postcss": "8", | |||
"rtlcss": "^3.2.1", | |||
"sass": "^1.49.9", | |||
"yargs": "^16.2.0" | |||
}, | |||
"snyk": true, | |||
@@ -29,6 +29,7 @@ maxminddb-geolite2==2018.703 | |||
num2words~=0.5.10 | |||
oauthlib~=3.1.0 | |||
openpyxl~=3.0.7 | |||
parse~=1.19.0 | |||
passlib~=1.7.4 | |||
paytmchecksum~=1.7.0 | |||
pdfkit~=0.6.1 | |||
@@ -36,6 +36,20 @@ | |||
codex-notifier "^1.1.2" | |||
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": | |||
version "2.1.4" | |||
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" | |||
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: | |||
version "10.2.5" | |||
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" | |||
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: | |||
version "1.0.2" | |||
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: | |||
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" | |||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2" | |||
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" | |||
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: | |||
version "3.1.0" | |||
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.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" | |||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001296.tgz" | |||
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: | |||
version "0.12.0" | |||
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" | |||
integrity sha512-WMDFJfoY3wqPZNpKUFdse3HhD5BHCbE9JCdxRzoVH+ywRITGOeWAHNkGEmyxLlErEpN9OLMWgdM9dWQtDk5dog== | |||
colorette@^1.2.1, colorette@^1.2.2: | |||
colorette@^1.2.2: | |||
version "1.2.2" | |||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" | |||
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" | |||
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: | |||
version "7.0.3" | |||
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-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: | |||
version "0.2.0" | |||
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" | |||
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: | |||
version "2.0.0-rc13" | |||
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" | |||
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: | |||
version "3.3.0" | |||
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" | |||
integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== | |||
nanoid@^3.1.22, nanoid@^3.1.23: | |||
nanoid@^3.1.23: | |||
version "3.2.0" | |||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" | |||
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: | |||
version "1.0.8" | |||
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" | |||
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: | |||
version "7.0.0" | |||
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-7.0.0.tgz#33ee7c2df299d51f682f13d79f3d2a562225788e" | |||
@@ -3059,11 +3098,6 @@ nth-check@^2.0.0: | |||
dependencies: | |||
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: | |||
version "1.0.1" | |||
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" | |||
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: | |||
version "2.2.3" | |||
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" | |||
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: | |||
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: | |||
version "5.2.18" | |||
@@ -3664,15 +3708,6 @@ postcss@^7.0.14: | |||
source-map "^0.6.1" | |||
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: | |||
version "8.3.5" | |||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.5.tgz#982216b113412bc20a86289e91eb994952a5b709" | |||
@@ -4242,6 +4277,15 @@ sass@^1.18.0, sass@^1.x: | |||
dependencies: | |||
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: | |||
version "1.2.4" | |||
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" | |||
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: | |||
version "0.6.2" | |||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" | |||