Merge branch 'develop' into refactor-uninstall-app

This commit is contained in:
Suraj Shetty 2020-07-01 12:13:23 +05:30 committed by GitHub
commit 8d5cb35297
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 787 additions and 424 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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={}) {

View file

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

View file

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

View file

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

View file

@ -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;">
&ndash; {%= data.comment_on %}</span>
{% if (!data.template) { %}
<span class="text-muted commented-on" style="font-weight: normal;">
&ndash; {%= data.comment_on %}
</span>
{% } %}
</div>
{% } %}
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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