Merge branch 'develop' into refactor-uninstall-app
This commit is contained in:
commit
8d5cb35297
40 changed files with 787 additions and 424 deletions
|
|
@ -4,8 +4,7 @@ pull_request_rules:
|
|||
- status-success=Sider
|
||||
- status-success=Semantic Pull Request
|
||||
- status-success=Travis CI - Pull Request
|
||||
- status-success=security/snyk - package.json (frappe)
|
||||
- status-success=security/snyk - requirements.txt (frappe)
|
||||
- status-success=security/snyk (frappe)
|
||||
- label!=don't-merge
|
||||
- label!=squash
|
||||
- "#approved-reviews-by>=1"
|
||||
|
|
@ -17,8 +16,7 @@ pull_request_rules:
|
|||
- status-success=Sider
|
||||
- status-success=Semantic Pull Request
|
||||
- status-success=Travis CI - Pull Request
|
||||
- status-success=security/snyk - package.json (frappe)
|
||||
- status-success=security/snyk - requirements.txt (frappe)
|
||||
- status-success=security/snyk (frappe)
|
||||
- label!=don't-merge
|
||||
- label=squash
|
||||
- "#approved-reviews-by>=1"
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class AssignmentRule(Document):
|
|||
def on_update(self): # pylint: disable=no-self-use
|
||||
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.name)
|
||||
|
||||
def after_rename(self): # pylint: disable=no-self-use
|
||||
def after_rename(self, old, new, merge): # pylint: disable=no-self-use
|
||||
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.name)
|
||||
|
||||
def apply_unassign(self, doc, assignments):
|
||||
|
|
|
|||
|
|
@ -108,12 +108,14 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
|
|||
@click.option('--install-app', multiple=True, help='Install app after installation')
|
||||
@click.option('--with-public-files', help='Restores the public files of the site, given path to its tar file')
|
||||
@click.option('--with-private-files', help='Restores the private files of the site, given path to its tar file')
|
||||
@click.option('--force', is_flag=True, default=False, help='Use a bit of force to get the job done')
|
||||
@pass_context
|
||||
def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_password=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, with_private_files=None):
|
||||
"Restore site database from an sql file"
|
||||
from frappe.installer import extract_sql_gzip, extract_tar_files
|
||||
# Extract the gzip file if user has passed *.sql.gz file instead of *.sql file
|
||||
from frappe.installer import extract_sql_gzip, extract_tar_files, is_downgrade
|
||||
force = context.force or force
|
||||
|
||||
# Extract the gzip file if user has passed *.sql.gz file instead of *.sql file
|
||||
if not os.path.exists(sql_file_path):
|
||||
base_path = '..'
|
||||
sql_file_path = os.path.join(base_path, sql_file_path)
|
||||
|
|
@ -125,7 +127,6 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
|
|||
else:
|
||||
base_path = '.'
|
||||
|
||||
|
||||
if sql_file_path.endswith('sql.gz'):
|
||||
decompressed_file_name = extract_sql_gzip(os.path.abspath(sql_file_path))
|
||||
else:
|
||||
|
|
@ -133,10 +134,16 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
|
|||
|
||||
site = get_site(context)
|
||||
frappe.init(site=site)
|
||||
|
||||
# dont allow downgrading to older versions of frappe without force
|
||||
if not force and is_downgrade(decompressed_file_name, verbose=True):
|
||||
warn_message = "This is not recommended and may lead to unexpected behaviour. Do you want to continue anyway?"
|
||||
click.confirm(warn_message, abort=True)
|
||||
|
||||
_new_site(frappe.conf.db_name, site, mariadb_root_username=mariadb_root_username,
|
||||
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
|
||||
verbose=context.verbose, install_apps=install_app, source_sql=decompressed_file_name,
|
||||
force=True)
|
||||
force=True, db_type=frappe.conf.db_type)
|
||||
|
||||
# Extract public and/or private files to the restored site, if user has given the path
|
||||
if with_public_files:
|
||||
|
|
|
|||
|
|
@ -102,6 +102,10 @@ frappe.ui.form.on('Data Import', {
|
|||
},
|
||||
|
||||
update_primary_action(frm) {
|
||||
if (frm.is_dirty()) {
|
||||
frm.enable_save();
|
||||
return;
|
||||
}
|
||||
frm.disable_save();
|
||||
if (frm.doc.status !== 'Success') {
|
||||
if (!frm.is_new() && (frm.has_import_file())) {
|
||||
|
|
@ -199,20 +203,12 @@ frappe.ui.form.on('Data Import', {
|
|||
},
|
||||
|
||||
download_template(frm) {
|
||||
if (
|
||||
frm.data_exporter &&
|
||||
frm.data_exporter.doctype === frm.doc.reference_doctype
|
||||
) {
|
||||
frm.data_exporter.exporting_for = frm.doc.import_type;
|
||||
frm.data_exporter.dialog.show();
|
||||
} else {
|
||||
frappe.require('/assets/js/data_import_tools.min.js', () => {
|
||||
frm.data_exporter = new frappe.data_import.DataExporter(
|
||||
frm.doc.reference_doctype,
|
||||
frm.doc.import_type
|
||||
);
|
||||
});
|
||||
}
|
||||
frappe.require('/assets/js/data_import_tools.min.js', () => {
|
||||
frm.data_exporter = new frappe.data_import.DataExporter(
|
||||
frm.doc.reference_doctype,
|
||||
frm.doc.import_type
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
reference_doctype(frm) {
|
||||
|
|
@ -301,8 +297,8 @@ frappe.ui.form.on('Data Import', {
|
|||
events: {
|
||||
remap_column(changed_map) {
|
||||
let template_options = JSON.parse(frm.doc.template_options || '{}');
|
||||
template_options.remap_column = template_options.remap_column || {};
|
||||
Object.assign(template_options.remap_column, changed_map);
|
||||
template_options.column_to_field_map = template_options.column_to_field_map || {};
|
||||
Object.assign(template_options.column_to_field_map, changed_map);
|
||||
frm.set_value('template_options', JSON.stringify(template_options));
|
||||
frm.save().then(() => frm.trigger('import_file'));
|
||||
}
|
||||
|
|
@ -435,10 +431,10 @@ frappe.ui.form.on('Data Import', {
|
|||
.join('');
|
||||
let id = frappe.dom.get_unique_id();
|
||||
html = `${messages}
|
||||
<button class="btn btn-default btn-xs margin-top" type="button" data-toggle="collapse" data-target="#${id}" aria-expanded="false" aria-controls="${id}">
|
||||
<button class="btn btn-default btn-xs" type="button" data-toggle="collapse" data-target="#${id}" aria-expanded="false" aria-controls="${id}" style="margin-top: 15px;">
|
||||
${__('Show Traceback')}
|
||||
</button>
|
||||
<div class="collapse margin-top" id="${id}">
|
||||
<div class="collapse" id="${id}" style="margin-top: 15px;">
|
||||
<div class="well">
|
||||
<pre>${log.exception}</pre>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@
|
|||
{
|
||||
"fieldname": "import_warnings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Warnings"
|
||||
"label": "Import File Errors and Warnings"
|
||||
},
|
||||
{
|
||||
"fieldname": "import_warnings",
|
||||
|
|
@ -127,7 +127,7 @@
|
|||
"label": "Import Warnings"
|
||||
},
|
||||
{
|
||||
"depends_on": "reference_doctype",
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "download_template",
|
||||
"fieldtype": "Button",
|
||||
"label": "Download Template"
|
||||
|
|
@ -159,7 +159,7 @@
|
|||
"label": "Import from Google Sheets"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.google_sheets_url",
|
||||
"depends_on": "eval:doc.google_sheets_url && !doc.__unsaved",
|
||||
"fieldname": "refresh_google_sheet",
|
||||
"fieldtype": "Button",
|
||||
"label": "Refresh Google Sheet"
|
||||
|
|
@ -167,7 +167,7 @@
|
|||
],
|
||||
"hide_toolbar": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-18 16:05:54.211034",
|
||||
"modified": "2020-06-24 14:33:03.173876",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Data Import",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from frappe.utils.xlsxutils import (
|
|||
read_xls_file_from_attached_file,
|
||||
)
|
||||
from frappe.model import no_value_fields, table_fields as table_fieldtypes
|
||||
from frappe.core.doctype.version.version import get_diff
|
||||
|
||||
INVALID_VALUES = ("", None)
|
||||
MAX_ROWS_IN_PREVIEW = 10
|
||||
|
|
@ -216,14 +217,22 @@ class Importer:
|
|||
def update_record(self, doc):
|
||||
id_field = get_id_field(self.doctype)
|
||||
existing_doc = frappe.get_doc(self.doctype, doc.get(id_field.fieldname))
|
||||
existing_doc.flags.updater_reference = {
|
||||
"doctype": self.data_import.doctype,
|
||||
"docname": self.data_import.name,
|
||||
"label": _("via Data Import"),
|
||||
}
|
||||
existing_doc.update(doc)
|
||||
existing_doc.save()
|
||||
return existing_doc
|
||||
|
||||
updated_doc = frappe.get_doc(self.doctype, doc.get(id_field.fieldname))
|
||||
updated_doc.update(doc)
|
||||
|
||||
if get_diff(existing_doc, updated_doc):
|
||||
# update doc if there are changes
|
||||
updated_doc.flags.updater_reference = {
|
||||
"doctype": self.data_import.doctype,
|
||||
"docname": self.data_import.name,
|
||||
"label": _("via Data Import"),
|
||||
}
|
||||
updated_doc.save()
|
||||
return updated_doc
|
||||
else:
|
||||
# throw if no changes
|
||||
frappe.throw('No changes to update')
|
||||
|
||||
def get_eta(self, current, total, processing_time):
|
||||
self.last_eta = getattr(self, "last_eta", 0)
|
||||
|
|
@ -306,8 +315,9 @@ class ImportFile:
|
|||
)
|
||||
self.column_to_field_map = self.template_options.column_to_field_map
|
||||
self.import_type = import_type
|
||||
self.warnings = []
|
||||
|
||||
self.file_doc = self.file_path = None
|
||||
self.file_doc = self.file_path = self.google_sheets_url = None
|
||||
if isinstance(file, frappe.string_types):
|
||||
if frappe.db.exists("File", {"file_url": file}):
|
||||
self.file_doc = frappe.get_doc("File", {"file_url": file})
|
||||
|
|
@ -462,38 +472,46 @@ class ImportFile:
|
|||
parent_doc[table_df.fieldname].append(child_doc)
|
||||
|
||||
doc = parent_doc
|
||||
# check if there is atleast one row for mandatory table fields
|
||||
meta = frappe.get_meta(self.doctype)
|
||||
mandatory_table_fields = [
|
||||
df
|
||||
for df in meta.fields
|
||||
if df.fieldtype in table_fieldtypes
|
||||
and df.reqd
|
||||
and len(doc.get(df.fieldname, [])) == 0
|
||||
]
|
||||
if len(mandatory_table_fields) == 1:
|
||||
self.warnings.append(
|
||||
{
|
||||
"row": first_row.row_number,
|
||||
"message": _("There should be atleast one row for {0} table").format(
|
||||
mandatory_table_fields[0].label
|
||||
),
|
||||
}
|
||||
)
|
||||
elif mandatory_table_fields:
|
||||
fields_string = ", ".join([df.label for df in mandatory_table_fields])
|
||||
message = _("There should be atleast one row for the following tables: {0}").format(
|
||||
fields_string
|
||||
)
|
||||
self.warnings.append({"row": first_row.row_number, "message": message})
|
||||
|
||||
if self.import_type == INSERT:
|
||||
# check if there is atleast one row for mandatory table fields
|
||||
meta = frappe.get_meta(self.doctype)
|
||||
mandatory_table_fields = [
|
||||
df
|
||||
for df in meta.fields
|
||||
if df.fieldtype in table_fieldtypes
|
||||
and df.reqd
|
||||
and len(doc.get(df.fieldname, [])) == 0
|
||||
]
|
||||
if len(mandatory_table_fields) == 1:
|
||||
self.warnings.append(
|
||||
{
|
||||
"row": first_row.row_number,
|
||||
"message": _("There should be atleast one row for {0} table").format(
|
||||
frappe.bold(mandatory_table_fields[0].label)
|
||||
),
|
||||
}
|
||||
)
|
||||
elif mandatory_table_fields:
|
||||
fields_string = ", ".join([df.label for df in mandatory_table_fields])
|
||||
message = _("There should be atleast one row for the following tables: {0}").format(
|
||||
fields_string
|
||||
)
|
||||
self.warnings.append({"row": first_row.row_number, "message": message})
|
||||
|
||||
return doc, rows, data[len(rows) :]
|
||||
|
||||
def get_warnings(self):
|
||||
warnings = []
|
||||
|
||||
# ImportFile warnings
|
||||
warnings += self.warnings
|
||||
|
||||
# Column warnings
|
||||
for col in self.header.columns:
|
||||
warnings += col.warnings
|
||||
|
||||
# Row warnings
|
||||
for row in self.data:
|
||||
warnings += row.warnings
|
||||
|
||||
|
|
@ -607,7 +625,7 @@ class Row:
|
|||
self.warnings.append(
|
||||
{
|
||||
"row": self.row_number,
|
||||
"field": df.as_dict(convert_dates_to_str=True),
|
||||
"field": df_as_json(df),
|
||||
"message": msg,
|
||||
}
|
||||
)
|
||||
|
|
@ -622,7 +640,7 @@ class Row:
|
|||
self.warnings.append(
|
||||
{
|
||||
"row": self.row_number,
|
||||
"field": df.as_dict(convert_dates_to_str=True),
|
||||
"field": df_as_json(df),
|
||||
"message": msg,
|
||||
}
|
||||
)
|
||||
|
|
@ -635,7 +653,7 @@ class Row:
|
|||
{
|
||||
"row": self.row_number,
|
||||
"col": col.column_number,
|
||||
"field": df.as_dict(convert_dates_to_str=True),
|
||||
"field": df_as_json(df),
|
||||
"message": _("Value {0} must in {1} format").format(
|
||||
frappe.bold(value), frappe.bold(get_user_format(col.date_format))
|
||||
),
|
||||
|
|
@ -646,7 +664,7 @@ class Row:
|
|||
return value
|
||||
|
||||
def link_exists(self, value, df):
|
||||
key = df.options + "::" + value
|
||||
key = df.options + "::" + cstr(value)
|
||||
if Row.link_values_exist_map.get(key) is None:
|
||||
Row.link_values_exist_map[key] = frappe.db.exists(df.options, value)
|
||||
return Row.link_values_exist_map.get(key)
|
||||
|
|
@ -755,19 +773,21 @@ class Row:
|
|||
|
||||
|
||||
class Header(Row):
|
||||
def __init__(self, index, row, doctype, raw_data, column_to_field_map):
|
||||
def __init__(self, index, row, doctype, raw_data, column_to_field_map=None):
|
||||
self.index = index
|
||||
self.row_number = index + 1
|
||||
self.data = row
|
||||
self.doctype = doctype
|
||||
column_to_field_map = column_to_field_map or frappe._dict()
|
||||
|
||||
self.seen = []
|
||||
self.columns = []
|
||||
|
||||
for j, header in enumerate(row):
|
||||
column_values = [get_item_at_index(r, j) for r in raw_data]
|
||||
map_to_field = column_to_field_map.get(str(j))
|
||||
column = Column(
|
||||
j, header, self.doctype, column_values, column_to_field_map.get(header), self.seen
|
||||
j, header, self.doctype, column_values, map_to_field, self.seen
|
||||
)
|
||||
self.seen.append(header)
|
||||
self.columns.append(column)
|
||||
|
|
@ -824,7 +844,7 @@ class Column:
|
|||
|
||||
self.meta = frappe.get_meta(doctype)
|
||||
self.parse()
|
||||
self.parse_date_format()
|
||||
self.validate_values()
|
||||
|
||||
def parse(self):
|
||||
header_title = self.header_title
|
||||
|
|
@ -897,10 +917,6 @@ class Column:
|
|||
self.df = df
|
||||
self.skip_import = skip_import
|
||||
|
||||
def parse_date_format(self):
|
||||
if self.df and self.df.fieldtype in ("Date", "Time", "Datetime"):
|
||||
self.date_format = self.guess_date_format_for_column()
|
||||
|
||||
def guess_date_format_for_column(self):
|
||||
""" Guesses date format for a column by parsing all the values in the column,
|
||||
getting the date format and then returning the one which has the maximum frequency
|
||||
|
|
@ -935,6 +951,26 @@ class Column:
|
|||
|
||||
return max_occurred_date_format
|
||||
|
||||
def validate_values(self):
|
||||
if not self.df:
|
||||
return
|
||||
|
||||
if self.df.fieldtype == 'Link':
|
||||
# find all values that dont exist
|
||||
values = list(set([v for v in self.column_values[1:] if v]))
|
||||
exists = [d.name for d in frappe.db.get_all(self.df.options, filters={'name': ('in', values)})]
|
||||
not_exists = list(set(values) - set(exists))
|
||||
if not_exists:
|
||||
missing_values = ', '.join(not_exists)
|
||||
self.warnings.append({
|
||||
'col': self.column_number,
|
||||
'message': "The following values do not exist for {}: {}".format(self.df.options, missing_values),
|
||||
'type': 'warning'
|
||||
})
|
||||
elif self.df.fieldtype in ("Date", "Time", "Datetime"):
|
||||
# guess date format
|
||||
self.date_format = self.guess_date_format_for_column()
|
||||
|
||||
def as_dict(self):
|
||||
d = frappe._dict()
|
||||
d.index = self.index
|
||||
|
|
@ -944,6 +980,9 @@ class Column:
|
|||
d.map_to_field = self.map_to_field
|
||||
d.date_format = self.date_format
|
||||
d.df = self.df
|
||||
if hasattr(self.df, 'is_child_table_field'):
|
||||
d.is_child_table_field = self.df.is_child_table_field
|
||||
d.child_table_df = self.df.child_table_df
|
||||
d.skip_import = self.skip_import
|
||||
d.warnings = self.warnings
|
||||
return d
|
||||
|
|
@ -1113,3 +1152,13 @@ def get_user_format(date_format):
|
|||
.replace("%m", "mm")
|
||||
.replace("%d", "dd")
|
||||
)
|
||||
|
||||
def df_as_json(df):
|
||||
return {
|
||||
'fieldname': df.fieldname,
|
||||
'fieldtype': df.fieldtype,
|
||||
'label': df.label,
|
||||
'options': df.options,
|
||||
'parent': df.parent,
|
||||
'default': df.default
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,16 +22,28 @@ class Role(Document):
|
|||
frappe.db.sql("delete from `tabHas Role` where role = %s", self.name)
|
||||
frappe.clear_cache()
|
||||
|
||||
def on_update(self):
|
||||
'''update system user desk access if this has changed in this update'''
|
||||
if frappe.flags.in_install: return
|
||||
if self.has_value_changed('desk_access'):
|
||||
for user_name in get_users(self.name):
|
||||
user = frappe.get_doc('User', user_name)
|
||||
user_type = user.user_type
|
||||
user.set_system_user()
|
||||
if user_type != user.user_type:
|
||||
user.save()
|
||||
|
||||
# Get email addresses of all users that have been assigned this role
|
||||
def get_emails_from_role(role):
|
||||
emails = []
|
||||
|
||||
users = frappe.get_list("Has Role", filters={"role": role, "parenttype": "User"},
|
||||
fields=["parent"])
|
||||
|
||||
for user in users:
|
||||
user_email, enabled = frappe.db.get_value("User", user.parent, ["email", "enabled"])
|
||||
for user in get_users(role):
|
||||
user_email, enabled = frappe.db.get_value("User", user, ["email", "enabled"])
|
||||
if enabled and user_email not in ["admin@example.com", "guest@example.com"]:
|
||||
emails.append(user_email)
|
||||
|
||||
return emails
|
||||
return emails
|
||||
|
||||
def get_users(role):
|
||||
return [d.parent for d in frappe.get_all("Has Role", filters={"role": role, "parenttype": "User"},
|
||||
fields=["parent"])]
|
||||
|
|
|
|||
|
|
@ -23,3 +23,28 @@ class TestUser(unittest.TestCase):
|
|||
|
||||
frappe.get_doc("User", "test@example.com").add_roles("_Test Role 3")
|
||||
self.assertTrue("_Test Role 3" in frappe.get_roles("test@example.com"))
|
||||
|
||||
def test_change_desk_access(self):
|
||||
'''if we change desk acecss from role, remove from user'''
|
||||
frappe.delete_doc_if_exists('User', 'test-user-for-desk-access@example.com')
|
||||
frappe.delete_doc_if_exists('Role', 'desk-access-test')
|
||||
user = frappe.get_doc(dict(
|
||||
doctype='User',
|
||||
email='test-user-for-desk-access@example.com',
|
||||
first_name='test')).insert()
|
||||
role = frappe.get_doc(dict(
|
||||
doctype = 'Role',
|
||||
role_name = 'desk-access-test',
|
||||
desk_access = 0
|
||||
)).insert()
|
||||
user.add_roles(role.name)
|
||||
user.save()
|
||||
self.assertTrue(user.user_type=='Website User')
|
||||
role.desk_access = 1
|
||||
role.save()
|
||||
user.reload()
|
||||
self.assertTrue(user.user_type=='System User')
|
||||
role.desk_access = 0
|
||||
role.save()
|
||||
user.reload()
|
||||
self.assertTrue(user.user_type=='Website User')
|
||||
|
|
|
|||
|
|
@ -811,6 +811,7 @@ def reset_password(user):
|
|||
frappe.clear_messages()
|
||||
return 'not found'
|
||||
|
||||
@frappe.whitelist()
|
||||
def user_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
from frappe.desk.reportview import get_match_cond
|
||||
|
||||
|
|
|
|||
|
|
@ -5,23 +5,23 @@ from __future__ import unicode_literals
|
|||
import frappe
|
||||
from frappe import _, throw
|
||||
import frappe.utils.user
|
||||
from frappe.permissions import check_admin_or_system_manager
|
||||
from frappe.permissions import check_admin_or_system_manager, rights
|
||||
from frappe.model import data_fieldtypes
|
||||
|
||||
def execute(filters=None):
|
||||
user, doctype, show_permissions = filters.get("user"), filters.get("doctype"), filters.get("show_permissions")
|
||||
|
||||
if not validate(user, doctype): return [], []
|
||||
|
||||
columns, fields = get_columns_and_fields(doctype)
|
||||
data = frappe.get_list(doctype, fields=fields, as_list=True, user=user)
|
||||
|
||||
if show_permissions:
|
||||
columns = columns + ["Read", "Write", "Create", "Delete", "Submit", "Cancel", "Amend", "Print", "Email",
|
||||
"Report", "Import", "Export", "Share"]
|
||||
columns = columns + [frappe.unscrub(right) + ':Check:80' for right in rights]
|
||||
data = list(data)
|
||||
for i,item in enumerate(data):
|
||||
temp = frappe.permissions.get_doc_permissions(frappe.get_doc(doctype, item[0]), False,user)
|
||||
data[i] = item+(temp.get("read"),temp.get("write"),temp.get("create"),temp.get("delete"),temp.get("submit"),temp.get("cancel"),temp.get("amend"),temp.get("print"),temp.get("email"),temp.get("report"),temp.get("import"),temp.get("export"),temp.get("share"),)
|
||||
for i, doc in enumerate(data):
|
||||
permission = frappe.permissions.get_doc_permissions(frappe.get_doc(doctype, doc[0]), user)
|
||||
data[i] = doc + tuple(permission.get(right) for right in rights)
|
||||
|
||||
return columns, data
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import frappe, subprocess, os
|
||||
from six.moves import input
|
||||
|
||||
def setup_database(force, source_sql, verbose):
|
||||
def setup_database(force, source_sql=None, verbose=False):
|
||||
root_conn = get_root_connection()
|
||||
root_conn.commit()
|
||||
root_conn.sql("DROP DATABASE IF EXISTS `{0}`".format(frappe.conf.db_name))
|
||||
|
|
@ -16,10 +16,12 @@ def setup_database(force, source_sql, verbose):
|
|||
subprocess_env = os.environ.copy()
|
||||
subprocess_env['PGPASSWORD'] = str(frappe.conf.db_password)
|
||||
# bootstrap db
|
||||
if not source_sql:
|
||||
source_sql = os.path.join(os.path.dirname(__file__), 'framework_postgres.sql')
|
||||
|
||||
subprocess.check_output([
|
||||
'psql', frappe.conf.db_name, '-h', frappe.conf.db_host or 'localhost', '-U',
|
||||
frappe.conf.db_name, '-f',
|
||||
os.path.join(os.path.dirname(__file__), 'framework_postgres.sql')
|
||||
frappe.conf.db_name, '-f', source_sql
|
||||
], env=subprocess_env)
|
||||
|
||||
frappe.connect()
|
||||
|
|
|
|||
|
|
@ -259,7 +259,8 @@ def get_aggregate_function(chart_type):
|
|||
def get_result(data, timegrain, from_date, to_date):
|
||||
start_date = getdate(from_date)
|
||||
end_date = getdate(to_date)
|
||||
result = []
|
||||
|
||||
result = [[start_date, 0.0]]
|
||||
|
||||
while start_date < end_date:
|
||||
next_date = get_next_expected_date(start_date, timegrain)
|
||||
|
|
@ -277,11 +278,8 @@ def get_result(data, timegrain, from_date, to_date):
|
|||
|
||||
def get_next_expected_date(date, timegrain):
|
||||
next_date = None
|
||||
if timegrain=='Daily':
|
||||
next_date = add_to_date(date, days=1)
|
||||
else:
|
||||
# given date is always assumed to be the period ending date
|
||||
next_date = get_period_ending(add_to_date(date, days=1), timegrain)
|
||||
# given date is always assumed to be the period ending date
|
||||
next_date = get_period_ending(add_to_date(date, days=1), timegrain)
|
||||
return getdate(next_date)
|
||||
|
||||
def get_period_ending(date, timegrain):
|
||||
|
|
|
|||
|
|
@ -4,13 +4,12 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import unittest, frappe
|
||||
from frappe.utils import getdate, formatdate
|
||||
from frappe.utils import getdate, formatdate, get_last_day
|
||||
from frappe.desk.doctype.dashboard_chart.dashboard_chart import (get,
|
||||
get_period_ending)
|
||||
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import calendar
|
||||
|
||||
class TestDashboardChart(unittest.TestCase):
|
||||
def test_period_ending(self):
|
||||
|
|
@ -35,9 +34,6 @@ class TestDashboardChart(unittest.TestCase):
|
|||
self.assertEqual(get_period_ending('2019-10-01', 'Quarterly'),
|
||||
getdate('2019-12-31'))
|
||||
|
||||
self.assertEqual(get_period_ending('2019-10-01', 'Yearly'),
|
||||
getdate('2019-12-31'))
|
||||
|
||||
def test_dashboard_chart(self):
|
||||
if frappe.db.exists('Dashboard Chart', 'Test Dashboard Chart'):
|
||||
frappe.delete_doc('Dashboard Chart', 'Test Dashboard Chart')
|
||||
|
|
@ -50,22 +46,24 @@ class TestDashboardChart(unittest.TestCase):
|
|||
based_on = 'creation',
|
||||
timespan = 'Last Year',
|
||||
time_interval = 'Monthly',
|
||||
filters_json = '[]',
|
||||
filters_json = '{}',
|
||||
timeseries = 1
|
||||
)).insert()
|
||||
|
||||
cur_date = datetime.now() - relativedelta(years=1)
|
||||
|
||||
result = get(chart_name ='Test Dashboard Chart', refresh = 1)
|
||||
for idx in range(13):
|
||||
month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1]))
|
||||
result = get(chart_name='Test Dashboard Chart', refresh=1)
|
||||
self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d')))
|
||||
|
||||
if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')):
|
||||
cur_date += relativedelta(months=1)
|
||||
|
||||
for idx in range(1, 13):
|
||||
month = get_last_day(cur_date)
|
||||
month = formatdate(month.strftime('%Y-%m-%d'))
|
||||
self.assertEqual(result.get('labels')[idx], month)
|
||||
cur_date += relativedelta(months=1)
|
||||
|
||||
# self.assertEqual(result.get('datasets')[0].get('values')[:-1],
|
||||
# [44, 28, 8, 11, 2, 6, 18, 6, 4, 5, 15, 13])
|
||||
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_empty_dashboard_chart(self):
|
||||
|
|
@ -88,9 +86,14 @@ class TestDashboardChart(unittest.TestCase):
|
|||
|
||||
cur_date = datetime.now() - relativedelta(years=1)
|
||||
|
||||
result = get(chart_name ='Test Empty Dashboard Chart', refresh = 1)
|
||||
for idx in range(13):
|
||||
month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1]))
|
||||
result = get(chart_name ='Test Empty Dashboard Chart', refresh=1)
|
||||
self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d')))
|
||||
|
||||
if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')):
|
||||
cur_date += relativedelta(months=1)
|
||||
|
||||
for idx in range(1, 13):
|
||||
month = get_last_day(cur_date)
|
||||
month = formatdate(month.strftime('%Y-%m-%d'))
|
||||
self.assertEqual(result.get('labels')[idx], month)
|
||||
cur_date += relativedelta(months=1)
|
||||
|
|
@ -121,8 +124,13 @@ class TestDashboardChart(unittest.TestCase):
|
|||
cur_date = datetime.now() - relativedelta(years=1)
|
||||
|
||||
result = get(chart_name ='Test Empty Dashboard Chart 2', refresh = 1)
|
||||
for idx in range(13):
|
||||
month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1]))
|
||||
self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d')))
|
||||
|
||||
if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')):
|
||||
cur_date += relativedelta(months=1)
|
||||
|
||||
for idx in range(1, 13):
|
||||
month = get_last_day(cur_date)
|
||||
month = formatdate(month.strftime('%Y-%m-%d'))
|
||||
self.assertEqual(result.get('labels')[idx], month)
|
||||
cur_date += relativedelta(months=1)
|
||||
|
|
@ -132,6 +140,60 @@ class TestDashboardChart(unittest.TestCase):
|
|||
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_group_by_chart_type(self):
|
||||
if frappe.db.exists('Dashboard Chart', 'Test Group By Dashboard Chart'):
|
||||
frappe.delete_doc('Dashboard Chart', 'Test Group By Dashboard Chart')
|
||||
|
||||
frappe.get_doc({"doctype":"ToDo", "description": "test"}).insert()
|
||||
|
||||
frappe.get_doc(dict(
|
||||
doctype = 'Dashboard Chart',
|
||||
chart_name = 'Test Group By Dashboard Chart',
|
||||
chart_type = 'Group By',
|
||||
document_type = 'ToDo',
|
||||
group_by_based_on = 'status',
|
||||
filters_json = '[]',
|
||||
)).insert()
|
||||
|
||||
result = get(chart_name ='Test Group By Dashboard Chart', refresh = 1)
|
||||
todo_status_count = frappe.db.count('ToDo', {'status': result.get('labels')[0]})
|
||||
|
||||
self.assertEqual(result.get('datasets')[0].get('values')[0], todo_status_count)
|
||||
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_daily_dashboard_chart(self):
|
||||
insert_test_records()
|
||||
|
||||
if frappe.db.exists('Dashboard Chart', 'Test Daily Dashboard Chart'):
|
||||
frappe.delete_doc('Dashboard Chart', 'Test Daily Dashboard Chart')
|
||||
|
||||
frappe.get_doc(dict(
|
||||
doctype = 'Dashboard Chart',
|
||||
chart_name = 'Test Daily Dashboard Chart',
|
||||
chart_type = 'Sum',
|
||||
document_type = 'Communication',
|
||||
based_on = 'communication_date',
|
||||
value_based_on = 'rating',
|
||||
timespan = 'Select Date Range',
|
||||
time_interval = 'Daily',
|
||||
from_date = datetime(2019, 1, 6),
|
||||
to_date = datetime(2019, 1, 11),
|
||||
filters_json = '[]',
|
||||
timeseries = 1
|
||||
)).insert()
|
||||
|
||||
result = get(chart_name ='Test Daily Dashboard Chart', refresh = 1)
|
||||
|
||||
self.assertEqual(result.get('datasets')[0].get('values'), [200.0, 400.0, 300.0, 0.0, 100.0, 0.0])
|
||||
self.assertEqual(
|
||||
result.get('labels'),
|
||||
[formatdate('2019-01-06'), formatdate('2019-01-07'), formatdate('2019-01-08'),\
|
||||
formatdate('2019-01-09'), formatdate('2019-01-10'), formatdate('2019-01-11')]
|
||||
)
|
||||
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_weekly_dashboard_chart(self):
|
||||
insert_test_records()
|
||||
|
||||
|
|
@ -155,37 +217,18 @@ class TestDashboardChart(unittest.TestCase):
|
|||
|
||||
result = get(chart_name ='Test Weekly Dashboard Chart', refresh = 1)
|
||||
|
||||
self.assertEqual(result.get('datasets')[0].get('values'), [200.0, 400.0, 0.0])
|
||||
self.assertEqual(result.get('labels'), [formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')])
|
||||
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_group_by_chart_type(self):
|
||||
if frappe.db.exists('Dashboard Chart', 'Test Group By Dashboard Chart'):
|
||||
frappe.delete_doc('Dashboard Chart', 'Test Group By Dashboard Chart')
|
||||
|
||||
frappe.get_doc({"doctype":"ToDo", "description": "test"}).insert()
|
||||
|
||||
frappe.get_doc(dict(
|
||||
doctype = 'Dashboard Chart',
|
||||
chart_name = 'Test Group By Dashboard Chart',
|
||||
chart_type = 'Group By',
|
||||
document_type = 'ToDo',
|
||||
group_by_based_on = 'status',
|
||||
filters_json = '[]',
|
||||
)).insert()
|
||||
|
||||
result = get(chart_name ='Test Group By Dashboard Chart', refresh = 1)
|
||||
todo_status_count = frappe.db.count('ToDo', {'status': result.get('labels')[0]})
|
||||
|
||||
self.assertEqual(result.get('datasets')[0].get('values')[0], todo_status_count)
|
||||
self.assertEqual(result.get('datasets')[0].get('values'), [50.0, 300.0, 800.0, 0.0])
|
||||
self.assertEqual(result.get('labels'), [formatdate('2018-12-30'), formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')])
|
||||
|
||||
frappe.db.rollback()
|
||||
|
||||
def insert_test_records():
|
||||
create_new_communication(datetime(2019, 1, 10), 100)
|
||||
create_new_communication(datetime(2018, 12, 30), 50)
|
||||
create_new_communication(datetime(2019, 1, 4), 100)
|
||||
create_new_communication(datetime(2019, 1, 6), 200)
|
||||
create_new_communication(datetime(2019, 1, 7), 400)
|
||||
create_new_communication(datetime(2019, 1, 8), 300)
|
||||
create_new_communication(datetime(2019, 1, 10), 100)
|
||||
|
||||
def create_new_communication(date, rating):
|
||||
communication = {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from frappe.modules import load_doctype_module
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_submitted_linked_docs(doctype, name, docs=None, linked=None):
|
||||
def get_submitted_linked_docs(doctype, name, docs=None, visited=None):
|
||||
"""
|
||||
Get all nested submitted linked doctype linkinfo
|
||||
|
||||
|
|
@ -31,26 +31,27 @@ def get_submitted_linked_docs(doctype, name, docs=None, linked=None):
|
|||
if not docs:
|
||||
docs = []
|
||||
|
||||
if not linked:
|
||||
linked = {}
|
||||
if not visited:
|
||||
visited = {}
|
||||
|
||||
if doctype not in visited:
|
||||
visited[doctype] = []
|
||||
|
||||
if name in visited[doctype]:
|
||||
return
|
||||
|
||||
linkinfo = get_linked_doctypes(doctype)
|
||||
linked_docs = get_linked_docs(doctype, name, linkinfo)
|
||||
|
||||
link_count = 0
|
||||
visited[doctype].append(name)
|
||||
|
||||
for link_doctype, link_names in linked_docs.items():
|
||||
if link_doctype not in linked:
|
||||
linked[link_doctype] = []
|
||||
|
||||
for link in link_names:
|
||||
if link['name'] == name:
|
||||
continue
|
||||
|
||||
if linked and name in linked[link_doctype]:
|
||||
continue
|
||||
|
||||
linked[link_doctype].append(link['name'])
|
||||
|
||||
docinfo = link.update({"doctype": link_doctype})
|
||||
validated_doc = validate_linked_doc(docinfo)
|
||||
|
||||
|
|
@ -58,16 +59,15 @@ def get_submitted_linked_docs(doctype, name, docs=None, linked=None):
|
|||
continue
|
||||
|
||||
link_count += 1
|
||||
if link.name in [doc.get("name") for doc in docs]:
|
||||
continue
|
||||
|
||||
links = get_submitted_linked_docs(link_doctype, link.name, docs, linked)
|
||||
docs.append({
|
||||
"doctype": link_doctype,
|
||||
"name": link.name,
|
||||
"docstatus": link.docstatus,
|
||||
"link_count": links.get("count")
|
||||
})
|
||||
links = get_submitted_linked_docs(link_doctype, link.name, docs, visited)
|
||||
if links:
|
||||
docs.append({
|
||||
"doctype": link_doctype,
|
||||
"name": link.name,
|
||||
"docstatus": link.docstatus,
|
||||
"link_count": links.get("count")
|
||||
})
|
||||
|
||||
# sort linked documents by ascending number of links
|
||||
docs.sort(key=lambda doc: doc.get("link_count"))
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ def get_docinfo(doc=None, doctype=None, name=None):
|
|||
"shared": frappe.share.get_users(doc.doctype, doc.name),
|
||||
"views": get_view_logs(doc.doctype, doc.name),
|
||||
"energy_point_logs": get_point_logs(doc.doctype, doc.name),
|
||||
"additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name),
|
||||
"milestones": get_milestones(doc.doctype, doc.name),
|
||||
"is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user),
|
||||
"tags": get_tags(doc.doctype, doc.name),
|
||||
|
|
@ -277,3 +278,14 @@ def get_document_email(doctype, name):
|
|||
|
||||
def get_automatic_email_link():
|
||||
return frappe.db.get_value("Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id")
|
||||
|
||||
def get_additional_timeline_content(doctype, docname):
|
||||
contents = []
|
||||
hooks = frappe.get_hooks().get('additional_timeline_content', {})
|
||||
methods_for_all_doctype = hooks.get('*', [])
|
||||
methods_for_current_doctype = hooks.get(doctype, [])
|
||||
|
||||
for method in methods_for_all_doctype + methods_for_current_doctype:
|
||||
contents.extend(frappe.get_attr(method)(doctype, docname) or [])
|
||||
|
||||
return contents
|
||||
|
|
@ -6,6 +6,7 @@ from __future__ import unicode_literals
|
|||
import frappe, json
|
||||
from frappe.utils import cstr, unique, cint
|
||||
from frappe.permissions import has_permission
|
||||
from frappe.handler import is_whitelisted
|
||||
from frappe import _
|
||||
from six import string_types
|
||||
import re
|
||||
|
|
@ -74,6 +75,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
|
|||
|
||||
if query and query.split()[0].lower()!="select":
|
||||
# by method
|
||||
is_whitelisted(query)
|
||||
frappe.response["values"] = frappe.call(query, doctype, txt,
|
||||
searchfield, start, page_length, filters, as_dict=as_dict)
|
||||
elif not query and doctype in standard_queries:
|
||||
|
|
|
|||
|
|
@ -274,6 +274,8 @@ class EmailAccount(Document):
|
|||
|
||||
for idx, msg in enumerate(incoming_mails):
|
||||
uid = None if not uid_list else uid_list[idx]
|
||||
self.flags.notify = True
|
||||
|
||||
try:
|
||||
args = {
|
||||
"uid": uid,
|
||||
|
|
@ -294,7 +296,11 @@ class EmailAccount(Document):
|
|||
|
||||
else:
|
||||
frappe.db.commit()
|
||||
if communication:
|
||||
if communication and self.flags.notify:
|
||||
|
||||
# If email already exists in the system
|
||||
# then do not send notifications for the same email.
|
||||
|
||||
attachments = []
|
||||
|
||||
if hasattr(communication, '_attachments'):
|
||||
|
|
@ -363,6 +369,9 @@ class EmailAccount(Document):
|
|||
name = names[0].get("name")
|
||||
# email is already available update communication uid instead
|
||||
frappe.db.set_value("Communication", name, "uid", uid, update_modified=False)
|
||||
|
||||
self.flags.notify = False
|
||||
|
||||
return frappe.get_doc("Communication", name)
|
||||
|
||||
if email.content_type == 'text/html':
|
||||
|
|
|
|||
|
|
@ -347,7 +347,7 @@ def flush(from_test=False):
|
|||
if not smtpserver:
|
||||
smtpserver = SMTPServer()
|
||||
smtpserver_dict[email.sender] = smtpserver
|
||||
|
||||
|
||||
if from_test:
|
||||
send_one(email.name, smtpserver, auto_commit)
|
||||
else:
|
||||
|
|
@ -390,12 +390,12 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False):
|
|||
where
|
||||
name=%s
|
||||
for update''', email, as_dict=True)
|
||||
|
||||
|
||||
if len(email):
|
||||
email = email[0]
|
||||
else:
|
||||
return
|
||||
|
||||
|
||||
recipients_list = frappe.db.sql('''select name, recipient, status from
|
||||
`tabEmail Queue Recipient` where parent=%s''', email.name, as_dict=1)
|
||||
|
||||
|
|
@ -417,6 +417,8 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False):
|
|||
if email.communication:
|
||||
frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit)
|
||||
|
||||
email_sent_to_any_recipient = None
|
||||
|
||||
try:
|
||||
message = None
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ source_link = "https://github.com/frappe/frappe"
|
|||
app_license = "MIT"
|
||||
app_logo_url = '/assets/frappe/images/frappe-framework-logo.png'
|
||||
|
||||
develop_version = '12.x.x-develop'
|
||||
develop_version = '13.x.x-develop'
|
||||
|
||||
app_email = "info@frappe.io"
|
||||
|
||||
|
|
|
|||
|
|
@ -348,3 +348,34 @@ def extract_tar_files(site_name, file_path, folder_name):
|
|||
frappe.destroy()
|
||||
|
||||
return tar_path
|
||||
|
||||
def is_downgrade(sql_file_path, verbose=False):
|
||||
"""checks if input db backup will get downgraded on current bench"""
|
||||
from semantic_version import Version
|
||||
head = "INSERT INTO `tabInstalled Application` VALUES"
|
||||
|
||||
with open(sql_file_path) as f:
|
||||
for line in f:
|
||||
if head in line:
|
||||
# 'line' (str) format: ('2056588823','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',1,'frappe','v10.1.71-74 (3c50d5e) (v10.x.x)','v10.x.x'),('855c640b8e','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',2,'your_custom_app','0.0.1','master')
|
||||
line = line.strip().lstrip(head).rstrip(";").strip()
|
||||
# 'all_apps' (list) format: [('frappe', '12.x.x-develop ()', 'develop'), ('your_custom_app', '0.0.1', 'master')]
|
||||
all_apps = [ x[-3:] for x in frappe.safe_eval(line) ]
|
||||
|
||||
for app in all_apps:
|
||||
app_name = app[0]
|
||||
app_version = app[1].split(" ")[0]
|
||||
|
||||
if app_name == "frappe":
|
||||
try:
|
||||
current_version = Version(frappe.__version__)
|
||||
backup_version = Version(app_version[1:] if app_version[0] == "v" else app_version)
|
||||
except ValueError:
|
||||
return 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))
|
||||
|
||||
return downgrade
|
||||
|
|
|
|||
|
|
@ -222,11 +222,13 @@ def upload_system_backup_to_google_drive():
|
|||
return _("Google Drive Backup Successful.")
|
||||
|
||||
def daily_backup():
|
||||
if frappe.db.get_single_value("Google Drive", "frequency") == "Daily":
|
||||
drive_settings = frappe.db.get_singles_dict('Google Drive')
|
||||
if drive_settings.enable and drive_settings.frequency == "Daily":
|
||||
upload_system_backup_to_google_drive()
|
||||
|
||||
def weekly_backup():
|
||||
if frappe.db.get_single_value("Google Drive", "frequency") == "Weekly":
|
||||
drive_settings = frappe.db.get_singles_dict('Google Drive')
|
||||
if drive_settings.enable and drive_settings.frequency == "Weekly":
|
||||
upload_system_backup_to_google_drive()
|
||||
|
||||
def get_absolute_path(filename):
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ from botocore.exceptions import ClientError
|
|||
class S3BackupSettings(Document):
|
||||
|
||||
def validate(self):
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
if not self.endpoint_url:
|
||||
self.endpoint_url = 'https://s3.amazonaws.com'
|
||||
conn = boto3.client(
|
||||
|
|
|
|||
|
|
@ -396,6 +396,11 @@ class Document(BaseDocument):
|
|||
def get_doc_before_save(self):
|
||||
return getattr(self, '_doc_before_save', None)
|
||||
|
||||
def has_value_changed(self, fieldname):
|
||||
'''Returns true if value is changed before and after saving'''
|
||||
previous = self.get_doc_before_save()
|
||||
return previous.get(fieldname)!=self.get(fieldname) if previous else True
|
||||
|
||||
def set_new_name(self, force=False, set_name=None, set_child_names=True):
|
||||
"""Calls `frappe.naming.set_new_name` for parent and child docs."""
|
||||
if self.flags.name_set and not force:
|
||||
|
|
@ -825,7 +830,7 @@ class Document(BaseDocument):
|
|||
|
||||
def run_notifications(self, method):
|
||||
"""Run notifications for this method"""
|
||||
if frappe.flags.in_import or frappe.flags.in_patch or frappe.flags.in_install:
|
||||
if (frappe.flags.in_import and frappe.flags.mute_emails) or frappe.flags.in_patch or frappe.flags.in_install:
|
||||
return
|
||||
|
||||
if self.flags.notifications_executed==None:
|
||||
|
|
|
|||
|
|
@ -289,4 +289,4 @@ execute:frappe.delete_doc("DocType", "Onboarding Slide Field")
|
|||
execute:frappe.delete_doc("DocType", "Onboarding Slide Help Link")
|
||||
frappe.patches.v13_0.update_date_filters_in_user_settings
|
||||
frappe.patches.v13_0.update_duration_options
|
||||
frappe.patches.v13_0.replace_old_data_import
|
||||
frappe.patches.v13_0.replace_old_data_import # 2020-06-24
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ import frappe
|
|||
|
||||
|
||||
def execute():
|
||||
if not frappe.db.exists("DocType", "Data Import Beta"):
|
||||
return
|
||||
|
||||
frappe.db.sql("DROP TABLE IF EXISTS `tabData Import Legacy`")
|
||||
frappe.rename_doc('DocType', 'Data Import', 'Data Import Legacy')
|
||||
frappe.db.commit()
|
||||
frappe.db.sql("DROP TABLE IF EXISTS `tabData Import`")
|
||||
frappe.reload_doc("core", "doctype", "data_import")
|
||||
frappe.get_doc("DocType", "Data Import").on_update()
|
||||
frappe.delete_doc_if_exists("DocType", "Data Import Beta")
|
||||
frappe.rename_doc('DocType', 'Data Import Beta', 'Data Import')
|
||||
|
|
|
|||
|
|
@ -13,36 +13,6 @@ frappe.data_import.DataExporter = class DataExporter {
|
|||
this.dialog = new frappe.ui.Dialog({
|
||||
title: __('Export Data'),
|
||||
fields: [
|
||||
{
|
||||
fieldtype: 'Select',
|
||||
fieldname: 'exporting_for',
|
||||
label: __('Exporting For'),
|
||||
options: [
|
||||
{
|
||||
label: __('Insert New Records'),
|
||||
value: 'Insert New Records'
|
||||
},
|
||||
{
|
||||
label: __('Update Existing Records'),
|
||||
value: 'Update Existing Records'
|
||||
}
|
||||
],
|
||||
change: () => {
|
||||
let exporting_for = this.dialog.get_value('exporting_for');
|
||||
this.dialog.set_value(
|
||||
'export_records',
|
||||
exporting_for === 'Insert New Records' ? 'blank_template' : 'all'
|
||||
);
|
||||
|
||||
// Force ID field to be exported when updating existing records
|
||||
let id_field = this.dialog.get_field(this.doctype).options[0];
|
||||
if (id_field.value === 'name' && id_field.$checkbox) {
|
||||
id_field.$checkbox
|
||||
.find('input')
|
||||
.prop('disabled', exporting_for === 'Update Existing Records');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
fieldtype: 'Select',
|
||||
fieldname: 'export_records',
|
||||
|
|
@ -65,7 +35,7 @@ frappe.data_import.DataExporter = class DataExporter {
|
|||
value: 'blank_template'
|
||||
}
|
||||
],
|
||||
default: 'blank_template',
|
||||
default: this.exporting_for === 'Insert New Records' ? 'blank_template' : 'all',
|
||||
change: () => {
|
||||
this.update_record_count_message();
|
||||
}
|
||||
|
|
@ -119,10 +89,6 @@ frappe.data_import.DataExporter = class DataExporter {
|
|||
on_page_show: () => this.select_mandatory()
|
||||
});
|
||||
|
||||
if (this.exporting_for) {
|
||||
this.dialog.set_value('exporting_for', this.exporting_for);
|
||||
}
|
||||
|
||||
this.make_filter_area();
|
||||
this.make_select_all_buttons();
|
||||
this.update_record_count_message();
|
||||
|
|
@ -172,15 +138,17 @@ frappe.data_import.DataExporter = class DataExporter {
|
|||
}
|
||||
|
||||
make_select_all_buttons() {
|
||||
let for_insert = this.exporting_for === 'Insert New Records';
|
||||
let section_title = for_insert ? __('Select Fields To Insert') : __('Select Fields To Update');
|
||||
let $select_all_buttons = $(`
|
||||
<div>
|
||||
<h6 class="form-section-heading uppercase">${__('Select fields to export')}</h6>
|
||||
<h6 class="form-section-heading uppercase">${section_title}</h6>
|
||||
<button class="btn btn-default btn-xs" data-action="select_all">
|
||||
${__('Select All')}
|
||||
</button>
|
||||
<button class="btn btn-default btn-xs" data-action="select_mandatory">
|
||||
${for_insert ? `<button class="btn btn-default btn-xs" data-action="select_mandatory">
|
||||
${__('Select Mandatory')}
|
||||
</button>
|
||||
</button>`: ''}
|
||||
<button class="btn btn-default btn-xs" data-action="unselect_all">
|
||||
${__('Unselect All')}
|
||||
</button>
|
||||
|
|
@ -285,11 +253,9 @@ frappe.data_import.DataExporter = class DataExporter {
|
|||
}
|
||||
|
||||
get_filters() {
|
||||
return this.filter_group.get_filters().reduce((acc, filter) => {
|
||||
return Object.assign(acc, {
|
||||
[filter[1]]: [filter[2], filter[3]]
|
||||
});
|
||||
}, {});
|
||||
return this.filter_group.get_filters().map(filter => {
|
||||
return filter.slice(0, 4);
|
||||
});
|
||||
}
|
||||
|
||||
get_multicheck_options(doctype, child_fieldname = null) {
|
||||
|
|
@ -308,6 +274,9 @@ frappe.data_import.DataExporter = class DataExporter {
|
|||
? this.column_map[child_fieldname]
|
||||
: this.column_map[doctype];
|
||||
|
||||
let is_field_mandatory = df => (df.fieldname === 'name' && !child_fieldname)
|
||||
|| (df.reqd && this.exporting_for == 'Insert New Records');
|
||||
|
||||
return fields
|
||||
.filter(df => {
|
||||
if (autoname_field && df.fieldname === autoname_field.fieldname) {
|
||||
|
|
@ -323,7 +292,7 @@ frappe.data_import.DataExporter = class DataExporter {
|
|||
return {
|
||||
label,
|
||||
value: df.fieldname,
|
||||
danger: df.reqd,
|
||||
danger: is_field_mandatory(df),
|
||||
checked: false,
|
||||
description: `${df.fieldname} ${df.reqd ? __('(Mandatory)') : ''}`
|
||||
};
|
||||
|
|
|
|||
|
|
@ -245,11 +245,12 @@ frappe.data_import.ImportPreview = class ImportPreview {
|
|||
let fieldname;
|
||||
if (!df) {
|
||||
fieldname = null;
|
||||
} else if (col.map_to_field) {
|
||||
fieldname = col.map_to_field;
|
||||
} else if (col.is_child_table_field) {
|
||||
fieldname = `${col.child_table_df.fieldname}.${df.fieldname}`;
|
||||
} else {
|
||||
fieldname =
|
||||
df.parent === this.doctype
|
||||
? df.fieldname
|
||||
: `${df.parent}:${df.fieldname}`;
|
||||
fieldname = df.fieldname;
|
||||
}
|
||||
return [
|
||||
{
|
||||
|
|
@ -272,7 +273,7 @@ frappe.data_import.ImportPreview = class ImportPreview {
|
|||
label: __("Don't Import"),
|
||||
value: "Don't Import"
|
||||
}
|
||||
].concat(column_picker_fields.get_fields_as_options()),
|
||||
].concat(get_fields_as_options(this.doctype, column_picker_fields)),
|
||||
default: fieldname || "Don't Import",
|
||||
change() {
|
||||
changed.push(i);
|
||||
|
|
@ -328,3 +329,29 @@ frappe.data_import.ImportPreview = class ImportPreview {
|
|||
});
|
||||
}
|
||||
};
|
||||
|
||||
function get_fields_as_options(doctype, column_map) {
|
||||
let keys = [doctype];
|
||||
frappe.meta.get_table_fields(doctype).forEach(df => {
|
||||
keys.push(df.fieldname);
|
||||
});
|
||||
// flatten array
|
||||
return [].concat(
|
||||
...keys.map(key => {
|
||||
return column_map[key].map(df => {
|
||||
let label = df.label;
|
||||
let value = df.fieldname;
|
||||
if (doctype !== key) {
|
||||
let table_field = frappe.meta.get_docfield(doctype, key);
|
||||
label = `${df.label} (${table_field.label})`;
|
||||
value = `${table_field.fieldname}.${df.fieldname}`;
|
||||
}
|
||||
return {
|
||||
label,
|
||||
value,
|
||||
description: value
|
||||
};
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
@ -91,12 +91,26 @@ frappe.db = {
|
|||
});
|
||||
},
|
||||
count: function(doctype, args={}) {
|
||||
return new Promise(resolve => {
|
||||
frappe.call({
|
||||
method: 'frappe.client.get_count',
|
||||
type: 'GET',
|
||||
args: Object.assign(args, { doctype })
|
||||
}).then(r => resolve(r.message));
|
||||
let filters = args.filters || {};
|
||||
const with_child_table_filter = Array.isArray(filters) && filters.some(filter => {
|
||||
return filter[0] !== doctype;
|
||||
});
|
||||
|
||||
const fields = [
|
||||
// cannot break this line as it adds extra \n's and \t's which breaks the query
|
||||
`count(${with_child_table_filter ? 'distinct': ''} ${frappe.model.get_full_column_name('name', doctype)}) AS total_count`
|
||||
];
|
||||
|
||||
return frappe.call({
|
||||
type: 'GET',
|
||||
method: 'frappe.desk.reportview.get',
|
||||
args: {
|
||||
doctype,
|
||||
filters,
|
||||
fields,
|
||||
}
|
||||
}).then(r => {
|
||||
return r.message.values[0][0];
|
||||
});
|
||||
},
|
||||
get_link_options(doctype, txt = '', filters={}) {
|
||||
|
|
|
|||
|
|
@ -120,9 +120,11 @@ frappe.ui.form.Timeline = class Timeline {
|
|||
display_automatic_link_email() {
|
||||
let docinfo = this.frm.get_docinfo();
|
||||
|
||||
if (docinfo.document_email){
|
||||
if (docinfo.document_email) {
|
||||
let link = __("Send an email to {0} to link it here", [`<b><a class="timeline-email-import-link copy-to-clipboard">${docinfo.document_email}</a></b>`]);
|
||||
$('.timeline-email-import').html(link);
|
||||
const email_link = $('.timeline-email-import');
|
||||
email_link.removeClass('hide');
|
||||
email_link.html(link);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -180,12 +182,15 @@ frappe.ui.form.Timeline = class Timeline {
|
|||
// append energy point logs
|
||||
timeline = timeline.concat(this.get_energy_point_logs());
|
||||
|
||||
// custom contents
|
||||
timeline = timeline.concat(this.get_additional_timeline_content());
|
||||
|
||||
// append milestones
|
||||
timeline = timeline.concat(this.get_milestones());
|
||||
|
||||
// sort
|
||||
timeline
|
||||
.filter(a => a.content)
|
||||
.filter(a => a.content || a.template)
|
||||
.sort((b, c) => me.compare_dates(b, c))
|
||||
.forEach(d => {
|
||||
d.frm = me.frm;
|
||||
|
|
@ -407,7 +412,10 @@ frappe.ui.form.Timeline = class Timeline {
|
|||
c.original_content = c.content;
|
||||
c.content = frappe.utils.toggle_blockquote(c.content);
|
||||
}
|
||||
if(!frappe.utils.is_html(c.content)) {
|
||||
|
||||
if (c.template) {
|
||||
c.content_html = frappe.render_template(c.template, c.template_data);
|
||||
} else if (!frappe.utils.is_html(c.content)) {
|
||||
c.content_html = frappe.markdown(__(c.content));
|
||||
} else {
|
||||
c.content_html = c.content;
|
||||
|
|
@ -529,6 +537,10 @@ frappe.ui.form.Timeline = class Timeline {
|
|||
return energy_point_logs;
|
||||
}
|
||||
|
||||
get_additional_timeline_content() {
|
||||
return this.frm.get_docinfo().additional_timeline_content || [];
|
||||
}
|
||||
|
||||
get_milestones() {
|
||||
let milestones = this.frm.get_docinfo().milestones;
|
||||
milestones.map(log => {
|
||||
|
|
|
|||
|
|
@ -101,19 +101,25 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
columns[1] = [];
|
||||
columns[2] = [];
|
||||
|
||||
Object.keys(this.setters).forEach((setter, index) => {
|
||||
let df_prop = frappe.meta.docfield_map[this.doctype][setter];
|
||||
|
||||
// Index + 1 to start filling from index 1
|
||||
// Since Search is a standrd field already pushed
|
||||
columns[(index + 1) % 3].push({
|
||||
fieldtype: df_prop.fieldtype,
|
||||
label: df_prop.label,
|
||||
fieldname: setter,
|
||||
options: df_prop.options,
|
||||
default: this.setters[setter]
|
||||
if ($.isArray(this.setters)) {
|
||||
this.setters.forEach((setter, index) => {
|
||||
columns[(index + 1) % 3].push(setter);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
Object.keys(this.setters).forEach((setter, index) => {
|
||||
let df_prop = frappe.meta.docfield_map[this.doctype][setter];
|
||||
|
||||
// Index + 1 to start filling from index 1
|
||||
// Since Search is a standrd field already pushed
|
||||
columns[(index + 1) % 3].push({
|
||||
fieldtype: df_prop.fieldtype,
|
||||
label: df_prop.label,
|
||||
fieldname: setter,
|
||||
options: df_prop.options,
|
||||
default: this.setters[setter]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal
|
||||
if (Object.seal) {
|
||||
|
|
@ -217,7 +223,13 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
let contents = ``;
|
||||
let columns = ["name"];
|
||||
|
||||
columns = columns.concat(Object.keys(this.setters));
|
||||
if ($.isArray(this.setters)) {
|
||||
for (let df of this.setters) {
|
||||
columns.push(df.fieldname);
|
||||
}
|
||||
} else {
|
||||
columns = columns.concat(Object.keys(this.setters));
|
||||
}
|
||||
|
||||
columns.forEach(function (column) {
|
||||
contents += `<div class="list-item__content ellipsis">
|
||||
|
|
@ -290,16 +302,24 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
let filters = this.get_query ? this.get_query().filters : {} || {};
|
||||
let filter_fields = [];
|
||||
|
||||
Object.keys(this.setters).forEach(function (setter) {
|
||||
var value = me.dialog.fields_dict[setter].get_value();
|
||||
if (me.dialog.fields_dict[setter].df.fieldtype == "Data" && value) {
|
||||
filters[setter] = ["like", "%" + value + "%"];
|
||||
} else {
|
||||
filters[setter] = value || undefined;
|
||||
me.args[setter] = filters[setter];
|
||||
filter_fields.push(setter);
|
||||
if ($.isArray(this.setters)) {
|
||||
for (let df of this.setters) {
|
||||
filters[df.fieldname] = me.dialog.fields_dict[df.fieldname].get_value() || undefined;
|
||||
me.args[df.fieldname] = filters[df.fieldname];
|
||||
filter_fields.push(df.fieldname);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Object.keys(this.setters).forEach(function (setter) {
|
||||
var value = me.dialog.fields_dict[setter].get_value();
|
||||
if (me.dialog.fields_dict[setter].df.fieldtype == "Data" && value) {
|
||||
filters[setter] = ["like", "%" + value + "%"];
|
||||
} else {
|
||||
filters[setter] = value || undefined;
|
||||
me.args[setter] = filters[setter];
|
||||
filter_fields.push(setter);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let filter_group = this.get_custom_filters();
|
||||
Object.assign(filters, filter_group);
|
||||
|
|
|
|||
|
|
@ -17,9 +17,7 @@
|
|||
{% } %}
|
||||
{% } %}
|
||||
</div>
|
||||
<div class="timeline-email-import text-muted small">
|
||||
|
||||
</div>
|
||||
<div class="timeline-email-import text-muted small hide"></div>
|
||||
<div class="timeline-items">
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<div class="media timeline-item {% if (data.user_content) { %} user-content {% } else { %} notification-content {% } %} {{ data.color || "" }}"
|
||||
<div class="media timeline-item
|
||||
{% if (data.user_content || data.template) { %} user-content {% } else { %} notification-content {% } %}
|
||||
{% if (data.template) { %} show-indicator {% }%} {{ data.color || "" }}"
|
||||
data-doctype="{{ data.doctype }}" data-name="{{ data.name }}" data-communication-type = "{{ data.communication_type }}">
|
||||
{% if (data.user_content) { %}
|
||||
<span class="pull-left avatar avatar-medium hidden-xs" style="margin-top: 1px">
|
||||
|
|
@ -186,7 +188,7 @@
|
|||
{% } %}
|
||||
{%= __("Liked by {0}", [data.fullname]) %}
|
||||
</span>
|
||||
{% } else if (data.comment_type == "Energy Points") { %}
|
||||
{% } else if (data.comment_type == "Energy Points" || data.template) { %}
|
||||
{{ data.content_html }}
|
||||
{% } else { %}
|
||||
<b title="{{ data.comment_by }}">{%= data.fullname %}</b>
|
||||
|
|
@ -200,8 +202,11 @@
|
|||
</a>
|
||||
{% } %}
|
||||
{% } %}
|
||||
<span class="text-muted commented-on" style="font-weight: normal;">
|
||||
– {%= data.comment_on %}</span>
|
||||
{% if (!data.template) { %}
|
||||
<span class="text-muted commented-on" style="font-weight: normal;">
|
||||
– {%= data.comment_on %}
|
||||
</span>
|
||||
{% } %}
|
||||
</div>
|
||||
{% } %}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -760,26 +760,10 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
let current_count = this.data.length;
|
||||
let count_without_children = this.data.uniqBy(d => d.name).length;
|
||||
|
||||
const filters = this.get_filters_for_args();
|
||||
const with_child_table_filter = filters.some(filter => {
|
||||
return filter[0] !== this.doctype;
|
||||
});
|
||||
|
||||
const fields = [
|
||||
// cannot break this line as it adds extra \n's and \t's which breaks the query
|
||||
`count(${with_child_table_filter ? 'distinct': ''}${frappe.model.get_full_column_name('name', this.doctype)}) AS total_count`
|
||||
];
|
||||
|
||||
return frappe.call({
|
||||
type: 'GET',
|
||||
method: this.method,
|
||||
args: {
|
||||
doctype: this.doctype,
|
||||
filters,
|
||||
fields,
|
||||
}
|
||||
}).then(r => {
|
||||
this.total_count = r.message.values[0][0] || current_count;
|
||||
return frappe.db.count(this.doctype, {
|
||||
filters: this.get_filters_for_args()
|
||||
}).then(total_count => {
|
||||
this.total_count = total_count || current_count;
|
||||
let str = __('{0} of {1}', [current_count, this.total_count]);
|
||||
if (count_without_children !== current_count) {
|
||||
str = __('{0} of {1} ({2} rows with children)', [count_without_children, this.total_count, current_count]);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
</div>
|
||||
<div class="col-sm-4 form-group">
|
||||
<div class="filter-field"></div>
|
||||
<div class="text-muted small filter-description"></div>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<div class="filter-actions">
|
||||
|
|
|
|||
|
|
@ -13,26 +13,26 @@ frappe.ui.Filter = class {
|
|||
|
||||
set_conditions() {
|
||||
this.conditions = [
|
||||
["=", __("Equals")],
|
||||
["!=", __("Not Equals")],
|
||||
["like", __("Like")],
|
||||
["not like", __("Not Like")],
|
||||
["in", __("In")],
|
||||
["not in", __("Not In")],
|
||||
["is", __("Is")],
|
||||
[">", ">"],
|
||||
["<", "<"],
|
||||
[">=", ">="],
|
||||
["<=", "<="],
|
||||
["Between", __("Between")],
|
||||
["Timespan", __("Timespan")],
|
||||
['=', __('Equals')],
|
||||
['!=', __('Not Equals')],
|
||||
['like', __('Like')],
|
||||
['not like', __('Not Like')],
|
||||
['in', __('In')],
|
||||
['not in', __('Not In')],
|
||||
['is', __('Is')],
|
||||
['>', '>'],
|
||||
['<', '<'],
|
||||
['>=', '>='],
|
||||
['<=', '<='],
|
||||
['Between', __('Between')],
|
||||
['Timespan', __('Timespan')],
|
||||
];
|
||||
|
||||
this.nested_set_conditions = [
|
||||
["descendants of", __("Descendants Of")],
|
||||
["not descendants of", __("Not Descendants Of")],
|
||||
["ancestors of", __("Ancestors Of")],
|
||||
["not ancestors of", __("Not Ancestors Of")],
|
||||
['descendants of', __('Descendants Of')],
|
||||
['not descendants of', __('Not Descendants Of')],
|
||||
['ancestors of', __('Ancestors Of')],
|
||||
['not ancestors of', __('Not Ancestors Of')],
|
||||
];
|
||||
|
||||
this.conditions.push(...this.nested_set_conditions);
|
||||
|
|
@ -42,10 +42,10 @@ frappe.ui.Filter = class {
|
|||
Datetime: ['like', 'not like'],
|
||||
Data: ['Between', 'Timespan'],
|
||||
Select: ['like', 'not like', 'Between', 'Timespan'],
|
||||
Link: ["Between", 'Timespan', '>', '<', '>=', '<='],
|
||||
Currency: ["Between", 'Timespan'],
|
||||
Color: ["Between", 'Timespan'],
|
||||
Check: this.conditions.map(c => c[0]).filter(c => c !== '=')
|
||||
Link: ['Between', 'Timespan', '>', '<', '>=', '<='],
|
||||
Currency: ['Between', 'Timespan'],
|
||||
Color: ['Between', 'Timespan'],
|
||||
Check: this.conditions.map((c) => c[0]).filter((c) => c !== '='),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -65,10 +65,11 @@ frappe.ui.Filter = class {
|
|||
}
|
||||
|
||||
make() {
|
||||
this.filter_edit_area = $(frappe.render_template("edit_filter", {
|
||||
conditions: this.conditions
|
||||
}))
|
||||
.appendTo(this.parent.find('.filter-edit-area'));
|
||||
this.filter_edit_area = $(
|
||||
frappe.render_template('edit_filter', {
|
||||
conditions: this.conditions,
|
||||
})
|
||||
).appendTo(this.parent.find('.filter-edit-area'));
|
||||
|
||||
this.make_select();
|
||||
this.set_events();
|
||||
|
|
@ -82,41 +83,51 @@ frappe.ui.Filter = class {
|
|||
filter_fields: this.filter_fields,
|
||||
select: (doctype, fieldname) => {
|
||||
this.set_field(doctype, fieldname);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if(this.fieldname) {
|
||||
if (this.fieldname) {
|
||||
this.fieldselect.set_value(this.doctype, this.fieldname);
|
||||
}
|
||||
}
|
||||
|
||||
set_events() {
|
||||
this.filter_edit_area.find("a.remove-filter").on("click", () => {
|
||||
this.filter_edit_area.find('a.remove-filter').on('click', () => {
|
||||
this.remove();
|
||||
});
|
||||
|
||||
this.filter_edit_area.find(".set-filter-and-run").on("click", () => {
|
||||
this.filter_edit_area.removeClass("new-filter");
|
||||
this.filter_edit_area.find('.set-filter-and-run').on('click', () => {
|
||||
this.filter_edit_area.removeClass('new-filter');
|
||||
this.on_change();
|
||||
this.update_filter_tag();
|
||||
});
|
||||
|
||||
this.filter_edit_area.find('.condition').change(() => {
|
||||
if(!this.field) return;
|
||||
if (!this.field) return;
|
||||
|
||||
let condition = this.get_condition();
|
||||
let fieldtype = null;
|
||||
|
||||
if(["in", "like", "not in", "not like"].includes(condition)) {
|
||||
if (['in', 'like', 'not in', 'not like'].includes(condition)) {
|
||||
fieldtype = 'Data';
|
||||
this.add_condition_help(condition);
|
||||
} else {
|
||||
this.filter_edit_area.find('.filter-description').empty();
|
||||
}
|
||||
|
||||
if (['Select', 'MultiSelect'].includes(this.field.df.fieldtype) && ["in", "not in"].includes(condition)) {
|
||||
if (
|
||||
['Select', 'MultiSelect'].includes(this.field.df.fieldtype) &&
|
||||
['in', 'not in'].includes(condition)
|
||||
) {
|
||||
fieldtype = 'MultiSelect';
|
||||
}
|
||||
|
||||
this.set_field(this.field.df.parent, this.field.df.fieldname, fieldtype, condition);
|
||||
this.set_field(
|
||||
this.field.df.parent,
|
||||
this.field.df.fieldname,
|
||||
fieldtype,
|
||||
condition
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -129,12 +140,12 @@ frappe.ui.Filter = class {
|
|||
setup_state(is_new) {
|
||||
let promise = Promise.resolve();
|
||||
if (is_new) {
|
||||
this.filter_edit_area.addClass("new-filter");
|
||||
this.filter_edit_area.addClass('new-filter');
|
||||
} else {
|
||||
promise = this.update_filter_tag();
|
||||
}
|
||||
|
||||
if(this.hidden) {
|
||||
if (this.hidden) {
|
||||
promise.then(() => this.$filter_tag.hide());
|
||||
}
|
||||
}
|
||||
|
|
@ -164,13 +175,13 @@ frappe.ui.Filter = class {
|
|||
set_values(doctype, fieldname, condition, value) {
|
||||
// presents given (could be via tags!)
|
||||
if (this.set_field(doctype, fieldname) === false) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.field.df.original_type==='Check') {
|
||||
value = (value==1) ? 'Yes' : 'No';
|
||||
if (this.field.df.original_type === 'Check') {
|
||||
value = value == 1 ? 'Yes' : 'No';
|
||||
}
|
||||
if(condition) this.set_condition(condition, true);
|
||||
if (condition) this.set_condition(condition, true);
|
||||
|
||||
// set value can be asynchronous, so update_filter_tag should happen after field is set
|
||||
this._filter_value_set = Promise.resolve();
|
||||
|
|
@ -190,11 +201,13 @@ frappe.ui.Filter = class {
|
|||
set_field(doctype, fieldname, fieldtype, condition) {
|
||||
// set in fieldname (again)
|
||||
let cur = {};
|
||||
if(this.field) for(let k in this.field.df) cur[k] = this.field.df[k];
|
||||
if (this.field) for (let k in this.field.df) cur[k] = this.field.df[k];
|
||||
|
||||
let original_docfield = (this.fieldselect.fields_by_name[doctype] || {})[fieldname];
|
||||
let original_docfield = (this.fieldselect.fields_by_name[doctype] || {})[
|
||||
fieldname
|
||||
];
|
||||
|
||||
if(!original_docfield) {
|
||||
if (!original_docfield) {
|
||||
console.warn(`Field ${fieldname} is not selectable.`);
|
||||
this.remove();
|
||||
return false;
|
||||
|
|
@ -214,8 +227,13 @@ frappe.ui.Filter = class {
|
|||
|
||||
// called when condition is changed,
|
||||
// don't change if all is well
|
||||
if(this.field && cur.fieldname == fieldname && df.fieldtype == cur.fieldtype &&
|
||||
df.parent == cur.parent && df.options == cur.options) {
|
||||
if (
|
||||
this.field &&
|
||||
cur.fieldname == fieldname &&
|
||||
df.fieldtype == cur.fieldtype &&
|
||||
df.parent == cur.parent &&
|
||||
df.options == cur.options
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -223,20 +241,25 @@ frappe.ui.Filter = class {
|
|||
this.fieldselect.selected_doctype = doctype;
|
||||
this.fieldselect.selected_fieldname = fieldname;
|
||||
|
||||
if (this.filters_config && this.filters_config[condition]
|
||||
&& this.filters_config[condition].valid_for_fieldtypes.includes(df.fieldtype)) {
|
||||
if (
|
||||
this.filters_config &&
|
||||
this.filters_config[condition] &&
|
||||
this.filters_config[condition].valid_for_fieldtypes.includes(df.fieldtype)
|
||||
) {
|
||||
let args = {};
|
||||
if (this.filters_config[condition].depends_on) {
|
||||
const field_name = this.filters_config[condition].depends_on;
|
||||
const filter_value = this.base_list.get_filter_value(field_name);
|
||||
args[field_name] = filter_value;
|
||||
}
|
||||
frappe.xcall(this.filters_config[condition].get_field, args).then(field => {
|
||||
df.fieldtype = field.fieldtype;
|
||||
df.options = field.options;
|
||||
df.fieldname = fieldname;
|
||||
this.make_field(df, cur.fieldtype);
|
||||
});
|
||||
frappe
|
||||
.xcall(this.filters_config[condition].get_field, args)
|
||||
.then(field => {
|
||||
df.fieldtype = field.fieldtype;
|
||||
df.options = field.options;
|
||||
df.fieldname = fieldname;
|
||||
this.make_field(df, cur.fieldtype);
|
||||
});
|
||||
} else {
|
||||
this.make_field(df, cur.fieldtype);
|
||||
}
|
||||
|
|
@ -255,16 +278,18 @@ frappe.ui.Filter = class {
|
|||
f.refresh();
|
||||
|
||||
this.field = f;
|
||||
if(old_text && f.fieldtype===old_fieldtype) {
|
||||
if (old_text && f.fieldtype === old_fieldtype) {
|
||||
this.field.set_value(old_text);
|
||||
}
|
||||
|
||||
// run on enter
|
||||
$(this.field.wrapper).find(':input').keydown(e => {
|
||||
if(e.which==13 && this.field.df.fieldtype !== 'MultiSelect') {
|
||||
this.on_change();
|
||||
}
|
||||
});
|
||||
$(this.field.wrapper)
|
||||
.find(':input')
|
||||
.keydown(e => {
|
||||
if (e.which == 13 && this.field.df.fieldtype !== 'MultiSelect') {
|
||||
this.on_change();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get_value() {
|
||||
|
|
@ -273,7 +298,7 @@ frappe.ui.Filter = class {
|
|||
this.field.df.fieldname,
|
||||
this.get_condition(),
|
||||
this.get_selected_value(),
|
||||
this.hidden
|
||||
this.hidden,
|
||||
];
|
||||
}
|
||||
get_selected_value() {
|
||||
|
|
@ -284,90 +309,101 @@ frappe.ui.Filter = class {
|
|||
return this.filter_edit_area.find('.condition').val();
|
||||
}
|
||||
|
||||
set_condition(condition, trigger_change=false) {
|
||||
set_condition(condition, trigger_change = false) {
|
||||
let $condition_field = this.filter_edit_area.find('.condition');
|
||||
$condition_field.val(condition);
|
||||
if(trigger_change) $condition_field.change();
|
||||
|
||||
if (trigger_change) $condition_field.change();
|
||||
}
|
||||
|
||||
make_tag() {
|
||||
if (!this.field) return;
|
||||
this.$filter_tag = this.get_filter_tag_element()
|
||||
.insertAfter(this.parent.find(".active-tag-filters .clear-filters"));
|
||||
this.$filter_tag = this.get_filter_tag_element().insertAfter(
|
||||
this.parent.find('.active-tag-filters .clear-filters')
|
||||
);
|
||||
this.set_filter_button_text();
|
||||
this.bind_tag();
|
||||
}
|
||||
|
||||
bind_tag() {
|
||||
this.$filter_tag.find(".remove-filter").on("click", this.remove.bind(this));
|
||||
this.$filter_tag.find('.remove-filter').on('click', this.remove.bind(this));
|
||||
|
||||
let filter_button = this.$filter_tag.find(".toggle-filter");
|
||||
filter_button.on("click", () => {
|
||||
filter_button.closest('.tag-filters-area').find('.filter-edit-area').show();
|
||||
let filter_button = this.$filter_tag.find('.toggle-filter');
|
||||
filter_button.on('click', () => {
|
||||
filter_button
|
||||
.closest('.tag-filters-area')
|
||||
.find('.filter-edit-area')
|
||||
.show();
|
||||
this.filter_edit_area.toggle();
|
||||
});
|
||||
}
|
||||
|
||||
set_filter_button_text() {
|
||||
this.$filter_tag.find(".toggle-filter").html(this.get_filter_button_text());
|
||||
this.$filter_tag.find('.toggle-filter').html(this.get_filter_button_text());
|
||||
}
|
||||
|
||||
get_filter_button_text() {
|
||||
let value = this.utils.get_formatted_value(this.field, this.get_selected_value());
|
||||
return `${__(this.field.df.label)} ${__(this.get_condition())} ${__(value)}`;
|
||||
let value = this.utils.get_formatted_value(
|
||||
this.field,
|
||||
this.get_selected_value()
|
||||
);
|
||||
return `${__(this.field.df.label)} ${__(this.get_condition())} ${__(
|
||||
value
|
||||
)}`;
|
||||
}
|
||||
|
||||
get_filter_tag_element() {
|
||||
return $(`<div class="filter-tag btn-group">
|
||||
<button class="btn btn-default btn-xs toggle-filter"
|
||||
title="${ __("Edit Filter") }">
|
||||
title="${__('Edit Filter')}">
|
||||
</button>
|
||||
<button class="btn btn-default btn-xs remove-filter"
|
||||
title="${ __("Remove Filter") }">
|
||||
title="${__('Remove Filter')}">
|
||||
<i class="fa fa-remove text-muted"></i>
|
||||
</button>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
add_condition_help(condition) {
|
||||
let $desc = this.field.desc_area;
|
||||
if(!$desc) {
|
||||
$desc = $('<div class="text-muted small">').appendTo(this.field.wrapper);
|
||||
}
|
||||
// set description
|
||||
$desc.html((in_list(["in", "not in"], condition)==="in"
|
||||
? __("values separated by commas")
|
||||
: __("use % as wildcard"))+'</div>');
|
||||
const description = ['in', 'not in'].includes(condition)
|
||||
? __('values separated by commas')
|
||||
: __('use % as wildcard');
|
||||
|
||||
this.filter_edit_area.find('.filter-description').html(description);
|
||||
}
|
||||
|
||||
hide_invalid_conditions(fieldtype, original_type) {
|
||||
let invalid_conditions = this.invalid_condition_map[original_type]
|
||||
|| this.invalid_condition_map[fieldtype] || [];
|
||||
let invalid_conditions =
|
||||
this.invalid_condition_map[original_type] ||
|
||||
this.invalid_condition_map[fieldtype] ||
|
||||
[];
|
||||
|
||||
for (let condition of this.conditions) {
|
||||
this.filter_edit_area.find(`.condition option[value="${condition[0]}"]`).toggle(
|
||||
!invalid_conditions.includes(condition[0])
|
||||
);
|
||||
this.filter_edit_area
|
||||
.find(`.condition option[value="${condition[0]}"]`)
|
||||
.toggle(!invalid_conditions.includes(condition[0]));
|
||||
}
|
||||
}
|
||||
|
||||
toggle_nested_set_conditions(df) {
|
||||
let show_condition = df.fieldtype === "Link" && frappe.boot.nested_set_doctypes.includes(df.options);
|
||||
this.nested_set_conditions.forEach(condition => {
|
||||
this.filter_edit_area.find(`.condition option[value="${condition[0]}"]`).toggle(show_condition);
|
||||
let show_condition =
|
||||
df.fieldtype === 'Link' &&
|
||||
frappe.boot.nested_set_doctypes.includes(df.options);
|
||||
this.nested_set_conditions.forEach((condition) => {
|
||||
this.filter_edit_area
|
||||
.find(`.condition option[value="${condition[0]}"]`)
|
||||
.toggle(show_condition);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
frappe.ui.filter_utils = {
|
||||
get_formatted_value(field, value) {
|
||||
if(field.df.fieldname==="docstatus") {
|
||||
value = {0:"Draft", 1:"Submitted", 2:"Cancelled"}[value] || value;
|
||||
} else if(field.df.original_type==="Check") {
|
||||
value = {0:"No", 1:"Yes"}[cint(value)];
|
||||
if (field.df.fieldname === 'docstatus') {
|
||||
value = { 0: 'Draft', 1: 'Submitted', 2: 'Cancelled' }[value] || value;
|
||||
} else if (field.df.original_type === 'Check') {
|
||||
value = { 0: 'No', 1: 'Yes' }[cint(value)];
|
||||
}
|
||||
return frappe.format(value, field.df, {only_value: 1});
|
||||
return frappe.format(value, field.df, { only_value: 1 });
|
||||
},
|
||||
|
||||
get_selected_value(field, condition) {
|
||||
|
|
@ -382,7 +418,7 @@ frappe.ui.filter_utils = {
|
|||
}
|
||||
|
||||
if (field.df.original_type == 'Check') {
|
||||
val = (val=='Yes' ? 1 :0);
|
||||
val = val == 'Yes' ? 1 : 0;
|
||||
}
|
||||
|
||||
if (condition.indexOf('like', 'not like') !== -1) {
|
||||
|
|
@ -390,12 +426,13 @@ frappe.ui.filter_utils = {
|
|||
if (val && !(val.startsWith('%') || val.endsWith('%'))) {
|
||||
val = '%' + val + '%';
|
||||
}
|
||||
} else if (in_list(["in", "not in"], condition)) {
|
||||
} else if (in_list(['in', 'not in'], condition)) {
|
||||
if (val) {
|
||||
val = val.split(',').map(v => strip(v));
|
||||
val = val.split(',').map((v) => strip(v));
|
||||
}
|
||||
} if (val === '%') {
|
||||
val = "";
|
||||
}
|
||||
if (val === '%') {
|
||||
val = '';
|
||||
}
|
||||
|
||||
return val;
|
||||
|
|
@ -404,7 +441,7 @@ frappe.ui.filter_utils = {
|
|||
get_default_condition(df) {
|
||||
if (df.fieldtype == 'Data') {
|
||||
return 'like';
|
||||
} else if (df.fieldtype == 'Date' || df.fieldtype == 'Datetime'){
|
||||
} else if (df.fieldtype == 'Date' || df.fieldtype == 'Datetime') {
|
||||
return 'Between';
|
||||
} else {
|
||||
return '=';
|
||||
|
|
@ -413,44 +450,73 @@ frappe.ui.filter_utils = {
|
|||
|
||||
set_fieldtype(df, fieldtype, condition) {
|
||||
// reset
|
||||
if(df.original_type)
|
||||
df.fieldtype = df.original_type;
|
||||
else
|
||||
df.original_type = df.fieldtype;
|
||||
if (df.original_type) df.fieldtype = df.original_type;
|
||||
else df.original_type = df.fieldtype;
|
||||
|
||||
df.description = ''; df.reqd = 0;
|
||||
df.description = '';
|
||||
df.reqd = 0;
|
||||
df.ignore_link_validation = true;
|
||||
|
||||
// given
|
||||
if(fieldtype) {
|
||||
if (fieldtype) {
|
||||
df.fieldtype = fieldtype;
|
||||
return;
|
||||
}
|
||||
|
||||
// scrub
|
||||
if(df.fieldname=="docstatus") {
|
||||
df.fieldtype="Select",
|
||||
df.options=[
|
||||
{value:0, label:__("Draft")},
|
||||
{value:1, label:__("Submitted")},
|
||||
{value:2, label:__("Cancelled")}
|
||||
if (df.fieldname == 'docstatus') {
|
||||
df.fieldtype = 'Select',
|
||||
df.options = [
|
||||
{ value: 0, label: __('Draft') },
|
||||
{ value: 1, label: __('Submitted') },
|
||||
{ value: 2, label: __('Cancelled') },
|
||||
];
|
||||
} else if(df.fieldtype=='Check') {
|
||||
df.fieldtype='Select';
|
||||
df.options='No\nYes';
|
||||
} else if(['Text','Small Text','Text Editor','Code','Tag','Comments',
|
||||
'Dynamic Link','Read Only','Assign'].indexOf(df.fieldtype)!=-1) {
|
||||
} else if (df.fieldtype == 'Check') {
|
||||
df.fieldtype = 'Select';
|
||||
df.options = 'No\nYes';
|
||||
} else if (
|
||||
[
|
||||
'Text',
|
||||
'Small Text',
|
||||
'Text Editor',
|
||||
'Code',
|
||||
'Tag',
|
||||
'Comments',
|
||||
'Dynamic Link',
|
||||
'Read Only',
|
||||
'Assign',
|
||||
].indexOf(df.fieldtype) != -1
|
||||
) {
|
||||
df.fieldtype = 'Data';
|
||||
} else if(df.fieldtype=='Link' && ['=', '!=', 'descendants of', 'ancestors of', 'not descendants of', 'not ancestors of'].indexOf(condition)==-1) {
|
||||
} else if (
|
||||
df.fieldtype == 'Link' &&
|
||||
[
|
||||
'=',
|
||||
'!=',
|
||||
'descendants of',
|
||||
'ancestors of',
|
||||
'not descendants of',
|
||||
'not ancestors of',
|
||||
].indexOf(condition) == -1
|
||||
) {
|
||||
df.fieldtype = 'Data';
|
||||
}
|
||||
if(df.fieldtype==="Data" && (df.options || "").toLowerCase()==="email") {
|
||||
if (
|
||||
df.fieldtype === 'Data' &&
|
||||
(df.options || '').toLowerCase() === 'email'
|
||||
) {
|
||||
df.options = null;
|
||||
}
|
||||
if(condition == "Between" && (df.fieldtype == 'Date' || df.fieldtype == 'Datetime')){
|
||||
if (
|
||||
condition == 'Between' &&
|
||||
(df.fieldtype == 'Date' || df.fieldtype == 'Datetime')
|
||||
) {
|
||||
df.fieldtype = 'DateRange';
|
||||
}
|
||||
if (condition == 'Timespan' && ['Date', 'Datetime', 'DateRange', 'Select'].includes(df.fieldtype)) {
|
||||
if (
|
||||
condition == 'Timespan' &&
|
||||
['Date', 'Datetime', 'DateRange', 'Select'].includes(df.fieldtype)
|
||||
) {
|
||||
df.fieldtype = 'Select';
|
||||
df.options = this.get_timespan_options(['Last', 'Today', 'This', 'Next']);
|
||||
}
|
||||
|
|
@ -466,15 +532,15 @@ frappe.ui.filter_utils = {
|
|||
|
||||
get_timespan_options(periods) {
|
||||
const period_map = {
|
||||
'Last': ['Week', 'Month', 'Quarter', '6 months', 'Year'],
|
||||
'Today': null,
|
||||
'This': ['Week', 'Month', 'Quarter', 'Year'],
|
||||
'Next': ['Week', 'Month', 'Quarter', '6 months', 'Year']
|
||||
Last: ['Week', 'Month', 'Quarter', '6 months', 'Year'],
|
||||
Today: null,
|
||||
This: ['Week', 'Month', 'Quarter', 'Year'],
|
||||
Next: ['Week', 'Month', 'Quarter', '6 months', 'Year'],
|
||||
};
|
||||
let options = [];
|
||||
periods.forEach(period => {
|
||||
periods.forEach((period) => {
|
||||
if (period_map[period]) {
|
||||
period_map[period].forEach(p => {
|
||||
period_map[period].forEach((p) => {
|
||||
options.push({
|
||||
label: __(`{0} {1}`, [period, p]),
|
||||
value: `${period.toLowerCase()} ${p.toLowerCase()}`,
|
||||
|
|
@ -488,5 +554,5 @@ frappe.ui.filter_utils = {
|
|||
}
|
||||
});
|
||||
return options;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -349,6 +349,9 @@ h6.uppercase, .h6.uppercase {
|
|||
.form-section {
|
||||
padding: 15px 7px;
|
||||
}
|
||||
.hide-border {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.help ol {
|
||||
|
|
@ -573,7 +576,13 @@ h6.uppercase, .h6.uppercase {
|
|||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.media-body:after, .media-body:before {
|
||||
.media-body {
|
||||
.left-arrow;
|
||||
}
|
||||
}
|
||||
|
||||
.left-arrow {
|
||||
&::after, &::before {
|
||||
right: 100%;
|
||||
top: 15px;
|
||||
border: solid transparent;
|
||||
|
|
@ -584,13 +593,13 @@ h6.uppercase, .h6.uppercase {
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
.media-body:after {
|
||||
&::after {
|
||||
border-color: rgba(136, 183, 213, 0);
|
||||
border-right-color: #fafbfc;
|
||||
border-width: 6px;
|
||||
margin-top: -6px;
|
||||
}
|
||||
.media-body:before {
|
||||
&::before {
|
||||
border-color: rgba(194, 225, 245, 0);
|
||||
border-right-color: @border-color;
|
||||
border-width: 7px;
|
||||
|
|
@ -638,6 +647,18 @@ h6.uppercase, .h6.uppercase {
|
|||
top: 5px;
|
||||
}
|
||||
|
||||
.timeline-item.user-content.show-indicator {
|
||||
position: relative;
|
||||
.media-body {
|
||||
margin-left: 50px;
|
||||
}
|
||||
&::before {
|
||||
.timeline-indicator();
|
||||
left: 13px;
|
||||
top: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-item.notification-content::before {
|
||||
.timeline-indicator();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,13 @@ class TestDocument(unittest.TestCase):
|
|||
|
||||
self.assertEqual(frappe.db.get_value(d.doctype, d.name, "subject"), "subject changed")
|
||||
|
||||
def test_value_changed(self):
|
||||
d = self.test_insert()
|
||||
d.subject = "subject changed again"
|
||||
d.save()
|
||||
self.assertTrue(d.has_value_changed('subject'))
|
||||
self.assertFalse(d.has_value_changed('event_type'))
|
||||
|
||||
def test_mandatory(self):
|
||||
# TODO: recheck if it is OK to force delete
|
||||
frappe.delete_doc_if_exists("User", "test_mandatory@example.com", 1)
|
||||
|
|
|
|||
|
|
@ -26,17 +26,24 @@ class BackupGenerator:
|
|||
If specifying db_file_name, also append ".sql.gz"
|
||||
"""
|
||||
def __init__(self, db_name, user, password, backup_path_db=None, backup_path_files=None,
|
||||
backup_path_private_files=None, db_host="localhost", db_port=3306, verbose=False):
|
||||
backup_path_private_files=None, db_host="localhost", db_port=None, verbose=False,
|
||||
db_type='mariadb'):
|
||||
global _verbose
|
||||
self.db_host = db_host
|
||||
self.db_port = db_port or 3306
|
||||
self.db_port = db_port
|
||||
self.db_name = db_name
|
||||
self.db_type = db_type
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.backup_path_files = backup_path_files
|
||||
self.backup_path_db = backup_path_db
|
||||
self.backup_path_private_files = backup_path_private_files
|
||||
|
||||
if not self.db_port and self.db_type == 'mariadb':
|
||||
self.db_port = 3306
|
||||
elif not self.db_port and self.db_type == 'postgres':
|
||||
self.db_port = 5432
|
||||
|
||||
site = frappe.local.site or frappe.generate_hash(length=8)
|
||||
self.site_slug = site.replace('.', '_')
|
||||
|
||||
|
|
@ -91,6 +98,7 @@ class BackupGenerator:
|
|||
backup_path_files = None
|
||||
backup_path_db = None
|
||||
backup_path_private_files = None
|
||||
site_config_backup_path = None
|
||||
|
||||
for this_file in file_list:
|
||||
this_file = cstr(this_file)
|
||||
|
|
@ -141,6 +149,17 @@ class BackupGenerator:
|
|||
for item in self.__dict__.copy().items())
|
||||
|
||||
cmd_string = """mysqldump --single-transaction --quick --lock-tables=false -u %(user)s -p%(password)s %(db_name)s -h %(db_host)s -P %(db_port)s | gzip > %(backup_path_db)s """ % args
|
||||
|
||||
if self.db_type == 'postgres':
|
||||
cmd_string = "pg_dump postgres://{user}:{password}@{db_host}:{db_port}/{db_name} | gzip > {backup_path_db}".format(
|
||||
user=args.get('user'),
|
||||
password=args.get('password'),
|
||||
db_host=args.get('db_host'),
|
||||
db_port=args.get('db_port'),
|
||||
db_name=args.get('db_name'),
|
||||
backup_path_db=args.get('backup_path_db')
|
||||
)
|
||||
|
||||
err, out = frappe.utils.execute_in_shell(cmd_string)
|
||||
|
||||
def send_email(self):
|
||||
|
|
@ -181,7 +200,8 @@ def get_backup():
|
|||
"""
|
||||
delete_temp_backups()
|
||||
odb = BackupGenerator(frappe.conf.db_name, frappe.conf.db_name,\
|
||||
frappe.conf.db_password, db_host = frappe.db.host)
|
||||
frappe.conf.db_password, db_host = frappe.db.host,\
|
||||
db_type=frappe.conf.db_type, db_port=frappe.conf.db_port)
|
||||
odb.get_backup()
|
||||
recipient_list = odb.send_email()
|
||||
frappe.msgprint(_("Download link for your backup will be emailed on the following email address: {0}").format(', '.join(recipient_list)))
|
||||
|
|
@ -201,6 +221,7 @@ def new_backup(older_than=6, ignore_files=False, backup_path_db=None, backup_pat
|
|||
backup_path_private_files=backup_path_private_files,
|
||||
db_host = frappe.db.host,
|
||||
db_port = frappe.db.port,
|
||||
db_type = frappe.conf.db_type,
|
||||
verbose=verbose)
|
||||
odb.get_backup(older_than, ignore_files, force=force)
|
||||
return odb
|
||||
|
|
@ -258,25 +279,38 @@ def backup(with_files=False, backup_path_db=None, backup_path_files=None, quiet=
|
|||
|
||||
if __name__ == "__main__":
|
||||
"""
|
||||
is_file_old db_name user password db_host
|
||||
get_backup db_name user password db_host
|
||||
is_file_old db_name user password db_host db_type db_port
|
||||
get_backup db_name user password db_host db_type db_port
|
||||
"""
|
||||
import sys
|
||||
cmd = sys.argv[1]
|
||||
|
||||
db_type = 'mariadb'
|
||||
try:
|
||||
db_type = sys.argv[6]
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
db_port = 3306
|
||||
try:
|
||||
db_port = int(sys.argv[7])
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
if cmd == "is_file_old":
|
||||
odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost")
|
||||
odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost", db_type=db_type, db_port=db_port)
|
||||
is_file_old(odb.db_file_name)
|
||||
|
||||
if cmd == "get_backup":
|
||||
odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost")
|
||||
odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost", db_type=db_type, db_port=db_port)
|
||||
odb.get_backup()
|
||||
|
||||
if cmd == "take_dump":
|
||||
odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost")
|
||||
odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost", db_type=db_type, db_port=db_port)
|
||||
odb.take_dump()
|
||||
|
||||
if cmd == "send_email":
|
||||
odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost")
|
||||
odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost", db_type=db_type, db_port=db_port)
|
||||
odb.send_email("abc.sql.gz")
|
||||
|
||||
if cmd == "delete_temp_backups":
|
||||
|
|
|
|||
|
|
@ -175,12 +175,18 @@ def getlink(doctype, name):
|
|||
return '<a href="#Form/%(doctype)s/%(name)s">%(name)s</a>' % locals()
|
||||
|
||||
def get_csv_content_from_google_sheets(url):
|
||||
# https://docs.google.com/spreadsheets/d/{sheetid}}/edit#gid={gid}
|
||||
validate_google_sheets_url(url)
|
||||
|
||||
# get gid, defaults to first sheet
|
||||
if "gid=" in url:
|
||||
gid = url.rsplit('gid=', 1)[1]
|
||||
else:
|
||||
gid = 0
|
||||
# remove /edit path
|
||||
url = url.rsplit('/edit', 1)[0]
|
||||
# add /export path, defaults to first sheet
|
||||
url = url + '/export?format=csv&gid=0'
|
||||
# add /export path,
|
||||
url = url + '/export?format=csv&gid={0}'.format(gid)
|
||||
|
||||
headers = {
|
||||
'Accept': 'text/csv'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ Faker==2.0.4
|
|||
future==0.18.2
|
||||
GitPython==2.1.15
|
||||
gitdb2==2.0.6;python_version<'3.4'
|
||||
google-api-python-client==1.7.11
|
||||
google-api-python-client==1.9.3
|
||||
google-auth-httplib2==0.0.3
|
||||
google-auth-oauthlib==0.4.1
|
||||
google-auth==1.17.1
|
||||
google-auth==1.18.0
|
||||
googlemaps==3.1.1
|
||||
gunicorn==19.10.0
|
||||
html2text==2016.9.19
|
||||
|
|
@ -29,7 +29,7 @@ ldap3==2.7
|
|||
markdown2==2.3.9
|
||||
maxminddb-geolite2==2018.703
|
||||
ndg-httpsclient==0.5.1
|
||||
num2words==0.5.5
|
||||
num2words==0.5.10
|
||||
oauthlib==3.1.0
|
||||
openpyxl==2.6.4
|
||||
passlib==1.7.2
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue